Skip to content

Commit b9921f0

Browse files
author
STGRAM\gffd9
committed
refreshToken, Blacklist 관리 추가
1 parent 1cb0fde commit b9921f0

File tree

7 files changed

+183
-9
lines changed

7 files changed

+183
-9
lines changed

src/main/java/com/oronaminc/join/global/config/CacheType.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
@AllArgsConstructor
66
public enum CacheType {
77
ROOM_BY_ID("roomById", 300, 1000),
8-
ROOM_BY_SECRET_CODE("roomBySecretCode", 300, 1000)
9-
;
10-
8+
ROOM_BY_SECRET_CODE("roomBySecretCode", 300, 1000),
9+
REFRESH_LATEST("refreshLatest", 60 * 60 * 24 * 14, 100_000), // 14일
10+
REFRESH_BLACKLIST("refreshBlacklist", 60 * 60 * 24 * 14, 100_000);
1111
public final String cacheName;
12-
public final long expireAfterWrite;
13-
public final long maximumSize;
12+
public final int expireAfterWrite;
13+
public final int maximumSize;
14+
1415
}

src/main/java/com/oronaminc/join/member/security/AuthController.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44
import com.oronaminc.join.member.dto.GuestLoginRequest;
55
import com.oronaminc.join.member.dto.KakaoLoginRequest;
66
import com.oronaminc.join.member.token.AuthTokenResponse;
7+
import com.oronaminc.join.member.token.JwtTokenProvider;
78
import com.oronaminc.join.member.token.JwtUtils;
89
import com.oronaminc.join.member.token.LoginResponse;
10+
import com.oronaminc.join.member.token.RefreshTokenStore;
911
import io.swagger.v3.oas.annotations.Operation;
1012
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1113
import io.swagger.v3.oas.annotations.tags.Tag;
1214
import jakarta.servlet.http.Cookie;
1315
import jakarta.servlet.http.HttpServletRequest;
1416
import jakarta.servlet.http.HttpServletResponse;
15-
import jakarta.servlet.http.HttpSession;
1617
import jakarta.validation.Valid;
1718
import java.util.Map;
1819
import lombok.RequiredArgsConstructor;
1920
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.ResponseCookie;
2022
import org.springframework.security.core.context.SecurityContextHolder;
2123
import org.springframework.web.bind.annotation.PostMapping;
2224
import org.springframework.web.bind.annotation.RequestBody;
@@ -31,6 +33,8 @@
3133
public class AuthController {
3234

3335
private final AuthService authService;
36+
private final JwtTokenProvider jwtTokenProvider;
37+
private final RefreshTokenStore refreshTokenStore;
3438

3539
@Operation(
3640
summary = "카카오 로그인"
@@ -84,7 +88,30 @@ public Map<String, AuthTokenResponse> guestLogin(
8488
@PostMapping("/logout")
8589
@ResponseStatus(HttpStatus.NO_CONTENT)
8690
public void logout(HttpServletRequest request, HttpServletResponse response) {
87-
HttpSession session = request.getSession();
91+
92+
String refresh = null;
93+
if(request.getCookies() != null){
94+
for (Cookie cookie : request.getCookies()) {
95+
if ("refreshToken".equals(cookie.getName())) refresh = cookie.getValue();
96+
}
97+
}
98+
99+
if (refresh != null) {
100+
try {
101+
var body = jwtTokenProvider.parseClaims(refresh);
102+
refreshTokenStore.isBlacklisted(refresh);
103+
refreshTokenStore.saveLatest(body.memberId(), "");
104+
}catch (Exception ignored){ }
105+
}
106+
107+
// 쿠키 제거
108+
ResponseCookie expired = ResponseCookie.from("refreshToken", "")
109+
.httpOnly(true).secure(true).sameSite("None")
110+
.path("/").maxAge(0).build();
111+
112+
SecurityContextHolder.clearContext();
113+
114+
/*HttpSession session = request.getSession();
88115
if (session != null) {
89116
session.invalidate();
90117
}
@@ -95,6 +122,6 @@ public void logout(HttpServletRequest request, HttpServletResponse response) {
95122
cookie.setPath("/");
96123
cookie.setHttpOnly(true);
97124
cookie.setMaxAge(0);
98-
response.addCookie(cookie);
125+
response.addCookie(cookie);*/
99126
}
100127
}

src/main/java/com/oronaminc/join/member/security/AuthService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.oronaminc.join.member.token.JwtMemberInfo;
1212
import com.oronaminc.join.member.token.JwtTokenProvider;
1313
import com.oronaminc.join.member.token.LoginResponse;
14+
import com.oronaminc.join.member.token.RefreshTokenStore;
1415
import com.oronaminc.join.member.token.TokenPair;
1516
import com.oronaminc.join.member.util.MemberMapper;
1617
import java.util.Map;
@@ -37,6 +38,7 @@ public class AuthService extends DefaultOAuth2UserService {
3738
private final MemberRepository memberRepository;
3839
private final MemberReader memberReader;
3940
private final JwtTokenProvider jwtTokenProvider;
41+
private final RefreshTokenStore refreshTokenStore;
4042

4143
private final RestTemplate restTemplate = new RestTemplate();
4244

@@ -79,6 +81,7 @@ public LoginResponse loadGuest(GuestLoginRequest guestLoginRequest) {
7981

8082
TokenPair tokenPair = jwtTokenProvider.generateTokenPair(
8183
new JwtMemberInfo(guest.getId(), guest.getNickname(), guest.getMemberType()));
84+
refreshTokenStore.saveLatest(guest.getId(), tokenPair.refreshToken());
8285

8386
AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(),
8487
tokenPair.accessTokenExpiresIn(), guest.getId(),
@@ -99,6 +102,8 @@ public LoginResponse kakaoLogin(String code) {
99102
TokenPair tokenPair = jwtTokenProvider.generateTokenPair(
100103
new JwtMemberInfo(member.getId(), member.getNickname(), member.getMemberType()));
101104

105+
refreshTokenStore.saveLatest(member.getId(), tokenPair.refreshToken());
106+
102107
AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(),
103108
tokenPair.accessTokenExpiresIn(), member.getId(),
104109
member.getNickname(), member.getMemberType());
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.oronaminc.join.member.security;
2+
3+
import com.oronaminc.join.member.token.JwtTokenProvider;
4+
import com.oronaminc.join.member.token.TokenBody;
5+
import jakarta.servlet.FilterChain;
6+
import jakarta.servlet.ServletException;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import java.io.IOException;
10+
import org.springframework.http.HttpHeaders;
11+
import java.util.List;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
14+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
import org.springframework.util.StringUtils;
17+
import org.springframework.web.filter.OncePerRequestFilter;
18+
19+
@RequiredArgsConstructor
20+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
21+
22+
private final JwtTokenProvider jwtTokenProvider;
23+
24+
25+
@Override
26+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
27+
FilterChain filterChain) throws ServletException, IOException {
28+
29+
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
30+
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
31+
String token = header.substring(7);
32+
try {
33+
TokenBody body = jwtTokenProvider.parseClaims(token);
34+
var auth = new UsernamePasswordAuthenticationToken(
35+
body.memberId(),
36+
null,
37+
List.of(new SimpleGrantedAuthority(body.role().name()))
38+
);
39+
SecurityContextHolder.getContext().setAuthentication(auth);
40+
}catch (Exception e){
41+
42+
}
43+
}
44+
45+
filterChain.doFilter(request,response);
46+
}
47+
}

src/main/java/com/oronaminc/join/member/security/SecurityConfig.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.oronaminc.join.member.security;
22

3+
import com.oronaminc.join.member.token.JwtTokenProvider;
34
import lombok.RequiredArgsConstructor;
45
import org.springframework.context.annotation.Bean;
56
import org.springframework.context.annotation.Configuration;
67
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
78
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
89
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
910
import org.springframework.security.web.SecurityFilterChain;
11+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1012
import org.springframework.web.cors.CorsConfiguration;
1113
import org.springframework.web.cors.CorsConfigurationSource;
1214
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -23,7 +25,7 @@ public class SecurityConfig {
2325
private final AuthService authService;
2426

2527
@Bean
26-
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
28+
public SecurityFilterChain filterChain(HttpSecurity http, JwtTokenProvider jwt) throws Exception {
2729
return http
2830
.csrf(csrf -> csrf.disable())
2931
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
@@ -55,6 +57,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5557
.formLogin(AbstractHttpConfigurer::disable)
5658
.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo
5759
.userService(authService)))
60+
.addFilterBefore(new JwtAuthenticationFilter(jwt),
61+
UsernamePasswordAuthenticationFilter.class)
5862
.logout(withDefaults())
5963
.build();
6064
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.oronaminc.join.member.security;
2+
3+
import com.oronaminc.join.member.token.AuthTokenResponse;
4+
import com.oronaminc.join.member.token.JwtMemberInfo;
5+
import com.oronaminc.join.member.token.JwtTokenProvider;
6+
import com.oronaminc.join.member.token.JwtUtils;
7+
import com.oronaminc.join.member.token.RefreshTokenStore;
8+
import com.oronaminc.join.member.token.TokenBody;
9+
import com.oronaminc.join.member.token.TokenPair;
10+
import jakarta.servlet.http.Cookie;
11+
import jakarta.servlet.http.HttpServletRequest;
12+
import jakarta.servlet.http.HttpServletResponse;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RestController;
17+
18+
@RestController
19+
@RequestMapping("/api/auth/token")
20+
@RequiredArgsConstructor
21+
public class TokenController {
22+
23+
private final JwtTokenProvider jwtTokenProvider;
24+
private final RefreshTokenStore refreshTokenStore;
25+
26+
@PostMapping("/refresh")
27+
public AuthTokenResponse refresh(HttpServletRequest request, HttpServletResponse response) {
28+
String refresh = extraRefreshCookie(request);
29+
if (refresh == null) throw new IllegalArgumentException("refresh cookie is null");
30+
31+
if (refreshTokenStore.isBlacklisted(refresh)) throw new IllegalArgumentException("refresh cookie is blacklisted");
32+
33+
TokenBody body = jwtTokenProvider.parseClaims(refresh);
34+
if (!refreshTokenStore.isLatest(body.memberId(), refresh)) throw new IllegalArgumentException("refresh token is invalid");
35+
36+
TokenPair tokenPair = jwtTokenProvider.generateTokenPair(new JwtMemberInfo(body.memberId(),
37+
body.nickname(), body.role()));
38+
refreshTokenStore.isBlacklisted(refresh);
39+
refreshTokenStore.saveLatest(body.memberId(), tokenPair.refreshToken());
40+
JwtUtils.addRefreshTokenCookie(response, tokenPair.refreshToken(), tokenPair.refreshTokenExpiresIn());
41+
42+
return new AuthTokenResponse(tokenPair.accessToken(), tokenPair.accessTokenExpiresIn(), body.memberId(), body.nickname(), body.role());
43+
}
44+
45+
private String extraRefreshCookie(HttpServletRequest request) {
46+
if (request.getCookies() == null) return null;
47+
for (Cookie cookie : request.getCookies()) {
48+
if ("refreshToken".equals(cookie.getName())) return cookie.getValue();
49+
}
50+
return null;
51+
}
52+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.oronaminc.join.member.token;
2+
3+
import java.util.Objects;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.cache.Cache;
6+
import org.springframework.cache.CacheManager;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
public class RefreshTokenStore {
12+
private final CacheManager cacheManager;
13+
14+
private Cache latest() {
15+
return cacheManager.getCache("refreshLatest");
16+
}
17+
private Cache blacklist() {
18+
return cacheManager.getCache("refreshBlacklist");
19+
}
20+
21+
public void saveLatest(Long memberId, String refreshToken) {
22+
latest().put(key(memberId), refreshToken);
23+
}
24+
25+
public boolean isLatest(Long memberId, String refreshToken) {
26+
String stored = latest().get(key(memberId), String.class);
27+
return Objects.equals(stored, refreshToken);
28+
}
29+
30+
public boolean isBlacklisted(String refreshToken) {
31+
Boolean v = blacklist().get(refreshToken, Boolean.class);
32+
return v != null && v;
33+
}
34+
35+
private String key(Long memberId) {
36+
return "refresh:" + memberId;
37+
}
38+
}

0 commit comments

Comments
 (0)