Skip to content

Commit e009402

Browse files
authored
Merge pull request #36 from Go-Socket-Project/refactor/jwt-refresh
๐Ÿ”€ Jwt Refresh
2 parents 5ef4d93 + c25e10d commit e009402

11 files changed

Lines changed: 275 additions & 59 deletions

File tree

โ€Žsrc/main/java/com/mycom/socket/auth/config/JWTProperties.javaโ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
@ConfigurationProperties(prefix = "jwt")
1111
public class JWTProperties {
1212
private String secret;
13-
private long accessTokenValidityInSeconds = 1800;
14-
private String cookieName = "Authorization";
13+
private long accessTokenValidityInSeconds;
14+
private long refreshTokenValidityInSeconds;
15+
private String accessTokenCookieName;
16+
private String refreshTokenCookieName;
1517
private String issuer = "go_socket";
1618
private boolean secureCookie = false;
1719
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.mycom.socket.auth.controller;
2+
3+
import com.mycom.socket.auth.config.JWTProperties;
4+
import com.mycom.socket.auth.dto.response.TokenResponse;
5+
import com.mycom.socket.auth.jwt.JWTUtil;
6+
import com.mycom.socket.auth.security.CookieUtil;
7+
import com.mycom.socket.global.exception.BadRequestException;
8+
import io.jsonwebtoken.JwtException;
9+
import jakarta.servlet.http.Cookie;
10+
import jakarta.servlet.http.HttpServletRequest;
11+
import jakarta.servlet.http.HttpServletResponse;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
import java.util.Arrays;
17+
import java.util.Optional;
18+
19+
@RestController
20+
@RequiredArgsConstructor
21+
public class RefreshController {
22+
23+
private final JWTUtil jwtUtil;
24+
private final CookieUtil cookieUtil;
25+
private final JWTProperties jwtProperties;
26+
27+
28+
@PostMapping("/refresh")
29+
public TokenResponse refreshAccessToken(HttpServletRequest request, HttpServletResponse response) {
30+
Optional<String> refreshTokenOpt = extractRefreshToken(request);
31+
if (refreshTokenOpt.isEmpty()) {
32+
return TokenResponse.of("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
33+
}
34+
35+
String refreshToken = refreshTokenOpt.get();
36+
if (!jwtUtil.validateToken(refreshToken, "REFRESH_TOKEN")) {
37+
response.addCookie(cookieUtil.createExpiredCookie(jwtProperties.getRefreshTokenCookieName()));
38+
return TokenResponse.of("์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์ž…๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
39+
}
40+
41+
String email = jwtUtil.getEmail(refreshToken);
42+
String newAccessToken = jwtUtil.createToken(email, jwtProperties.getAccessTokenValidityInSeconds(), "ACCESS_TOKEN");
43+
String newRefreshToken = jwtUtil.createToken(email, jwtProperties.getRefreshTokenValidityInSeconds(), "REFRESH_TOKEN");
44+
45+
response.addCookie(cookieUtil.createAuthCookie(newAccessToken));
46+
response.addCookie(cookieUtil.createRefreshCookie(newRefreshToken));
47+
48+
return TokenResponse.of(newAccessToken);
49+
}
50+
51+
private Optional<String> extractRefreshToken(HttpServletRequest request) {
52+
if (request.getCookies() == null) {
53+
return Optional.empty();
54+
}
55+
56+
return Arrays.stream(request.getCookies())
57+
.filter(cookie -> jwtProperties.getRefreshTokenCookieName().equals(cookie.getName()))
58+
.map(Cookie::getValue)
59+
.findFirst();
60+
}
61+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.mycom.socket.auth.dto.response;
2+
3+
public record TokenResponse(
4+
String accessToken,
5+
String message,
6+
boolean success
7+
) {
8+
public static TokenResponse of(String message) {
9+
return new TokenResponse(null, message, false);
10+
}
11+
}

โ€Žsrc/main/java/com/mycom/socket/auth/jwt/JWTFilter.javaโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ protected void doFilterInternal(HttpServletRequest request,
3131
FilterChain filterChain) throws ServletException, IOException {
3232
try {
3333
String token = resolveTokenFromCookie(request);
34-
if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
34+
if (StringUtils.hasText(token) && jwtUtil.validateToken(token, "ACCESS_TOKEN")) {
3535
setAuthentication(token);
3636
}
3737
} catch (Exception e) {
@@ -46,7 +46,7 @@ private String resolveTokenFromCookie(HttpServletRequest request) {
4646
Cookie[] cookies = request.getCookies();
4747
if (cookies != null) {
4848
for (Cookie cookie : cookies) {
49-
if (jwtProperties.getCookieName().equals(cookie.getName())) {
49+
if (jwtProperties.getAccessTokenCookieName().equals(cookie.getName())) {
5050
return cookie.getValue();
5151
}
5252
}

โ€Žsrc/main/java/com/mycom/socket/auth/jwt/JWTUtil.javaโ€Ž

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.mycom.socket.auth.jwt;
22

33
import com.mycom.socket.auth.config.JWTProperties;
4+
import io.jsonwebtoken.ExpiredJwtException;
5+
import io.jsonwebtoken.JwtException;
6+
import io.jsonwebtoken.JwtParser;
47
import io.jsonwebtoken.Jwts;
58
import io.jsonwebtoken.security.Keys;
69
import lombok.extern.slf4j.Slf4j;
@@ -18,53 +21,122 @@ public class JWTUtil {
1821
private final SecretKey secretKey;
1922
private final JWTProperties jwtProperties;
2023

24+
/**
25+
* JWTUtil ์ƒ์„ฑ์ž
26+
* ์„ค์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ HMAC-SHA ์•Œ๊ณ ๋ฆฌ์ฆ˜์šฉ SecretKey๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
27+
*
28+
* @param jwtProperties JWT ๊ด€๋ จ ์„ค์ •๊ฐ’์„ ๋‹ด๊ณ  ์žˆ๋Š” ํ”„๋กœํผํ‹ฐ ๊ฐ์ฒด
29+
*/
2130
public JWTUtil(JWTProperties jwtProperties) {
2231
this.jwtProperties = jwtProperties;
2332
this.secretKey = Keys.hmacShaKeyFor(
2433
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
2534
);
2635
}
2736

37+
/**
38+
* JWT Parser ์ƒ์„ฑ
39+
* ํ† ํฐ ๊ฒ€์ฆ ๋ฐ ์ •๋ณด ์ถ”์ถœ์— ์‚ฌ์šฉ๋˜๋Š” ๊ณตํ†ต Parser๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
40+
*
41+
* @return ์„ค์ •๋œ JWT Parser ๊ฐ์ฒด
42+
*/
43+
private JwtParser createParser() {
44+
return Jwts.parser()
45+
.verifyWith(secretKey)
46+
.requireIssuer(jwtProperties.getIssuer())
47+
.build();
48+
}
49+
50+
2851
/**
2952
* JWT ํ† ํฐ ์ƒ์„ฑ
53+
* ์ฃผ์–ด์ง„ ์ด๋ฉ”์ผ๊ณผ ์œ ํšจ๊ธฐ๊ฐ„์œผ๋กœ ์ƒˆ๋กœ์šด JWT๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
54+
*
55+
* @param email ํ† ํฐ์— ํฌํ•จ๋  ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ
56+
* @param validityInSeconds ํ† ํฐ ์œ ํšจ ๊ธฐ๊ฐ„ (์ดˆ)
57+
* @param accessToken
58+
* @return ์ƒ์„ฑ๋œ JWT ๋ฌธ์ž์—ด
59+
* @throws IllegalStateException ํ† ํฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ
3060
*/
31-
public String createToken(String email) {
61+
public String createToken(String email, long validityInSeconds, String accessToken) {
3262
Date now = new Date();
33-
Date validity = new Date(now.getTime() +
34-
(jwtProperties.getAccessTokenValidityInSeconds() * 1000));
35-
36-
return Jwts.builder()
37-
.issuer(jwtProperties.getIssuer())
38-
.subject(email)
39-
.issuedAt(now)
40-
.expiration(validity)
41-
.signWith(secretKey)
42-
.compact();
63+
Date validity = new Date(now.getTime() + (validityInSeconds * 1000));
64+
65+
try {
66+
return Jwts.builder()
67+
.issuer(jwtProperties.getIssuer())
68+
.subject(email)
69+
.issuedAt(now)
70+
.expiration(validity)
71+
.claim("type", accessToken)
72+
.signWith(secretKey)
73+
.compact();
74+
} catch (JwtException e) {
75+
log.error("ํ† ํฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", e);
76+
throw new IllegalStateException("ํ† ํฐ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", e);
77+
}
4378
}
4479

4580
/**
4681
* ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
82+
* ์ฃผ์–ด์ง„ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ์˜ ์„œ๋ช…, ๋งŒ๋ฃŒ ์—ฌ๋ถ€, ๋ฐœ๊ธ‰์ž ๋“ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
83+
*
84+
* @param token ๊ฒ€์ฆํ•  JWT ๋ฌธ์ž์—ด
85+
* @return ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false
4786
*/
48-
public boolean validateToken(String token) {
49-
try {
50-
if (!StringUtils.hasText(token)) {
51-
return false;
52-
}
87+
public boolean validateToken(String token, String expectedType) {
88+
if (!StringUtils.hasText(token)) {
89+
return false;
90+
}
5391

54-
Jwts.parser()
92+
try {
93+
var claims = Jwts.parser()
5594
.verifyWith(secretKey)
5695
.requireIssuer(jwtProperties.getIssuer())
5796
.build()
58-
.parseSignedClaims(token);
59-
return true;
60-
} catch (Exception e) {
61-
log.warn("JWT ํ† ํฐ ๊ฒ€์ฆ ์‹คํŒจ", e);
97+
.parseSignedClaims(token)
98+
.getPayload();
99+
100+
// ํ† ํฐ ํƒ€์ž… ๊ฒ€์ฆ
101+
String tokenType = claims.get("type", String.class);
102+
if (!expectedType.equals(tokenType)) {
103+
log.warn("์ž˜๋ชป๋œ ํ† ํฐ ํƒ€์ž…์ž…๋‹ˆ๋‹ค. expected: {}, actual: {}", expectedType, tokenType);
104+
return false;
105+
}
106+
107+
return new Date().before(claims.getExpiration());
108+
} catch (ExpiredJwtException e) {
109+
log.warn("๋งŒ๋ฃŒ๋œ JWT ํ† ํฐ์ž…๋‹ˆ๋‹ค.");
110+
return false;
111+
} catch (JwtException e) {
112+
log.warn("์œ ํšจํ•˜์ง€ ์•Š์€ JWT ํ† ํฐ์ž…๋‹ˆ๋‹ค.", e);
62113
return false;
63114
}
64115
}
65116

66117
/**
67-
* ํ† ํฐ์—์„œ ์ด๋ฉ”์ผ ์ถ”์ถœ
118+
* ํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ถ”์ถœ
119+
*
120+
* @param token JWT ๋ฌธ์ž์—ด
121+
* @return ํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„
122+
*/
123+
private Date getExpirationFromToken(String token) {
124+
return Jwts.parser()
125+
.verifyWith(secretKey)
126+
.build()
127+
.parseSignedClaims(token)
128+
.getPayload()
129+
.getExpiration();
130+
}
131+
132+
133+
/**
134+
* ํ† ํฐ์—์„œ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ ์ถ”์ถœ
135+
* JWT์˜ subject ํด๋ ˆ์ž„์—์„œ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค.
136+
*
137+
* @param token JWT ๋ฌธ์ž์—ด
138+
* @return ํ† ํฐ์— ํฌํ•จ๋œ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ
139+
* @throws IllegalStateException ํ† ํฐ์—์„œ ์ด๋ฉ”์ผ์„ ์ถ”์ถœํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ
68140
*/
69141
public String getEmail(String token) {
70142
return Jwts.parser()

โ€Žsrc/main/java/com/mycom/socket/auth/security/CookieUtil.javaโ€Ž

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,65 @@ public class CookieUtil {
1111
private final JWTProperties jwtProperties;
1212

1313
/**
14-
* ์ธ์ฆ ์ฟ ํ‚ค ์ƒ์„ฑ
14+
* ๊ณตํ†ต ์ฟ ํ‚ค ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ
15+
* ๋ชจ๋“  ์ข…๋ฅ˜์˜ ์ฟ ํ‚ค ์ƒ์„ฑ์— ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋ณธ ๋ฉ”์†Œ๋“œ์ž…๋‹ˆ๋‹ค.
16+
*
17+
* @param name ์ฟ ํ‚ค์˜ ์ด๋ฆ„
18+
* @param value ์ฟ ํ‚ค์— ์ €์žฅ๋  ๊ฐ’ (ํ† ํฐ)
19+
* @param maxAge ์ฟ ํ‚ค์˜ ์œ ํšจ ์‹œ๊ฐ„ (์ดˆ ๋‹จ์œ„)
20+
* @param secure HTTPS ํ”„๋กœํ† ์ฝœ์—์„œ๋งŒ ์ „์†ก ์—ฌ๋ถ€
21+
* @return ์ƒ์„ฑ๋œ ์ฟ ํ‚ค ๊ฐ์ฒด
1522
*/
16-
public Cookie createAuthCookie(String token) {
17-
Cookie cookie = new Cookie(jwtProperties.getCookieName(), token);
23+
private Cookie createCookie(String name, String value, long maxAge, boolean secure) {
24+
Cookie cookie = new Cookie(name, value);
1825
cookie.setHttpOnly(true);
19-
cookie.setSecure(jwtProperties.isSecureCookie());
20-
cookie.setPath("/");
21-
cookie.setMaxAge((int) jwtProperties.getAccessTokenValidityInSeconds());
26+
cookie.setSecure(secure);
27+
cookie.setPath("/api/auth");
28+
cookie.setMaxAge((int) maxAge);
29+
cookie.setAttribute("SameSite", "Strict"); //CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง€ ์„ค์ • ์ถ”๊ฐ€
2230
return cookie;
2331
}
2432

2533
/**
26-
* ์ธ์ฆ ์ฟ ํ‚ค ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ
34+
* Access Token์„ ์ €์žฅํ•˜๋Š” ์ฟ ํ‚ค ์ƒ์„ฑ
35+
* ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ์— ์‚ฌ์šฉ๋˜๋Š” Access Token์„ ์ฟ ํ‚ค์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
36+
*
37+
* @param token JWT Access Token ๋ฌธ์ž์—ด
38+
* @return Access Token์ด ์ €์žฅ๋œ ์ฟ ํ‚ค
2739
*/
28-
public Cookie createExpiredAuthCookie() {
29-
Cookie cookie = new Cookie(jwtProperties.getCookieName(), null);
30-
cookie.setHttpOnly(true);
31-
cookie.setSecure(true);
32-
cookie.setPath("/");
33-
cookie.setMaxAge(0); // ์ฆ‰์‹œ ๋งŒ๋ฃŒ
34-
return cookie;
40+
public Cookie createAuthCookie(String token) {
41+
return createCookie(
42+
jwtProperties.getAccessTokenCookieName(),
43+
token,
44+
jwtProperties.getAccessTokenValidityInSeconds(),
45+
jwtProperties.isSecureCookie()
46+
);
47+
}
48+
49+
/**
50+
* Refresh Token์„ ์ €์žฅํ•˜๋Š” ์ฟ ํ‚ค ์ƒ์„ฑ
51+
* Access Token ์žฌ๋ฐœ๊ธ‰์— ์‚ฌ์šฉ๋˜๋Š” Refresh Token์„ ์ฟ ํ‚ค์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
52+
*
53+
* @param token JWT Refresh Token ๋ฌธ์ž์—ด
54+
* @return Refresh Token์ด ์ €์žฅ๋œ ์ฟ ํ‚ค
55+
*/
56+
public Cookie createRefreshCookie(String token) {
57+
return createCookie(
58+
jwtProperties.getRefreshTokenCookieName(),
59+
token,
60+
jwtProperties.getRefreshTokenValidityInSeconds(),
61+
jwtProperties.isSecureCookie()
62+
);
63+
}
64+
65+
/**
66+
* ๋งŒ๋ฃŒ๋œ ์ฟ ํ‚ค ์ƒ์„ฑ
67+
* ๋กœ๊ทธ์•„์›ƒ ๋˜๋Š” ํ† ํฐ ๋ฌดํšจํ™” ์‹œ ๊ธฐ์กด ์ฟ ํ‚ค๋ฅผ ๋งŒ๋ฃŒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
68+
*
69+
* @param name ๋งŒ๋ฃŒ์‹œํ‚ฌ ์ฟ ํ‚ค์˜ ์ด๋ฆ„
70+
* @return ์ฆ‰์‹œ ๋งŒ๋ฃŒ๋˜๋„๋ก ์„ค์ •๋œ ์ฟ ํ‚ค
71+
*/
72+
public Cookie createExpiredCookie(String name) {
73+
return createCookie(name, null, 0, true);
3574
}
3675
}

โ€Žsrc/main/java/com/mycom/socket/auth/security/LoginFilter.javaโ€Ž

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.mycom.socket.auth.security;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.mycom.socket.auth.config.JWTProperties;
45
import com.mycom.socket.auth.jwt.JWTUtil;
56
import com.mycom.socket.global.dto.ApiResponse;
67
import com.mycom.socket.auth.dto.request.LoginRequest;
@@ -28,6 +29,7 @@ public class LoginFilter extends UsernamePasswordAuthenticationFilter {
2829
private final AuthenticationManager authenticationManager;
2930
private final CookieUtil cookieUtil;
3031
private final ObjectMapper objectMapper;
32+
private final JWTProperties jwtProperties;
3133

3234
@Override
3335
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
@@ -51,11 +53,23 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
5153
MemberDetails memberDetails = (MemberDetails) authResult.getPrincipal();
5254
Member member = memberDetails.getMember();
5355

54-
String token = jwtUtil.createToken(member.getEmail());
56+
// JWT ํ† ํฐ ์ƒ์„ฑ
57+
String accessToken = jwtUtil.createToken(
58+
member.getEmail(),
59+
jwtProperties.getAccessTokenValidityInSeconds(),
60+
"ACCESS_TOKEN"
61+
);
62+
String refreshToken = jwtUtil.createToken(
63+
member.getEmail(),
64+
jwtProperties.getRefreshTokenValidityInSeconds(),
65+
"REFRESH_TOKEN"
66+
);
5567

5668
// ์ฟ ํ‚ค ์ƒ์„ฑ ๋ฐ ์„ค์ •
57-
Cookie authCookie = cookieUtil.createAuthCookie(token);
58-
response.addCookie(authCookie);
69+
Cookie accessTokenCookie = cookieUtil.createAuthCookie(accessToken); //์•ก์„ธ์Šค ํ† ํฐ ์ฟ ํ‚ค
70+
Cookie refreshTokenCookie = cookieUtil.createRefreshCookie(refreshToken); //๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ฟ ํ‚ค
71+
response.addCookie(accessTokenCookie);
72+
response.addCookie(refreshTokenCookie);
5973

6074
// ๋กœ๊ทธ์ธ ์‘๋‹ต ์ƒ์„ฑ
6175
LoginResponse loginResponse = new LoginResponse(member.getEmail(), member.getNickname());

0 commit comments

Comments
ย (0)