-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#21 #27
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
base: main
Are you sure you want to change the base?
Feat/#21 #27
Changes from all commits
b7a7e9c
3a167c8
892732e
09d90db
2f8427a
b0d1f5e
0bd4441
399adce
48f72e1
9fa9b67
c055ad3
a080a30
a8bd809
7664881
8d7a0d7
2d28a33
9dcb648
78cc3b4
97c5f77
8fbb7e0
e94d67b
a1079c9
f9e22d8
48f7845
bc958e1
16bbb13
083a8ec
86bddae
8b56458
2c12c05
b1ae1ca
5bce66e
1711dbd
6fbb581
74811e2
6dd4c7d
145f9a5
323dc51
4d084f6
5a23071
4718698
b7189bf
38dd656
26511a2
8bc7856
c4a294a
399a43d
3d89dd9
55a324d
a0a8743
a02a603
165217c
09d0140
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 |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |
| import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; | ||
| import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; | ||
| import kr.ac.ks.cs_web_back.domain.auth.service.AuthService; | ||
| import kr.ac.ks.cs_web_back.domain.member.model.Member; | ||
| import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; | ||
| import kr.ac.ks.cs_web_back.global.response.CsResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
@@ -23,4 +25,13 @@ public CsResponse<AuthLoginResponse> login( | |
| AuthLoginResponse token = authService.loginMember(request); | ||
| return CsResponse.of(AuthSuccessCode.LOGIN_SUCCESS, token); | ||
| } | ||
|
Contributor
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. 메서드 사이 한줄 띄워줘요 |
||
|
|
||
| @PostMapping("/logout") | ||
| public CsResponse<Void> logout ( | ||
| @Valid @RequestHeader("Authorization") String authorization, | ||
| @IdentifiedUser Member member | ||
| ) { | ||
| authService.logout(authorization, member.getEmail()); | ||
| return CsResponse.of(AuthSuccessCode.LOGOUT_SUCCESS); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,19 @@ | ||
| package kr.ac.ks.cs_web_back.domain.auth.controller; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; | ||
| import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; | ||
| import kr.ac.ks.cs_web_back.domain.member.model.Member; | ||
| import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; | ||
| import kr.ac.ks.cs_web_back.global.response.CsResponse; | ||
| import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse; | ||
| import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.web.bind.annotation.RequestHeader; | ||
|
|
||
| @Tag(name = "인증 API", description = "인증 관련 API") | ||
| public interface SpringDocAuthController { | ||
|
|
@@ -30,4 +34,17 @@ CsResponse<AuthLoginResponse> login( | |
| @RequestBody AuthLoginRequest request | ||
| ); | ||
|
|
||
|
Contributor
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. 여기도 한줄 띄워줘요 |
||
| @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") | ||
| @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") | ||
| @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { | ||
| @ErrorCase(description = "유효하지 않은 토큰", code = 8001, exampleMessage = "유효하지 않은 토큰입니다."), | ||
| @ErrorCase(description = "인증 실패", code = 8002, exampleMessage = "인증에 실패했습니다.") | ||
| }) | ||
| @ApiErrorResponse(status = HttpStatus.INTERNAL_SERVER_ERROR, instance = "/auth/logout", errorCases = { | ||
| @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") | ||
| }) | ||
| CsResponse<Void> logout( | ||
| @RequestHeader("Authorization") String authorizationHeader, | ||
| @Parameter(hidden=true)@IdentifiedUser Member member | ||
| ); | ||
| } | ||
|
Contributor
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. AuthController 리뷰에서 언급한 필터 체인 사용과 별개로, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package kr.ac.ks.cs_web_back.domain.auth.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class JwtBlackListService { | ||
|
|
||
| private final RedisTemplate<String, String> redisTemplate; | ||
|
|
||
| public void blacklist(String accessToken, long remainingTime) { | ||
| redisTemplate.opsForValue().set( | ||
| accessToken, | ||
| "logout", | ||
| remainingTime, | ||
| TimeUnit.MILLISECONDS | ||
| ); | ||
| } | ||
|
|
||
| public boolean isBlackListed(String accessToken) { | ||
| return redisTemplate.opsForValue().get(accessToken) != null; | ||
| } | ||
| } |
|
Contributor
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. Authorization 헤더가 누락된 요청을 받는 경우 컨트롤러까지 요청이 전달되지 않고 Jwt 인증 필터 체인에서 예외를 발생하기 때문에 컨트롤러에서 발생시키는 헤더를 핸들링하는 GlobalExceptionHandler에는 메서드가 필요하지 않을 것으로 보입니다 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package kr.ac.ks.cs_web_back.global.jwt; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.PropertySource; | ||
|
|
||
| @Configuration | ||
| @EnableConfigurationProperties(JwtProperties.class) | ||
| @PropertySource("classpath:env.properties") | ||
| @RequiredArgsConstructor | ||
| public class JwtConfig { | ||
|
|
||
| private final JwtProperties jwtProperties; | ||
|
|
||
| @Bean(name = "accessTokenProvider") | ||
| public TokenProvider accessTokenProvider() { | ||
| return new TokenProvider(jwtProperties.accessSecret(), jwtProperties.accessTokenExpireTime()); | ||
| } | ||
|
|
||
| @Bean(name = "refreshTokenProvider") | ||
| public TokenProvider refreshTokenProvider() { | ||
| return new TokenProvider(jwtProperties.refreshSecret(), jwtProperties.refreshTokenExpireTime()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package kr.ac.ks.cs_web_back.global.jwt; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @ConfigurationProperties(prefix = "jwt") | ||
| public record JwtProperties( | ||
| String accessSecret, | ||
| String refreshSecret, | ||
| Long accessTokenExpireTime, | ||
| Long refreshTokenExpireTime | ||
| ) { | ||
| } |
|
Contributor
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. JwtUtil의 책임이 당초 설계할 때 예상했던 것 보다 점점 무거워지는 것 같아요 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,54 +14,76 @@ | |
|
|
||
| @Component | ||
| public class JwtUtil { | ||
| @Value("${jwt.secret}") | ||
| private String secret; | ||
| @Value("${jwt.accessSecret}") | ||
| private String aSecret; | ||
|
|
||
| @Value("${jwt.refreshSecret}") | ||
| private String rSecret; | ||
|
|
||
| @Value("${jwt.accessTokenExpireTime}") | ||
| private Long accessTokenExpireTime; | ||
|
|
||
| @Value("${jwt.refreshTokenExpireTime}") | ||
| private Long refreshTokenExpireTime; | ||
|
|
||
| private Key hmacKey; | ||
| private Key hmacAccessKey; | ||
| private Key hmacRefreshKey; | ||
|
|
||
| @PostConstruct | ||
| public void init() { | ||
| byte[] decodedKey = Base64.getDecoder().decode(secret); | ||
| this.hmacKey = new SecretKeySpec(decodedKey, SignatureAlgorithm.HS256.getJcaName()); | ||
| } | ||
| public String generateAccessToken(String email) { | ||
| return generateToken(email, accessTokenExpireTime); | ||
| } | ||
| byte[] accessDecodedKey = Base64.getDecoder().decode(aSecret); | ||
| this.hmacAccessKey = new SecretKeySpec(accessDecodedKey, SignatureAlgorithm.HS256.getJcaName()); | ||
|
|
||
| public String generateRefreshToken(String email) { | ||
| return generateToken(email, refreshTokenExpireTime); | ||
| byte[] refreshDecodedKey = Base64.getDecoder().decode(rSecret); | ||
| this.hmacRefreshKey = new SecretKeySpec(refreshDecodedKey, SignatureAlgorithm.HS256.getJcaName()); | ||
| } | ||
|
|
||
| private String generateToken(String email, Long expireTime) { | ||
| private String generateToken(String email, Long expireTime, Key key) { | ||
| Date now = new Date(); | ||
| Date expiryDate = new Date(now.getTime() + expireTime); | ||
| return Jwts.builder().setSubject(email) | ||
| .setIssuedAt(now) | ||
| .setExpiration(expiryDate) | ||
| .signWith(hmacKey) | ||
| .signWith(key) | ||
| .compact(); | ||
| } | ||
|
|
||
| public String getEmailFromToken(String token) { | ||
| Claims claims = Jwts.parserBuilder() | ||
| .setSigningKey(hmacKey) | ||
| public String generateAccessToken(String email) { | ||
| return generateToken(email, accessTokenExpireTime, hmacAccessKey); | ||
| } | ||
|
|
||
| public String generateRefreshToken(String email) { | ||
| return generateToken(email, refreshTokenExpireTime, hmacRefreshKey); | ||
| } | ||
|
|
||
| public String generateTestToken(String email, Long expireTime) { | ||
|
Contributor
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. generateTestToken 제거해 주세요! |
||
| return generateToken(email, expireTime, hmacAccessKey); | ||
| } | ||
|
|
||
| private Claims getAllClaimsFromToken(String token, Key key) { | ||
| return Jwts.parserBuilder() | ||
| .setSigningKey(key) | ||
| .build() | ||
| .parseClaimsJws(token) | ||
| .getBody(); | ||
| } | ||
|
|
||
| public Date getExpirationDateFromAccessToken(String token) { | ||
| return getAllClaimsFromToken(token, hmacAccessKey).getExpiration(); | ||
| } | ||
|
|
||
| public Date getExpirationDateFromRefreshToken(String token) { | ||
| return getAllClaimsFromToken(token, hmacRefreshKey).getExpiration(); | ||
| } | ||
|
|
||
| return claims.getSubject(); | ||
| public String getEmailFromAccessToken(String token) { | ||
| return getAllClaimsFromToken(token, hmacAccessKey).getSubject(); | ||
| } | ||
|
|
||
| public void validateToken(String token) { | ||
| try { | ||
| Jwts.parserBuilder() | ||
| .setSigningKey(hmacKey) | ||
| .setSigningKey(hmacAccessKey) | ||
| .build() | ||
| .parseClaimsJws(token); | ||
| } catch (ExpiredJwtException e) { | ||
|
|
||
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.
헤더로 액세스 토큰을 받는 것과 더불어 @IdentifiedUser 어노테이션을 사용해 Member 객체 또한 받는 것이 좋을 것 같습니다.
현재 로직으로는 인증 필터 체인을 거치지 않고 컨트롤러 단으로 요청이 바로 전달되기 때문에, 서비스 로직에서 토큰 추출, 인증, 관리까지 모두 담당하게 되어 책임이 지나치게 무거워집니다.
구현되어 있는 인증 필터 체인을 사용해서 토큰 추출 및 인증에 대한 책임을 필터 체인에 인계하고, 서비스 로직에서는 토큰 블랙리스트 관리만 담당하게 하는 것이 좋은 코드가 될 것 같습니다~!