diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 57a42223d..8940c108e 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -113,9 +113,10 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( + @AuthorizedUser long siteUserId, @Valid @RequestBody ReissueRequest reissueRequest ) { - ReissueResponse reissueResponse = authService.reissue(reissueRequest); + ReissueResponse reissueResponse = authService.reissue(siteUserId, reissueRequest); return ResponseEntity.ok(reissueResponse); } diff --git a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java index c94e891aa..3456a2171 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java +++ b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java @@ -1,11 +1,14 @@ package com.example.solidconnection.auth.service; +import com.example.solidconnection.siteuser.domain.Role; + public record AccessToken( Subject subject, + Role role, String token ) { - public AccessToken(String subject, String token) { - this(new Subject(subject), token); + public AccessToken(String subject, Role role, String token) { + this(new Subject(subject), role, token); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 2737360c2..0d54c7672 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -28,7 +28,12 @@ public class AuthService { * - 리프레시 토큰을 삭제한다. * */ public void signOut(String token) { - AccessToken accessToken = authTokenProvider.toAccessToken(token); + Subject subject = authTokenProvider.parseSubject(token); + long siteUserId = Long.parseLong(subject.value()); + SiteUser siteUser = siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + + AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); tokenBlackListService.addToBlacklist(accessToken); } @@ -53,15 +58,17 @@ public void quit(long siteUserId, String token) { * - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다. * - 그렇지 않으면 예외를 발생시킨다. * */ - public ReissueResponse reissue(ReissueRequest reissueRequest) { + public ReissueResponse reissue(long siteUserId, ReissueRequest reissueRequest) { // 리프레시 토큰 확인 String requestedRefreshToken = reissueRequest.refreshToken(); if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 + SiteUser siteUser = siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); Subject subject = authTokenProvider.parseSubject(requestedRefreshToken); - AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject); + AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); return ReissueResponse.from(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index f15a3e7b4..6f335256e 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -1,7 +1,9 @@ package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; +import java.util.Map; import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -11,12 +13,16 @@ @RequiredArgsConstructor public class AuthTokenProvider { + private static final String ROLE_CLAIM_KEY = "role"; + private final RedisTemplate redisTemplate; private final TokenProvider tokenProvider; - public AccessToken generateAccessToken(Subject subject) { - String token = tokenProvider.generateToken(subject.value(), TokenType.ACCESS); - return new AccessToken(subject, token); + public AccessToken generateAccessToken(Subject subject, Role role) { + String token = tokenProvider.generateToken( + subject.value(), Map.of(ROLE_CLAIM_KEY, role.name()), TokenType.ACCESS + ); + return new AccessToken(subject, role, token); } public RefreshToken generateAndSaveRefreshToken(Subject subject) { @@ -51,8 +57,4 @@ public Subject parseSubject(String token) { public Subject toSubject(SiteUser siteUser) { return new Subject(siteUser.getId().toString()); } - - public AccessToken toAccessToken(String token) { - return new AccessToken(parseSubject(token), token); - } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index c2b129214..2b6a57cbe 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -16,7 +16,7 @@ public class SignInService { public SignInResponse signIn(SiteUser siteUser) { resetQuitedAt(siteUser); Subject subject = authTokenProvider.toSubject(siteUser); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); return SignInResponse.of(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 2cd93fb8e..22120b084 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -2,11 +2,14 @@ import com.example.solidconnection.auth.domain.TokenType; import io.jsonwebtoken.Claims; +import java.util.Map; public interface TokenProvider { String generateToken(String string, TokenType tokenType); + String generateToken(String string, Map claims, TokenType tokenType); + String saveToken(String token, TokenType tokenType); String parseSubject(String token); diff --git a/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java index f9f5b20ff..d7c968ccf 100644 --- a/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java @@ -10,6 +10,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; +import java.util.Map; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -24,11 +25,21 @@ public class JwtTokenProvider implements TokenProvider { @Override public final String generateToken(String string, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(string); + return generateJwtTokenValue(string, Map.of(), tokenType.getExpireTime()); + } + + @Override + public String generateToken(String string, Map customClaims, TokenType tokenType) { + return generateJwtTokenValue(string, customClaims, tokenType.getExpireTime()); + } + + private String generateJwtTokenValue(String subject, Map claims, long expireTime) { + Claims jwtClaims = Jwts.claims().setSubject(subject); + jwtClaims.putAll(claims); Date now = new Date(); - Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); + Date expiredDate = new Date(now.getTime() + expireTime); return Jwts.builder() - .setClaims(claims) + .setClaims(jwtClaims) .setIssuedAt(now) .setExpiration(expiredDate) .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 88b835ee2..707c3dbbb 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -15,6 +15,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -43,12 +44,19 @@ class AuthServiceTest { @Autowired private SiteUserRepository siteUserRepository; + private SiteUser siteUser; + private Subject subject; + private AccessToken accessToken; + + @BeforeEach + void setUp() { + siteUser = siteUserFixture.사용자(); + subject = authTokenProvider.toSubject(siteUser); + accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); + } + @Test void 로그아웃한다() { - // given - Subject subject = new Subject("subject"); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject); - // when authService.signOut(accessToken.token()); @@ -62,18 +70,13 @@ class AuthServiceTest { @Test void 탈퇴한다() { - // given - SiteUser user = siteUserFixture.사용자(); - Subject subject = authTokenProvider.toSubject(user); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject); - // when - authService.quit(user.getId(), accessToken.token()); + authService.quit(siteUser.getId(), accessToken.token()); // then LocalDate tomorrow = LocalDate.now().plusDays(1); String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); - SiteUser actualSitUser = siteUserRepository.findById(user.getId()).orElseThrow(); + SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow(); assertAll( () -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow), () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), @@ -91,7 +94,7 @@ class 토큰을_재발급한다 { ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token()); // when - ReissueResponse reissuedAccessToken = authService.reissue(reissueRequest); + ReissueResponse reissuedAccessToken = authService.reissue(siteUser.getId(), reissueRequest); // then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다. Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token()); @@ -102,11 +105,11 @@ class 토큰을_재발급한다 { @Test void 요청의_리프레시_토큰이_저장되어있지_않다면_예외가_발생한다() { // given - String invalidRefreshToken = authTokenProvider.generateAccessToken(new Subject("subject")).token(); + String invalidRefreshToken = accessToken.token(); ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken); // when, then - assertThatCode(() -> authService.reissue(reissueRequest)) + assertThatCode(() -> authService.reissue(siteUser.getId(), reissueRequest)) .isInstanceOf(CustomException.class) .hasMessage(REFRESH_TOKEN_EXPIRED.getMessage()); } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index b2e588a59..6a7cc40a3 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.support.TestContainerSpringBootTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,11 +33,16 @@ void setUp() { @Test void 액세스_토큰을_생성한다() { // when - AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + Role expectedRole = Role.MENTEE; + AccessToken accessToken = authTokenProvider.generateAccessToken(subject, expectedRole); // then String actualSubject = authTokenProvider.parseSubject(accessToken.token()).value(); - assertThat(actualSubject).isEqualTo(subject.value()); + assertAll( + () -> assertThat(actualSubject).isEqualTo(subject.value()), + () -> assertThat(accessToken.role()).isEqualTo(expectedRole), + () -> assertThat(accessToken.token()).isNotNull() + ); } @Nested @@ -61,7 +67,7 @@ class 리프레시_토큰을_제공한다 { void 유효한_리프레시_토큰인지_확인한다() { // given RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); - AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject); + AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE); // when, then assertAll( @@ -74,7 +80,7 @@ class 리프레시_토큰을_제공한다 { void 액세스_토큰에_해당하는_리프레시_토큰을_삭제한다() { // given authTokenProvider.generateAndSaveRefreshToken(subject); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject); + AccessToken accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE); // when authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); @@ -88,7 +94,7 @@ class 리프레시_토큰을_제공한다 { @Test void 토큰으로부터_Subject_를_추출한다() { // given - String accessToken = authTokenProvider.generateAccessToken(subject).token(); + String accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE).token(); // when Subject actualSubject = authTokenProvider.parseSubject(accessToken); diff --git a/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java index 813a827b3..62655df2a 100644 --- a/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/JwtTokenProviderTest.java @@ -35,22 +35,46 @@ class JwtTokenProviderTest { @Autowired private RedisTemplate redisTemplate; - @Test - void 토큰을_생성한다() { - // given - String actualSubject = "subject123"; - TokenType actualTokenType = TokenType.ACCESS; + @Nested + class 토큰을_생성한다 { - // when - String token = tokenProvider.generateToken(actualSubject, actualTokenType); + @Test + void subject_만_있는_토큰을_생성한다() { + // given + String actualSubject = "subject123"; + TokenType actualTokenType = TokenType.ACCESS; - // then - subject와 만료 시간이 일치하는지 검증 - Claims claims = tokenProvider.parseClaims(token); - long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); - assertAll( - () -> assertThat(claims.getSubject()).isEqualTo(actualSubject), - () -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime()) - ); + // when + String token = tokenProvider.generateToken(actualSubject, actualTokenType); + + // then - subject와 만료 시간이 일치하는지 검증 + Claims claims = tokenProvider.parseClaims(token); + long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); + assertAll( + () -> assertThat(claims.getSubject()).isEqualTo(actualSubject), + () -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime()) + ); + } + + @Test + void subject_와_claims_가_있는_토큰을_생성한다() { + // given + String actualSubject = "subject123"; + Map customClaims = Map.of("key1", "value1", "key2", "value2"); + TokenType actualTokenType = TokenType.ACCESS; + + // when + String token = tokenProvider.generateToken(actualSubject, customClaims, actualTokenType); + + // then - subject와 커스텀 클레임이 일치하는지 검증 + Claims claims = tokenProvider.parseClaims(token); + long expectedExpireTime = claims.getExpiration().getTime() - claims.getIssuedAt().getTime(); + assertAll( + () -> assertThat(claims.getSubject()).isEqualTo(actualSubject), + () -> assertThat(claims).containsAllEntriesOf(customClaims), + () -> assertThat(expectedExpireTime).isEqualTo(actualTokenType.getExpireTime()) + ); + } } @Test diff --git a/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java index 49328315a..5267f88f3 100644 --- a/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/TokenBlackListServiceTest.java @@ -4,12 +4,16 @@ import static org.assertj.core.api.Assertions.assertThat; import com.example.solidconnection.auth.token.TokenBlackListService; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; +@DisplayName("토큰 블랙리스트 서비스 테스트") @TestContainerSpringBootTest class TokenBlackListServiceTest { @@ -19,11 +23,16 @@ class TokenBlackListServiceTest { @Autowired private RedisTemplate redisTemplate; + private AccessToken accessToken; + + @BeforeEach + void setUp() { + accessToken = new AccessToken("subject", Role.MENTEE, "token"); + } + + @Test void 액세스_토큰을_블랙리스트에_추가한다() { - // given - AccessToken accessToken = new AccessToken("subject", "token"); - // when tokenBlackListService.addToBlacklist(accessToken); @@ -39,7 +48,6 @@ class 블랙리스트에_있는_토큰인지_확인한다 { @Test void 블랙리스트에_토큰이_있는_경우() { // given - AccessToken accessToken = new AccessToken("subject", "token"); tokenBlackListService.addToBlacklist(accessToken); // when, then @@ -48,9 +56,6 @@ class 블랙리스트에_있는_토큰인지_확인한다 { @Test void 블랙리스트에_토큰이_없는_경우() { - // given - AccessToken accessToken = new AccessToken("subject", "token"); - // when, then assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isFalse(); }