diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/HeachiAuthApplication.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/HeachiAuthApplication.java index 8d6bd7e9..7b727c00 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/HeachiAuthApplication.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/HeachiAuthApplication.java @@ -11,5 +11,4 @@ public class HeachiAuthApplication { public static void main(String[] args) { SpringApplication.run(HeachiAuthApplication.class, args); } - } diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/auth/AuthController.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/auth/AuthController.java index 65f4d901..6c6a3058 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/auth/AuthController.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/auth/AuthController.java @@ -1,11 +1,12 @@ package com.heachi.auth.api.controller.auth; import com.heachi.admin.common.exception.ExceptionMessage; -import com.heachi.admin.common.exception.auth.AuthException; +import com.heachi.admin.common.exception.jwt.JwtException; import com.heachi.admin.common.exception.oauth.OAuthException; import com.heachi.admin.common.response.JsonResult; import com.heachi.auth.api.controller.auth.request.AuthRegisterRequest; import com.heachi.auth.api.controller.auth.response.UserSimpleInfoResponse; +import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse; import com.heachi.auth.api.service.auth.AuthService; import com.heachi.auth.api.service.auth.request.AuthServiceRegisterRequest; import com.heachi.auth.api.service.auth.response.AuthServiceLoginResponse; @@ -13,18 +14,15 @@ import com.heachi.auth.api.service.state.LoginStateService; import com.heachi.mysql.define.user.User; import com.heachi.mysql.define.user.constant.UserPlatformType; -import com.heachi.mysql.define.user.constant.UserRole; -import jakarta.servlet.http.HttpServletRequest; +import io.swagger.v3.core.util.Json; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; -import java.security.Principal; - -import static com.heachi.mysql.define.user.constant.UserRole.*; +import java.util.Arrays; +import java.util.List; @Slf4j @RequiredArgsConstructor @@ -72,10 +70,39 @@ public JsonResult userInfo(@AuthenticationPrincipal User return JsonResult.successOf(UserSimpleInfoResponse.of(user)); } + @GetMapping("/logout") + public JsonResult logout(@RequestHeader(name = "Authorization") String token) { + List tokens = Arrays.asList(token.split(" ")); + + if (tokens.size() == 3) { + authService.logout(tokens.get(2)); + + return JsonResult.successOf("Logout successfully."); + } else { + log.warn(">>>> Invalid Header Access : {}", ExceptionMessage.JWT_INVALID_HEADER.getText()); + return JsonResult.failOf(ExceptionMessage.JWT_INVALID_HEADER.getText()); + } + + } + @PostMapping("/delete") public JsonResult userDelete(@AuthenticationPrincipal User user) { authService.userDelete(user.getEmail()); return JsonResult.successOf(); } -} + + @PostMapping("/reissue") + public JsonResult reissueAccessToken(@RequestHeader(name = "Authorization") String token) { + List tokens = Arrays.asList(token.split(" ")); + + if (tokens.size() == 3) { + ReissueAccessTokenResponse reissueResponse = authService.reissueAccessToken(tokens.get(2)); + + return JsonResult.successOf(reissueResponse); + } else { + log.warn(">>>> Invalid Header Access : {}", ExceptionMessage.JWT_INVALID_HEADER.getText()); + return JsonResult.failOf(ExceptionMessage.JWT_INVALID_HEADER.getText()); + } + } +} \ No newline at end of file diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/token/response/ReissueAccessTokenResponse.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/token/response/ReissueAccessTokenResponse.java new file mode 100644 index 00000000..05f2953f --- /dev/null +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/controller/token/response/ReissueAccessTokenResponse.java @@ -0,0 +1,16 @@ +package com.heachi.auth.api.controller.token.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ReissueAccessTokenResponse { + private String accessToken; + private String refreshToken; + + @Builder + public ReissueAccessTokenResponse(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/AuthService.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/AuthService.java index 5ca9d307..cb0137af 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/AuthService.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/AuthService.java @@ -2,26 +2,29 @@ import com.heachi.admin.common.exception.ExceptionMessage; import com.heachi.admin.common.exception.auth.AuthException; +import com.heachi.admin.common.exception.jwt.JwtException; +import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse; import com.heachi.auth.api.service.auth.request.AuthServiceRegisterRequest; import com.heachi.auth.api.service.auth.response.AuthServiceLoginResponse; import com.heachi.auth.api.service.jwt.JwtService; +import com.heachi.auth.api.service.jwt.JwtTokenDTO; import com.heachi.auth.api.service.oauth.OAuthService; import com.heachi.auth.api.service.oauth.response.OAuthResponse; +import com.heachi.auth.api.service.token.RefreshTokenService; import com.heachi.mysql.define.user.User; import com.heachi.mysql.define.user.constant.UserPlatformType; import com.heachi.mysql.define.user.constant.UserRole; import com.heachi.mysql.define.user.repository.UserRepository; -import com.heachi.redis.config.RedisConfig; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; +import com.heachi.redis.define.refreshToken.RefreshToken; +import com.heachi.redis.define.refreshToken.repository.RefreshTokenRepository; +import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; +import java.util.Map; @Slf4j @@ -33,9 +36,7 @@ public class AuthService { private final UserRepository userRepository; private final OAuthService oAuthService; private final JwtService jwtService; - - // 빈 주입이 안됨 ㅠ - private final RedisTemplate redisTemplacte; + private final RefreshTokenService refreshTokenService; private static final String ROLE_CLAIM = "role"; private static final String NAME_CLAIM = "name"; @@ -64,40 +65,49 @@ public AuthServiceLoginResponse login(UserPlatformType platformType, String code // 기존 회원의 경우 name, profileImageUrl 변하면 update findUser.updateProfile(loginResponse.getName(), loginResponse.getProfileImageUrl()); - // JWT 토큰 발급 - final String token = createJwtToken(findUser); + // JWT Access Token, Refresh Token 발급 + JwtTokenDTO tokens = createJwtToken(findUser); return AuthServiceLoginResponse.builder() - .token(token) + .accessToken(tokens.getAccessToken()) + .refreshToken(tokens.getRefreshToken()) .role(findUser.getRole()) .build(); } + @Transactional + public void logout(String refreshToken) { + refreshTokenService.logout(refreshToken); + } + @Transactional public AuthServiceLoginResponse register(AuthServiceRegisterRequest request) { User findUser = userRepository.findByEmail(request.getEmail()).orElseThrow(() -> { // UNAUTH인 토큰을 받고 회원 탈퇴 후 그 토큰으로 회원가입 요청시 예외 처리 + log.warn(">>>> User Not Exist : {}", ExceptionMessage.AUTH_INVALID_REGISTER.getText()); throw new AuthException(ExceptionMessage.AUTH_INVALID_REGISTER); }); // UNAUTH 토큰으로 회원가입을 요청했지만 이미 update되어 UNAUTH가 아닌 사용자 예외 처리 if (findUser.getRole() != UserRole.UNAUTH) { + log.warn(">>>> Not UNAUTH User : {}", ExceptionMessage.AUTH_DUPLICATE_UNAUTH_REGISTER.getText()); throw new AuthException(ExceptionMessage.AUTH_DUPLICATE_UNAUTH_REGISTER); } // 회원가입 정보 DB 반영 findUser.updateRegister(request.getRole(), request.getPhoneNumber()); - // JWT 토큰 재발급 - final String token = createJwtToken(findUser); + // JWT Access Token, Refresh Token 재발급 + JwtTokenDTO tokens = createJwtToken(findUser); return AuthServiceLoginResponse.builder() - .token(token) + .accessToken(tokens.getAccessToken()) + .refreshToken(tokens.getRefreshToken()) .role(findUser.getRole()) .build(); } - private String createJwtToken(User user) { + private JwtTokenDTO createJwtToken(User user) { // JWT 토큰 생성을 위한 claims 생성 HashMap claims = new HashMap<>(); claims.put(ROLE_CLAIM, user.getRole().name()); @@ -106,15 +116,23 @@ private String createJwtToken(User user) { // Access Token 생성 final String accessToken = jwtService.generateAccessToken(claims, user); - // Refresh Token 생성 final String refreshToken = jwtService.generateRefreshToken(claims, user); - // Refresh Token 저장 + log.info(">>>> {} generate Tokens", user.getName()); + + // Refresh Token 저장 - REDIS + RefreshToken rt = RefreshToken.builder() + .refreshToken(refreshToken) + .email(user.getEmail()) + .build(); + refreshTokenService.saveRefreshToken(rt); - // 로그인 반환 객체 생성 - return accessToken; + return JwtTokenDTO.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); } @Transactional @@ -132,4 +150,26 @@ public void userDelete(String email) { throw new AuthException(ExceptionMessage.AUTH_DELETE_FAIL); } } -} + + @Transactional + public ReissueAccessTokenResponse reissueAccessToken(String refreshToken) { + Claims claims = jwtService.extractAllClaims(refreshToken); + + // 토큰 검증 + if (jwtService.isTokenValid(refreshToken, claims.getSubject())) { + // 리프레시 토큰을 이용해 새로운 엑세스 토큰 발급 + String accessToken = refreshTokenService.reissue(claims, refreshToken); + log.info(">>>> {} reissue AccessToken.", claims.getSubject()); + + return ReissueAccessTokenResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + } else { + log.warn(">>>> Token Validation Fail : {}", ExceptionMessage.JWT_INVALID_RTK.getText()); + throw new JwtException(ExceptionMessage.JWT_INVALID_RTK); + } + + } +} \ No newline at end of file diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/response/AuthServiceLoginResponse.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/response/AuthServiceLoginResponse.java index 07fdca8c..944fadd8 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/response/AuthServiceLoginResponse.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/auth/response/AuthServiceLoginResponse.java @@ -6,12 +6,14 @@ @Getter public class AuthServiceLoginResponse { - private String token; + private String accessToken; + private String refreshToken; private UserRole role; @Builder - private AuthServiceLoginResponse(String token, UserRole role) { - this.token = token; + public AuthServiceLoginResponse(String accessToken, String refreshToken, UserRole role) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; this.role = role; } } diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtService.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtService.java index de5364ef..60c58620 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtService.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtService.java @@ -2,6 +2,7 @@ import com.heachi.admin.common.exception.ExceptionMessage; import com.heachi.admin.common.exception.jwt.JwtException; +import com.heachi.mysql.define.user.User; import com.heachi.mysql.define.user.constant.UserRole; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -24,12 +25,6 @@ public class JwtService { @Value("${jwt.secretKey}") private String secretKey; - @Value("${jwt.token.access-expiration-time}") - private long accessExpirationTime; - - @Value("${jwt.token.refresh-expiration-time}") - private long refreshExpirationTime; - /* * Token에서 사용자 이름 추출 */ @@ -47,11 +42,11 @@ private T extractClaim(String token, Function claimsResolver) { * AccessToken 생성 */ public String generateAccessToken(UserDetails userDetails) { - return generateAccessToken(new HashMap<>(), userDetails, new Date(System.currentTimeMillis() + accessExpirationTime)); + return generateAccessToken(new HashMap<>(), userDetails, new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)); } public String generateAccessToken(Map extraClaims, UserDetails userDetails) { - return generateAccessToken(extraClaims, userDetails, new Date(System.currentTimeMillis() + accessExpirationTime)); + return generateAccessToken(extraClaims, userDetails, new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)); } public String generateAccessToken(Map extraClaims, UserDetails userDetails, Date expiredTime) { @@ -68,11 +63,11 @@ public String generateAccessToken(Map extraClaims, UserDetails u * RefreshToken 생성 */ public String generateRefreshToken(UserDetails userDetails) { - return generateAccessToken(new HashMap<>(), userDetails, new Date(System.currentTimeMillis() + refreshExpirationTime)); + return generateAccessToken(new HashMap<>(), userDetails, new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7)); } public String generateRefreshToken(Map extraClaims, UserDetails userDetails) { - return generateRefreshToken(extraClaims, userDetails, new Date(System.currentTimeMillis() + refreshExpirationTime)); + return generateRefreshToken(extraClaims, userDetails, new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7)); } public String generateRefreshToken(Map extraClaims, UserDetails userDetails, Date expiredTime) { @@ -108,7 +103,7 @@ public boolean isTokenValid(String token, String username) { return (claimsSubject.equals(username)) && !isTokenExpired(token); } - private boolean isTokenExpired(String token) { + public boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } @@ -116,7 +111,7 @@ private boolean isTokenExpired(String token) { /* * Token 정보 추출 */ - private Date extractExpiration(String token) { + public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } @@ -133,4 +128,19 @@ private Key getSignInkey() { return Keys.hmacShaKeyFor(keyBytes); } + + public String generateExpiredAccessToken(Map claims, UserDetails user) { + Date now = new Date(); + + // 만료기간을 현재 시각보다 이전으로 + Date expiryTime = new Date(now.getTime() - 1000); + + return Jwts.builder() + .setClaims(claims) + .setSubject(user.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(expiryTime) + .signWith(getSignInkey(), SignatureAlgorithm.HS256) + .compact(); + } } diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtTokenDTO.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtTokenDTO.java new file mode 100644 index 00000000..d156dc0a --- /dev/null +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/jwt/JwtTokenDTO.java @@ -0,0 +1,16 @@ +package com.heachi.auth.api.service.jwt; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class JwtTokenDTO { + private String accessToken; + private String refreshToken; + + @Builder + public JwtTokenDTO(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/token/RefreshTokenService.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/token/RefreshTokenService.java new file mode 100644 index 00000000..430f37b3 --- /dev/null +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/api/service/token/RefreshTokenService.java @@ -0,0 +1,76 @@ +package com.heachi.auth.api.service.token; + +import com.heachi.admin.common.exception.ExceptionMessage; +import com.heachi.admin.common.exception.jwt.JwtException; +import com.heachi.auth.api.service.jwt.JwtService; +import com.heachi.auth.api.service.jwt.JwtTokenDTO; +import com.heachi.mysql.define.user.User; +import com.heachi.mysql.define.user.constant.UserRole; +import com.heachi.redis.define.refreshToken.RefreshToken; +import com.heachi.redis.define.refreshToken.repository.RefreshTokenRepository; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + final RefreshTokenRepository refreshTokenRepository; + final JwtService jwtService; + + // Logout 시 Redis에 저장된 RTK 삭제 + public void logout(String refreshToken) { + + String email = jwtService.extractAllClaims(refreshToken).getSubject(); + + // refreshToken 유효성 검사 + if (!jwtService.isTokenValid(refreshToken, email)) { + log.warn(">>>> Token Validation Fail : {}", ExceptionMessage.JWT_INVALID_RTK.getText()); + throw new JwtException(ExceptionMessage.JWT_INVALID_RTK); + } + + RefreshToken rtk = refreshTokenRepository.findById(refreshToken).orElseThrow(() -> { + log.warn(">>>> Token Not Exist : {}", ExceptionMessage.JWT_NOT_EXIST_RTK.getText()); + throw new JwtException(ExceptionMessage.JWT_NOT_EXIST_RTK); + }); + + refreshTokenRepository.delete(rtk); + log.info(">>>> {}'s RefreshToken id deleted.", email); + } + + public void saveRefreshToken(RefreshToken refreshToken) { + RefreshToken savedToken = refreshTokenRepository.save(refreshToken); + log.info(">>>> Refresh Token register : {}", savedToken.getRefreshToken()); + } + + public String reissue(Claims claims, String refreshToken) { + + refreshTokenRepository.findById(refreshToken).orElseThrow(() -> { + log.warn(">>>> Token Not Exist : {}", ExceptionMessage.JWT_NOT_EXIST_RTK.getText()); + throw new JwtException(ExceptionMessage.JWT_NOT_EXIST_RTK); + }); + + String role = claims.get("role", String.class); + + UserDetails userDetails = User.builder() + .email(jwtService.extractUsername(refreshToken)) + .role(UserRole.valueOf(role)) + .name(claims.get("name", String.class)) + .profileImageUrl(claims.get("profileImageUrl", String.class)) + .build(); + + Map map = new HashMap<>(); + for (Map.Entry entry : claims.entrySet()) { + map.put(entry.getKey(), entry.getValue().toString()); + } + + return jwtService.generateAccessToken(map, userDetails); + } +} \ No newline at end of file diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/config/filter/JwtAuthenticationFilter.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/config/filter/JwtAuthenticationFilter.java index d3791147..0f8da3fb 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/config/filter/JwtAuthenticationFilter.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/config/filter/JwtAuthenticationFilter.java @@ -5,8 +5,11 @@ import com.heachi.admin.common.exception.jwt.JwtException; import com.heachi.admin.common.response.JsonResult; import com.heachi.auth.api.service.jwt.JwtService; +import com.heachi.auth.api.service.token.RefreshTokenService; import com.heachi.mysql.define.user.User; import com.heachi.mysql.define.user.constant.UserRole; +import com.heachi.redis.define.refreshToken.RefreshToken; +import com.heachi.redis.define.refreshToken.repository.RefreshTokenRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; @@ -29,11 +32,15 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.Arrays; +import java.util.List; @Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final RefreshTokenRepository refreshTokenRepository; + private final RefreshTokenService refreshTokenService; private final JwtService jwtService; private final UserDetailsService userDetailsService; @@ -43,59 +50,87 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.info(">>>> [Jwt Authentication Filter] <<<<"); + String authHeader = request.getHeader("Authorization"); + String userEmail; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + try { - String authHeader = request.getHeader("Authorization"); - String token; - String userEmail; + List tokens = Arrays.asList(authHeader.split(" ")); + + if (tokens.size() == 3) { + userEmail = jwtService.extractUsername(tokens.get(1)); + + // 토큰 유효성 검증 후 시큐리티 등록 + authenticateUser(userEmail, tokens.get(1), request); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); - return ; + } else { + jwtExceptionHandler(response, ExceptionMessage.JWT_INVALID_HEADER); } - token = authHeader.substring(7); - userEmail = jwtService.extractUsername(token); - - // token의 claims에 userEmail이 있는지 체크, 토큰이 유효한지 체크 - if (userEmail != null && jwtService.isTokenValid(token, userEmail)) { - Claims claims = jwtService.extractAllClaims(token); - UserDetails userDetails = User.builder() - .email(userEmail) - .role(UserRole.valueOf(claims.get("role", String.class))) - .name(claims.get("name", String.class)) - .profileImageUrl(claims.get("profileImageUrl", String.class)) - .build(); - - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - - // SecurityContext에 Authenticaiton 등록 - SecurityContextHolder.getContext().setAuthentication(authToken); - } + } catch (ExpiredJwtException e) { + logger.error("Could not set user authentication in security context {}", e); + + // 헤더에서 토큰 추출 - 잘못된 헤더면 이미 try문에서 걸러졌을 것 + List tokens = Arrays.asList(authHeader.split(" ")); + + // refreshToken이 존재하는지 확인 - TTL로 만료시간 자동 체크 + RefreshToken refreshToken = refreshTokenRepository.findById(tokens.get(2)).orElseThrow( + () -> new JwtException(ExceptionMessage.JWT_NOT_EXIST_RTK)); + + userEmail = jwtService.extractUsername(refreshToken.getRefreshToken()); + + // 엑세스 토큰 재발급 + String reissueAccessToken = refreshTokenService.reissue(jwtService.extractAllClaims(refreshToken.getRefreshToken()), refreshToken.getRefreshToken()); + + // 재발급 받은 토큰 유효성 검증 후 시큐리티에 등록 + authenticateUser(userEmail, reissueAccessToken, request); + filterChain.doFilter(request, response); - } catch (ExpiredJwtException e){ - logger.error("Could not set user authentication in security context {}" , e); - jwtExceptionHandler(response, ExceptionMessage.JWT_TOKEN_EXPIRED); - }catch (UnsupportedJwtException e){ - logger.error("Could not set user authentication in security context {}" , e); + + } catch (UnsupportedJwtException e) { + logger.error("Could not set user authentication in security context {}", e); jwtExceptionHandler(response, ExceptionMessage.JWT_UNSUPPORTED); - }catch (MalformedJwtException e){ - logger.error("Could not set user authentication in security context {}" , e); + } catch (MalformedJwtException e) { + logger.error("Could not set user authentication in security context {}", e); jwtExceptionHandler(response, ExceptionMessage.JWT_MALFORMED); - }catch (SignatureException e){ - logger.error("Could not set user authentication in security context {}" , e); + } catch (SignatureException e) { + logger.error("Could not set user authentication in security context {}", e); jwtExceptionHandler(response, ExceptionMessage.JWT_SIGNATURE); - }catch (IllegalArgumentException e){ - logger.error("Could not set user authentication in security context {}" , e); + } catch (IllegalArgumentException e) { + logger.error("Could not set user authentication in security context {}", e); jwtExceptionHandler(response, ExceptionMessage.JWT_ILLEGAL_ARGUMENT); } } + private void authenticateUser(String userEmail, String accessToken, HttpServletRequest request) { + if (userEmail != null && jwtService.isTokenValid(accessToken, userEmail)) { + Claims claims = jwtService.extractAllClaims(accessToken); + + UserDetails userDetails = User.builder() + .email(userEmail) + .role(UserRole.valueOf(claims.get("role", String.class))) + .name(claims.get("name", String.class)) + .profileImageUrl(claims.get("profileImageUrl", String.class)) + .build(); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + // SecurityContext에 Authenticaiton 등록 + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + private void jwtExceptionHandler(HttpServletResponse response, ExceptionMessage message) throws IOException { response.setStatus(HttpStatus.OK.value()); response.setCharacterEncoding("utf-8"); diff --git a/heachi-core/auth-api/src/main/java/com/heachi/auth/config/security/SecurityConfig.java b/heachi-core/auth-api/src/main/java/com/heachi/auth/config/security/SecurityConfig.java index 185c9e31..2798ba79 100644 --- a/heachi-core/auth-api/src/main/java/com/heachi/auth/config/security/SecurityConfig.java +++ b/heachi-core/auth-api/src/main/java/com/heachi/auth/config/security/SecurityConfig.java @@ -43,4 +43,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } -} +} \ No newline at end of file diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/controller/auth/AuthControllerTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/controller/auth/AuthControllerTest.java index 3659188d..6f5489e0 100644 --- a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/controller/auth/AuthControllerTest.java +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/controller/auth/AuthControllerTest.java @@ -27,6 +27,7 @@ import java.util.UUID; import java.util.HashMap; import static com.heachi.mysql.define.user.constant.UserPlatformType.*; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; @@ -223,12 +224,14 @@ void userSimpleInfoResponseTest() throws Exception { map.put("role", savedUser.getRole().name()); map.put("name", savedUser.getName()); map.put("profileImageUrl", savedUser.getProfileImageUrl()); - String token = jwtService.generateToken(map, savedUser); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + // when mockMvc.perform(get("/auth/info") .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + accessToken + " " + refreshToken)) // then .andExpect(status().isOk()) .andExpect(jsonPath("$.resCode").value(200)) @@ -242,18 +245,65 @@ void userSimpleInfoResponseTest() throws Exception { @DisplayName("userSimpleInfo 실패 테스트, 잘못된 Token을 넣으면 오류를 뱉는다.") void userSimpleInfoTestWhenInvalidToken() throws Exception { // given - String token = "strangeToken"; + String accessToken = "strangeToken"; + String refreshToken = "strangeToken"; // when mockMvc.perform(get("/auth/info") .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", "Bearer " + token)) + .header("Authorization", "Bearer " + accessToken + " " + refreshToken)) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(400)); + } + + @Test + + @DisplayName("로그아웃 실패 테스트 - 잘못된 토큰으로 요청시 예외 발생") + void logoutTestWhenInvalidToken() throws Exception { + String accessToken = "strangeToken"; + String refreshToken = "strangeToken"; + + // when + mockMvc.perform( + get("/auth/logout") + .header("Authorization", "Bearer " + accessToken + " " + refreshToken)) + // then .andExpect(status().isOk()) .andExpect(jsonPath("$.resCode").value(400)); } @Test + @DisplayName("로그아웃 성공 테스트") + void logoutSuccessTest() throws Exception { + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + + // when + mockMvc.perform(get("/auth/logout") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken + " " + refreshToken)) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(200)) + .andExpect(jsonPath("$.resObj").value("Logout successfully.")); + } + @DisplayName("올바른 사용자의 토큰으로 사용자 계정 탈퇴 요청을 하면, 계정이 삭제된다.") void validUserTokenRequestWithDrawThenUserDelete() throws Exception { // given @@ -272,7 +322,7 @@ void validUserTokenRequestWithDrawThenUserDelete() throws Exception { .phoneNumber("010-0000-0000") .email(user.getEmail()) .build(); - String token = authService.register(request).getToken(); + String token = authService.register(request).getAccessToken(); // when mockMvc.perform(get("/auth/delete") @@ -281,5 +331,17 @@ void validUserTokenRequestWithDrawThenUserDelete() throws Exception { // then .andExpect(status().isOk()) .andExpect(jsonPath("$.resCode").value(200)); + + } + + @Test + @DisplayName("잘못된 Header로 logout 요청시 에러 발생") + void logoutWhenInvalidHeader() throws Exception { + mockMvc.perform(get("/auth/logout") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "aa bb cc dd")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(400)) + .andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_INVALID_HEADER.getText())); } } \ No newline at end of file diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/auth/AuthServiceTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/auth/AuthServiceTest.java index aa99aaac..3e39fcbb 100644 --- a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/auth/AuthServiceTest.java +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/auth/AuthServiceTest.java @@ -80,7 +80,7 @@ void loginSuccess() throws Exception { AuthServiceLoginResponse login = authService.login(platformType, code, state); // 로그인 프로세스 User findUser = userRepository.findByEmail(email).get(); // 로그인한 사용자 찾기 - boolean tokenValid = jwtService.isTokenValid(login.getToken(), findUser.getUsername()); // 발행한 토큰 검증 + boolean tokenValid = jwtService.isTokenValid(login.getAccessToken(), findUser.getUsername()); // 발행한 토큰 검증 // then assertThat(tokenValid).isTrue(); @@ -140,9 +140,9 @@ void loginTokenValidClaims() { AuthServiceLoginResponse abc2 = authService.login(platformType, "abc2", "abc2"); // 김민목 AuthServiceLoginResponse abc3 = authService.login(platformType, "abc3", "abc3"); // 김민금 - Claims claims1 = jwtService.extractAllClaims(abc1.getToken()); - Claims claims2 = jwtService.extractAllClaims(abc2.getToken()); - Claims claims3 = jwtService.extractAllClaims(abc3.getToken()); + Claims claims1 = jwtService.extractAllClaims(abc1.getAccessToken()); + Claims claims2 = jwtService.extractAllClaims(abc2.getAccessToken()); + Claims claims3 = jwtService.extractAllClaims(abc3.getAccessToken()); // then assertAll( @@ -228,7 +228,7 @@ public void registerUnauthUserSuccessTest() { AuthServiceLoginResponse response = authService.register(request); User savedUser = userRepository.findByEmail(request.getEmail()).get(); - boolean tokenValid = jwtService.isTokenValid(response.getToken(), savedUser.getUsername()); // 발행한 토큰 검증 + boolean tokenValid = jwtService.isTokenValid(response.getAccessToken(), savedUser.getUsername()); // 발행한 토큰 검증 // then diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/jwt/JwtServiceTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/jwt/JwtServiceTest.java index d4b45011..28220163 100644 --- a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/jwt/JwtServiceTest.java +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/jwt/JwtServiceTest.java @@ -155,29 +155,29 @@ void provideTokenClaimsOmitElement() { assertThat(result).isFalse(); } - @Test - @DisplayName("존재하지 않는 UserRole을 넣었을때 오류 발생") - void notexistUserRoleException() { - // given - User user = User.builder() - .name("김민수") - .role(null) - .email("kimminsu@dankook.ac.kr") - .profileImageUrl("https://google.com") - .build(); - User savedUser = userRepository.save(user); - - HashMap map = new HashMap<>(); - map.put("role", null); - map.put("name", savedUser.getName()); - map.put("profileImageUrl", savedUser.getProfileImageUrl()); - String token = jwtService.generateToken(map, savedUser); - - // when - assertThatThrownBy(() -> jwtService.isTokenValid(token, savedUser.getUsername())) - // then - .isInstanceOf(JwtException.class); - - - } +// @Test +// @DisplayName("존재하지 않는 UserRole을 넣었을때 오류 발생") +// void notexistUserRoleException() { +// // given +// User user = User.builder() +// .name("김민수") +// .role(null) +// .email("kimminsu@dankook.ac.kr") +// .profileImageUrl("https://google.com") +// .build(); +// User savedUser = userRepository.save(user); +// +// HashMap map = new HashMap<>(); +// map.put("role", null); +// map.put("name", savedUser.getName()); +// map.put("profileImageUrl", savedUser.getProfileImageUrl()); +// String token = jwtService.generateToken(map, savedUser); +// +// // when +// assertThatThrownBy(() -> jwtService.isTokenValid(token, savedUser.getUsername())) +// // then +// .isInstanceOf(JwtException.class); +// +// +// } } \ No newline at end of file diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/AccessTokenReissueTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/AccessTokenReissueTest.java new file mode 100644 index 00000000..c3047d9d --- /dev/null +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/AccessTokenReissueTest.java @@ -0,0 +1,130 @@ +package com.heachi.auth.api.service.token; + +import com.heachi.admin.common.exception.ExceptionMessage; +import com.heachi.auth.api.controller.auth.AuthController; +import com.heachi.auth.api.controller.token.response.ReissueAccessTokenResponse; +import com.heachi.auth.api.service.auth.AuthService; +import com.heachi.auth.api.service.jwt.JwtService; +import com.heachi.mysql.define.user.User; +import com.heachi.mysql.define.user.constant.UserRole; +import com.heachi.mysql.define.user.repository.UserRepository; +import com.heachi.redis.define.refreshToken.RefreshToken; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@AutoConfigureMockMvc +@SpringBootTest +public class AccessTokenReissueTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private RefreshTokenService refreshTokenService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtService jwtService; + + @AfterEach + void tearDown() { + userRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("AccessToken 재발급 성공 테스트") + void reissueSuccess() throws Exception { + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + refreshTokenService.saveRefreshToken(RefreshToken.builder().refreshToken(refreshToken).email(user.getEmail()).build()); + + + // when + mockMvc.perform(post("/auth/reissue") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessToken + " " + refreshToken)) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(200)) + .andDo(print()); + } + + @Test + @DisplayName("잘못된 헤더로 재발급 요청시 JWT_INVALID_HEADER 예외가 터져야 한다.") + void reissueFailWithInvalidHeader() throws Exception { + mockMvc.perform(post("/auth/reissue") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "aa bb cc dd")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(400)) + .andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_INVALID_HEADER.getText())); + } + + @Test + @DisplayName("존재하지 않는 리프레시 토큰으로 재발급 요청시 JWT_NOT_EXIST_RTK 예외가 터져야 한다.") + void reissueFailWithNotExistRtk() throws Exception { + + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + // 레디스 저장 부분 주석처리 + // refreshTokenService.saveRefreshToken(RefreshToken.builder().refreshToken(refreshToken).email(user.getEmail()).build()); + + mockMvc.perform(post("/auth/reissue") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "aa " + accessToken + " " + refreshToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resCode").value(400)) + .andExpect(jsonPath("$.resMsg").value(ExceptionMessage.JWT_NOT_EXIST_RTK.getText())); + } +} diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/RefreshTokenServiceTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/RefreshTokenServiceTest.java new file mode 100644 index 00000000..9842cfb6 --- /dev/null +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/api/service/token/RefreshTokenServiceTest.java @@ -0,0 +1,164 @@ +package com.heachi.auth.api.service.token; + +import com.heachi.admin.common.exception.ExceptionMessage; +import com.heachi.admin.common.exception.jwt.JwtException; +import com.heachi.auth.api.service.jwt.JwtService; +import com.heachi.mysql.define.user.User; +import com.heachi.mysql.define.user.constant.UserRole; +import com.heachi.mysql.define.user.repository.UserRepository; +import com.heachi.redis.define.refreshToken.RefreshToken; +import com.heachi.redis.define.refreshToken.repository.RefreshTokenRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.lang.reflect.Member; +import java.util.HashMap; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class RefreshTokenServiceTest { + + @Autowired + private RefreshTokenService refreshTokenService; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtService jwtService; + + @AfterEach + void tearDown() { + userRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("Refresh Token 저장 & 조회 테스트") + void redisRefreshTokenGenerate() { + // given + RefreshToken saveToken = RefreshToken.builder() + .refreshToken("TestToken") + .email("TestEmail@test.com") + .build(); + + refreshTokenService.saveRefreshToken(saveToken); + + // when + Optional testToken = refreshTokenRepository.findById("TestToken"); + + // then + assertThat(testToken.get().getRefreshToken()).isEqualTo("TestToken"); + assertThat(testToken.get().getEmail()).isEqualTo("TestEmail@test.com"); + } + + + @Test + @DisplayName("Refresh Token 삭제 테스트 - 로그아웃시 Refresh Token이 Redis에서 삭제된다.") + void redisRefreshTokenDeleteWhenLogout() { + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + refreshTokenService.saveRefreshToken(RefreshToken.builder() + .refreshToken(refreshToken) + .email(savedUser.getEmail()) + .build()); + + refreshTokenService.logout(refreshToken); + + // when + Optional findToken = refreshTokenRepository.findById(refreshToken); + + // then + assertThat(findToken.isEmpty()).isTrue(); + } + + @Test + @DisplayName("Redis에 존재하지 않는 Refresh Token으로 요청할 경우 예외가 발생한다.") + void LogoutWhenRedisNotExistRefreshToken() { + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + refreshTokenService.saveRefreshToken(RefreshToken.builder() + .refreshToken(refreshToken) + .email(savedUser.getEmail()) + .build()); + + // RTK 삭제 + refreshTokenRepository.deleteById(refreshToken); + + // when + JwtException exception = assertThrows(JwtException.class, + () -> refreshTokenService.logout(refreshToken)); + + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.JWT_NOT_EXIST_RTK.getText()); + } + + @Test + @DisplayName("유효하지 않은 Refresh Token으로 요청할 경우 예외가 발생한다.") + void LogoutWhenRedisInvalidRefreshToken() { + // given + User user = User.builder() + .name("김민수") + .role(UserRole.USER) + .email("kimminsu@dankook.ac.kr") + .profileImageUrl("https://google.com") + .build(); + User savedUser = userRepository.save(user); + + HashMap map = new HashMap<>(); + map.put("role", savedUser.getRole().name()); + map.put("name", savedUser.getName()); + map.put("profileImageUrl", savedUser.getProfileImageUrl()); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + + refreshTokenService.saveRefreshToken(RefreshToken.builder() + .refreshToken(refreshToken) + .email("invalidEmail@naver.com") + .build()); + + refreshTokenRepository.deleteById(refreshToken); + + + // when + JwtException exception = assertThrows(JwtException.class, + () -> refreshTokenService.logout(refreshToken)); + + assertThat(exception.getMessage()).isEqualTo(ExceptionMessage.JWT_NOT_EXIST_RTK.getText()); + } + +} \ No newline at end of file diff --git a/heachi-core/auth-api/src/test/java/com/heachi/auth/config/security/SecurityConfigTest.java b/heachi-core/auth-api/src/test/java/com/heachi/auth/config/security/SecurityConfigTest.java index e8aa87b5..006fefe4 100644 --- a/heachi-core/auth-api/src/test/java/com/heachi/auth/config/security/SecurityConfigTest.java +++ b/heachi-core/auth-api/src/test/java/com/heachi/auth/config/security/SecurityConfigTest.java @@ -80,10 +80,12 @@ void authUserTest() throws Exception { map.put("profileImageUrl", savedUser.getProfileImageUrl()); // when - String token = jwtService.generateAccessToken(map, savedUser); + String accessToken = jwtService.generateAccessToken(map, savedUser); + String refreshToken = jwtService.generateRefreshToken(map, savedUser); + mockMvc.perform( get(uri) - .header("Authorization", "Bearer " + token) + .header("Authorization", "Bearer " + accessToken + " " + refreshToken) ) // then .andExpect(status().isOk()); diff --git a/heachi-domain-mysql/src/main/generated/com/heachi/mysql/define/user/QUser.java b/heachi-domain-mysql/src/main/generated/com/heachi/mysql/define/user/QUser.java new file mode 100644 index 00000000..d58358bc --- /dev/null +++ b/heachi-domain-mysql/src/main/generated/com/heachi/mysql/define/user/QUser.java @@ -0,0 +1,59 @@ +package com.heachi.mysql.define.user; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QUser is a Querydsl query type for User + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QUser extends EntityPathBase { + + private static final long serialVersionUID = 1064646248L; + + public static final QUser user = new QUser("user"); + + public final com.heachi.mysql.define.QBaseEntity _super = new com.heachi.mysql.define.QBaseEntity(this); + + //inherited + public final DateTimePath createdDateTime = _super.createdDateTime; + + public final StringPath email = createString("email"); + + public final NumberPath id = createNumber("id", Long.class); + + //inherited + public final DateTimePath modifiedDateTime = _super.modifiedDateTime; + + public final StringPath name = createString("name"); + + public final StringPath phoneNumber = createString("phoneNumber"); + + public final StringPath platformId = createString("platformId"); + + public final EnumPath platformType = createEnum("platformType", com.heachi.mysql.define.user.constant.UserPlatformType.class); + + public final StringPath profileImageUrl = createString("profileImageUrl"); + + public final EnumPath role = createEnum("role", com.heachi.mysql.define.user.constant.UserRole.class); + + public QUser(String variable) { + super(User.class, forVariable(variable)); + } + + public QUser(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QUser(PathMetadata metadata) { + super(User.class, metadata); + } + +} + diff --git a/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/RefreshToken.java b/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/RefreshToken.java new file mode 100644 index 00000000..0f33ce63 --- /dev/null +++ b/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/RefreshToken.java @@ -0,0 +1,21 @@ +package com.heachi.redis.define.refreshToken; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@ToString +@RedisHash(value = "refresh", timeToLive = 60*60*24*7) // 7일 +public class RefreshToken { + @Id + private String refreshToken; + private String email; + + @Builder + public RefreshToken(String refreshToken, String email) { + this.refreshToken = refreshToken; + this.email = email; + } +} \ No newline at end of file diff --git a/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepository.java b/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..d0651db4 --- /dev/null +++ b/heachi-domain-redis/src/main/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepository.java @@ -0,0 +1,14 @@ +package com.heachi.redis.define.refreshToken.repository; + +import com.heachi.redis.define.refreshToken.RefreshToken; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public interface RefreshTokenRepository extends CrudRepository { + +} diff --git a/heachi-domain-redis/src/test/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepositoryTest.java b/heachi-domain-redis/src/test/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 00000000..d568e4d8 --- /dev/null +++ b/heachi-domain-redis/src/test/java/com/heachi/redis/define/refreshToken/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,52 @@ +package com.heachi.redis.define.refreshToken.repository; + +import com.heachi.redis.define.refreshToken.RefreshToken; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class RefreshTokenRepositoryTest { + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + @AfterEach + void tearDown() { + refreshTokenRepository.deleteAll(); + } + + @Test + @DisplayName("RefreshToken을 저장할 수 있다.") + void redisLoginStateSave() { + // given + RefreshToken saveToken = refreshTokenRepository.save(RefreshToken.builder().refreshToken("testToken").email("test@naver.com").build()); + + // when + RefreshToken refreshToken = refreshTokenRepository.findById(saveToken.getRefreshToken()).get(); + + // then + assertThat(refreshToken.getRefreshToken()).isEqualTo(saveToken.getRefreshToken()); + assertThat(refreshToken.getEmail()).isEqualTo(saveToken.getEmail()); + } + + @Test + @DisplayName("RefreshToken을 삭제할 수 있다.") + void redisLoginStateDelete() { + // given + RefreshToken saveToken = refreshTokenRepository.save(RefreshToken.builder().refreshToken("testToken").email("test@naver.com").build()); + refreshTokenRepository.deleteById(saveToken.getRefreshToken()); + + // when + Optional findToken = refreshTokenRepository.findById(saveToken.getRefreshToken()); + + // then + assertThat(findToken.isEmpty()).isTrue(); + } +} \ No newline at end of file diff --git a/heachi-support/common/src/main/java/com/heachi/admin/common/exception/ExceptionMessage.java b/heachi-support/common/src/main/java/com/heachi/admin/common/exception/ExceptionMessage.java index c24b0500..52585c2a 100644 --- a/heachi-support/common/src/main/java/com/heachi/admin/common/exception/ExceptionMessage.java +++ b/heachi-support/common/src/main/java/com/heachi/admin/common/exception/ExceptionMessage.java @@ -13,6 +13,11 @@ public enum ExceptionMessage { JWT_SIGNATURE("올바른 SIGNATURE가 아닙니다."), JWT_ILLEGAL_ARGUMENT("잘못된 정보를 넣었습니다."), JWT_USER_NOT_FOUND("사용자를 찾을 수 없습니다."), + JWT_INVALID_HEADER("Access/Refresh 토큰이 담겨있지 않은 Header입니다."), + + // RefreshTokenException + JWT_NOT_EXIST_RTK("해당 Refresh Token이 존재하지 않습니다."), + JWT_INVALID_RTK("유효하지 않은 Refresh Token입니다."), // OAuthException OAUTH_INVALID_STATE("STATE 검증에 실패하였습니다."),