diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 670a3f0f7..a7f0554a3 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +@ConfigurationPropertiesScan @EnableScheduling @EnableJpaAuditing @EnableCaching diff --git a/src/main/java/com/example/solidconnection/config/token/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java similarity index 52% rename from src/main/java/com/example/solidconnection/config/token/TokenType.java rename to src/main/java/com/example/solidconnection/auth/domain/TokenType.java index d5fc1717f..7fa6045f7 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -1,13 +1,13 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.domain; import lombok.Getter; @Getter public enum TokenType { - ACCESS("", 1000 * 60 * 60), - REFRESH("refresh:", 1000 * 60 * 60 * 24 * 7), - KAKAO_OAUTH("kakao:", 1000 * 60 * 60); + ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour + REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour private final String prefix; private final int expireTime; @@ -17,7 +17,7 @@ public enum TokenType { this.expireTime = expireTime; } - public String addTokenPrefixToSubject(String subject) { + public String addPrefixToSubject(String subject) { return prefix + subject; } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index caf78074d..29fb1b347 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -2,8 +2,6 @@ import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -16,15 +14,18 @@ import java.time.LocalDate; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.config.token.TokenValidator.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @RequiredArgsConstructor @Service public class AuthService { + public static final String SIGN_OUT_VALUE = "signOut"; + private final RedisTemplate redisTemplate; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; /* @@ -36,9 +37,9 @@ public class AuthService { * */ public void signOut(String email) { redisTemplate.opsForValue().set( - TokenType.REFRESH.addTokenPrefixToSubject(email), + REFRESH.addPrefixToSubject(email), SIGN_OUT_VALUE, - TokenType.REFRESH.getExpireTime(), + REFRESH.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -62,14 +63,14 @@ public void quit(String email) { * */ public ReissueResponse reissue(String email) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(email); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenService.generateToken(email, TokenType.ACCESS); - tokenService.saveToken(newAccessToken, TokenType.ACCESS); + String newAccessToken = tokenProvider.generateToken(email, ACCESS); + tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index f6adda20d..2cd356d73 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -6,8 +6,7 @@ import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; @@ -18,7 +17,7 @@ @Service public class SignInService { - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final KakaoOAuthClient kakaoOAuthClient; @@ -58,15 +57,15 @@ private void resetQuitedAt(String email) { } private SignInResponse getSignInInfo(String email) { - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignInResponse(true, accessToken, refreshToken); } private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = tokenService.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); - tokenService.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = tokenProvider.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); + tokenProvider.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index f10f40dbd..5cbd781eb 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -2,9 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -29,7 +27,7 @@ public class SignUpService { private final TokenValidator tokenValidator; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -51,7 +49,7 @@ public class SignUpService { public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = tokenService.getEmail(signUpRequest.kakaoOauthToken()); + String email = tokenProvider.getEmail(signUpRequest.kakaoOauthToken()); validateNicknameDuplicated(signUpRequest.nickname()); validateUserNotDuplicated(email); @@ -64,9 +62,9 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignUpResponse(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java new file mode 100644 index 000000000..693a968ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; + +@RequiredArgsConstructor +@Component +public class TokenProvider { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + + public String generateToken(String email, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + } + + public String saveToken(String token, TokenType tokenType) { + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + redisTemplate.opsForValue().set( + tokenType.addPrefixToSubject(subject), + token, + tokenType.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return token; + } + + public String getEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java similarity index 68% rename from src/main/java/com/example/solidconnection/config/token/TokenValidator.java rename to src/main/java/com/example/solidconnection/auth/service/TokenValidator.java index 9a63a21f5..8c17ad00c 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -12,18 +13,18 @@ import java.util.Date; import java.util.Objects; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; -import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; @Component @RequiredArgsConstructor public class TokenValidator { - public static final String SIGN_OUT_VALUE = "signOut"; - private final RedisTemplate redisTemplate; @Value("${jwt.secret}") @@ -31,20 +32,19 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.ACCESS); - validateNotSignOut(token); + validateTokenNotExpired(token, ACCESS); validateRefreshToken(token); } public void validateKakaoToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); + validateTokenNotExpired(token, KAKAO_OAUTH); validateKakaoTokenNotUsed(token); } private void validateTokenNotEmpty(String token) { if (!StringUtils.hasText(token)) { - throw new CustomException(INVALID_TOKEN); + throw new CustomException(EMPTY_TOKEN); } } @@ -52,32 +52,25 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { Date expiration = getClaim(token).getExpiration(); long now = new Date().getTime(); if ((expiration.getTime() - now) < 0) { - if (tokenType.equals(TokenType.ACCESS)) { + if (tokenType.equals(ACCESS)) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } - if (token.equals(TokenType.KAKAO_OAUTH)) { + if (token.equals(KAKAO_OAUTH)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } } - private void validateNotSignOut(String token) { - String email = getClaim(token).getSubject(); - if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { - throw new CustomException(USER_ALREADY_SIGN_OUT); - } - } - private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { + if (redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { + if (!Objects.equals(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email)), token)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java deleted file mode 100644 index 68144d733..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Getter -@Setter -@ConfigurationProperties(prefix = "cors") -@Configuration -public class CorsPropertiesConfig { - - private List allowedOrigins; -} diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java deleted file mode 100644 index 00f3cf411..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - private final CorsPropertiesConfig corsProperties; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) - .allowedMethods("*") - .allowedHeaders("*") - .allowCredentials(true); - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/CorsProperties.java b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java new file mode 100644 index 000000000..f851692c6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties(prefix = "cors") +public record CorsProperties(List allowedOrigins) { +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java new file mode 100644 index 000000000..84692709a --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.config.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtAuthentication extends AbstractAuthenticationToken { + + private final String token; + private final Object principal; + + public JwtAuthentication(Object principal, String token, Collection authorities) { + super(authorities); + this.token = token; + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return this.token; + } + + @Override + public Object getPrincipal() { + return this.principal; + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 69f5a2f2d..7487858be 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -12,7 +12,6 @@ import java.io.IOException; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -25,24 +24,20 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + writeResponse(response, errorResponse); } - public void expiredCommence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(new CustomException(ACCESS_TOKEN_EXPIRED)); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + writeResponse(response, errorResponse); } - public void customCommence(HttpServletRequest request, HttpServletResponse response, - CustomException customException) throws IOException { + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(customException); + writeResponse(response, errorResponse); + } + + private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index a618bec04..e01009be1 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,113 +1,60 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.JwtExpiredTokenException; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.ObjectUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.HashSet; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - public static final String TOKEN_HEADER = "Authorization"; - public static final String TOKEN_PREFIX = "Bearer "; + private static final String REISSUE_URI = "/auth/reissue"; + private static final String REISSUE_METHOD = "post"; - private final TokenService tokenService; - private final TokenValidator tokenValidator; + private final JwtProperties jwtProperties; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - - // 인증 정보를 저장할 필요 없는 url - AntPathMatcher pathMatcher = new AntPathMatcher(); - for (String endpoint : getPermitAllEndpoints()) { - if (pathMatcher.match(endpoint, request.getRequestURI())) { - filterChain.doFilter(request, response); - return; - } + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || isReissueRequest(request)) { + filterChain.doFilter(request, response); + return; } - // 토큰 검증 try { - String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 - try { - String requestURI = request.getRequestURI(); - if (requestURI.equals("/auth/reissue")) { - Authentication auth = this.tokenService.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - return; - } - tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 - } catch (ExpiredJwtException e) { - throw new JwtExpiredTokenException(ACCESS_TOKEN_EXPIRED.getMessage()); - } - Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 - SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 - } - } catch (JwtExpiredTokenException e) { - SecurityContextHolder.clearContext(); - jwtAuthenticationEntryPoint.expiredCommence(request, response, e); - return; + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } catch (AuthenticationException e) { - SecurityContextHolder.clearContext(); jwtAuthenticationEntryPoint.commence(request, response, e); - return; } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(request, response, e); - return; - } - filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 - } - - private String resolveAccessTokenFromRequest(HttpServletRequest request) { - String token = request.getHeader(TOKEN_HEADER); - - if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) { // 토큰이 비어 있지 않고, Bearer로 시작한다면 - return token.substring(TOKEN_PREFIX.length()); // Bearer 제외한 실제 토큰 부분 반환 + jwtAuthenticationEntryPoint.customCommence(response, e); + } catch (Exception e) { + jwtAuthenticationEntryPoint.generalCommence(response, e); } - return null; } - private HashSet getPermitAllEndpoints() { - var permitAllEndpoints = new HashSet(); - - // 서버 정상 작동 확인 - permitAllEndpoints.add("/"); - permitAllEndpoints.add("/index.html"); - permitAllEndpoints.add("/favicon.ico"); - - // 이미지 업로드 - permitAllEndpoints.add("/file/profile/pre"); - - // 토큰이 필요하지 않은 인증 - permitAllEndpoints.add("/auth/kakao"); - permitAllEndpoints.add("/auth/sign-up"); - - // 대학교 정보 - permitAllEndpoints.add("/university/search/**"); - - return permitAllEndpoints; + private boolean isReissueRequest(HttpServletRequest request) { + return REISSUE_URI.equals(request.getRequestURI()) && REISSUE_METHOD.equals(request.getMethod()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtProperties.java b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java new file mode 100644 index 000000000..e0c63da46 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties(String secret) { +} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java similarity index 60% rename from src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java rename to src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java index 5d992adaf..b3bbda5fa 100644 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java @@ -1,26 +1,21 @@ -package com.example.solidconnection.custom.userdetails; +package com.example.solidconnection.config.security; -import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class CustomUserDetails implements UserDetails {//todo: principal 을 썼을 때 바로 SiteUser를 반환하게 하면 안되나?? +public class JwtUserDetails implements UserDetails { - private final SiteUser siteUser; + private final String userName; - public CustomUserDetails(SiteUser siteUser) { - this.siteUser = siteUser; - } - - public String getEmail() { - return siteUser.getEmail(); + public JwtUserDetails(String userName) { + this.userName = userName; } @Override public String getUsername() { - return siteUser.getEmail(); + return this.userName; } @Override diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 70bcf6c37..d28d883ca 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,65 +1,52 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.cors.CorsPropertiesConfig; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; - @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfiguration { + private final CorsProperties corsProperties; + private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final CorsPropertiesConfig corsProperties; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(corsProperties.getAllowedOrigins()); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedOrigins(corsProperties.allowedOrigins()); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); + return source; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + return http .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) - .sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authorizeRequest - -> authorizeRequest - .requestMatchers( - "/", "/index.html", "/favicon.ico", - "/file/profile/pre", - "/auth/kakao", "/auth/sign-up", "/auth/reissue", - "/university/detail/**", "/university/search/**", "/university/recommends", - "/actuator/**" - ) - .permitAll() - .anyRequest().authenticated()) - .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .formLogin(AbstractHttpConfigurer::disable); - - return http.build(); + .formLogin(AbstractHttpConfigurer::disable) + .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java new file mode 100644 index 000000000..3c1218d13 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; + +@Component +@RequiredArgsConstructor +public class SignOutCheckFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || !isSignOut(token)) { + filterChain.doFilter(request, response); + return; + } + + jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + } + + private boolean isSignOut(String accessToken) { + String subject = parseSubject(accessToken, jwtProperties.secret()); + String refreshToken = REFRESH.addPrefixToSubject(subject); + return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java deleted file mode 100644 index fc9ccea31..000000000 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.solidconnection.config.token; - -import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import java.util.Date; -import java.util.concurrent.TimeUnit; - -@RequiredArgsConstructor -@Component -public class TokenService { - - private final RedisTemplate redisTemplate; - private final CustomUserDetailsService customUserDetailsService; - - @Value("${jwt.secret}") - private String secretKey; - - public String generateToken(String email, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(email); - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiredDate) - .signWith(SignatureAlgorithm.HS512, this.secretKey) - .compact(); - } - - public void saveToken(String token, TokenType tokenType) { - redisTemplate.opsForValue().set( - tokenType.addTokenPrefixToSubject(getClaim(token).getSubject()), - token, - tokenType.getExpireTime(), - TimeUnit.MILLISECONDS - ); - } - - public Authentication getAuthentication(String token) { - String email = getClaim(token).getSubject(); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } - - public String getEmail(String token) { - return getClaim(token).getSubject(); - } - - private Claims getClaim(String token) { - try { - return Jwts.parser() - .setSigningKey(secretKey) - .parseClaimsJws(token) - .getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } - } -} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 765013303..8c3032284 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -29,7 +29,8 @@ public enum ErrorCode { // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + EMPTY_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java deleted file mode 100644 index c9f1b1606..000000000 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.custom.userdetails; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - - private final SiteUserRepository siteUserRepository; - - @Override - public UserDetails loadUserByUsername(String username) { - SiteUser siteUser = siteUserRepository.findByEmail(username) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND, username)); - return new CustomUserDetails(siteUser); - } -} diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java new file mode 100644 index 000000000..a3775365d --- /dev/null +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; + +@Component +public class JwtUtils { + + private static final String TOKEN_HEADER = "Authorization"; + private static final String TOKEN_PREFIX = "Bearer "; + + private JwtUtils() { + } + + public static String parseTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(TOKEN_HEADER); + if (token == null || token.isBlank() || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + return token.substring(TOKEN_PREFIX.length()); + } + + public static String parseSubject(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + public static String parseSubjectOrElseThrow(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + throw new CustomException(INVALID_TOKEN); + } + } + + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java new file mode 100644 index 000000000..c0256f75a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -0,0 +1,127 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("토큰 인증 필터 테스트") +class JwtAuthenticationFilterTest { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + SecurityContextHolder.clearContext(); + } + + @Test + public void 유효한_토큰에_대한_인증_정보를_저장한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(JwtAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + + @Test + public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + then(filterChain).should().doFilter(request, response); + } + + @Nested + class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { + + @Test + public void 만료된_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + public void 서명하지_않은_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, "wrongSecretKey") + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + } + + private HttpServletRequest createRequestWithToken(String token) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java new file mode 100644 index 000000000..13544152b --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -0,0 +1,106 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.util.Date; +import java.util.Objects; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("로그아웃 체크 필터 테스트") +class SignOutCheckFilterTest { + + @Autowired + private SignOutCheckFilter signOutCheckFilter; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + private final String subject = "subject"; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + Objects.requireNonNull(redisTemplate.getConnectionFactory()) + .getConnection() + .serverCommands() + .flushDb(); + } + + @Test + void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { + // given + request = createTokenRequest(subject); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + void 토큰이_없으면_다음_필터로_전달한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @Test + void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { + // given + request = createTokenRequest(subject); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + private HttpServletRequest createTokenRequest(String subject) { + String token = Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java new file mode 100644 index 000000000..d3992a33a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java @@ -0,0 +1,96 @@ +package com.example.solidconnection.config.token; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerSpringBootTest +@DisplayName("TokenProvider 테스트") +class TokenProviderTest { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 토큰을_생성한다() { + // when + String subject = "subject123"; + String token = tokenProvider.generateToken(subject, TokenType.ACCESS); + + // then + String extractedSubject = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + assertThat(subject).isEqualTo(extractedSubject); + } + + @Nested + class 토큰을_저장한다 { + + @Test + void 토큰이_유효하면_저장한다() { + // given + String subject = "subject321"; + String token = createValidToken(subject); + + // when + tokenProvider.saveToken(token, TokenType.ACCESS); + + // then + String savedToken = redisTemplate.opsForValue().get(TokenType.ACCESS.addPrefixToSubject(subject)); + assertThat(savedToken).isEqualTo(token); + } + + @Test + void 토큰이_유효하지않으면_예외가_발생한다() { + // given + String token = createInvalidToken(); + + // when & then + assertThatCode(() -> tokenProvider.saveToken(token, TokenType.REFRESH)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createInvalidToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 2f69d6cf7..6b739248b 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,8 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -36,7 +36,7 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { ApplicationRepository applicationRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; private String accessToken; private String adminAccessToken; @@ -60,17 +60,17 @@ public void setUpUserAndToken() { SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); - adminAccessToken = tokenService.generateToken("email5", TokenType.ACCESS); - String adminRefreshToken = tokenService.generateToken("email5", TokenType.REFRESH); - tokenService.saveToken(adminRefreshToken, TokenType.REFRESH); + adminAccessToken = tokenProvider.generateToken("email5", TokenType.ACCESS); + String adminRefreshToken = tokenProvider.generateToken("email5", TokenType.REFRESH); + tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); - user6AccessToken = tokenService.generateToken("email6", TokenType.ACCESS); - String user6RefreshToken = tokenService.generateToken("email6", TokenType.REFRESH); - tokenService.saveToken(user6RefreshToken, TokenType.REFRESH); + user6AccessToken = tokenProvider.generateToken("email6", TokenType.ACCESS); + String user6RefreshToken = tokenProvider.generateToken("email6", TokenType.REFRESH); + tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); // setUp - 사용자 정보 저장 SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 059e00cde..fb42216c9 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -23,7 +23,7 @@ class MyPageTest extends BaseEndToEndTest { @Autowired private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @BeforeEach @@ -32,9 +32,9 @@ public void setUpUserAndToken() { siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index cb058fe3a..6d7f52032 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; @@ -31,7 +31,7 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -46,9 +46,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 8f1bd1018..efd5ad1d7 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -5,7 +5,6 @@ import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; @@ -19,6 +18,8 @@ import java.time.LocalDate; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; @@ -64,7 +65,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), () -> assertThat(response.kakaoOauthToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email))) .as("카카오 인증 토큰을 저장한다.") .isEqualTo(response.kakaoOauthToken()); } @@ -94,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -128,7 +129,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index eff3e54b5..07dafb539 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -2,8 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -27,6 +26,8 @@ import java.util.List; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; @@ -54,7 +55,7 @@ class SignUpTest extends BaseEndToEndTest { InterestedCountyRepository interestedCountyRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; @Autowired RedisTemplate redisTemplate; @@ -69,8 +70,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -108,7 +109,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -122,8 +123,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -148,8 +149,8 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = tokenService.generateToken(alreadyExistEmail, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(alreadyExistEmail, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index dc8401700..947f44fd0 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -24,7 +24,7 @@ class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -36,9 +36,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 37f922e4e..06bd175dc 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -45,7 +45,7 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -57,9 +57,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index ee46733a1..00afbc8e3 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.InterestedCountyRepository; @@ -38,7 +38,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; @Autowired private GeneralRecommendUniversities generalRecommendUniversities; @@ -54,9 +54,9 @@ void setUp() { generalRecommendUniversities.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 1187fb0ad..4859f9fe2 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -33,7 +33,7 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -45,9 +45,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java new file mode 100644 index 000000000..4dfc11540 --- /dev/null +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -0,0 +1,120 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.Date; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("JwtUtils 테스트") +class JwtUtilsTest { + + private final String jwtSecretKey = "jwt-secret-key"; + + @Nested + class 요청으로부터_토큰을_추출한다 { + + @Test + void 토큰이_있으면_토큰을_반환한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + String token = "token"; + request.addHeader("Authorization", "Bearer " + token); + + // when + String extractedToken = parseTokenFromRequest(request); + + // then + assertThat(extractedToken).isEqualTo(token); + } + + @Test + void 토큰이_없으면_null_을_반환한다() { + // given + MockHttpServletRequest noHeader = new MockHttpServletRequest(); + MockHttpServletRequest wrongPrefix = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Wrong token"); + MockHttpServletRequest emptyToken = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Bearer "); + + // when & then + assertAll( + () -> assertThat(parseTokenFromRequest(noHeader)).isNull(), + () -> assertThat(parseTokenFromRequest(wrongPrefix)).isNull(), + () -> assertThat(parseTokenFromRequest(emptyToken)).isNull() + ); + } + } + + @Nested + class 토큰으로부터_subject_를_추출한다 { + + @Test + void 유효한_토큰의_subject_를_추출한다() { + // given + String subject = "subject000"; + String token = createValidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출한다() { + // given + String subject = "subject999"; + String token = createInvalidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createInvalidToken(subject); + + // when + assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } + + private String createInvalidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } +}