Skip to content

Commit 45bfcfd

Browse files
authored
�feat: JWT 기능 구현 (#30)
* feat(jwt): Add JJWT dependencies - jjwt-api, jjwt-impl, jjwt-jackson 의존성 추가 (버전 0.12.6) * feat(jwt): JWT 처리용 인터페이스 추가 - 토큰 생성을 위한 JwtGenerator 인터페이스 추가 - 토큰 파싱을 위한 JwtParser 인터페이스 추가 - 토큰 검증을 위한 JwtValidator 인터페이스 추가 - JWT 작업 통합을 위한 JwtUseCase 인터페이스 추가 * feat(jwt): JWT 처리용 클래스 구현 및 시크릿 키 관리 설정 추가 - JwtGenerator, JwtParser, JwtValidator, JwtUseCase 인터페이스의 구현체 추가 - HS256 알고리즘을 사용 - SecretKey 관리를 JwtConfig로 통합 - Refresh token을 Redis에 저장하는 작업은 미완성 * feat(TokenType): 토큰 타입 구분을 위한 ENUM 추가 - 작동 시간 ms 단위 저장 및 s 단위 반환 기능 구현 * feat(auth): EncodedToken 레코드 추가 - JWT 토큰의 Encoded Value를 표현하기 위한 레코드 클래스 추가 * refactor(UserRole): 패키지 이동 * feat(JwtGenerator): HmacJwtGenerator 개선 및 EncodedToken 반환 방식으로 변경 - JWT 생성 로직 수정: - 기존 String 반환 방식에서 EncodedToken 객체 반환 방식으로 변경 - TokenType을 사용하여 토큰 유효 기간 동적으로 설정 가능 (ACCESS_TOKEN, REFRESH_TOKEN 구분) - 클레임(Claims) 생성 방식 개선: - buildClaims 메서드로 사용자 ID와 역할(role) 추가 * feat(Jwt): 토큰 관리 방식을 String에서 EncodedToken으로 변경 * feat(RefreshToken): Redis 기반 RefreshToken 관리 기능 구현 - RefreshToken 엔티티 추가: - @RedisHash 어노테이션으로 Redis에 저장 - userId(@id)를 Redis 키로 사용하여 유저별 고유 RefreshToken 관리 - TTL(Time-to-Live) 설정: 7일 - accessToken 갱신 메서드 추가 - 매니저 주요 기능: - AccessToken으로 RefreshToken 조회 - RefreshToken 저장 - RefreshToken 삭제 (로그아웃 구현 예정) * feat(JwtRefresher): AccessToken 갱신 로직 추가 - RefreshTokenManager를 통해 RefreshToken 조회 - validateToken 메서드로 RefreshToken 검증 - JwtParser를 통해 AccessToken의 Claims 정보 추출 - generateAccessToken 메서드로 새로운 AccessToken 생성 및 갱신 - 갱신된 RefreshToken을 RefreshTokenManager를 통해 저장 * feat(Jwt): JWT 통합 관리 기능 추가 - 주요 로직: - JwtGenerator를 통해 AccessToken 및 RefreshToken 생성 - JwtValidator로 토큰 유효성 검증: - 유효하지 않은 경우 JwtRefresher를 통해 AccessToken 갱신 - JwtParser로 클레임 정보 추출 * feat(GenerateTokensOnLogin): 로그인 시 AccessToken 및 RefreshToken 생성 로직 구현 * style: delete unused import * fix(jacocoPattern): Jwt -> jwt 오타 수정 * fix(jacocoPattern): *jwt* -> jwt/* 수정 * feat(TokenType): token period 타입 변경 - long to int * fix(jacocoPattern): **/jwt/* -> **/jwt/** * style: 마지막 라인 개행
1 parent 37b08bb commit 45bfcfd

22 files changed

+439
-2
lines changed

build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ dependencies {
4848
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
4949
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
5050
implementation 'org.springframework.boot:spring-boot-starter-security'
51+
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
52+
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
53+
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
54+
5155

5256
// Web Layer
5357
implementation 'org.springframework.boot:spring-boot-starter-web'
@@ -105,8 +109,8 @@ def jacocoExcludePatterns = [
105109
'**/*Response*',
106110
'**/*Entity*',
107111
'**/*Dto*',
108-
'**/*Jwt*',
109-
'**/auth/*',
112+
'**/jwt/**',
113+
'**/auth/**',
110114
'**/domain/*',
111115
'**/domains/*',
112116
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.somemore.auth.jwt.config;
2+
3+
import io.jsonwebtoken.security.Keys;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
8+
import javax.crypto.SecretKey;
9+
import java.nio.charset.StandardCharsets;
10+
11+
@Configuration
12+
public class JwtConfig {
13+
14+
@Bean
15+
public SecretKey secretKey(@Value("${jwt.secret}") String rawSecretKey) {
16+
return Keys.hmacShaKeyFor(rawSecretKey.getBytes(StandardCharsets.UTF_8));
17+
}
18+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.somemore.auth.jwt.domain;
2+
3+
public record EncodedToken(String value) {
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.somemore.auth.jwt.domain;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum TokenType {
7+
ACCESS(1000 * 60 * 30),
8+
REFRESH(1000 * 60 * 60 * 24 * 7);
9+
10+
private final int period;
11+
12+
TokenType(int period) {
13+
this.period = period;
14+
}
15+
16+
public int getPeriodInSeconds() {
17+
return Math.toIntExact(period / 1000);
18+
}
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.somemore.auth.jwt.domain;
2+
3+
public enum UserRole {
4+
VOLUNTEER,
5+
CENTER,
6+
ADMIN
7+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.somemore.auth.jwt.generator;
2+
3+
import com.somemore.auth.jwt.domain.EncodedToken;
4+
import com.somemore.auth.jwt.domain.TokenType;
5+
import io.jsonwebtoken.Claims;
6+
import io.jsonwebtoken.Jwts;
7+
import io.jsonwebtoken.security.MacAlgorithm;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
11+
import javax.crypto.SecretKey;
12+
import java.time.Instant;
13+
import java.util.Date;
14+
15+
@Component
16+
@RequiredArgsConstructor
17+
public class HmacJwtGenerator implements JwtGenerator {
18+
19+
public static final MacAlgorithm ALGORITHM = Jwts.SIG.HS256;
20+
private final SecretKey secretKey;
21+
22+
public EncodedToken generateToken(String userId, String role, TokenType tokenType) {
23+
Claims claims = buildClaims(userId, role);
24+
Instant now = Instant.now();
25+
Instant expiration = now.plusMillis(tokenType.getPeriod());
26+
27+
return new EncodedToken(Jwts.builder()
28+
.claims(claims)
29+
.issuedAt(Date.from(now))
30+
.expiration(Date.from(expiration))
31+
.signWith(secretKey, ALGORITHM)
32+
.compact());
33+
}
34+
35+
private static Claims buildClaims(String userId, String role) {
36+
final String ID = "id";
37+
final String ROLE = "role";
38+
39+
return Jwts.claims()
40+
.add(ID, userId)
41+
.add(ROLE, role)
42+
.build();
43+
}
44+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.somemore.auth.jwt.generator;
2+
3+
import com.somemore.auth.jwt.domain.EncodedToken;
4+
import com.somemore.auth.jwt.domain.TokenType;
5+
6+
public interface JwtGenerator {
7+
EncodedToken generateToken(String userId, String role, TokenType tokenType);
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.somemore.auth.jwt.parser;
2+
3+
import com.somemore.auth.jwt.domain.EncodedToken;
4+
import io.jsonwebtoken.Claims;
5+
import io.jsonwebtoken.Jwts;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Component;
8+
9+
import javax.crypto.SecretKey;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class DefaultJwtParser implements JwtParser {
14+
15+
private final SecretKey secretKey;
16+
17+
public Claims parseToken(EncodedToken token) {
18+
return Jwts.parser()
19+
.verifyWith(secretKey)
20+
.build()
21+
.parseSignedClaims(token.value())
22+
.getPayload();
23+
}
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.somemore.auth.jwt.parser;
2+
3+
import com.somemore.auth.jwt.domain.EncodedToken;
4+
import io.jsonwebtoken.Claims;
5+
6+
public interface JwtParser {
7+
Claims parseToken(EncodedToken token);
8+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.somemore.auth.jwt.refresh.domain;
2+
3+
import com.somemore.auth.jwt.domain.EncodedToken;
4+
import lombok.AccessLevel;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import org.springframework.data.annotation.Id;
9+
import org.springframework.data.redis.core.RedisHash;
10+
import org.springframework.data.redis.core.index.Indexed;
11+
12+
@Getter
13+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
14+
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 7)
15+
public class RefreshToken {
16+
17+
@Id
18+
private String userId;
19+
20+
@Indexed
21+
private String accessToken;
22+
private String refreshToken;
23+
24+
@Builder
25+
public RefreshToken(String userId, EncodedToken accessToken, EncodedToken refreshToken) {
26+
this.userId = userId;
27+
this.accessToken = accessToken.value();
28+
this.refreshToken = refreshToken.value();
29+
}
30+
31+
public void updateAccessToken(EncodedToken accessToken) {
32+
this.accessToken = accessToken.value();
33+
}
34+
}

0 commit comments

Comments
 (0)