-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature/#17] refresh token 발급 시 Redis에 저장하도록 설정 #18
Changes from all commits
fd9b834
2b061bd
ef58406
16451df
ff0c4bb
0d9000f
9e5e1d4
2d277fd
35962f8
ed0580a
ad342b4
a0b4e2a
4cf5396
a1e2d19
6959ceb
a4fee28
9dd2def
8ca7024
f975196
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package com.backend.auth.application; | ||
|
||
import com.backend.auth.domain.RefreshToken; | ||
import com.backend.auth.domain.RefreshTokenRepository; | ||
import com.backend.global.common.code.ErrorCode; | ||
import com.backend.global.exception.BusinessException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class RefreshTokenService { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redis에 RefreshToken을 저장, 조회하기 위한 서비스 클래스입니다! |
||
|
||
private final RefreshTokenRepository refreshTokenRepository; | ||
|
||
public void saveRefreshToken(String refreshToken, String uid){ | ||
refreshTokenRepository.save(new RefreshToken(refreshToken, uid)); | ||
} | ||
public String findUidByRefreshToken(String refreshToken){ | ||
RefreshToken result = refreshTokenRepository.findById(refreshToken) | ||
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_ERROR)); | ||
return result.getUid(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.backend.auth.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.RedisStandaloneConfiguration; | ||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; | ||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; | ||
|
||
@Configuration | ||
@EnableRedisRepositories | ||
public class RedisConfig { | ||
|
||
@Value("${spring.data.redis.host}") | ||
private String host; | ||
|
||
@Value("${spring.data.redis.port}") | ||
private int port; | ||
|
||
@Bean | ||
public RedisConnectionFactory connectionFactory(){ | ||
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(); | ||
redisConfiguration.setHostName(host); | ||
redisConfiguration.setPort(port); | ||
return new LettuceConnectionFactory(redisConfiguration); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.backend.auth.domain; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import org.springframework.data.annotation.Id; | ||
import org.springframework.data.redis.core.RedisHash; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 14) | ||
public class RefreshToken { | ||
@Id | ||
private String refreshToken; | ||
private String uid; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.backend.auth.domain; | ||
|
||
import org.springframework.data.repository.CrudRepository; | ||
|
||
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
package com.backend.auth.jwt; | ||
|
||
import com.backend.member.domain.Member; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import com.backend.auth.application.RefreshTokenService; | ||
import com.backend.auth.domain.RefreshToken; | ||
import com.backend.global.common.code.ErrorCode; | ||
import com.backend.global.exception.BusinessException; | ||
import io.jsonwebtoken.*; | ||
import io.jsonwebtoken.io.Decoders; | ||
import io.jsonwebtoken.security.Keys; | ||
import org.springframework.beans.factory.annotation.Value; | ||
|
@@ -20,29 +20,39 @@ public class TokenProvider { | |
|
||
private final Key key; | ||
|
||
public TokenProvider(@Value("${jwt.secret}") String secretKey){ | ||
private final RefreshTokenService refreshTokenService; | ||
|
||
public TokenProvider(@Value("${jwt.secret}") String secretKey, RefreshTokenService refreshTokenService){ | ||
this.refreshTokenService = refreshTokenService; | ||
byte[] keyBytes = Decoders.BASE64.decode(secretKey); | ||
this.key = Keys.hmacShaKeyFor(keyBytes); | ||
} | ||
|
||
public String generateAccessToken(Member member){ | ||
return generateToken(member, ACCESS_TOKEN_EXPIRE_TIME); | ||
public String generateAccessToken(String uid){ | ||
return generateToken(uid, ACCESS_TOKEN_EXPIRE_TIME); | ||
} | ||
|
||
public String generateRefreshToken(Member member){ | ||
return generateToken(member, REFRESH_TOKEN_EXPIRE_TIME); | ||
public String generateRefreshToken(String uid){ | ||
String refreshToken = generateToken(uid, REFRESH_TOKEN_EXPIRE_TIME); | ||
refreshTokenService.saveRefreshToken(refreshToken, uid); | ||
return refreshToken; | ||
} | ||
|
||
public String generateToken(Member member, Long expireTime){ | ||
public String generateToken(String uid, Long expireTime){ | ||
Date now = new Date(); | ||
return Jwts.builder() | ||
.setSubject(member.getSocialId()) | ||
.setSubject(uid) | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + expireTime)) | ||
.signWith(key, SignatureAlgorithm.HS256) | ||
.compact(); | ||
} | ||
Comment on lines
+41
to
49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 실제 JWT 생성 과정에서 uid(구 socialId)만 사용하기 때문에 uid만 필드에 받아오는 것으로 변경하였습니다. |
||
|
||
public String reissueAccessToken(String refreshToken) { | ||
String uid = refreshTokenService.findUidByRefreshToken(refreshToken); | ||
return generateAccessToken(uid); | ||
} | ||
|
||
public String getPayload(String token){ | ||
return Jwts.parserBuilder() | ||
.setSigningKey(key) | ||
|
@@ -52,19 +62,16 @@ public String getPayload(String token){ | |
.getSubject(); | ||
} | ||
|
||
public boolean validateToken(String token){ | ||
public void validateToken(String token) throws Exception { | ||
try{ | ||
Jwts.parserBuilder() | ||
.setSigningKey(key) | ||
.build() | ||
.parseClaimsJws(token); | ||
return true; | ||
} catch (ExpiredJwtException e){ | ||
throw e; | ||
} catch (UnsupportedJwtException e){ | ||
throw e; | ||
} catch (IllegalArgumentException e){ | ||
throw e; | ||
throw new BusinessException(ErrorCode.TOKEN_EXPIRED); | ||
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e){ | ||
throw new Exception("잘못된 형식의 토큰입니다."); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,45 @@ | ||
package com.backend.auth.presentation; | ||
|
||
import com.backend.auth.application.OAuthService; | ||
import com.backend.auth.presentation.dto.response.LoginResponse; | ||
import com.backend.auth.presentation.dto.TokenReissueRequest; | ||
import com.backend.auth.presentation.dto.response.AccessTokenResponse; | ||
import com.backend.auth.presentation.dto.response.TokenResponse; | ||
import com.backend.global.common.code.SuccessCode; | ||
import com.backend.global.common.response.CustomResponse; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import static com.backend.global.common.code.SuccessCode.*; | ||
|
||
@Tag(name = "회원 인증", description = "소셜 로그인 API입니다.") | ||
@RequiredArgsConstructor | ||
@RestController | ||
public class OAuthController { | ||
|
||
private final OAuthService oauthService; | ||
|
||
@Operation(summary = "소셜 로그인", description = "소셜 로그인 후 사용자 토큰 발급") | ||
@ApiResponses({ | ||
@ApiResponse(responseCode = "200", description = "소셜 로그인 성공"), | ||
@ApiResponse(responseCode = "400", description = "잘못된 요청으로 인한 실패"), | ||
@ApiResponse(responseCode = "401", description = "접근 권한 없음") | ||
}) | ||
@Operation(summary = "소셜 로그인", | ||
description = "카카오, 애플 서버에서 로그인한 사용자의 userId를 통해 access token과 refresh token을 반환합니다.") | ||
@PostMapping("/auth/{provider}") | ||
public ResponseEntity<LoginResponse> generateAccessToken(@PathVariable String provider, @RequestParam String userId) { | ||
return ResponseEntity.ok(oauthService.login(provider, userId)); | ||
public ResponseEntity<CustomResponse> generateAccessTokenAndRefreshToken( | ||
@Parameter(description = "kakao, apple 중 현재 로그인하는 소셜 타입", in = ParameterIn.PATH) @PathVariable String provider, | ||
@Parameter(description = "사용자 ID") @RequestParam String userId) { | ||
return CustomResponse.success(LOGIN_SUCCESS, oauthService.login(provider, userId)); | ||
} | ||
|
||
} | ||
@Operation(summary = "토큰 재발급", | ||
description = "access token 만료 시 refresh token을 통해 access token을 재발급합니다.") | ||
@PostMapping("/reissue") | ||
@ExceptionHandler({UnsupportedJwtException.class, MalformedJwtException.class, IllegalArgumentException.class}) | ||
public ResponseEntity<CustomResponse> reissue(@Valid @RequestBody TokenReissueRequest reissueRequest) throws Exception { | ||
return CustomResponse.success(LOGIN_SUCCESS, oauthService.reissue(reissueRequest.refreshToken())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.backend.auth.presentation.dto; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record TokenReissueRequest( | ||
@NotNull(message = "access token reissue 시 refresh token 입력은 필수입니다.") | ||
String refreshToken | ||
) { } | ||
Comment on lines
+5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 궁금한 부분이 있는데요, reissue를 하는 과정에서 그럼 발생할 수 있는 문제가
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드에 문제가 있는게 아니라 궁금증 입니다😊 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.backend.auth.presentation.dto.response; | ||
|
||
public record AccessTokenResponse ( | ||
String accessToken | ||
){ } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
package com.backend.global.common.code; | ||
|
||
import lombok.Getter; | ||
import org.springframework.http.HttpStatus; | ||
|
||
import static org.springframework.http.HttpStatus.*; | ||
import static org.springframework.http.HttpStatus.NOT_FOUND; | ||
import static org.springframework.http.HttpStatus.UNAUTHORIZED; | ||
|
||
@Getter | ||
public enum ErrorCode { | ||
|
@@ -54,9 +56,10 @@ public enum ErrorCode { | |
*/ | ||
|
||
/* Plan */ | ||
PLAN_NOT_FOUND(NOT_FOUND.value(), "PLAN-001", "상위 목표가 존재하지 않습니다."); | ||
|
||
PLAN_NOT_FOUND(NOT_FOUND.value(), "PLAN-001", "상위 목표가 존재하지 않습니다."), | ||
|
||
/* Auth */ | ||
TOKEN_EXPIRED(UNAUTHORIZED.value(), "AUTH-001", "토큰의 유효기간이 만료되었습니다."); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음 여기서는 토큰 유효기간이 만료됐다는 메세지만 만들고 나머지는 exception를 잡는 exceptionHandler에 걸려서 reason 컬럼으로 에러가 출력되도록 해도 될것 같아요! 기본 JWT 예외를 사용하는게 어떻겠냐는 부분에 대한 답도 되는것 같습니다~ |
||
private final int status; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
build.gradle에 redis 라이브러리 추가했습니다! 로컬에서도 Redis 프로그램을 localhost:6379 환경으로 세팅해야 관련 코드가 정상적으로 동작합니다:)