From eabbf25084073da6ec89ad709bb884b7010e6415 Mon Sep 17 00:00:00 2001 From: HongGit Date: Fri, 31 May 2024 01:42:36 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=93=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/sukbum.iml | 2 +- spring/build.gradle | 3 ++ .../common/auth/redis/config/RedisConfig.java | 32 +++++++++++++++++++ .../common/auth/redis/domain/Token.java | 31 ++++++++++++++++++ .../repository/RedisTokenRepository.java | 11 +++++++ .../spring/common/jwt/JwtTokenProvider.java | 7 +++- .../member/controller/MemberController.java | 1 - .../spring/member/service/MemberService.java | 11 ++++++- .../member/service/dto/UserJoinResponse.java | 4 ++- 9 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 spring/src/main/java/org/sopt/spring/common/auth/redis/config/RedisConfig.java create mode 100644 spring/src/main/java/org/sopt/spring/common/auth/redis/domain/Token.java create mode 100644 spring/src/main/java/org/sopt/spring/common/auth/redis/repository/RedisTokenRepository.java diff --git a/.idea/sukbum.iml b/.idea/sukbum.iml index d6ebd48..8e36ed6 100644 --- a/.idea/sukbum.iml +++ b/.idea/sukbum.iml @@ -1,6 +1,6 @@ - + diff --git a/spring/build.gradle b/spring/build.gradle index b182928..1cde899 100644 --- a/spring/build.gradle +++ b/spring/build.gradle @@ -47,6 +47,9 @@ dependencies { //Multipart file implementation("software.amazon.awssdk:bom:2.21.0") implementation("software.amazon.awssdk:s3:2.21.0") + + //Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE' } tasks.named('test') { diff --git a/spring/src/main/java/org/sopt/spring/common/auth/redis/config/RedisConfig.java b/spring/src/main/java/org/sopt/spring/common/auth/redis/config/RedisConfig.java new file mode 100644 index 0000000..c43bc82 --- /dev/null +++ b/spring/src/main/java/org/sopt/spring/common/auth/redis/config/RedisConfig.java @@ -0,0 +1,32 @@ +package org.sopt.spring.common.auth.redis.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; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} \ No newline at end of file diff --git a/spring/src/main/java/org/sopt/spring/common/auth/redis/domain/Token.java b/spring/src/main/java/org/sopt/spring/common/auth/redis/domain/Token.java new file mode 100644 index 0000000..bddd3dc --- /dev/null +++ b/spring/src/main/java/org/sopt/spring/common/auth/redis/domain/Token.java @@ -0,0 +1,31 @@ +package org.sopt.spring.common.auth.redis.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "", timeToLive = 60 * 60 * 24 * 1000L * 14) +@AllArgsConstructor +@Getter +@Builder +public class Token { + + @Id + private Long id; + + @Indexed + private String refreshToken; + + public static Token of( + final Long id, + final String refreshToken + ) { + return Token.builder() + .id(id) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/spring/src/main/java/org/sopt/spring/common/auth/redis/repository/RedisTokenRepository.java b/spring/src/main/java/org/sopt/spring/common/auth/redis/repository/RedisTokenRepository.java new file mode 100644 index 0000000..757eeac --- /dev/null +++ b/spring/src/main/java/org/sopt/spring/common/auth/redis/repository/RedisTokenRepository.java @@ -0,0 +1,11 @@ +package org.sopt.spring.common.auth.redis.repository; + +import org.sopt.spring.common.auth.redis.domain.Token; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RedisTokenRepository extends CrudRepository { + Optional findByRefreshToken(final String refreshToken); + Optional findById(final Long id); +} \ No newline at end of file diff --git a/spring/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java b/spring/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java index 228da34..9d10149 100644 --- a/spring/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java +++ b/spring/src/main/java/org/sopt/spring/common/jwt/JwtTokenProvider.java @@ -17,7 +17,8 @@ public class JwtTokenProvider { private static final String USER_ID = "userId"; - private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L; + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; @Value("${jwt.secret}") private String JWT_SECRET; @@ -26,6 +27,9 @@ public class JwtTokenProvider { public String issueAccessToken(final Authentication authentication) { return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); } + public String issueRefreshToken(final Authentication authentication) { + return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } public String generateToken(Authentication authentication, Long tokenExpirationTime) { @@ -43,6 +47,7 @@ public String generateToken(Authentication authentication, Long tokenExpirationT .compact(); } + private SecretKey getSigningKey() { String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 diff --git a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java index c315883..d9c033d 100644 --- a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java +++ b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java @@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.net.URI; import java.util.List; @RestController diff --git a/spring/src/main/java/org/sopt/spring/member/service/MemberService.java b/spring/src/main/java/org/sopt/spring/member/service/MemberService.java index 5d9de5e..c289bf0 100644 --- a/spring/src/main/java/org/sopt/spring/member/service/MemberService.java +++ b/spring/src/main/java/org/sopt/spring/member/service/MemberService.java @@ -3,6 +3,8 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.sopt.spring.common.auth.UserAuthentication; +import org.sopt.spring.common.auth.redis.domain.Token; +import org.sopt.spring.common.auth.redis.repository.RedisTokenRepository; import org.sopt.spring.common.exception.ErrorMessage; import org.sopt.spring.common.jwt.JwtTokenProvider; import org.sopt.spring.member.domain.Member; @@ -23,6 +25,7 @@ public class MemberService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; + private final RedisTokenRepository redisTokenRepository; @Transactional public UserJoinResponse createMember( @@ -35,7 +38,13 @@ public UserJoinResponse createMember( String accessToken = jwtTokenProvider.issueAccessToken( UserAuthentication.createUserAuthentication(memberId) ); - return UserJoinResponse.of(accessToken, memberId.toString()); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + //레디스에 저*장 + redisTokenRepository.save(Token.of(memberId, refreshToken)); + + return UserJoinResponse.of(accessToken, refreshToken, memberId.toString()); } public MemberFindDto findMemberById(Long memberId) { diff --git a/spring/src/main/java/org/sopt/spring/member/service/dto/UserJoinResponse.java b/spring/src/main/java/org/sopt/spring/member/service/dto/UserJoinResponse.java index bcae9d3..7ab293d 100644 --- a/spring/src/main/java/org/sopt/spring/member/service/dto/UserJoinResponse.java +++ b/spring/src/main/java/org/sopt/spring/member/service/dto/UserJoinResponse.java @@ -2,14 +2,16 @@ public record UserJoinResponse( String accessToken, + String refreshToken, String userId ) { public static UserJoinResponse of( String accessToken, + String refreshToken, String userId ) { - return new UserJoinResponse(accessToken, userId); + return new UserJoinResponse(accessToken, refreshToken, userId); } } From 6bed1b8610288b9ebab681df4c1b213860ad883e Mon Sep 17 00:00:00 2001 From: HongGit Date: Fri, 31 May 2024 01:57:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=EC=9D=80=20=ED=95=84=ED=84=B0=20=EC=95=88?= =?UTF-8?q?=EA=B1=B0=EC=B9=98=EA=B2=8C=20=EC=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/spring/common/auth/SecurityConfig.java | 2 +- .../spring/member/controller/MemberController.java | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java b/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java index 80dc1a5..7face59 100644 --- a/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java +++ b/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java @@ -21,7 +21,7 @@ public class SecurityConfig { private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"}; + private static final String[] AUTH_WHITE_LIST = {"/api/v1/member", "/api/v1/member/refresh"}; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java index d9c033d..f2a3f40 100644 --- a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java +++ b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java @@ -36,6 +36,15 @@ public ResponseEntity createMember( userJoinResponse ); } + @PostMapping("/refresh") + public ResponseEntity refreshAccessToken() { + UserJoinResponse userJoinResponse = memberService.createMember(memberCreateDto); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", userJoinResponse.userId()) + .body( + userJoinResponse + ); + } @GetMapping("/{memberId}") public ResponseEntity findMemberById(@PathVariable Long memberId) { @@ -43,7 +52,9 @@ public ResponseEntity findMemberById(@PathVariable Long memberId) } @DeleteMapping("/{memberId}") - public ResponseEntity deleteMemberById(@PathVariable Long memberId){ + public ResponseEntity deleteMemberById( + @PathVariable Long memberId + ){ memberService.deleteMemberById(memberId); return ResponseEntity.noContent().build(); } From f3e6f5369f780f5457498c710d958382077de0aa Mon Sep 17 00:00:00 2001 From: HongGit Date: Sat, 1 Jun 2024 17:51:04 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C,=20=EC=97=91=EC=84=B8=EC=8A=A4=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/common/auth/SecurityConfig.java | 2 +- .../spring/common/exception/ErrorMessage.java | 1 + .../member/controller/MemberController.java | 9 ++++++-- .../spring/member/service/MemberService.java | 21 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java b/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java index 7face59..80dc1a5 100644 --- a/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java +++ b/spring/src/main/java/org/sopt/spring/common/auth/SecurityConfig.java @@ -21,7 +21,7 @@ public class SecurityConfig { private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private static final String[] AUTH_WHITE_LIST = {"/api/v1/member", "/api/v1/member/refresh"}; + private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"}; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/spring/src/main/java/org/sopt/spring/common/exception/ErrorMessage.java b/spring/src/main/java/org/sopt/spring/common/exception/ErrorMessage.java index 74c9726..4eaab25 100644 --- a/spring/src/main/java/org/sopt/spring/common/exception/ErrorMessage.java +++ b/spring/src/main/java/org/sopt/spring/common/exception/ErrorMessage.java @@ -12,6 +12,7 @@ public enum ErrorMessage { BLOG_CANT_USE(HttpStatus.NOT_FOUND.value(), "사용자의 블로그가 아닙니다."), POSTING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ID에 해당하는 사용자가 존재하지 않습니다"), JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다."), ; private final int status; private final String message; diff --git a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java index f2a3f40..c6f3c28 100644 --- a/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java +++ b/spring/src/main/java/org/sopt/spring/member/controller/MemberController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; +import org.sopt.spring.common.auth.PrincipalHandler; import org.sopt.spring.member.service.MemberService; import org.sopt.spring.member.dto.MemberCreateDto; import org.sopt.spring.member.dto.MemberFindDto; @@ -24,6 +25,7 @@ public class MemberController { private final MemberService memberService; + private final PrincipalHandler principalHandler; @PostMapping public ResponseEntity createMember( @@ -36,9 +38,12 @@ public ResponseEntity createMember( userJoinResponse ); } + @PostMapping("/refresh") - public ResponseEntity refreshAccessToken() { - UserJoinResponse userJoinResponse = memberService.createMember(memberCreateDto); + public ResponseEntity refreshToken(){ + UserJoinResponse userJoinResponse = memberService.refreshToken( + principalHandler.getUserIdFromPrincipal() + ); return ResponseEntity.status(HttpStatus.CREATED) .header("Location", userJoinResponse.userId()) .body( diff --git a/spring/src/main/java/org/sopt/spring/member/service/MemberService.java b/spring/src/main/java/org/sopt/spring/member/service/MemberService.java index c289bf0..e4c9e4b 100644 --- a/spring/src/main/java/org/sopt/spring/member/service/MemberService.java +++ b/spring/src/main/java/org/sopt/spring/member/service/MemberService.java @@ -6,6 +6,7 @@ import org.sopt.spring.common.auth.redis.domain.Token; import org.sopt.spring.common.auth.redis.repository.RedisTokenRepository; import org.sopt.spring.common.exception.ErrorMessage; +import org.sopt.spring.common.exception.UnauthorizedException; import org.sopt.spring.common.jwt.JwtTokenProvider; import org.sopt.spring.member.domain.Member; import org.sopt.spring.common.exception.NotFoundException; @@ -72,4 +73,24 @@ public Member findById(Long memberId) { () -> new NotFoundException(ErrorMessage.MEMBER_NOT_FOUND) ); } + + @Transactional + public UserJoinResponse refreshToken(Long memberId) { + //Refresh 토큰 만료: Redis에 해당 Refresh 토큰이 존재하지 않음 + if(!redisTokenRepository.existsById(memberId.toString())){ + throw new UnauthorizedException(ErrorMessage.INVALID_REFRESH_TOKEN); + } + //DB에 해당하는 유저 아이디가 있는지 확인 + findById(memberId); + + String accessToken = jwtTokenProvider.issueAccessToken( + UserAuthentication.createUserAuthentication(memberId) + ); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + //레디스에 저*장 + redisTokenRepository.save(Token.of(memberId, refreshToken)); + return UserJoinResponse.of(accessToken, refreshToken, memberId.toString()); + } }