그 중에서 Spring Security와 JWT를 활용하여 로그인을 구현하는 부분이 있었다.
(여태 나는 세션과 쿠키 방식의 로그인을 구현했었다.
프로젝트를 해본 경험은 있었지만 JWT를 구현해보지는 않았었다.)
프로젝트를 진행하며 알게 된 내용과 함께, springboot 버전마다 달라진 jwt관련 변경 사항들을 공유하고자 한다.
모든 코드는 Github에 있으니 참고하세요.
📚 목차
목차는 다음과 같다.
- JWT(JSON Web Token) 소개
- build.gradle 라이브러리 추가
- spring security, JWT 구현
- Spring Boot 3.x 이상의 Security Config 코드 변경 사항
- REST API 구현 & POSTMAN 확인
- 참고자료
- 회고
JWT(JSON Web Token) 소개
- 세션, 쿠키의 로그인 방식과 JWT의 가장 큰 차이점은 중 하나는
JWT 방식에서는 서버는 클라이언트의 상태를 완전히 저장하지 않는무상태성(Stateless)을 유지
할 수 있다는 점이다. - 사용자가 로그인 시에 암호화 방식(ex: 공개키, 비밀키 방식인 RSA)으로 클라이언트에게
암호화된 토큰을 전달
하게 되는데,
해당 토큰을 다시 서버에 전달해서 토큰을 복호화해서 풀어내는 방식을 사용하기 때문에
서버가 클라이언트의 상태를 저장하고 있지 않고 단순히 복호화만 하기 때문에 무상태성을 유지할 수 있다.
세션을 사용하는 로그인 방식은 서버 메모리에 사용자 세션 값을 들고있으므로
(서버가 아니라 DB에 넣더라도) 무상태성이라고 볼 수 없다.
우리는 CSR(Client Side Rendering) 방식의 React 사용하여 JWT를 구축할 예정이다.
CSR
말 그대로 SSR과 달리 렌더링이 클라이언트 쪽에서 일어난다.
즉, 서버는 요청을 받으면 클라이언트에 HTML과 JS를 보내주고, 클라이언트는 그것을 받아 렌더링을 시작한다.
쉽게 말하자면, HTML 파일 안에는 아무런 내용이 없다는 것이다.
그 내용은 JS 파일을 받아 실행을 시켜야 그제서야 만들어진다.
build.gradle 라이브러리 추가
참고로 나는 다음과 같은 버전을 사용했다.
- Intellij community 버전 2023.1
- SpringBoot 버전 3.1
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-common:2.0.2'
//spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
spring security, JWT 구현
아래와 같은 구조로 코드를 작성하였다. 하나씩 살펴보면서 정리하자.
1. JWT 인증 필터 생성: JwtAuthenticationFilter
이 코드는 Spring Security에서 사용되는 JWT 인증 필터를 정의한다.
클라이언트 요청이 서버에 도달하기 전에 실행되어 JWT 토큰의 유효성을 검사하고, 해당 토큰에 기반한 사용자 인증을 수행한다.
OncePerRequestFilter를 상속받아 요청당 1번만 필터가 실행되도록 한다.
- JWT 토큰이 유효하고 사용자 정보가 존재하는 경우,
해당 사용자를 인증하고 Spring Security의 SecurityContextHolder에 인증 정보를 설정한다. - 인증이 성공하면
해당 요청에 대해 사용자가 인증되었음을 나타내는 정보가 SecurityContextHolder에 설정된다.
인증을 실패하면, 로그로 확인한다. - 마지막으로, 필터 체인을 계속 진행하여 다음 필터로 요청을 전달한다.
package inf.questpartner.config.login.jwt;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 토큰의 유효성을 검사하고, 인증
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService; // 사용자 정보를 제공하는 서비스
private final JwtProperties jwtProperties; // JWT 관련 속성 클래스
@Value("${jwt.header}") private String HEADER_STRING; // HTTP 요청 헤더에서 JWT를 찾을 헤더 이름 -> "Authorization"
@Value("${jwt.prefix}") private String TOKEN_PREFIX; // JWT가 시작하는 접두사 -> "Bearer"
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Thread currentThread = Thread.currentThread();
log.info("현재 실행 중인 스레드: " + currentThread.getName());
// 토큰을 가져와 저장할 변수 선언
String header = request.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
// 1. JWT 토큰을 가지고 있는 경우, 토큰을 추출한다.
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX," ");
try {
// JWT에서 사용자 이름 추출
username = this.jwtProperties.getUsernameFromToken(authToken);
} catch (IllegalArgumentException ex) {
log.info("사용자 ID 가져오기 실패");
ex.printStackTrace();
} catch (ExpiredJwtException ex) {
log.info("토큰 만료");
ex.printStackTrace();
} catch (MalformedJwtException ex) {
log.info("잘못된 JWT !!");
System.out.println();
ex.printStackTrace();
} catch (Exception e) {
log.info("JWT 토큰 가져오기 실패 !!");
e.getStackTrace();
}
}
// 2. JWT 토큰이 없는 경우, "인증 실패" 로그를 남긴다.
else {
log.info("JWT가 Bearer로 시작하지 않습니다 !!");
}
//3. 요청에는 사용자의 식별 정보가 포함되어 있으면서, 현재 요청이 인증되지 않았을 때
// -> (사용자가 인증되지 않은 상태에서 인증된 상태로 전환하는 과정이다.)
if ((username != null) && (SecurityContextHolder.getContext().getAuthentication() == null)) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// JWT 토큰 유효성 검사
if (this.jwtProperties.validateToken(authToken, userDetails)) {
// 인증 정보 생성
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 인증된 사용자 정보 설정
authenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("인증된 사용자 " + username + ", 보안 컨텍스트 설정");
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 4. 인증되지 않은 JWT 토큰임을 로그로 알린다.
else {
log.info("잘못된 JWT 토큰 !!");
}
}
// "요청 정보가 없다."
else {
log.info("사용자 이름이 null이거나 컨텍스트가 null입니다 !!");
}
// 5. 다음 필터로 요청 전달
filterChain.doFilter(request, response);
}
}
2. 인증에서 인증이 실패했을 때 처리: JwtAuthenticationEntryPoint
이 코드는 Spring Security에서 JWT(JSON Web Token) 기반의 인증에서 인증이 실패했을 때 처리를 담당하는 클래스이다.
메서드 내에서는 HttpServletResponse를 통해 HTTP 상태코드 HttpServletResponse.SC_UNAUTHORIZED를 전송하고, "Unauthorized"라는 메시지를 출력한다. 이는 클라이언트에게 인증되지 않은 요청임을 알리는 응답이다.
유효한 자격 증명을 제공하지 않고 접근하려 할때 401 Unauthorized 에러를 리턴해준다.
package inf.questpartner.config.login.jwt;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
// 유효한 자격증명을 제공하지 않고 접근하려 할때, 401상태코드 반환
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
3. JWT의 생성, 유효성 검사 및 사용자 정보를 관리 : JwtProperties
JWT(JSON Web Token)의 생성, 유효성 검사 및 정보 추출을 담당하는 클래스이다.
Spring Security에서 사용자 인증에 필요한 JWT 관련 기능을 제공한다.
- JWT 토큰 생성
- JWT 토큰에서 클레임 추출 (사용자 이름, 만료 날짜 등)
- JWT 토큰 유효성 검사
- 사용자 정보를 기반으로 JWT 토큰 생성
package inf.questpartner.config.login.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.security.Key;
@Component
public class JwtProperties implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
@Value("${jwt.tokenExpirationTime}") private Integer tokenExpirationTime; // 토큰 만료 시간
@Value("${jwt.secret}") private String secret; // JWT 을 위한 비밀 키
private final Key key; // 비밀키를 Key 형태로 변환
// 생성자에서 비밀 키를 초기화합니다.
// 빈이 생성되고 주입을 받은 후에 secret값을 Base64 Decode해서 key 변수에 할당하기 위해
public JwtProperties(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// JWT에서 이메일 추출
public String getEmailFromJwt(String jwt) {
String token = jwt.substring(7);
String subject = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject();
return subject;
}
// JWT 토큰에서 사용자 이름 추출
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// JWT 토큰에서 만료 날짜 추출
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// JWT 토큰에서 클레임 추출
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// JWT 토큰에서 모든 클레임 추출
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
// JWT 토큰 만료 여부 확인
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 사용자 정보를 기반으로 JWT 토큰 생성
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
// JWT 토큰 생성
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + tokenExpirationTime * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
// JWT 토큰 유효성 검사
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// JWT 토큰 유효성 검사
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
4. security config 설정: SecurityConfig
웹 보안
구성을 정의하고 JWT(JSON Web Token) 인증 및 권한 부여
를 구현한다.
SecurityConfig 클래스는 @Configuration 및 @EnableWebSecurity 어노테이션을 사용하여 Spring Security의 설정 클래스임을 나타낸다.
package inf.questpartner.config.login;
import inf.questpartner.config.login.jwt.JwtAuthenticationEntryPoint;
import inf.questpartner.config.login.jwt.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CorsConfigurationSource corsConfigurationSource;
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.httpBasic(httpBasic -> httpBasic.disable())
// token을 사용하는 방식이기 때문에 csrf를 disable 처리한다.
.csrf(csrf -> csrf.disable())
// CORS 설정
.cors(cors -> cors.configurationSource(corsConfigurationSource))
// 요청 권한 설정
.authorizeHttpRequests(authorize
-> authorize
.requestMatchers("/view/**", "/roomList",
"/rooms/search","/randomQuote", "rooms/list",
"/user/checkId",
"/user/register",
"/user/login",
"/static/**", "/public/**",
"/webjars/**", "/ws-stomp/**", "/{chatBoxId}").permitAll()
// 'USER' 역할을 가진 사용자만 접근 가능
.requestMatchers("/user/**").hasRole("USER")
.requestMatchers("/rooms/**").hasRole("USER")
.requestMatchers("/rooms/{roomId}/**").hasRole("USER"))
// 세션을 사용하지 않기 때문에 STATELESS로 설정한다.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 예외 처리 설정 (JWT 인증 설정)
.exceptionHandling(excep -> excep.authenticationEntryPoint(jwtAuthenticationEntryPoint))
// JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
✔️ authenticationManager() 메서드
- authenticationManager() 메서드는 인증 매니저 빈을 생성한다.
- 이 빈은 Spring Security의 인증을 관리하는 데 사용된다.
✔️ filterChain() 메서드
(1) CSRF 보호를 비활성화한다.
.csrf(csrf -> csrf.disable())
REST API에서는 CSRF 방어가 필요가 없고, CSRF 토큰을 주고 받을 필요가 없기 때문에 CSRF 설정을 해제한다.
CSRF를 켜두면 서버는 클라이언트 영역에 CSRF 토큰을 보낼 수 있다.
(2) 세션을 사용하지 않으므로, STATELESS로 설정한다.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
서버를 Stateless하게 유지한다. 이걸 설정하면 Spring Security에서 세션을 만들지 않는다.
상태유지를 끈 상태에서도 JWT를 사용하면, 클라이언트가 토큰을 서버로 전달하면 된다.
클라이언트가 서버로 요청을 보낼 때마다, 헤더 또는 요청의 일부로 JWT를 포함시켜야 한다. 서버는 이 토큰을 검증하여 사용자를 인증하고 요청을 처리한다.
설정을 통해 서버를 상태유지(Stateless)로 유지한다는 것은 서버가 클라이언트의 상태를 저장하지 않는 것을 의미한다.
일반적으로, Spring Security에서 이 설정을 사용하면 세션을 생성하지 않는다. 이것은 JWT (JSON Web Token) 인증 방식과 관련이 있다.
만약에 켜둔다면 "JWT Token으로 로그인하더라도" "클라이언트에서 Token값을 서버에 전달하지 않더라도" 세션 값으로 로그인이 된다.
(3) 보안 필터 체인
예외 처리를 설정하고, JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 추가한다.
.exceptionHandling(excep -> excep.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
5. UserDeailsService를 통해 가입된 회원인지 확인 : User, CustomUserDetailsService
Spring Security를 설정하다 보면 UserDetailsService를 통해 가입된 회원인지 확인하여 반환하는 메서드를 구현하게 된다.
UserDetails
는 Spring Security가 사용자의 인증을 처리하는 데 필요한 사용자 정보를 제공한다.
사용자의 정보 ( 이름, 비밀번호, 권한, 계정 만료, 비밀번호 만료, 계정 잠금 등)를 담고 있는 인터페이스이다.
UserDetails의 인터페이스는 다음과 같다.
1
Collection<? extends GrantedAuthority> getAuthority()- 사용자에게 부여된 권한(role)을 GrantedAuthority 객체의 컬렉션으로 반환한다.
- 즉, 계정이 가지고 있는 권한 목록을 반환한다.
2
String getPassword()- 사용자의 암호화된 비밀번호를 반환한다.
3
String getUsername()- 사용자의 이름을 반환한다.
4
boolean isAccountNonExpired()- 사용자 계정이 만료되지 않았는지 여부를 반환한다.
- true는 완료되지 않음을 의미한다.
5
boolean isAccountNonLocked()- 사용자 계정이 잠기지 않았는지 여부를 반환한다.
- true는 잠기지 않음을 의미한다.
6
boolean isCredentialsNonExpired()- 사용자의 비밀번호가 만료되지 않았는지 여부를 반환한다.
- true는 만료되지 않음을 의미한다.
7
boolean isEnabled()- 사용자 계정이 활성화되었는지 여부를 반환한다.
- true는 활성화 상태를 의미한다.
이제 UserDetails를 사용하여 프로젝트에 맞춰 User를 구현해보자.
(1) User Entity (사용자 정보)
User Entity를 UserDetails으로 사용할 수 있다.
UserDetails의 구현체로 implements를 받으면 된다. UserDetails의 인터페이스를 구현하자.
package inf.questpartner.domain.users.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import inf.questpartner.domain.room.Room;
import inf.questpartner.domain.studytree.StudyTree;
import inf.questpartner.domain.users.common.UserBase;
import inf.questpartner.domain.users.common.UserLevel;
import inf.questpartner.domain.users.common.UserProfileImg;
import inf.questpartner.domain.users.common.UserStatus;
import inf.questpartner.dto.users.res.UserDetailResponse;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DiscriminatorValue("USER")
@Entity
public class User extends UserBase implements UserDetails {
...
@Builder
public User(Long id, String email, String password, String nickname) {
super(id, email, password, UserLevel.USER);
this.nickname = nickname;
this.userStatus = UserStatus.NORMAL;
this.profileImg = UserProfileImg.IMG_FINN;
this.studyTime = 0;
}
...
//========== UserDetails implements ==========//
/**
* Token을 고유한 Email 값으로 생성합니다
* @return email;
*/
@Override
public String getUsername() {
return email;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add( new SimpleGrantedAuthority("ROLE_" + this.userLevel.name()));
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
이제 User 엔티티는 앞으로 토큰을 생성할때 토큰의 정보로 사용될 정보와 권한 정보를 갖게 된다.
Username을 Email로 설정했기 때문에, email 값을 토큰 생성 정보로 사용할 수 있다.
@Override
public String getUsername() {
return email;
}
(2) CustomUserDetailsService
리포지토리를 통해 UserDetails 가져오는 서비스를 생성한다.
username을 가지고 UserDetails 객체를 리턴하는데, UserDetails의 구현체로 User Entity를 생성하였기에 User Entity를 리턴했다.
참고로, 지금 프로젝트는 username을 email(사용자 이메일 정보)로 설정했다.
package inf.questpartner.config.login.auth;
import inf.questpartner.repository.users.UserRepository;
import inf.questpartner.util.exception.ResourceNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 리포지토리를 통해 UserDetails 가져오는 서비스 생성
*/
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService{
private final UserRepository userRepository;
/**
* username을 가지고 UserDetails 객체를 리턴하는데,
* UserDetails의 구현체로 User Entity를 생성하였기에
* User Entity를 리턴하게끔 구현한 것!
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return this.userRepository.findByEmail(username).orElseThrow(
() -> new ResourceNotFoundException("User", "User Email : ", username));
}
}
Spring Boot 3.x 이상의 Security Config 코드 변경 사항
이전 버전의 코드와 최신 버전의 코드 간에는 주요한 차이가 있습니다. 주요한 변경 사항은 다음과 같다.
✔️ CSRF(Cross-Site Request Forgery) 설정
- 이전 버전: `.csrf().disable()`
- 최신 버전: `.csrf(AbstractHttpConfigurer::disable)`
✔️ 세션 관리(Session Management) 설정
- 이전 버전: `.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)`
- 최신 버전: `.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))`
✔️ 로그인 폼 설정
- 이전 버전: `.formLogin().disable()`
- 최신 버전: `.formLogin(login -> login.loginPage("/login").permitAll())`
✔️ HTTP 기본 인증 설정
- 이전 버전: `.httpBasic().disable()`
- 최신 버전: 해당 설정이 없음
✔️ authorizeRequests() 메서드 사용
- 이전 버전: `authorizeRequests().antMatchers(...)...`
- 최신 버전: `authorizeRequests(authorize -> authorize.antMatchers(...)...)`
이전 버전에서는 각 구성 요소를 따로 설정하고 체이닝하여 보안 필터 체인을 구성했었다
최신 버전에서는 authorizeRequests()
메서드를 사용하여 권한 설정을 직접 지정할 수 있다. 또한 일부 메서드에 대한 사용법이 변경되었으며, 람다 표현식
을 사용하여 세부 설정을 더 명확하게 지정할 수 있게 수정했다.
REST API 구현 & POSTMAN 확인
REST API
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class LoginApiController {
...
@PostMapping("/register")
public ResponseEntity<UserResponse> register(@RequestBody SignupRequest dto) {
UserResponse successMember = userService.signUp(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(successMember);
}
@PostMapping("/login")
public ResponseEntity<UserTokenDto> login(@RequestBody LoginRequest dto) {
UserTokenDto loginDTO = userService.login(dto);
return ResponseEntity.status(HttpStatus.OK).body(loginDTO);
}
@GetMapping
public ResponseEntity<ResUserPreview> getUserInfo(@AuthenticationPrincipal User user) {
ResUserPreview dto = userService.getUserPreview(user);
return ResponseEntity.status(HttpStatus.OK).body(dto);
}
api 요청은 다음과 같다.
- "/user/register" : 회원가입 요청
- "/user/login" : 로그인 요청
- "/user" : 로그인 정보 확인 해보기
POSTMAN 확인
✔️ "/user/register" : 회원가입 요청
✔️ "/user/login" : 로그인 요청
로그인 요청을 성공했다면, 다음과 같이 JWT Token을 받게 된다.
✔️ "/user" : 로그인 정보 확인
token을 header에 담아 get 요청을 보내면, 로그인된 정보를 조회할 수 있다.
참고자료
회고
프론트엔드 팀원에게 로그인 정보를 전달하는 데는 JWT가 좋을 것 같다고 생각했었다.
클라이언트의 로컬 스토리지나 쿠키에 JWT를 저장할 수 있기 때문에 편리하다는 장점이 있거든요.
장점과는 별개로 많은 삽질 끝에 완성할 수 있었다.
JWT를 공부해보고 팀프로젝트에 맞게 코드를 작성하는 과정이 생각보다 복잡하고 상당한 시간이 소요되었다.
(그렇게 팀플 시간이 연장되고 연장되고...)
번외로 팀플에 대한 의견을 말하고자 한다.
생각보다 백엔드와 프론트엔드의 소통 과정에서 고려해야 할 것이 많았었다.
그 중 하나를 말하자면...
API 명세서를 전달할 때, 로그인 요청에 필요한 HTTP를 정리해서 작성하는게 생각보다 오래 걸리기도 했다.
글을 작성하면서 생각을 정리하고, 협업 과정에서 필요한 커뮤니케이션에 대해 다시 한번 고민해 볼 수 있었다.
(팀플 에피소드는 회고 편으로 가지고 올 생각이다. )
아무튼!
이를 계기로 팀원들과의 소통이 더 원활해지고 프로젝트 진행이 더욱 효율적으로 이루어질 것으로 기대한다.
'프로젝트 > 팀프로젝트 qp편' 카테고리의 다른 글
JPA와 테이블 설계 (0) | 2024.03.22 |
---|---|
SpringBoot 3.x 버전 QueryDSL 설정 (0) | 2024.03.21 |
JPA N+1 발생 케이스과 MultipleBagFetchException 해결책 (0) | 2024.03.20 |
관계형 데이터베이스에서의 컬렉션 처리 : Room 엔티티의 구조 개선 (0) | 2024.03.18 |
Stomp를 활용한 실시간 채팅 프로그램 구현 (0) | 2024.03.15 |
댓글