From a51818882662dccb92faf3bcda0c5b4d88546e40 Mon Sep 17 00:00:00 2001 From: jiseon Date: Sat, 14 Sep 2024 20:13:35 +0900 Subject: [PATCH 1/4] ITDS-44 feat: redis setting --- build.gradle | 1 + .../dissonance/itit/config/RedisConfig.java | 20 ++++++++++++++ .../dissonance/itit/service/RedisService.java | 26 +++++++++++++++++++ src/main/resources/application.yml | 4 +++ 4 files changed, 51 insertions(+) create mode 100644 src/main/java/com/dissonance/itit/config/RedisConfig.java create mode 100644 src/main/java/com/dissonance/itit/service/RedisService.java diff --git a/build.gradle b/build.gradle index 63ec671..11ccf5e 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,7 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api" implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } def QDomains = [] diff --git a/src/main/java/com/dissonance/itit/config/RedisConfig.java b/src/main/java/com/dissonance/itit/config/RedisConfig.java new file mode 100644 index 0000000..91b2f0d --- /dev/null +++ b/src/main/java/com/dissonance/itit/config/RedisConfig.java @@ -0,0 +1,20 @@ +package com.dissonance.itit.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private Integer port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } +} diff --git a/src/main/java/com/dissonance/itit/service/RedisService.java b/src/main/java/com/dissonance/itit/service/RedisService.java new file mode 100644 index 0000000..f92ba7a --- /dev/null +++ b/src/main/java/com/dissonance/itit/service/RedisService.java @@ -0,0 +1,26 @@ +package com.dissonance.itit.service; + +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class RedisService { + private final RedisTemplate redisTemplate; + + public String getValues(String key) { + return redisTemplate.opsForValue().get(key); + } + + public void setValuesWithTimeout(String key, String value, long duration) { + redisTemplate.opsForValue().set(key, value, duration, TimeUnit.MILLISECONDS); + } + + public void deleteValues(String key) { + redisTemplate.delete(key); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9661b71..4133967 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,6 +30,10 @@ spring: multipart: max-request-size: 10MB max-file-size: 10MB + data: + redis: + port: ${REDIS_PORT} + host: ${REDIS_HOST} jwt: token: From c5397e5de4c659255dd1ca16e0d64e908bce97d2 Mon Sep 17 00:00:00 2001 From: jiseon Date: Sat, 14 Sep 2024 20:14:12 +0900 Subject: [PATCH 2/4] =?UTF-8?q?ITDS-44=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83,=20=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itit/common/exception/ErrorCode.java | 3 ++ .../itit/common/jwt/util/JwtUtil.java | 13 +++++---- .../itit/controller/UserController.java | 23 +++++++++++++++ .../itit/domain/enums/JwtTokenExpiration.java | 2 +- .../itit/dto/request/RefreshTokenReq.java | 6 ++++ .../dissonance/itit/service/UserService.java | 29 +++++++++++++++++++ 6 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/dissonance/itit/dto/request/RefreshTokenReq.java diff --git a/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java b/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java index 36dabbb..aacba7c 100644 --- a/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java +++ b/src/main/java/com/dissonance/itit/common/exception/ErrorCode.java @@ -16,6 +16,9 @@ public enum ErrorCode { INVALID_APPLE_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않는 Apple Token입니다."), INVALID_JSON_FORMAT(HttpStatus.BAD_REQUEST, "잘못된 JSON 형식입니다."), + // 403 + UNAUTHORIZED_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "권한없는 Refresh Token입니다."), + // 404 NON_EXISTENT_USER_ID(HttpStatus.NOT_FOUND, "해당 id의 사용자가 존재하지 않습니다."), NON_EXISTENT_EMAIL(HttpStatus.NOT_FOUND, "해당 email의 사용자가 존재하지 않습니다."), diff --git a/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java b/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java index 562b0a4..f5829d8 100644 --- a/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java +++ b/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java @@ -13,6 +13,7 @@ import com.dissonance.itit.domain.entity.UserDetailsImpl; import com.dissonance.itit.dto.response.GeneratedToken; +import com.dissonance.itit.service.RedisService; import com.dissonance.itit.service.UserDetailsServiceImpl; import io.jsonwebtoken.Claims; @@ -36,9 +37,7 @@ public class JwtUtil { private String secretKey; private final UserDetailsServiceImpl userDetailsService; - - // TODO: redis를 이용한 refresh token 재발급 구현 - // private final RedisService redisService; + private final RedisService redisService; @PostConstruct protected void init() { @@ -49,6 +48,8 @@ public GeneratedToken generateToken(String email, String role) { String refreshToken = generateRefreshToken(email, role); String accessToken = generateAccessToken(email, role); + redisService.setValuesWithTimeout(email, refreshToken, REFRESH_TOKEN_EXPIRATION_TIME.getValue()); + return GeneratedToken.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -86,9 +87,9 @@ public String generateAccessToken(String email, String role) { public boolean verifyToken(String token) { try { - // if (redisService.getValues(token) != null && redisService.getValues(token).equals("logout")) { - // throw new JwtException("Invalid JWT Token - logout"); - // } + if (redisService.getValues(token) != null && redisService.getValues(token).equals("logout")) { + throw new JwtException("Invalid JWT Token - logout"); + } Jws claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token); diff --git a/src/main/java/com/dissonance/itit/controller/UserController.java b/src/main/java/com/dissonance/itit/controller/UserController.java index 517009a..bad1829 100644 --- a/src/main/java/com/dissonance/itit/controller/UserController.java +++ b/src/main/java/com/dissonance/itit/controller/UserController.java @@ -1,12 +1,17 @@ package com.dissonance.itit.controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.dissonance.itit.common.annotation.CurrentUser; import com.dissonance.itit.common.util.ApiResponse; import com.dissonance.itit.domain.entity.User; +import com.dissonance.itit.dto.request.RefreshTokenReq; +import com.dissonance.itit.dto.response.GeneratedToken; import com.dissonance.itit.dto.response.LoginUserInfoRes; import com.dissonance.itit.service.UserService; @@ -26,4 +31,22 @@ public ApiResponse getUserInfo(@CurrentUser User loginUser) { return ApiResponse.success(userInfoRes); } + + @PostMapping("reissue") + @Operation(summary = "토큰 재발급", description = "access token과 refresh token을 재발급합니다.") + public ApiResponse reissue(@RequestHeader("Authorization") String requestAccessToken, + @RequestBody RefreshTokenReq tokenRequest) { + GeneratedToken reissuedToken = userService.accessTokenByRefreshToken(requestAccessToken, + tokenRequest.refreshToken()); + + return ApiResponse.success(reissuedToken); + } + + @GetMapping("/logout") + @Operation(summary = "로그아웃", description = "access token을 만료시킵니다.") + public ApiResponse logout(@RequestHeader("Authorization") String requestAccessToken) { + userService.logout(requestAccessToken); + + return ApiResponse.success("logout success"); + } } diff --git a/src/main/java/com/dissonance/itit/domain/enums/JwtTokenExpiration.java b/src/main/java/com/dissonance/itit/domain/enums/JwtTokenExpiration.java index 97884d3..41613a2 100644 --- a/src/main/java/com/dissonance/itit/domain/enums/JwtTokenExpiration.java +++ b/src/main/java/com/dissonance/itit/domain/enums/JwtTokenExpiration.java @@ -6,7 +6,7 @@ @Getter @AllArgsConstructor public enum JwtTokenExpiration { - ACCESS_TOKEN_EXPIRED_TIME("1시간", 1000L * 60 * 60 * 100000), // TODO: RT 토큰 도입 후 만료 시간 적용 + ACCESS_TOKEN_EXPIRED_TIME("1시간", 1000L * 60 * 60), REFRESH_TOKEN_EXPIRATION_TIME("2주", 1000L * 60 * 60 * 24 * 14); private final String description; diff --git a/src/main/java/com/dissonance/itit/dto/request/RefreshTokenReq.java b/src/main/java/com/dissonance/itit/dto/request/RefreshTokenReq.java new file mode 100644 index 0000000..453a061 --- /dev/null +++ b/src/main/java/com/dissonance/itit/dto/request/RefreshTokenReq.java @@ -0,0 +1,6 @@ +package com.dissonance.itit.dto.request; + +public record RefreshTokenReq( + String refreshToken +) { +} \ No newline at end of file diff --git a/src/main/java/com/dissonance/itit/service/UserService.java b/src/main/java/com/dissonance/itit/service/UserService.java index 97467d3..27a63c5 100644 --- a/src/main/java/com/dissonance/itit/service/UserService.java +++ b/src/main/java/com/dissonance/itit/service/UserService.java @@ -23,6 +23,7 @@ @Service public class UserService { private final JwtUtil jwtUtil; + private final RedisService redisService; private final UserRepository userRepository; private final OAuthServiceFactory oAuthServiceFactory; @@ -79,4 +80,32 @@ public LoginUserInfoRes getUserInfo(User loginUser) { private boolean isAdmin(User loginUser) { return loginUser.getRole().equals(Role.ADMIN); } + + public GeneratedToken accessTokenByRefreshToken(String accessTokenHeader, String refreshToken) { + String accessToken = jwtUtil.resolveToken(accessTokenHeader); + + String uid = jwtUtil.getUid(accessToken); + String data = redisService.getValues(uid); + + if (data == null || !data.equals(refreshToken)) { + log.info("Invalid Token"); + throw new CustomException(ErrorCode.UNAUTHORIZED_REFRESH_TOKEN); + } + String role = jwtUtil.getRole(accessToken); + + return jwtUtil.generateToken(uid, role); + } + + @Transactional + public void logout(String accessTokenHeader) { + String accessToken = jwtUtil.resolveToken(accessTokenHeader); + + jwtUtil.verifyToken(accessToken); + + String uid = jwtUtil.getUid(accessToken); + long time = jwtUtil.getExpiration(accessToken); + + redisService.setValuesWithTimeout(accessToken, "logout", time); + redisService.deleteValues(uid); + } } \ No newline at end of file From 2486ebd2baaed2c4ca98cc954834ddbf5cc1e421 Mon Sep 17 00:00:00 2001 From: jiseon Date: Thu, 19 Sep 2024 15:06:59 +0900 Subject: [PATCH 3/4] =?UTF-8?q?ITDS-44=20feat:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=83=88=ED=87=B4=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dissonance/itit/controller/UserController.java | 11 ++++++++++- .../java/com/dissonance/itit/service/UserService.java | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dissonance/itit/controller/UserController.java b/src/main/java/com/dissonance/itit/controller/UserController.java index bad1829..7fc73d1 100644 --- a/src/main/java/com/dissonance/itit/controller/UserController.java +++ b/src/main/java/com/dissonance/itit/controller/UserController.java @@ -1,5 +1,6 @@ package com.dissonance.itit.controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -47,6 +48,14 @@ public ApiResponse reissue(@RequestHeader("Authorization") Strin public ApiResponse logout(@RequestHeader("Authorization") String requestAccessToken) { userService.logout(requestAccessToken); - return ApiResponse.success("logout success"); + return ApiResponse.success("로그아웃되었습니다."); + } + + @DeleteMapping + @Operation(summary = "회원 탈퇴", description = "로그인 유저의 계정을 탈퇴시킵니다.") + public ApiResponse withdraw(@CurrentUser User loginUser) { + userService.withdraw(loginUser.getId()); + + return ApiResponse.success("회원 탈퇴가 완료되었습니다."); } } diff --git a/src/main/java/com/dissonance/itit/service/UserService.java b/src/main/java/com/dissonance/itit/service/UserService.java index 27a63c5..a658ad3 100644 --- a/src/main/java/com/dissonance/itit/service/UserService.java +++ b/src/main/java/com/dissonance/itit/service/UserService.java @@ -108,4 +108,9 @@ public void logout(String accessTokenHeader) { redisService.setValuesWithTimeout(accessToken, "logout", time); redisService.deleteValues(uid); } + + @Transactional + public void withdraw(Long loginUserId) { + userRepository.deleteById(loginUserId); + } } \ No newline at end of file From fe91010ad6d5edd58a6ba210f47a4baa0ff98a98 Mon Sep 17 00:00:00 2001 From: jiseon Date: Thu, 19 Sep 2024 15:20:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?ITDS-44=20refactor:=20refresh=20token=20cla?= =?UTF-8?q?im=20=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dissonance/itit/common/jwt/util/JwtUtil.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java b/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java index f5829d8..bc964e1 100644 --- a/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java +++ b/src/main/java/com/dissonance/itit/common/jwt/util/JwtUtil.java @@ -45,7 +45,7 @@ protected void init() { } public GeneratedToken generateToken(String email, String role) { - String refreshToken = generateRefreshToken(email, role); + String refreshToken = generateRefreshToken(); String accessToken = generateAccessToken(email, role); redisService.setValuesWithTimeout(email, refreshToken, REFRESH_TOKEN_EXPIRATION_TIME.getValue()); @@ -56,15 +56,10 @@ public GeneratedToken generateToken(String email, String role) { .build(); } - public String generateRefreshToken(String email, String role) { - // Claim에 이메일, 권한 세팅 - Claims claims = Jwts.claims().setSubject(email); - claims.put("role", role); - + public String generateRefreshToken() { Date now = new Date(); return Jwts.builder() - .setClaims(claims) .setIssuedAt(now) // 발행일자 .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME.getValue())) // 만료 일시 .signWith(SignatureAlgorithm.HS256, secretKey) // HS256 알고리즘과 secretKey로 서명