Skip to content

Commit 9713f51

Browse files
authored
refactor: 문자열 토큰을 Token 클래스로 (#297)
* feat: 토큰 객체 생성 * refactor: authTokenProvider에 토큰 객체 적용 * refactor: authService에 토큰 객체 적용 * refactor: signInService에 토큰 객체 적용 * test: AuthTokenProvider 테스트 보충 * test: AuthService 테스트 보충 * refactor: 인터페이스로 DIP 구현 * test: 리프레시토큰 조회 함수 변경에 따른 테스트 수정 * test: 블랙리스트 저장 방법 변경에 따른 테스트 수정 * test: 깨진 테스트 수정 * refactor: 엑세스 토큰 자체를 블랙리스트로 저장하도록 * refactor: AuthController에서 인증 정보를 String으로 받도록
1 parent fe0c2c0 commit 9713f51

File tree

15 files changed

+182
-184
lines changed

15 files changed

+182
-184
lines changed

src/main/java/com/example/solidconnection/auth/controller/AuthController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ public ResponseEntity<SignInResponse> signUp(
9797
public ResponseEntity<Void> signOut(
9898
Authentication authentication
9999
) {
100-
String token = authentication.getCredentials().toString();
101-
if (token == null) {
100+
String accessToken = (String) authentication.getCredentials();
101+
if (accessToken == null || accessToken.isBlank()) {
102102
throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다.");
103103
}
104-
authService.signOut(token);
104+
authService.signOut(accessToken);
105105
return ResponseEntity.ok().build();
106106
}
107107

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package com.example.solidconnection.auth.dto;
22

3+
import com.example.solidconnection.auth.service.AccessToken;
4+
35
public record ReissueResponse(
4-
String accessToken) {
6+
String accessToken
7+
) {
8+
9+
public static ReissueResponse from(AccessToken accessToken) {
10+
return new ReissueResponse(accessToken.token());
11+
}
512
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
package com.example.solidconnection.auth.dto;
22

3+
import com.example.solidconnection.auth.service.AccessToken;
4+
import com.example.solidconnection.auth.service.RefreshToken;
5+
36
public record SignInResponse(
47
String accessToken,
58
String refreshToken
69
) {
10+
11+
public static SignInResponse of(AccessToken accessToken, RefreshToken refreshToken) {
12+
return new SignInResponse(accessToken.token(), refreshToken.token());
13+
}
714
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.solidconnection.auth.service;
2+
3+
public record AccessToken(
4+
Subject subject,
5+
String token
6+
) {
7+
8+
public AccessToken(String subject, String token) {
9+
this(new Subject(subject), token);
10+
}
11+
}
Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,30 @@
11
package com.example.solidconnection.auth.service;
22

3-
43
import com.example.solidconnection.auth.dto.ReissueRequest;
54
import com.example.solidconnection.auth.dto.ReissueResponse;
6-
import com.example.solidconnection.config.security.JwtProperties;
75
import com.example.solidconnection.custom.exception.CustomException;
86
import com.example.solidconnection.siteuser.domain.SiteUser;
97
import lombok.RequiredArgsConstructor;
108
import org.springframework.stereotype.Service;
119
import org.springframework.transaction.annotation.Transactional;
1210

1311
import java.time.LocalDate;
14-
import java.util.Objects;
15-
import java.util.Optional;
1612

1713
import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
18-
import static com.example.solidconnection.util.JwtUtils.parseSubject;
1914

2015
@RequiredArgsConstructor
2116
@Service
2217
public class AuthService {
2318

2419
private final AuthTokenProvider authTokenProvider;
25-
private final JwtProperties jwtProperties;
2620

2721
/*
2822
* 로그아웃 한다.
2923
* - 엑세스 토큰을 블랙리스트에 추가한다.
3024
* */
31-
public void signOut(String accessToken) {
32-
authTokenProvider.generateAndSaveBlackListToken(accessToken);
25+
public void signOut(String token) {
26+
AccessToken accessToken = authTokenProvider.toAccessToken(token);
27+
authTokenProvider.addToBlacklist(accessToken);
3328
}
3429

3530
/*
@@ -45,19 +40,18 @@ public void quit(SiteUser siteUser) {
4540

4641
/*
4742
* 액세스 토큰을 재발급한다.
48-
* - 요청된 리프레시 토큰과 동일한 subject 의 토큰이 저장되어 있으며 값이 일치할 경우, 액세스 토큰을 재발급한다.
49-
* - 그렇지 않으면 예외를 반환한다.
43+
* - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다.
44+
* - 그렇지 않으면 예외를 발생시킨다.
5045
* */
5146
public ReissueResponse reissue(ReissueRequest reissueRequest) {
5247
// 리프레시 토큰 확인
5348
String requestedRefreshToken = reissueRequest.refreshToken();
54-
String subject = parseSubject(requestedRefreshToken, jwtProperties.secret());
55-
Optional<String> savedRefreshToken = authTokenProvider.findRefreshToken(subject);
56-
if (!Objects.equals(requestedRefreshToken, savedRefreshToken.orElse(null))) {
49+
if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) {
5750
throw new CustomException(REFRESH_TOKEN_EXPIRED);
5851
}
5952
// 액세스 토큰 재발급
60-
String newAccessToken = authTokenProvider.generateAccessToken(subject);
61-
return new ReissueResponse(newAccessToken);
53+
Subject subject = authTokenProvider.parseSubject(requestedRefreshToken);
54+
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject);
55+
return ReissueResponse.from(newAccessToken);
6256
}
6357
}

src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,68 @@
33
import com.example.solidconnection.auth.domain.TokenType;
44
import com.example.solidconnection.config.security.JwtProperties;
55
import com.example.solidconnection.siteuser.domain.SiteUser;
6+
import com.example.solidconnection.util.JwtUtils;
67
import org.springframework.data.redis.core.RedisTemplate;
78
import org.springframework.stereotype.Component;
89

9-
import java.util.Optional;
10+
import java.util.Objects;
1011

1112
@Component
12-
public class AuthTokenProvider extends TokenProvider {
13+
public class AuthTokenProvider extends TokenProvider implements BlacklistChecker {
1314

1415
public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate<String, String> redisTemplate) {
1516
super(jwtProperties, redisTemplate);
1617
}
1718

18-
public String generateAccessToken(SiteUser siteUser) {
19-
String subject = getSubject(siteUser);
20-
return generateToken(subject, TokenType.ACCESS);
19+
public AccessToken generateAccessToken(Subject subject) {
20+
String token = generateToken(subject.value(), TokenType.ACCESS);
21+
return new AccessToken(subject, token);
2122
}
2223

23-
public String generateAccessToken(String subject) {
24-
return generateToken(subject, TokenType.ACCESS);
24+
public RefreshToken generateAndSaveRefreshToken(Subject subject) {
25+
String token = generateToken(subject.value(), TokenType.REFRESH);
26+
saveToken(token, TokenType.REFRESH);
27+
return new RefreshToken(subject, token);
2528
}
2629

27-
public String generateAndSaveRefreshToken(SiteUser siteUser) {
28-
String subject = getSubject(siteUser);
29-
String refreshToken = generateToken(subject, TokenType.REFRESH);
30-
return saveToken(refreshToken, TokenType.REFRESH);
30+
/*
31+
* 액세스 토큰을 블랙리스트에 저장한다.
32+
* - key = BLACKLIST:{accessToken}
33+
* - value = "signOut" -> key 의 존재만 확인하므로, value 에는 무슨 값이 들어가도 상관없다.
34+
* */
35+
public void addToBlacklist(AccessToken accessToken) {
36+
String blackListKey = TokenType.BLACKLIST.addPrefix(accessToken.token());
37+
redisTemplate.opsForValue().set(blackListKey, "signOut");
3138
}
3239

33-
public String generateAndSaveBlackListToken(String accessToken) {
34-
String blackListToken = generateToken(accessToken, TokenType.BLACKLIST);
35-
return saveToken(blackListToken, TokenType.BLACKLIST);
40+
/*
41+
* 유효한 리프레시 토큰인지 확인한다.
42+
* - 요청된 토큰과 같은 subject 의 리프레시 토큰을 조회한다.
43+
* - 조회된 리프레시 토큰과 요청된 토큰이 같은지 비교한다.
44+
* */
45+
public boolean isValidRefreshToken(String requestedRefreshToken) {
46+
String subject = JwtUtils.parseSubject(requestedRefreshToken, jwtProperties.secret());
47+
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject);
48+
String foundRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey);
49+
return Objects.equals(requestedRefreshToken, foundRefreshToken);
3650
}
3751

38-
public Optional<String> findRefreshToken(String subject) {
39-
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject);
40-
return Optional.ofNullable(redisTemplate.opsForValue().get(refreshTokenKey));
52+
@Override
53+
public boolean isTokenBlacklisted(String accessToken) {
54+
String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken);
55+
return redisTemplate.hasKey(blackListTokenKey);
56+
}
57+
58+
public Subject parseSubject(String token) {
59+
String subject = JwtUtils.parseSubject(token, jwtProperties.secret());
60+
return new Subject(subject);
4161
}
4262

43-
public Optional<String> findBlackListToken(String subject) {
44-
String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject);
45-
return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey));
63+
public Subject toSubject(SiteUser siteUser) {
64+
return new Subject(siteUser.getId().toString());
4665
}
4766

48-
private String getSubject(SiteUser siteUser) {
49-
return siteUser.getId().toString();
67+
public AccessToken toAccessToken(String token) {
68+
return new AccessToken(parseSubject(token), token);
5069
}
5170
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example.solidconnection.auth.service;
2+
3+
public interface BlacklistChecker {
4+
5+
boolean isTokenBlacklisted(String token);
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.solidconnection.auth.service;
2+
3+
public record RefreshToken(
4+
Subject subject,
5+
String token
6+
) {
7+
8+
RefreshToken(String subject, String token) {
9+
this(new Subject(subject), token);
10+
}
11+
}

src/main/java/com/example/solidconnection/auth/service/SignInService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ public class SignInService {
1515
@Transactional
1616
public SignInResponse signIn(SiteUser siteUser) {
1717
resetQuitedAt(siteUser);
18-
String accessToken = authTokenProvider.generateAccessToken(siteUser);
19-
String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
20-
return new SignInResponse(accessToken, refreshToken);
18+
Subject subject = authTokenProvider.toSubject(siteUser);
19+
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
20+
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
21+
return SignInResponse.of(accessToken, refreshToken);
2122
}
2223

2324
private void resetQuitedAt(SiteUser siteUser) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example.solidconnection.auth.service;
2+
3+
public record Subject(
4+
String value
5+
) {
6+
}

0 commit comments

Comments
 (0)