From 40aee44b260ba0096bc8480b25111848c903f8c7 Mon Sep 17 00:00:00 2001 From: bo-ram-bo-ram Date: Sat, 1 Jun 2024 05:12:24 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat=20:=20refresh=20token=EC=9D=84=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=83=9D=EC=84=B1api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- week3/build.gradle | 4 +++- .../sopt/seminar/common/dto/ErrorMessage.java | 3 +++ .../sopt/seminar/service/MemberService.java | 19 ++++++++----------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/week3/build.gradle b/week3/build.gradle index a8c6732..d4af8b6 100644 --- a/week3/build.gradle +++ b/week3/build.gradle @@ -28,7 +28,6 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' runtimeOnly 'com.h2database:h2' - // implementation group: 'org.postgresql',name:'postgresql',version:'42.7.3' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' testImplementation 'io.rest-assured:rest-assured' @@ -46,6 +45,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/week3/src/main/java/com/sopt/seminar/common/dto/ErrorMessage.java b/week3/src/main/java/com/sopt/seminar/common/dto/ErrorMessage.java index 1a01cfe..6abb3e3 100644 --- a/week3/src/main/java/com/sopt/seminar/common/dto/ErrorMessage.java +++ b/week3/src/main/java/com/sopt/seminar/common/dto/ErrorMessage.java @@ -18,6 +18,9 @@ public enum ErrorMessage { //401 JWT_UNAUTHORIZED_EXCEPTION(HttpStatus.UNAUTHORIZED.value(), "사용자의 로그인 검증을 실패했습니다."), + + //404 + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND.value(),"refresh 토큰이 존재하지 않습니다."), ; private final int status; private final String message; diff --git a/week3/src/main/java/com/sopt/seminar/service/MemberService.java b/week3/src/main/java/com/sopt/seminar/service/MemberService.java index f4fb402..e2919cc 100644 --- a/week3/src/main/java/com/sopt/seminar/service/MemberService.java +++ b/week3/src/main/java/com/sopt/seminar/service/MemberService.java @@ -1,7 +1,8 @@ package com.sopt.seminar.service; -import com.sopt.seminar.config.auth.UserAuthentication; import com.sopt.seminar.common.dto.ErrorMessage; +import com.sopt.seminar.config.auth.UserAuthentication; +import com.sopt.seminar.config.auth.redis.Service.TokenService; import com.sopt.seminar.config.jwt.JwtTokenProvider; import com.sopt.seminar.domain.Member; import com.sopt.seminar.exception.NotFoundException; @@ -21,15 +22,7 @@ public class MemberService { private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; - - // @Transactional -// public String createMember( -// MemberCreateDto memberCreateDto -// ) { -// Member member = Member.create(memberCreateDto.name(), memberCreateDto.part(), memberCreateDto.age()); -// memberRepository.save(member); -// return member.getId().toString(); -// } + private final TokenService tokenService; @Transactional public UserJoinResponse createMember( @@ -42,7 +35,11 @@ public UserJoinResponse createMember( String accessToken = jwtTokenProvider.issueAccessToken( UserAuthentication.createUserAuthentication(memberId) ); - return UserJoinResponse.of(accessToken, memberId.toString()); + String refreshToken = jwtTokenProvider.issueRefreshToken( + UserAuthentication.createUserAuthentication(memberId) + ); + tokenService.saveRefreshToken(memberId, refreshToken); + return UserJoinResponse.of(accessToken,refreshToken, memberId.toString()); } public MemberFindDto findMemberById(Long memberId) { From 4010ca212d5dd7183217f00ebc1c2f670f4da4c7 Mon Sep 17 00:00:00 2001 From: bo-ram-bo-ram Date: Sat, 1 Jun 2024 05:13:14 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=20=EC=A3=BC=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seminar/controller/BlogController.java | 18 ------------------ .../seminar/controller/MemberController.java | 5 ----- 2 files changed, 23 deletions(-) diff --git a/week3/src/main/java/com/sopt/seminar/controller/BlogController.java b/week3/src/main/java/com/sopt/seminar/controller/BlogController.java index f56aae9..e08f30e 100644 --- a/week3/src/main/java/com/sopt/seminar/controller/BlogController.java +++ b/week3/src/main/java/com/sopt/seminar/controller/BlogController.java @@ -33,24 +33,6 @@ public ResponseEntity createBlog( principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build(); } -// @PostMapping("/blog") -// public ResponseEntity createBlog( -// BlogCreateRequest blogCreateRequest -// ) { -// return ResponseEntity.created(URI.create(blogService.create( -// principalHandler.getUserIdFromPrincipal(), blogCreateRequest))).build(); -// } - -// @PostMapping("/blog") -// public ResponseEntity createBlog( -// @RequestHeader(name = "memberId") Long memberId, -// @RequestBody BlogCreateRequest blogCreateRequest -// ) { return ResponseEntity.status(HttpStatus.CREATED).header( -// "Location", -// blogService.create(memberId, blogCreateRequest)) -// .body(SuccessStatusResponse.of(SuccessMessage.BLOG_CREATE_SUCCESS)); -// } - @PatchMapping("/blog/{blogId}/title") public ResponseEntity updateBlogTitle( @PathVariable Long blogId, diff --git a/week3/src/main/java/com/sopt/seminar/controller/MemberController.java b/week3/src/main/java/com/sopt/seminar/controller/MemberController.java index 693657b..340a38d 100644 --- a/week3/src/main/java/com/sopt/seminar/controller/MemberController.java +++ b/week3/src/main/java/com/sopt/seminar/controller/MemberController.java @@ -22,11 +22,6 @@ public class MemberController { private final MemberService memberService; -// @PostMapping -// public ResponseEntity createMember(@RequestBody MemberCreateDto memberCreate) { -// return ResponseEntity.created(URI.create(memberService.createMember(memberCreate))).build(); -// } - @GetMapping("/{memberId}") public ResponseEntity findMemberById(@PathVariable Long memberId){ return ResponseEntity.ok(memberService.findMemberById(memberId)); From f9b403f454d1666d37ebfda2e6fe43c1e3ccd046 Mon Sep 17 00:00:00 2001 From: bo-ram-bo-ram Date: Sun, 2 Jun 2024 23:53:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[feat]=20Redis=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=20Refresh=20Token=EC=9C=BC=EB=A1=9C=20Access?= =?UTF-8?q?=20Token=EC=9D=84=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20api=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 --- .../auth/redis/Service/TokenService.java | 37 ++++++++ .../redis/controller/TokenController.java | 24 ++++++ .../config/auth/redis/domain/Token.java | 31 +++++++ .../redis/repository/TokenRepository.java | 10 +++ .../seminar/config/jwt/JwtTokenProvider.java | 85 +++++++++++++++++++ .../seminar/config/jwt/JwtValidationType.java | 10 +++ 6 files changed, 197 insertions(+) create mode 100644 week3/src/main/java/com/sopt/seminar/config/auth/redis/Service/TokenService.java create mode 100644 week3/src/main/java/com/sopt/seminar/config/auth/redis/controller/TokenController.java create mode 100644 week3/src/main/java/com/sopt/seminar/config/auth/redis/domain/Token.java create mode 100644 week3/src/main/java/com/sopt/seminar/config/auth/redis/repository/TokenRepository.java create mode 100644 week3/src/main/java/com/sopt/seminar/config/jwt/JwtTokenProvider.java create mode 100644 week3/src/main/java/com/sopt/seminar/config/jwt/JwtValidationType.java diff --git a/week3/src/main/java/com/sopt/seminar/config/auth/redis/Service/TokenService.java b/week3/src/main/java/com/sopt/seminar/config/auth/redis/Service/TokenService.java new file mode 100644 index 0000000..28ca4c2 --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/auth/redis/Service/TokenService.java @@ -0,0 +1,37 @@ +package com.sopt.seminar.config.auth.redis.Service; + +import com.sopt.seminar.config.auth.UserAuthentication; +import com.sopt.seminar.config.auth.redis.domain.Token; +import com.sopt.seminar.config.auth.redis.repository.TokenRepository; +import com.sopt.seminar.config.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public void saveRefreshToken( + final Long userId, + final String refreshToken + ) { + tokenRepository.save( + Token.of( + userId, + refreshToken + ) + ); + } + + public String reissueAccessTokenByRefreshToken( + Long userId + ){ + String newAccessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(userId)); + return newAccessToken; + } +} diff --git a/week3/src/main/java/com/sopt/seminar/config/auth/redis/controller/TokenController.java b/week3/src/main/java/com/sopt/seminar/config/auth/redis/controller/TokenController.java new file mode 100644 index 0000000..3d99d93 --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/auth/redis/controller/TokenController.java @@ -0,0 +1,24 @@ +package com.sopt.seminar.config.auth.redis.controller; + +import com.sopt.seminar.common.dto.ApiResponse; +import com.sopt.seminar.common.dto.SuccessMessage; +import com.sopt.seminar.config.auth.PrincipalHandler; +import com.sopt.seminar.config.auth.redis.Service.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class TokenController { + + private final TokenService tokenService; + private final PrincipalHandler principalHandler; + + @PostMapping("api/v1/refresh-token") + public ApiResponse reissueAccessToken(){ + return ApiResponse.success( + SuccessMessage.ACCESS_TOKEN_REFRESH_SUCCESS, + tokenService.reissueAccessTokenByRefreshToken(principalHandler.getUserIdFromPrincipal())); + } +} diff --git a/week3/src/main/java/com/sopt/seminar/config/auth/redis/domain/Token.java b/week3/src/main/java/com/sopt/seminar/config/auth/redis/domain/Token.java new file mode 100644 index 0000000..d47d34c --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/auth/redis/domain/Token.java @@ -0,0 +1,31 @@ +package com.sopt.seminar.config.auth.redis.domain; + +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +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/week3/src/main/java/com/sopt/seminar/config/auth/redis/repository/TokenRepository.java b/week3/src/main/java/com/sopt/seminar/config/auth/redis/repository/TokenRepository.java new file mode 100644 index 0000000..04177fb --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/auth/redis/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package com.sopt.seminar.config.auth.redis.repository; + +import com.sopt.seminar.config.auth.redis.domain.Token; +import java.util.Optional; +import org.springframework.data.repository.CrudRepository; + +public interface TokenRepository extends CrudRepository { + Optional findByRefreshToken(final String refreshToken); + Optional findById(final Long id); +} diff --git a/week3/src/main/java/com/sopt/seminar/config/jwt/JwtTokenProvider.java b/week3/src/main/java/com/sopt/seminar/config/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..17d423a --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/jwt/JwtTokenProvider.java @@ -0,0 +1,85 @@ +package com.sopt.seminar.config.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 30 * 60L; + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; + + @Value("${jwt.secret}") + private String JWT_SECRET; + + 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) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + + claims.put(USER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header + .setClaims(claims) // Claim + .signWith(getSigningKey()) // Signature + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 + return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } +} diff --git a/week3/src/main/java/com/sopt/seminar/config/jwt/JwtValidationType.java b/week3/src/main/java/com/sopt/seminar/config/jwt/JwtValidationType.java new file mode 100644 index 0000000..671a938 --- /dev/null +++ b/week3/src/main/java/com/sopt/seminar/config/jwt/JwtValidationType.java @@ -0,0 +1,10 @@ +package com.sopt.seminar.config.jwt; + +public enum JwtValidationType { + VALID_JWT, // 유효한 JWT + INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 + INVALID_JWT_TOKEN, // 유효하지 않은 토큰 + EXPIRED_JWT_TOKEN, // 만료된 토큰 + UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 + EMPTY_JWT // 빈 JWT +}