Skip to content

Commit 66aa670

Browse files
authored
feat: access token이 role을 cliam 으로 갖도록 (#407)
* feat: claims가 있는 토큰 생성하는 함수 추가 * refactor: AccessToken 생성자 인자 변경 * test: AccessToken 생성과 관련없는 테스트에 변경 전파가 최소화되도록 리팩터링 * refactor: 재발급하는 accessToken도 role을 응답하도록
1 parent 11e589f commit 66aa670

File tree

11 files changed

+122
-57
lines changed

11 files changed

+122
-57
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,10 @@ public ResponseEntity<Void> quit(
113113

114114
@PostMapping("/reissue")
115115
public ResponseEntity<ReissueResponse> reissueToken(
116+
@AuthorizedUser long siteUserId,
116117
@Valid @RequestBody ReissueRequest reissueRequest
117118
) {
118-
ReissueResponse reissueResponse = authService.reissue(reissueRequest);
119+
ReissueResponse reissueResponse = authService.reissue(siteUserId, reissueRequest);
119120
return ResponseEntity.ok(reissueResponse);
120121
}
121122

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.example.solidconnection.auth.service;
22

3+
import com.example.solidconnection.siteuser.domain.Role;
4+
35
public record AccessToken(
46
Subject subject,
7+
Role role,
58
String token
69
) {
710

8-
public AccessToken(String subject, String token) {
9-
this(new Subject(subject), token);
11+
public AccessToken(String subject, Role role, String token) {
12+
this(new Subject(subject), role, token);
1013
}
1114
}

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ public class AuthService {
2828
* - 리프레시 토큰을 삭제한다.
2929
* */
3030
public void signOut(String token) {
31-
AccessToken accessToken = authTokenProvider.toAccessToken(token);
31+
Subject subject = authTokenProvider.parseSubject(token);
32+
long siteUserId = Long.parseLong(subject.value());
33+
SiteUser siteUser = siteUserRepository.findById(siteUserId)
34+
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
35+
36+
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
3237
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
3338
tokenBlackListService.addToBlacklist(accessToken);
3439
}
@@ -53,15 +58,17 @@ public void quit(long siteUserId, String token) {
5358
* - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다.
5459
* - 그렇지 않으면 예외를 발생시킨다.
5560
* */
56-
public ReissueResponse reissue(ReissueRequest reissueRequest) {
61+
public ReissueResponse reissue(long siteUserId, ReissueRequest reissueRequest) {
5762
// 리프레시 토큰 확인
5863
String requestedRefreshToken = reissueRequest.refreshToken();
5964
if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) {
6065
throw new CustomException(REFRESH_TOKEN_EXPIRED);
6166
}
6267
// 액세스 토큰 재발급
68+
SiteUser siteUser = siteUserRepository.findById(siteUserId)
69+
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
6370
Subject subject = authTokenProvider.parseSubject(requestedRefreshToken);
64-
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject);
71+
AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
6572
return ReissueResponse.from(newAccessToken);
6673
}
6774
}

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.example.solidconnection.auth.service;
22

33
import com.example.solidconnection.auth.domain.TokenType;
4+
import com.example.solidconnection.siteuser.domain.Role;
45
import com.example.solidconnection.siteuser.domain.SiteUser;
6+
import java.util.Map;
57
import java.util.Objects;
68
import lombok.RequiredArgsConstructor;
79
import org.springframework.data.redis.core.RedisTemplate;
@@ -11,12 +13,16 @@
1113
@RequiredArgsConstructor
1214
public class AuthTokenProvider {
1315

16+
private static final String ROLE_CLAIM_KEY = "role";
17+
1418
private final RedisTemplate<String, String> redisTemplate;
1519
private final TokenProvider tokenProvider;
1620

17-
public AccessToken generateAccessToken(Subject subject) {
18-
String token = tokenProvider.generateToken(subject.value(), TokenType.ACCESS);
19-
return new AccessToken(subject, token);
21+
public AccessToken generateAccessToken(Subject subject, Role role) {
22+
String token = tokenProvider.generateToken(
23+
subject.value(), Map.of(ROLE_CLAIM_KEY, role.name()), TokenType.ACCESS
24+
);
25+
return new AccessToken(subject, role, token);
2026
}
2127

2228
public RefreshToken generateAndSaveRefreshToken(Subject subject) {
@@ -51,8 +57,4 @@ public Subject parseSubject(String token) {
5157
public Subject toSubject(SiteUser siteUser) {
5258
return new Subject(siteUser.getId().toString());
5359
}
54-
55-
public AccessToken toAccessToken(String token) {
56-
return new AccessToken(parseSubject(token), token);
57-
}
5860
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class SignInService {
1616
public SignInResponse signIn(SiteUser siteUser) {
1717
resetQuitedAt(siteUser);
1818
Subject subject = authTokenProvider.toSubject(siteUser);
19-
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
19+
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
2020
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
2121
return SignInResponse.of(accessToken, refreshToken);
2222
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import com.example.solidconnection.auth.domain.TokenType;
44
import io.jsonwebtoken.Claims;
5+
import java.util.Map;
56

67
public interface TokenProvider {
78

89
String generateToken(String string, TokenType tokenType);
910

11+
String generateToken(String string, Map<String, String> claims, TokenType tokenType);
12+
1013
String saveToken(String token, TokenType tokenType);
1114

1215
String parseSubject(String token);

src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.jsonwebtoken.Jwts;
1111
import io.jsonwebtoken.SignatureAlgorithm;
1212
import java.util.Date;
13+
import java.util.Map;
1314
import java.util.concurrent.TimeUnit;
1415
import lombok.RequiredArgsConstructor;
1516
import org.springframework.data.redis.core.RedisTemplate;
@@ -24,11 +25,21 @@ public class JwtTokenProvider implements TokenProvider {
2425

2526
@Override
2627
public final String generateToken(String string, TokenType tokenType) {
27-
Claims claims = Jwts.claims().setSubject(string);
28+
return generateJwtTokenValue(string, Map.of(), tokenType.getExpireTime());
29+
}
30+
31+
@Override
32+
public String generateToken(String string, Map<String, String> customClaims, TokenType tokenType) {
33+
return generateJwtTokenValue(string, customClaims, tokenType.getExpireTime());
34+
}
35+
36+
private String generateJwtTokenValue(String subject, Map<String, String> claims, long expireTime) {
37+
Claims jwtClaims = Jwts.claims().setSubject(subject);
38+
jwtClaims.putAll(claims);
2839
Date now = new Date();
29-
Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime());
40+
Date expiredDate = new Date(now.getTime() + expireTime);
3041
return Jwts.builder()
31-
.setClaims(claims)
42+
.setClaims(jwtClaims)
3243
.setIssuedAt(now)
3344
.setExpiration(expiredDate)
3445
.signWith(SignatureAlgorithm.HS512, jwtProperties.secret())

src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
1616
import com.example.solidconnection.support.TestContainerSpringBootTest;
1717
import java.time.LocalDate;
18+
import org.junit.jupiter.api.BeforeEach;
1819
import org.junit.jupiter.api.DisplayName;
1920
import org.junit.jupiter.api.Nested;
2021
import org.junit.jupiter.api.Test;
@@ -43,12 +44,19 @@ class AuthServiceTest {
4344
@Autowired
4445
private SiteUserRepository siteUserRepository;
4546

47+
private SiteUser siteUser;
48+
private Subject subject;
49+
private AccessToken accessToken;
50+
51+
@BeforeEach
52+
void setUp() {
53+
siteUser = siteUserFixture.사용자();
54+
subject = authTokenProvider.toSubject(siteUser);
55+
accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole());
56+
}
57+
4658
@Test
4759
void 로그아웃한다() {
48-
// given
49-
Subject subject = new Subject("subject");
50-
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
51-
5260
// when
5361
authService.signOut(accessToken.token());
5462

@@ -62,18 +70,13 @@ class AuthServiceTest {
6270

6371
@Test
6472
void 탈퇴한다() {
65-
// given
66-
SiteUser user = siteUserFixture.사용자();
67-
Subject subject = authTokenProvider.toSubject(user);
68-
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
69-
7073
// when
71-
authService.quit(user.getId(), accessToken.token());
74+
authService.quit(siteUser.getId(), accessToken.token());
7275

7376
// then
7477
LocalDate tomorrow = LocalDate.now().plusDays(1);
7578
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value());
76-
SiteUser actualSitUser = siteUserRepository.findById(user.getId()).orElseThrow();
79+
SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow();
7780
assertAll(
7881
() -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow),
7982
() -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(),
@@ -91,7 +94,7 @@ class 토큰을_재발급한다 {
9194
ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token());
9295

9396
// when
94-
ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest);
97+
ReissueResponse reissuedAccessToken = authService.reissue(siteUser.getId(), reissueRequest);
9598

9699
// then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다.
97100
Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token());
@@ -102,11 +105,11 @@ class 토큰을_재발급한다 {
102105
@Test
103106
void 요청의_리프레시_토큰이_저장되어있지_않다면_예외가_발생한다() {
104107
// given
105-
String invalidRefreshToken = authTokenProvider.generateAccessToken(new Subject("subject")).token();
108+
String invalidRefreshToken = accessToken.token();
106109
ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken);
107110

108111
// when, then
109-
assertThatCode(() -> authService.reissue(reissueRequest))
112+
assertThatCode(() -> authService.reissue(siteUser.getId(), reissueRequest))
110113
.isInstanceOf(CustomException.class)
111114
.hasMessage(REFRESH_TOKEN_EXPIRED.getMessage());
112115
}

src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertAll;
55

66
import com.example.solidconnection.auth.domain.TokenType;
7+
import com.example.solidconnection.siteuser.domain.Role;
78
import com.example.solidconnection.support.TestContainerSpringBootTest;
89
import org.junit.jupiter.api.BeforeEach;
910
import org.junit.jupiter.api.DisplayName;
@@ -32,11 +33,16 @@ void setUp() {
3233
@Test
3334
void 액세스_토큰을_생성한다() {
3435
// when
35-
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
36+
Role expectedRole = Role.MENTEE;
37+
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, expectedRole);
3638

3739
// then
3840
String actualSubject = authTokenProvider.parseSubject(accessToken.token()).value();
39-
assertThat(actualSubject).isEqualTo(subject.value());
41+
assertAll(
42+
() -> assertThat(actualSubject).isEqualTo(subject.value()),
43+
() -> assertThat(accessToken.role()).isEqualTo(expectedRole),
44+
() -> assertThat(accessToken.token()).isNotNull()
45+
);
4046
}
4147

4248
@Nested
@@ -61,7 +67,7 @@ class 리프레시_토큰을_제공한다 {
6167
void 유효한_리프레시_토큰인지_확인한다() {
6268
// given
6369
RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject);
64-
AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject);
70+
AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE);
6571

6672
// when, then
6773
assertAll(
@@ -74,7 +80,7 @@ class 리프레시_토큰을_제공한다 {
7480
void 액세스_토큰에_해당하는_리프레시_토큰을_삭제한다() {
7581
// given
7682
authTokenProvider.generateAndSaveRefreshToken(subject);
77-
AccessToken accessToken = authTokenProvider.generateAccessToken(subject);
83+
AccessToken accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE);
7884

7985
// when
8086
authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
@@ -88,7 +94,7 @@ class 리프레시_토큰을_제공한다 {
8894
@Test
8995
void 토큰으로부터_Subject_를_추출한다() {
9096
// given
91-
String accessToken = authTokenProvider.generateAccessToken(subject).token();
97+
String accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE).token();
9298

9399
// when
94100
Subject actualSubject = authTokenProvider.parseSubject(accessToken);

src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,46 @@ class JwtTokenProviderTest {
3535
@Autowired
3636
private RedisTemplate<String, String> redisTemplate;
3737

38-
@Test
39-
void 토큰을_생성한다() {
40-
// given
41-
String actualSubject = "subject123";
42-
TokenType actualTokenType = TokenType.ACCESS;
38+
@Nested
39+
class 토큰을_생성한다 {
4340

44-
// when
45-
String token = tokenProvider.generateToken(actualSubject, actualTokenType);
41+
@Test
42+
void subject_만_있는_토큰을_생성한다() {
43+
// given
44+
String actualSubject = "subject123";
45+
TokenType actualTokenType = TokenType.ACCESS;
4646

47-
// then - subject와 만료 시간이 일치하는지 검증
48-
Claims claims = tokenProvider.parseClaims(token);
49-
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
50-
assertAll(
51-
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
52-
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
53-
);
47+
// when
48+
String token = tokenProvider.generateToken(actualSubject, actualTokenType);
49+
50+
// then - subject와 만료 시간이 일치하는지 검증
51+
Claims claims = tokenProvider.parseClaims(token);
52+
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
53+
assertAll(
54+
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
55+
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
56+
);
57+
}
58+
59+
@Test
60+
void subject_와_claims_가_있는_토큰을_생성한다() {
61+
// given
62+
String actualSubject = "subject123";
63+
Map<String, String> customClaims = Map.of("key1", "value1", "key2", "value2");
64+
TokenType actualTokenType = TokenType.ACCESS;
65+
66+
// when
67+
String token = tokenProvider.generateToken(actualSubject, customClaims, actualTokenType);
68+
69+
// then - subject와 커스텀 클레임이 일치하는지 검증
70+
Claims claims = tokenProvider.parseClaims(token);
71+
long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
72+
assertAll(
73+
() -> assertThat(claims.getSubject()).isEqualTo(actualSubject),
74+
() -> assertThat(claims).containsAllEntriesOf(customClaims),
75+
() -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime())
76+
);
77+
}
5478
}
5579

5680
@Test

0 commit comments

Comments
 (0)