From 5825446acef96231d19ac4d58e47bf3d6f2c3eb2 Mon Sep 17 00:00:00 2001 From: heejjinkim <06.hjhj.12@gmail.com> Date: Sat, 7 Sep 2024 12:32:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20token=20=EC=9E=AC=EB=B0=9C=EA=B8=89,=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그인 시, refresh token 저장 로직도 추가 related to: #11 --- .../wepro/auth/dto/request/AuthRequest.java | 12 ++++ .../wepro/auth/dto/response/AuthResponse.java | 2 - .../_119/wepro/auth/jwt/JwtTokenProvider.java | 42 ++++++++++--- .../auth/presentation/AuthController.java | 23 ++++++- .../{SignInService.java => AuthService.java} | 15 +++-- .../wepro/auth/service/RefreshService.java | 60 +++++++++++++++++++ .../exception/errorcode/CommonErrorCode.java | 1 + 7 files changed, 137 insertions(+), 18 deletions(-) rename src/main/java/com/_119/wepro/auth/service/{SignInService.java => AuthService.java} (94%) create mode 100644 src/main/java/com/_119/wepro/auth/service/RefreshService.java diff --git a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java index 10fc7c0..f3c6cee 100644 --- a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java +++ b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java @@ -25,6 +25,18 @@ public static class SignInRequest { private String idToken; } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RefreshRequest { + @NotNull + private String accessToken; + + @NotNull + private String refreshToken; + } + @Getter @Builder @NoArgsConstructor diff --git a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java index c1741cb..e6b4bab 100644 --- a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java +++ b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java @@ -2,13 +2,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; public class AuthResponse { @Getter @AllArgsConstructor - @NoArgsConstructor public static class SignInResponse { private boolean newMember; diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java index 6687d4d..7da32e1 100644 --- a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -4,6 +4,7 @@ import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.global.util.RedisUtil; import com._119.wepro.global.enums.Role; import com._119.wepro.global.exception.RestApiException; import io.jsonwebtoken.Claims; @@ -30,21 +31,25 @@ @Component public class JwtTokenProvider { - private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일 + private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 1일 private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일 - + private static final String AUTHORITIES_KEY = "auth"; + private final RedisUtil redisUtil; private SecretKey secretKey; - public JwtTokenProvider(@Value("${jwt.secret}") String key) { + public JwtTokenProvider(@Value("${jwt.secret}") String key, RedisUtil redisUtil) { + this.redisUtil = redisUtil; byte[] keyBytes = key.getBytes(); this.secretKey = Keys.hmacShaKeyFor(keyBytes); } public TokenInfo generateToken(Long memberId, Role memberRole) { String accessToken = generateAccessToken(memberId, memberRole); - // TODO: refresh redis에 저장 String refreshToken = generateRefreshToken(); + deleteInvalidRefreshToken(memberId.toString()); + redisUtil.setData(memberId.toString(), refreshToken); + return new TokenInfo("Bearer", accessToken, refreshToken); } @@ -61,7 +66,7 @@ public boolean validateToken(String token) { public Authentication getAuthentication(String accessToken) { Claims claims = parseClaims(accessToken); - if (claims.get("auth") == null) { + if (claims.get(AUTHORITIES_KEY) == null) { throw new RestApiException(INVALID_TOKEN); } List authority = getAuthorities(claims); @@ -89,7 +94,6 @@ private boolean isValidAuthority(SimpleGrantedAuthority authority) { private Claims parseClaims(String accessToken) { try { - // TODO: 블랙 리스트 여부 추가 return Jwts.parserBuilder() .setSigningKey(secretKey) .build() @@ -114,7 +118,7 @@ private String generateAccessToken(Long memberId, Role memberRole) { return Jwts.builder() .setSubject(memberId.toString()) - .claim("auth", memberRole.name()) + .claim(AUTHORITIES_KEY, memberRole.name()) .setIssuedAt(now) .setExpiration(expiredDate) .signWith(secretKey, SignatureAlgorithm.HS256) @@ -133,6 +137,28 @@ private String generateRefreshToken() { private List getAuthorities(Claims claims) { return Collections.singletonList(new SimpleGrantedAuthority( - claims.get("auth").toString())); + claims.get(AUTHORITIES_KEY).toString())); + } + + public String getRefreshToken(String memberId){ + return redisUtil.getData(memberId); + } + + public void deleteInvalidRefreshToken(String memberId) { + redisUtil.deleteData(memberId); + } + + public Claims parseExpiredToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } catch (JwtException e) { + throw new RestApiException(INVALID_TOKEN); + } } } diff --git a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java index 0e71987..f9f8c8f 100644 --- a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java +++ b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java @@ -1,8 +1,10 @@ package com._119.wepro.auth.presentation; import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.dto.response.TokenInfo; import com._119.wepro.auth.service.KakaoService; -import com._119.wepro.auth.service.SignInService; +import com._119.wepro.auth.service.RefreshService; +import com._119.wepro.auth.service.AuthService; import com._119.wepro.auth.dto.request.AuthRequest.*; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -23,13 +25,28 @@ public class AuthController { private final KakaoService kakaoService; - private final SignInService signInService; + private final AuthService authService; + private final RefreshService refreshService; @PostMapping("/login") @Operation(summary = "Kakao idToken 받아 소셜 로그인") public ResponseEntity signIn( @RequestBody @Valid SignInRequest request) { - return ResponseEntity.ok(signInService.signIn(request)); + return ResponseEntity.ok(authService.signIn(request)); + } + + @PostMapping("/refresh") + @Operation(summary = "access token 재발급") + public ResponseEntity refresh( + @RequestBody @Valid RefreshRequest request) { + return ResponseEntity.ok(refreshService.refresh(request)); + } + + @PostMapping("/logout") + @Operation(summary = "로그아웃") + public ResponseEntity logout(Authentication authentication){ + authService.logOut(authentication.getName()); + return ResponseEntity.ok().build(); } @PostMapping("/signup") diff --git a/src/main/java/com/_119/wepro/auth/service/SignInService.java b/src/main/java/com/_119/wepro/auth/service/AuthService.java similarity index 94% rename from src/main/java/com/_119/wepro/auth/service/SignInService.java rename to src/main/java/com/_119/wepro/auth/service/AuthService.java index 38c114d..09903f8 100644 --- a/src/main/java/com/_119/wepro/auth/service/SignInService.java +++ b/src/main/java/com/_119/wepro/auth/service/AuthService.java @@ -3,32 +3,32 @@ import static com._119.wepro.global.enums.Provider.APPLE; import static com._119.wepro.global.enums.Provider.KAKAO; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; import com._119.wepro.global.enums.Provider; import com._119.wepro.global.enums.Role; -import com._119.wepro.auth.jwt.JwtTokenProvider; import com._119.wepro.member.domain.Member; import com._119.wepro.member.domain.repository.MemberRepository; -import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import jakarta.transaction.Transactional; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.stereotype.Service; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor -public class SignInService { +public class AuthService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; @@ -52,6 +52,11 @@ public SignInResponse signIn(SignInRequest request) { return new SignInResponse(isNewMember, tokenInfo); } + @Transactional + public void logOut(String memberId) { + jwtTokenProvider.deleteInvalidRefreshToken(memberId); + } + private Member getOrSaveUser(SignInRequest request, OidcUser oidcDecodePayload) { Optional member = memberRepository.findByProviderAndProviderId( diff --git a/src/main/java/com/_119/wepro/auth/service/RefreshService.java b/src/main/java/com/_119/wepro/auth/service/RefreshService.java new file mode 100644 index 0000000..a569541 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/RefreshService.java @@ -0,0 +1,60 @@ +package com._119.wepro.auth.service; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.REFRESH_DENIED; + +import com._119.wepro.auth.dto.request.AuthRequest.RefreshRequest; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.global.exception.errorcode.UserErrorCode; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RefreshService { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + + public TokenInfo refresh(RefreshRequest request) { + String accessToken = request.getAccessToken(); + String refreshToken = request.getRefreshToken(); + + if (!isTokenExpired(accessToken)) { + throw new RestApiException(REFRESH_DENIED); + } + String memberId = jwtTokenProvider.parseExpiredToken(accessToken) + .getSubject(); + Member member = memberRepository.findById(Long.parseLong(memberId)) + .orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND)); + + validateRefreshToken(refreshToken, memberId); + return jwtTokenProvider.generateToken(Long.parseLong(memberId), member.getRole()); + } + + private boolean isTokenExpired(String accessToken) { + try { + jwtTokenProvider.validateToken(accessToken); + throw new RestApiException(REFRESH_DENIED); + } catch (RestApiException e) { + if (e.getErrorCode() == EXPIRED_TOKEN) { + return true; + } + throw e; + } + } + + private void validateRefreshToken(String refreshToken, String memberId) { + String savedRefreshToken = jwtTokenProvider.getRefreshToken(memberId); + if (!refreshToken.equals(savedRefreshToken)) { + throw new RestApiException(INVALID_TOKEN); + } + } +} diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java index 5030011..f3cf9cb 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java @@ -13,6 +13,7 @@ public enum CommonErrorCode implements ErrorCode { INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"), NOT_EXIST_BEARER_SUFFIX(HttpStatus.UNAUTHORIZED, "Bearer prefix is missing."), + REFRESH_DENIED(HttpStatus.FORBIDDEN, "Refresh denied"), ; private final HttpStatus httpStatus;