From d27afbb75c57da433ff422e487cf80362f0e2743 Mon Sep 17 00:00:00 2001 From: Ahn Jiwan Date: Wed, 24 Jan 2024 18:34:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?:sparkles:=20feat:=20AccessToken=20&=20Refr?= =?UTF-8?q?eshToken=20=EC=9D=B4=EB=B6=84=ED=99=94=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/component/JwtTokenProvider.java | 28 +++++++++++++++++-- .../auth/controller/AuthController.java | 4 +-- .../auth/service/CustomUserDetailService.java | 1 + .../diareat/user/service/UserService.java | 3 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java b/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java index 9bb6aaf..d64c936 100644 --- a/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java +++ b/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java @@ -6,6 +6,7 @@ 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; @@ -16,6 +17,7 @@ import javax.servlet.http.HttpServletRequest; import java.util.Base64; import java.util.Date; +import java.util.concurrent.TimeUnit; @RequiredArgsConstructor @Component @@ -26,6 +28,8 @@ public class JwtTokenProvider { private final UserDetailsService userDetailsService; + private final RedisTemplate redisTemplate; + // 객체 초기화, secretKey를 Base64로 인코딩 @PostConstruct protected void init() { @@ -33,17 +37,37 @@ protected void init() { } // 토큰 생성 - public String createToken(String userPk) { + public String createAccessToken(String userPk) { Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위 Date now = new Date(); return Jwts.builder() .setClaims(claims) // 정보 저장 .setIssuedAt(now) // 토큰 발행 시간 정보 - .setExpiration(new Date(now.getTime() + (720 * 60 * 1000L))) // 토큰 유효시각 설정 (12시간) + .setExpiration(new Date(now.getTime() + (60 * 60 * 1000L))) // 토큰 유효시각 설정 (1시간) .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘과, secret 값 .compact(); } + public String createRefreshToken(String userPk) { + Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위 + Date now = new Date(); + String refreshToken = Jwts.builder() + .setClaims(claims) // 정보 저장 + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(new Date(now.getTime() + (7 * 24 * 60 * 60 * 1000L))) // 토큰 유효시각 설정 (1주일) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + redisTemplate.opsForValue().set( + userPk, + refreshToken, + 7 * 24 * 60 * 60 * 1000L, + TimeUnit.MILLISECONDS + ); + + return refreshToken; + } + // 인증 정보 조회 public Authentication getAuthentication(String token) { UserDetails userDetails = userDetailsService.loadUserByUsername(String.valueOf(this.getUserPk(token))); diff --git a/src/main/java/com/diareat/diareat/auth/controller/AuthController.java b/src/main/java/com/diareat/diareat/auth/controller/AuthController.java index 78a5e8c..e44ef7f 100644 --- a/src/main/java/com/diareat/diareat/auth/controller/AuthController.java +++ b/src/main/java/com/diareat/diareat/auth/controller/AuthController.java @@ -29,7 +29,7 @@ public class AuthController { @PostMapping("/login") public ApiResponse authCheck(@RequestHeader String accessToken) { Long userId = kakaoAuthService.isSignedUp(accessToken); // 유저 고유번호 추출 - String jwt = (userId == null) ? null : jwtTokenProvider.createToken(userId.toString()); // 고유번호가 null이 아니라면 Jwt 토큰 발급 + String jwt = (userId == null) ? null : jwtTokenProvider.createAccessToken(userId.toString()); // 고유번호가 null이 아니라면 Jwt 토큰 발급 return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_LOGIN_SUCCESS.getMessage()); } @@ -38,7 +38,7 @@ public ApiResponse authCheck(@RequestHeader String accessToken) @PostMapping("/join") public ApiResponse saveUser(@Valid @RequestBody JoinUserDto joinUserDto) { Long userId = userService.saveUser(kakaoAuthService.createUserDto(joinUserDto)); - String jwt = jwtTokenProvider.createToken(userId.toString()); + String jwt = jwtTokenProvider.createAccessToken(userId.toString()); return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_CREATE_SUCCESS.getMessage()); } diff --git a/src/main/java/com/diareat/diareat/auth/service/CustomUserDetailService.java b/src/main/java/com/diareat/diareat/auth/service/CustomUserDetailService.java index f74e6fd..fd5b14b 100644 --- a/src/main/java/com/diareat/diareat/auth/service/CustomUserDetailService.java +++ b/src/main/java/com/diareat/diareat/auth/service/CustomUserDetailService.java @@ -16,6 +16,7 @@ public class CustomUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { + return userRepository.findById(Long.parseLong(id)) .orElseThrow(() -> new UsernameNotFoundException(ResponseCode.USER_NOT_FOUND.getMessage())); } diff --git a/src/main/java/com/diareat/diareat/user/service/UserService.java b/src/main/java/com/diareat/diareat/user/service/UserService.java index 259b749..965e9c7 100644 --- a/src/main/java/com/diareat/diareat/user/service/UserService.java +++ b/src/main/java/com/diareat/diareat/user/service/UserService.java @@ -42,7 +42,8 @@ public Long saveUser(CreateUserDto createUserDto) { throw new UserException(ResponseCode.USER_NAME_ALREADY_EXIST); } if (userRepository.existsByKeyCode(createUserDto.getKeyCode())) { - log.info("이미 존재하는 키코드입니다 by {}", createUserDto.getKeyCode()); + log.info("이미 존재하는 " + + "con키코드입니다 by {}", createUserDto.getKeyCode()); throw new UserException(ResponseCode.USER_ALREADY_EXIST); } From 35e95b81d8a33bf82691858bafd41b96a68c6721 Mon Sep 17 00:00:00 2001 From: Ahn Jiwan Date: Wed, 24 Jan 2024 18:55:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:=20feat:=20refreshToken=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diareat/auth/component/JwtAuthFilter.java | 2 +- .../auth/component/JwtTokenProvider.java | 11 +++++- .../auth/controller/AuthController.java | 36 +++++++++++++++---- .../diareat/auth/dto/ResponseJwtDto.java | 10 +++--- .../diareat/util/api/ResponseCode.java | 2 ++ 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/diareat/diareat/auth/component/JwtAuthFilter.java b/src/main/java/com/diareat/diareat/auth/component/JwtAuthFilter.java index e90b21a..e0f61e5 100644 --- a/src/main/java/com/diareat/diareat/auth/component/JwtAuthFilter.java +++ b/src/main/java/com/diareat/diareat/auth/component/JwtAuthFilter.java @@ -23,7 +23,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); // 토큰이 유효하다면 - if (token != null && jwtTokenProvider.validateToken(token)) { + if (token != null && jwtTokenProvider.validateAccessToken(token)) { // 토큰으로부터 유저 정보를 받아 Authentication authentication = jwtTokenProvider.getAuthentication(token); // SecurityContext 에 객체 저장 diff --git a/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java b/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java index d64c936..eaf4ddf 100644 --- a/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java +++ b/src/main/java/com/diareat/diareat/auth/component/JwtTokenProvider.java @@ -1,5 +1,7 @@ package com.diareat.diareat.auth.component; +import com.diareat.diareat.util.api.ResponseCode; +import com.diareat.diareat.util.exception.BaseException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; @@ -80,7 +82,7 @@ public Long getUserPk(String token) { } // 토큰 유효성, 만료일자 확인 - public boolean validateToken(String jwtToken) { + public boolean validateAccessToken(String jwtToken) { try { Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken); return !claims.getBody().getExpiration().before(new Date()); @@ -89,6 +91,13 @@ public boolean validateToken(String jwtToken) { } } + public void validateRefreshToken(Long userPK, String refreshToken) { + String redisRefreshToken = redisTemplate.opsForValue().get(String.valueOf(userPK)); + if (redisRefreshToken == null || !redisRefreshToken.equals(refreshToken)) { + throw new BaseException(ResponseCode.REFRESH_TOKEN_VALIDATION_FAILURE); + } + } + // Request의 Header에서 token 값 가져오기 public String resolveToken(HttpServletRequest request) { return request.getHeader("accessToken"); diff --git a/src/main/java/com/diareat/diareat/auth/controller/AuthController.java b/src/main/java/com/diareat/diareat/auth/controller/AuthController.java index e44ef7f..96fde04 100644 --- a/src/main/java/com/diareat/diareat/auth/controller/AuthController.java +++ b/src/main/java/com/diareat/diareat/auth/controller/AuthController.java @@ -29,8 +29,13 @@ public class AuthController { @PostMapping("/login") public ApiResponse authCheck(@RequestHeader String accessToken) { Long userId = kakaoAuthService.isSignedUp(accessToken); // 유저 고유번호 추출 - String jwt = (userId == null) ? null : jwtTokenProvider.createAccessToken(userId.toString()); // 고유번호가 null이 아니라면 Jwt 토큰 발급 - return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_LOGIN_SUCCESS.getMessage()); + + ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder() + .accessToken(jwtTokenProvider.createAccessToken(userId.toString())) + .refreshToken(jwtTokenProvider.createRefreshToken(userId.toString())) + .build(); + + return ApiResponse.success(responseJwtDto, ResponseCode.USER_LOGIN_SUCCESS.getMessage()); } // 회원가입 (성공 시 Jwt 토큰 발급) @@ -38,14 +43,33 @@ public ApiResponse authCheck(@RequestHeader String accessToken) @PostMapping("/join") public ApiResponse saveUser(@Valid @RequestBody JoinUserDto joinUserDto) { Long userId = userService.saveUser(kakaoAuthService.createUserDto(joinUserDto)); - String jwt = jwtTokenProvider.createAccessToken(userId.toString()); - return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_CREATE_SUCCESS.getMessage()); + + ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder() + .accessToken(jwtTokenProvider.createAccessToken(userId.toString())) + .refreshToken(jwtTokenProvider.createRefreshToken(userId.toString())) + .build(); + + return ApiResponse.success(responseJwtDto, ResponseCode.USER_CREATE_SUCCESS.getMessage()); } // 토큰 검증 (Jwt 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 True 혹은 예외 반환) @Operation(summary = "[토큰 검증] 토큰 검증", description = "클라이언트가 가지고 있던 Jwt 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 OK 혹은 예외를 반환합니다.") @GetMapping("/token") - public ApiResponse tokenCheck(@RequestHeader String jwtToken) { - return ApiResponse.success(jwtTokenProvider.validateToken(jwtToken), ResponseCode.TOKEN_CHECK_SUCCESS.getMessage()); + public ApiResponse tokenCheck(@RequestHeader String accessToken) { + return ApiResponse.success(jwtTokenProvider.validateAccessToken(accessToken), ResponseCode.TOKEN_CHECK_SUCCESS.getMessage()); + } + + @Operation(summary = "[토큰 재발급] 토큰 재발급", description = "클라이언트가 가지고 있던 Refresh 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 OK 혹은 예외를 반환합니다.") + @PostMapping("/reissue") + public ApiResponse reissueToken(@RequestHeader String refreshToken) { + Long userId = jwtTokenProvider.getUserPk(refreshToken); + jwtTokenProvider.validateRefreshToken(userId, refreshToken); + + ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder() + .accessToken(jwtTokenProvider.createAccessToken(userId.toString())) + .refreshToken(jwtTokenProvider.createRefreshToken(userId.toString())) + .build(); + + return ApiResponse.success(responseJwtDto, ResponseCode.TOKEN_REISSUE_SUCCESS.getMessage()); } } diff --git a/src/main/java/com/diareat/diareat/auth/dto/ResponseJwtDto.java b/src/main/java/com/diareat/diareat/auth/dto/ResponseJwtDto.java index b26e30e..41df37b 100644 --- a/src/main/java/com/diareat/diareat/auth/dto/ResponseJwtDto.java +++ b/src/main/java/com/diareat/diareat/auth/dto/ResponseJwtDto.java @@ -1,16 +1,14 @@ package com.diareat.diareat.auth.dto; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; @Getter +@Builder @AllArgsConstructor public class ResponseJwtDto { - private Long id; - private String jwt; - - public static ResponseJwtDto of(Long id, String jwt) { - return new ResponseJwtDto(id, jwt); - } + private String accessToken; + private String refreshToken; } diff --git a/src/main/java/com/diareat/diareat/util/api/ResponseCode.java b/src/main/java/com/diareat/diareat/util/api/ResponseCode.java index ae34056..5943e1d 100644 --- a/src/main/java/com/diareat/diareat/util/api/ResponseCode.java +++ b/src/main/java/com/diareat/diareat/util/api/ResponseCode.java @@ -14,6 +14,7 @@ public enum ResponseCode { // 401 Unauthorized TOKEN_VALIDATION_FAILURE(HttpStatus.UNAUTHORIZED, false, "토큰 검증 실패"), + REFRESH_TOKEN_VALIDATION_FAILURE(HttpStatus.UNAUTHORIZED, false, "Refresh 토큰 검증 실패"), // 403 Forbidden FORBIDDEN(HttpStatus.FORBIDDEN, false, "권한이 없습니다."), @@ -56,6 +57,7 @@ public enum ResponseCode { FOOD_RANK_READ_SUCCESS(HttpStatus.OK, true, "식습관 점수 기반 랭킹 조회 성공"), TOKEN_CHECK_SUCCESS(HttpStatus.OK, true, "토큰 검증 완료"), + TOKEN_REISSUE_SUCCESS(HttpStatus.OK, true, "토큰 재발급 완료"), // 201 Created