Spring Security란?
Spring Security는 Spring에서 인증(Authentication)과 인가(Authorization) 기능을 지원하는 보안 프레임워크로써, Spring MVC 기반 애플리케이션에 보안을 적용하기 위한 표준이다.
Spring Security 덕분에 Interceptor나 Servlet Filter를 이용해서 직접 Security를 구현할 필요가 없다. 우리는 직접 구현하기보다 잘 만들어진 Spring Security를 이용하는 것이 좋은 선택이다.
JWT를 사용한 이유
세션 기반 인증 방식은 사용자의 로그인 정보를 서버 측에서 관리하기 때문에, 서버에 부하가 발생할 수 있다. 그리고 우리는 REST API를 이용한 CSR 방식의 백엔드 서버를 개발할 것이기 때문에 무상태성을 유지하기 위해서는 세션 기반 인증 방식은 적절하지 않다.
JWT를 사용하면 무상태성을 유지하면서 인증된 사용자의 자격을 증명할 수 있다. 즉, 사용자가 누구인지 기억할 필요 없이 토큰에 있는 정보에 접근 권한이 있는지만 체크하면 된다.
하지만 토큰이 탈취되면 사용자 정보를 그대로 제공하는 꼴이 되기 때문에 민감한 정보는 토큰에 포함하지 말아야 한다. Access Token과 Refresh Token 두 가지의 토큰으로 나누어 Access Token의 유효 기간을 짧게 가져가고 만료되면 Refresh Token을 통해 Access Token을 새로 발급하는 방식으로 안전성을 높인다.(하지만 완벽하지 않다)
로그인 흐름
- 클라이언트에서 사용자의 ID, Password를 받아 서버에 로그인을 요청한다.
- 서버는 전달받은 ID, Password를 가진 User 객체를 조회하고 존재한다면, Access Token과 Refresh Token을 생성한다.
- 생성한 Access Token과 Refresh Token을 DB에 저장한 후 HttpServletResponse 헤더에 담아 전달한다.
- 클라이언트는 발급받은 Access Token을 HttpServletRequest 헤더에 담아서 서버가 허용한 권한 범위 내에서 API를 사용할 수 있게 된다.
구현
JwtTokenizer
JwtTokenizer는 JWT를 생성하고 검증하는 클래스이다.
@Slf4j
@Component
public class JwtTokenProvider {
public static final String BEARER_TYPE = "Bearer";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String REFRESH_HEADER = "Refresh";
public static final String BEARER_PREFIX = "Bearer ";
@Getter
@Value("${jwt.secret-key}")
private String secretKey;
@Getter
@Value("${jwt.access-token-expiration-millis}")
private long accessTokenExpirationMillis;
@Getter
@Value("${jwt.refresh-token-expiration-millis}")
private long refreshTokenExpirationMillis;
private Key key;
// Bean 등록후 Key SecretKey HS256 decode
@PostConstruct
public void init() {
String base64EncodedSecretKey = encodeBase64SecretKey(this.secretKey);
this.key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
}
public String encodeBase64SecretKey(String secretKey) {
return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public TokenDto generateTokenDto(CustomUserDetails customUserDetails) {
Date accessTokenExpiresIn = getTokenExpiration(accessTokenExpirationMillis);
Date refreshTokenExpiresIn = getTokenExpiration(refreshTokenExpirationMillis);
Map<String, Object> claims = new HashMap<>();
claims.put("role", customUserDetails.getRole());
String accessToken = Jwts.builder()
.setClaims(claims)
.setSubject(customUserDetails.getEmail())
.setExpiration(accessTokenExpiresIn)
.setIssuedAt(Calendar.getInstance().getTime())
.signWith(key, SignatureAlgorithm.HS256)
.compact();
String refreshToken = Jwts.builder()
.setSubject(customUserDetails.getEmail())
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(refreshTokenExpiresIn)
.signWith(key)
.compact();
return TokenDto.builder()
.grantType(BEARER_TYPE)
.authorizationType(AUTHORIZATION_HEADER)
.accessToken(accessToken)
.accessTokenExpiresIn(accessTokenExpiresIn.getTime())
.refreshToken(refreshToken)
.build();
}
// JWT 토큰을 복호화하여 토큰 정보를 반환
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if (claims.get("role") == null) {
throw new BusinessLogicException(ExceptionCode.NO_ACCESS_TOKEN);
}
String authority = claims.get("role").toString();
CustomUserDetails customUserDetails = CustomUserDetails.of(
claims.getSubject(),
authority);
log.info("# AuthMember.getRoles 권한 체크 = {}", customUserDetails.getAuthorities().toString());
return new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
}
// 토큰 검증
public boolean validateToken(String token, HttpServletResponse response) {
try {
parseClaims(token);
} catch (MalformedJwtException e) {
log.info("Invalid JWT token");
log.trace("Invalid JWT token trace = {}", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token");
log.trace("Expired JWT token trace = {}", e);
Responder.sendErrorResponse(response, ExceptionCode.TOKEN_EXPIRED);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token");
log.trace("Unsupported JWT token trace = {}", e);
Responder.sendErrorResponse(response, ExceptionCode.TOKEN_UNSUPPORTED);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.");
log.trace("JWT claims string is empty trace = {}", e);
Responder.sendErrorResponse(response, ExceptionCode.TOKEN_ILLEGAL_ARGUMENT);
}
return true;
}
private Date getTokenExpiration(long expirationMillisecond) {
Date date = new Date();
return new Date(date.getTime() + expirationMillisecond);
}
// Token 복호화 및 예외 발생(토큰 만료, 시그니처 오류)시 Claims 객체가 안만들어짐.
public Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
public void accessTokenSetHeader(String accessToken, HttpServletResponse response) {
String headerValue = BEARER_PREFIX + accessToken;
response.setHeader(AUTHORIZATION_HEADER, headerValue);
}
public void refresshTokenSetHeader(String refreshToken, HttpServletResponse response) {
response.setHeader("Refresh", refreshToken);
}
// Request Header에 Access Token 정보를 추출하는 메서드
public String resolveAccessToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// Request Header에 Refresh Token 정보를 추출하는 메서드
public String resolveRefreshToken(HttpServletRequest request) {
String bearerToken = request.getHeader(REFRESH_HEADER);
if (StringUtils.hasText(bearerToken)) {
return bearerToken;
}
return null;
}
}
TokenDto
@Data
@Builder
public class TokenDto {
private final String grantType;
private final String authorizationType;
private final String accessToken;
private final String refreshToken;
private final Long accessTokenExpiresIn;
}
- JWT 생성 시 들어갈 정보는 다음과 같다.
- setClaims() : JWT에 포함시킬 Custom Claims를 추가한다. Custom Claims는 주로 인증된 사용자 정보를 넣는다.
- setSubject() : JWT에 대한 제목을 넣는다.
- setIssuedAt() : JWT 발행 일자를 넣는다. 파라미터 타입은 java.util.Date 타입이다.
- setExpiration() : JWT의 만료기한을 지정한다. 파라미터 타입은 java.util.Date 타입이다.
- signWith() : 서명을 위한 Key (java.security.Key) 객체를 설정한다.
- compact() : JWT를 생성하고 직렬화한다.
- getKeyFromBase64EncodedKey : JWT 서명에 사용될 SecretKey를 생성한다. Decoders.BASE64.decode() 메서드를 통해 byte[]를 반환한 후, Keys.hmacShaKeyFor() 메서드로 HMAC 알고리즘을 적용한 Key 객체를 생성한다. jjwt.0.9.x 버전까지는 HMAC을 지정해줘야 했지만 현재는 적절한 HMAC을 알아서 지정해 준다.
- validateToken : JWT에 포함된 Signature를 검증하여 위조 여부를 확인한다. 검증에 성공하면 JWT를 파싱 해서 Claims를 얻어온다.
- parseClaims() : JWT를 파싱해서 Claims를 얻어온다.
- generateTokenDto : CustomUserDetials의 유저 정보를 기반으로 Access Token과 Refresh Token을 생성하는 메서드
JwtAuthenticationFilter
JwtAuthenticationFitler 클래스는 로그인 검증 및 JWT 발급을 담당한다. JwtAutheticationFitler가 상속받고 있는 UsernamePasswordAuthenticationFilter는 Spring Security에서 사용자 이름과 암호를 받아 인증을 시도하는 필터이다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final AES128Config aes128Config;
private final MemberService memberService;
private final RedisService redisService;
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// ServletInputStream을 LoginDto 객체로 역직렬화
ObjectMapper objectMapper = new ObjectMapper();
LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
TokenDto tokenDto = jwtTokenProvider.generateTokenDto(customUserDetails);
String accessToken = tokenDto.getAccessToken();
String refreshToken = tokenDto.getRefreshToken();
String encryptedRefreshToken = aes128Config.encryptAes(refreshToken);
jwtTokenProvider.accessTokenSetHeader(accessToken, response);
jwtTokenProvider.refresshTokenSetHeader(encryptedRefreshToken, response);
Member findMember = memberService.findMemberAndCheckMemberExists(customUserDetails.getId());
Responder.loginSuccessResponse(response, findMember);
// 로그인 성공시 Refresh Token Redis 저장 ( key = Email / value = Refresh Token )
long refreshTokenExpirationMillis = jwtTokenProvider.getRefreshTokenExpirationMillis();
redisService.setValues(findMember.getEmail(), refreshToken, Duration.ofMillis(refreshTokenExpirationMillis));
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
}
- attemptAuthentication() : 인증을 시도하는 메서드이다. HttpServletRequest와 HttpServletResponse를 매개변수로 받아 사용자가 입력한 로그인 정보를 추출하고 AuthenticationManager를 사용해 인증을 시도한다. 인증에 성공하면 Authentication 객체를 반환하고 인증이 실패하면 AuthenticationException을 throw한다. 위 코드에서는 @SneakyThrows 애너테이션으로 발생한 예외를 throw해주고 있다.
- successfulAuthentication() : 사용자 인증이 성공했을 때 호출되는 메서드이다. 인증 성공 후에 응답을 한다거나, 인증 정보를 저장하는 등의 추가 작업을 수행할 수 있다. 위 코드에서는 로그인을 성공했다는 정보를 응답해 주고 생성한 Email과 RefreshToken을 key -value 값으로 Redis에 저장하고 있다.
CustomUserDetailService
UserDetailsService를 구현한 클래스. UserDetailsService 인터페이스는 Spring Security에서 인증 정보를 조회하기 위해 사용된다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return memberRepository.findByEmail(email)
.map(this::createUserDetails)
.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
}
private UserDetails createUserDetails(Member member) {
return CustomUserDetails.of(member);
}
}
- loadByUsername() : 사용자 이름(email)을 입력받아 MemberRepository에서 사용자 정보를 조회한다. 조회한 Member 객체가 존재하면 createUserDetails() 메서드를 사용해서 CusotmUserDetails 객체를 생성하고 반환한다.
CustomUserDetails
UserDetails 인터페이스를 구현한 클래스. Spring Security에서 관리하는 User 정보를 관리한다.
@Getter
@NoArgsConstructor
@ToString
public class CustomUserDetails extends Member implements UserDetails {
private Long id;
private String email;
private String role;
private String password;
private CustomUserDetails(Member member) {
this.id = member.getId();
this.email = member.getEmail();
this.password = member.getPassword();
this.role = member.getRole();
}
private CustomUserDetails(String email, String role) {
this.email = email;
this.role = role;
}
private CustomUserDetails(String email, String password, String role) {
this.email = email;
this.password = password;
this.role = role;
}
public static CustomUserDetails of(Member member) {
return new CustomUserDetails(member);
}
public static CustomUserDetails of(String email, String role) {
return new CustomUserDetails(email, role);
}
public static CustomUserDetails of(String email, String password, String role) {
return new CustomUserDetails(email, password, role);
}
@Override
public List<GrantedAuthority> getAuthorities() {
return CustomAuthorityUtils.createAuthorities(role);
}
@Override
public String getUsername() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- getAuthorities() : 권한을 생성하고 List<GrantedAuthority> 타입으로 반환한다.
- isAccountNonExpired() , isAccountNonLocked(), isCredentialsNonExpired(), isEnabled() : 사용자 계정이 만료되지 않았는지, 잠금 상태인지, 인증 정보가 만료되지 않았는지, 활성화 상태인지 여부를 반환하는 메서드이다. 모두 true를 반환하도록 구현했다.
CustomAuthorityUtils
권한 정보를 생성하고 검증하는 유틸리티 클래스
@Slf4j
public class CustomAuthorityUtils {
public static List<GrantedAuthority> createAuthorities(String role) {
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
}
public static void verifiedRole(String role) {
if (role == null) {
throw new BusinessLogicException(ExceptionCode.MEMBER_ROLE_DOES_NOT_EXISTS);
} else if (!role.equals(USER.toString()) && !role.equals(ADMIN.toString())) {
throw new BusinessLogicException(ExceptionCode.MEMBER_ROLE_INVALID);
}
}
}
- createAuthorities() : 입력된 role 값을 기반으로 권한 정보를 생성하여 List<GrantedAuthority> 타입으로 변환한다. 권한 정보는 “ROLE_USER”, “ROLE_ADMIN” 형식으로 생성된다.
- verifiedRole() : 입력된 role 값이 유효한 권한인지 검증한다.
SercurityConfiguration
Spring Security 설정을 담당하는 클래스
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtTokenProvider jwtTokenProvider;
private final MemberService memberService;
private final AES128Config aes128Config;
private final RedisService redisService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
.formLogin().disable()
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.apply(new CustomFilterConfigurer())
.and()
// TODO: 추후 권한 별 페이지 접근제어 설정 예정
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
configuration.setAllowCredentials(true);
configuration.addExposedHeader("Authorization");
configuration.addExposedHeader("Refresh");
configuration.addAllowedHeader("*");
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
log.info("SecurityConfiguration.CustomFilterConfigurer.configure excute");
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager,
jwtTokenProvider, aes128Config, memberService, redisService);
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenProvider, redisService);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new LoginFailurHandler());
builder
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}
}
}
filterChain() 메서드부터 살펴보자
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
.formLogin().disable()
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.apply(new CustomFilterConfigurer())
.and()
// TODO: 추후 권한 별 페이지 접근제어 설정 예정
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll());
return http.build();
}
- headers().frameOptions().sameOrigin() : X-Frame-Options 헤더 설정을 SAMEORIGIN으로 설정하여, 웹 페이지를 iframe으로 삽입하는 공격 방지를 위한 설정
- http.csrf().disable() : jwt를 사용하기 때문에 CSRF(Cross-Site Request Forgery) 공격 방지 기능을 사용하지 않는다.
- http.cors().configurationSource(corsConfigurationSource()) : CORS(Cross-Origin Resource Sharing)를 활성화하고, 허용할 origin, method, header 등을 설정한 corsConfigurationSource() 메서드를 지정
- http.formLogin().disable() : 폼 기반 로그인 방식을 비활성화
- http.httpBasic().disable() : HTTP 기본 인증 방식을 비활성화
- http.sessionManagement().sessionCreationPolicy(STATELESS) : 인증에 사용할 세션을 생성하지 않도록 설정
- http.exceptionHandling() : 예외 처리를 설정
- authenticationEntryPoint(new CustomAuthenticationEntryPoint()) : 인증되지 않은 사용자가 보호된 리소스에 접근할 때 호출할 엔드포인트를 설정
- accessDeniedHandler(new CustomAccessDeniedHandler()) : 인가되지 않은 사용자가 보호된 리소스에 접근할 때 호출할 핸들러를 설정
- apply(new CustomFilterConfigurer()) : 사용자 정의 필터를 적용
- authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) : 모든 HTTP 요청에 대해 접근을 허용. 추후 권한 별 접근 범위 지정할 예정
corsConfigurationSource() 메서드는 CORS를 설정하는 메서드이다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE"));
configuration.setAllowCredentials(true);
configuration.addExposedHeader("Authorization");
configuration.addExposedHeader("Refresh");
configuration.addAllowedHeader("*");
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
- setAllowedOrigins(List.of("*")) : 모든 Origin에서 접근이 가능하도록 해놨다. 이후 Origin 이 확정되면 변경해줘야 한다.
- setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE")) : HTTP 요청 메서드 중 GET, POST, PATCH, DELETE 만 허용
- setAllowCredentials(true) : true로 설정하면 Access-Control-Allow-Credentials 헤더가 설정된다.
- addExposedHeader() : 클라이언트에게 노출할 헤더 값을 설정
- addAllowedHeader() : 클라이언트가 전송할 수 있는 헤더 값을 설정
- setMaxAge() : 클라이언트가 다시 preflight 요청을 보내지 않아도 되는 시간을 설정
- registerCorsConfiguration("/**", configuration) : CORS 구성을 등록, “/**” 는 모든 경로에서 CORS가 적용되도록 설정한다.
CustomFilterConfigurer 클래스는 인증 및 권한 부여를 위해 사용되는 필터 체인을 구성하는 데 사용된다. AbstractHttpConfigurer 를 상속하며 configure() 메서드를 재정의한다.
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
log.info("SecurityConfiguration.CustomFilterConfigurer.configure excute");
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager,
jwtTokenProvider, aes128Config, memberService, redisService);
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenProvider, redisService);
jwtAuthenticationFilter.setFilterProcessesUrl("/auth/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new LoginFailurHandler());
builder
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}
}
- setFilterProcessesUrl("/auth/login") : 로그인 URL을 설정
- setAuthenticationSuccessHandler() , setAuthenticationFailureHandler() : 로그인 성공 및 실패 시 호출되는 핸들러를 설정
- addFitler() 메서드를 사용하여 필터를 추가하고, addFilterAfter() 메서드를 사용하여 JwtAuthenticationFilter 다음에 JwtVerificationFilter 를 추가한다.
테스트
로그인 기능이 잘 작동하는지 테스트를 해보자
class AuthIntegrationTest extends BaseIntegrationTest {
private final String BASE_URL = "/auth";
private final String EMAIL = "email@gmail.com";
@Autowired
private MemberService memberService;
@Autowired
private RedisService redisService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AES128Config aes128Config;
@BeforeEach
void befroeEach() {
MemberDto.SignUp signUpDto = StubData.MockMember.getSignUpDto();
memberService.signUp(signUpDto);
}
@AfterEach
void afterEach() {
memberService.deleteMember(EMAIL);
}
@Test
@DisplayName("로그인 성공")
void loginSuccessTest() throws Exception {
// given
LoginDto loginSuccessDto = StubData.MockMember.getLoginSuccessDto();
LoginResponse expectedResponseDto = StubData.MockMember.getLoginResponseDto();
// when
String uri = UriComponentsBuilder.newInstance().path(BASE_URL + "/login")
.build().toUri().toString();
String json = ObjectMapperUtils.asJsonString(loginSuccessDto);
ResultActions actions = ResultActionsUtils.getRequest(mvc, uri, json);
// then
LoginResponse responseDto = ObjectMapperUtils.actionsSingleResponseToLoginDto(actions);
assertThat(expectedResponseDto.getEmail()).isEqualTo(responseDto.getEmail());
assertThat(expectedResponseDto.getNickname()).isEqualTo(responseDto.getNickname());
assertThat(expectedResponseDto.getRole()).isEqualTo(responseDto.getRole());
actions
.andExpect(status().isOk())
.andDo(document("login-success",
getRequestPreProcessor(),
getResponsePreProcessor(),
getLoginSnippet(),
getLonginSuccessResponseSnippet()));
}
@Test
@DisplayName("로그인 실패")
void loginFailTest() throws Exception {
// given
LoginDto loginFailDto = StubData.MockMember.getLoginFailDto();
// when
String uri = UriComponentsBuilder.newInstance().path(BASE_URL + "/login")
.build().toUri().toString();
String json = ObjectMapperUtils.asJsonString(loginFailDto);
ResultActions actions = ResultActionsUtils.getRequest(mvc, uri, json);
// then
actions
.andExpect(status().isUnauthorized())
.andDo(document("login-fail",
getRequestPreProcessor(),
getResponsePreProcessor(),
getLoginSnippet(),
getFieldErrorSnippets()));
}
}
통합테스트에 대한 내용은 아래 링크에 정리해 두었다.
마무리
다음으로 JWT 검증에 대해 알아보자
'Spring' 카테고리의 다른 글
Spring - Spring Security + JWT 적용기 3편: 로그아웃 (3) | 2023.04.14 |
---|---|
Spring - Spring Security + JWT 적용기 2편: JWT 검증 (0) | 2023.04.14 |
Spring - 나의 첫 통합 테스트(Integration Test) (0) | 2023.04.12 |
Spring - Spring Profile로 다양한 개발 환경 설정 관리하기 (0) | 2023.04.12 |
Spring Security - Spring Security란? (0) | 2023.03.27 |