Skip to content

Commit 362720f

Browse files
authored
[T3-53] add redis service for refresh token (#8)
* chore: update directory structure * chore: update directory structure for model, request, response * feat: change the token generation method * feat: update JWT error code * feat: add redis hash for refreshToken * refactor: change class name to TokenResponse * feat: add redis service for refreshToken * feat: add an api for reissuing tokens * refactor: delete a subdirectory of jwt * refactor: move service logic from the controller layer to the service layer * refactor: move class about request or response from model to request or response * remove: delete unnecessary Kakao variables * refactor: update class name about user's auth * refactor: change to common error handling * refactor: change directory name to kakao from oauth2 * feat: add annotation in ProdRedisConfig * refactor: Change from RestTemplate to FeignClient * feat: add a nickname field to your kakao profile * refactor: change name from RedisService to AuthRedisService * refactor: change directory name from model, entity to domain * feat: add domain object to map Kakao, Apple membership information * refactor: change to use one social login for both Apple and Kakao * refactor: change to improved switch statements
1 parent b166295 commit 362720f

34 files changed

Lines changed: 470 additions & 399 deletions

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ repositories {
1717
mavenCentral()
1818
}
1919

20+
dependencyManagement {
21+
imports {
22+
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2025.0.0"
23+
}
24+
}
25+
2026
dependencies {
2127
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2228
implementation 'org.springframework.boot:spring-boot-starter-web'
2329
implementation 'org.springframework.boot:spring-boot-starter-security'
2430
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
2531
implementation 'org.springframework.boot:spring-boot-starter-validation'
32+
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
2633

2734
// jwt
2835
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'

config

src/main/java/bitnagil/bitnagil_backend/BitnagilBackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.cloud.openfeign.EnableFeignClients;
56

67
@SpringBootApplication
8+
@EnableFeignClients
79
public class BitnagilBackendApplication {
810

911
public static void main(String[] args) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package bitnagil.bitnagil_backend.auth.jwt;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.stereotype.Component;
6+
7+
import lombok.RequiredArgsConstructor;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
public class AuthRedisService {
12+
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
13+
14+
// 🔸 저장
15+
public void saveRefreshToken(Long userId, String token) {
16+
RefreshToken refreshToken = RefreshToken.builder()
17+
.userId(String.valueOf(userId))
18+
.refreshToken(token)
19+
.build();
20+
refreshTokenRedisRepository.save(refreshToken);
21+
}
22+
23+
// 🔸 조회 by userId
24+
public Optional<RefreshToken> getRefreshTokenByUserId(Long userId) {
25+
return refreshTokenRedisRepository.findById(String.valueOf(userId));
26+
}
27+
28+
// 🔸 조회 by refreshToken
29+
public Optional<RefreshToken> getRefreshTokenByToken(String token) {
30+
return refreshTokenRedisRepository.findByRefreshToken(token);
31+
}
32+
33+
// 🔸 삭제
34+
public void deleteRefreshToken(Long userId) {
35+
refreshTokenRedisRepository.deleteById(String.valueOf(userId));
36+
}
37+
}

src/main/java/bitnagil/bitnagil_backend/infrastructure/jwt/handler/JwtAccessDeniedHandler.java renamed to src/main/java/bitnagil/bitnagil_backend/auth/jwt/JwtAccessDeniedHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package bitnagil.bitnagil_backend.infrastructure.jwt.handler;
1+
package bitnagil.bitnagil_backend.auth.jwt;
22

33
import java.io.IOException;
44
import java.nio.charset.StandardCharsets;

src/main/java/bitnagil/bitnagil_backend/infrastructure/jwt/handler/JwtAuthenticationEntryPoint.java renamed to src/main/java/bitnagil/bitnagil_backend/auth/jwt/JwtAuthenticationEntryPoint.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package bitnagil.bitnagil_backend.infrastructure.jwt.handler;
1+
package bitnagil.bitnagil_backend.auth.jwt;
22

33
import java.io.IOException;
44
import java.nio.charset.StandardCharsets;

src/main/java/bitnagil/bitnagil_backend/infrastructure/jwt/service/JwtAuthenticationFilter.java renamed to src/main/java/bitnagil/bitnagil_backend/auth/jwt/JwtAuthenticationFilter.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package bitnagil.bitnagil_backend.infrastructure.jwt.service;
1+
package bitnagil.bitnagil_backend.auth.jwt;
22

33
import java.io.IOException;
44

@@ -21,7 +21,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
2121
public static final String AUTHORIZATION_HEADER = "Authorization";
2222
public static final String BEARER_PREFIX = "Bearer ";
2323

24-
private final JwtTokenProvider jwtTokenProvider;
24+
private final JwtProvider jwtProvider;
2525

2626
// 실제 필터링 로직은 doFilterInternal 에 들어감
2727
// JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext 에 저장하는 역할 수행
@@ -34,8 +34,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
3434

3535
// 2. validateToken 으로 토큰 유효성 검사
3636
// 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장
37-
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
38-
Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
37+
if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) {
38+
Authentication authentication = jwtProvider.getAuthentication(jwt);
3939
SecurityContextHolder.getContext().setAuthentication(authentication);
4040
}
4141

@@ -45,8 +45,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4545
// Request Header 에서 토큰 정보를 꺼내오기
4646
private String resolveToken(HttpServletRequest request) {
4747
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
48+
4849
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
49-
return bearerToken.substring(7);
50+
return bearerToken.substring(BEARER_PREFIX.length());
5051
}
5152
return null;
5253
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package bitnagil.bitnagil_backend.auth.jwt;
2+
3+
import java.security.Key;
4+
import java.util.Collection;
5+
import java.util.Collections;
6+
import java.util.Date;
7+
8+
import org.springframework.beans.factory.annotation.Value;
9+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.security.core.GrantedAuthority;
12+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
13+
import org.springframework.stereotype.Service;
14+
15+
import bitnagil.bitnagil_backend.global.errorcode.ErrorCode;
16+
import bitnagil.bitnagil_backend.global.exception.CustomException;
17+
import bitnagil.bitnagil_backend.user.Repository.UserRepository;
18+
import bitnagil.bitnagil_backend.user.domain.User;
19+
import io.jsonwebtoken.Claims;
20+
import io.jsonwebtoken.ExpiredJwtException;
21+
import io.jsonwebtoken.Jwts;
22+
import io.jsonwebtoken.MalformedJwtException;
23+
import io.jsonwebtoken.SignatureAlgorithm;
24+
import io.jsonwebtoken.UnsupportedJwtException;
25+
import io.jsonwebtoken.io.Decoders;
26+
import io.jsonwebtoken.security.Keys;
27+
import jakarta.annotation.PostConstruct;
28+
import lombok.RequiredArgsConstructor;
29+
import lombok.extern.slf4j.Slf4j;
30+
31+
@Slf4j
32+
@Service
33+
@RequiredArgsConstructor
34+
public class JwtProvider {
35+
private final AuthRedisService authRedisService;
36+
37+
@Value("${jwt.secret}")
38+
private String secretKey;
39+
40+
private static final Long ACCESS_TOKEN_EXPIRE_TIME = (long)(1000 * 60 * 30); // 30분
41+
private static final Long REFRESH_TOKEN_EXPIRE_TIME = (long)(1000 * 60 * 60 * 24 * 7);
42+
private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
43+
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";// 7일
44+
45+
private Key key;
46+
47+
private final UserRepository userRepository;
48+
49+
// jwt.secret을 사용해서 암호화 키값 생성
50+
@PostConstruct
51+
protected void init() {
52+
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
53+
this.key = Keys.hmacShaKeyFor(keyBytes);
54+
}
55+
56+
public Token generateToken(Long userId) {
57+
Date now = new Date();
58+
59+
// Access Token 생성
60+
Date accessTokenExpiresIn = new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME);
61+
62+
String accessToken = Jwts.builder()
63+
.setSubject(ACCESS_TOKEN_SUBJECT)
64+
.claim("userId", userId)
65+
.setExpiration(accessTokenExpiresIn)
66+
.signWith(key, SignatureAlgorithm.HS512)
67+
.compact();
68+
69+
// Refresh Token 생성
70+
Date refreshTokenExpiresIn = new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_TIME);
71+
String refreshToken = Jwts.builder()
72+
.setSubject(REFRESH_TOKEN_SUBJECT)
73+
.claim("userId", userId)
74+
.setExpiration(refreshTokenExpiresIn)
75+
.signWith(key, SignatureAlgorithm.HS512)
76+
.compact();
77+
78+
authRedisService.saveRefreshToken(userId, refreshToken);
79+
80+
return Token.builder()
81+
.accessToken(accessToken)
82+
.accessTokenExpiresIn(accessTokenExpiresIn.getTime())
83+
.refreshToken(refreshToken)
84+
.build();
85+
}
86+
87+
public Authentication getAuthentication(String accessToken) {
88+
// 토큰 복호화
89+
Claims claims = parseClaims(accessToken);
90+
91+
Long userId = Long.valueOf(claims.get("userId", Integer.class));
92+
User user = userRepository.findById(userId).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER));
93+
94+
return new UsernamePasswordAuthenticationToken(user, null, getAuthorities(user));
95+
// TODO 추후 회원탈퇴한 유저를 어떻게 관리하는지에 따라 추가 검증 필요
96+
}
97+
98+
public boolean validateToken(String token) {
99+
try {
100+
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
101+
102+
return claims.getExpiration().after(new Date());
103+
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
104+
throw new CustomException(ErrorCode.INVALID_JWT_SIGNATURE);
105+
} catch (ExpiredJwtException e) {
106+
throw new CustomException(ErrorCode.EXPIRED_JWT_TOKEN);
107+
} catch (UnsupportedJwtException e) {
108+
throw new CustomException(ErrorCode.UNSUPPORTED_JWT_TOKEN);
109+
} catch (IllegalArgumentException e) {
110+
throw new CustomException(ErrorCode.INVALID_JWT_TOKEN);
111+
}
112+
}
113+
114+
public Claims parseClaims(String accessToken) {
115+
try {
116+
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
117+
} catch (ExpiredJwtException e) {
118+
throw new CustomException(ErrorCode.EXPIRED_JWT_TOKEN);
119+
}
120+
}
121+
122+
private Collection<GrantedAuthority> getAuthorities(User user) {
123+
return Collections.singletonList(
124+
new SimpleGrantedAuthority("ROLE_" + user.getRole().toString())
125+
);
126+
}
127+
128+
129+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package bitnagil.bitnagil_backend.auth.jwt;
2+
3+
import org.springframework.data.annotation.Id;
4+
import org.springframework.data.redis.core.RedisHash;
5+
import org.springframework.data.redis.core.index.Indexed;
6+
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Getter
12+
@NoArgsConstructor
13+
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 7) // 토큰 만료 기한 7일
14+
public class RefreshToken {
15+
16+
@Id
17+
private String userId;
18+
19+
@Indexed
20+
private String refreshToken;
21+
22+
@Builder
23+
public RefreshToken(String userId, String refreshToken) {
24+
this.userId = userId;
25+
this.refreshToken = refreshToken;
26+
}
27+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package bitnagil.bitnagil_backend.auth.jwt;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.data.repository.CrudRepository;
6+
7+
public interface RefreshTokenRedisRepository extends CrudRepository<RefreshToken, String> {
8+
Optional<RefreshToken> findByRefreshToken(String refreshToken);
9+
}

0 commit comments

Comments
 (0)