๐ ๋ฌธ์
JWT ํ ํฐ์ด ๋ง๋ฃ๋์์ ๋ ๋ค์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ Refresh Token์ ํ์ฉํ ์ฌ๋ฐ๊ธ ๋ฐฉ์์ด ์ผ๋ฐ์ ์ด๋ค.
์๋ฅผ ๋ค์ด, ๊ฒ์๊ธ ์์ฑ ์ค ํ ํฐ์ด ๋ง๋ฃ ๋์์ ๋๋ ์ฌ์ฉ์๊ฐ ์ฌ๋ก๊ทธ์ธ/์๋ ๋ก๊ทธ์ธ ์๊ฐ ์ฐ์ฅ ํ์ง ์๊ณ refresh ์ฒ๋ฆฌ๊ฐ ๋์ด์ผ ํ๋ค!!
โ 1. MSA ํ๊ฒฝ์์ JWT ์ธ์ฆ ํ๋ฆ
1๏ธโฃ ์ฌ์ฉ์ ๋ก๊ทธ์ธ → ์ธ์ฆ ์๋ฒ(Auth Service)์์ JWT ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ ๋ฐ๊ธ
- ์ก์ธ์ค ํ ํฐ: ๋น๊ต์ ์งง์ ๋ง๋ฃ ์๊ฐ (์: 30๋ถ~1์๊ฐ)
- ๋ฆฌํ๋ ์ ํ ํฐ: ๋น๊ต์ ๊ธด ๋ง๋ฃ ์๊ฐ (์: 7์ผ~30์ผ)
- ํด๋ผ์ด์ธํธ๋ ์ก์ธ์ค ํ ํฐ์ HTTP ํค๋์ ํฌํจํ์ฌ API ์์ฒญ
2๏ธโฃ ๊ฒ์ดํธ์จ์ด(API Gateway)์์ JWT ํ ํฐ ๊ฒ์ฆ ํ ํด๋น ๋ง์ดํฌ๋ก์๋น์ค(Post Service ๋ฑ)๋ก ์์ฒญ ์ ๋ฌ
- JWT ํ ํฐ์ด ์ ํจํ๋ฉด ๊ทธ๋๋ก ์์ฒญ์ ์๋น์ค๋ก ์ ๋ฌ
- JWT ํ ํฐ์ด ๋ง๋ฃ๋์์ ๊ฒฝ์ฐ → 401(Unauthorized) ์๋ต ๋ฐํ
3๏ธโฃ ํด๋ผ์ด์ธํธ๋ 401 ์๋ต์ ๋ฐ์ผ๋ฉด Refresh Token์ ์ด์ฉํด Auth Service์ ์๋ก์ด Access Token ์์ฒญ
- Refresh Token์ ์ธ์ฆ ์๋ฒ์์๋ง ๊ฒ์ฆ ๊ฐ๋ฅ
- ์ธ์ฆ ์๋ฒ์์ ๋ฆฌํ๋ ์ ํ ํฐ์ ๊ฒ์ฆ ํ, ์ ์ก์ธ์ค ํ ํฐ์ ๋ฐ๊ธ
- ์ด๋ refresh token๋ 1ํ์ฉ์ผ๋ก ์ฐ๊ณ ์ฌ๋ฐ๊ธ ํ๊ธฐ๋ ํจ.
4๏ธโฃ ์๋ก์ด JWT ์ก์ธ์ค ํ ํฐ์ ๋ฐ์์ ๋ค์ API ์์ฒญ
- ์ดํ ๋ค์ API Gateway๋ฅผ ํตํด ์์ฒญ ์งํ
โ 2. ๊ตฌํ ๋ฐฉ๋ฒ
โ 2-1. ํด๋ผ์ด์ธํธ(ํ๋ก ํธ์๋)์์์ ๊ตฌํ ๋ฐฉ์
ํด๋ผ์ด์ธํธ๋ Interceptor (Axios, Fetch API, Retrofit ๋ฑ)๋ฅผ ์ด์ฉํ์ฌ ์๋์ผ๋ก ํ ํฐ ๊ฐฑ์ ์ ์ฒ๋ฆฌํ๋ค. ์๋์ผ๋ก ํ ํฐ์ ๊ฐฑ์ ํ์ฌ ์ฌ์ฉ์๊ฐ ์ฌ๋ก๊ทธ์ธ ์์ด ๊ณ์ ์์ ๊ฐ๋ฅ!!
ํด๋น ๊ณผ์ ์ด ์ธ์ฆ์ ํ์๋ก ํ๋ api์์ฒญ์์ ๊ณตํต์ผ๋ก ํ์ํ๊ธฐ์ ๋๋ ‘axiosWhitAuth’๋ชจ๋์ ๋ณ๋ ์ ์ ํ์ฌ ์ฌ์ฉํ๋ค.
๐ ํธ์ถ ์์
**const response = await axiosWhitAuth.post(url, formData);**
๐ ๋ชจ๋ ์์ : Vue3(Axios)
import axios from "axios";
import router from "@/router";
const axiosWhitAuth = axios.create({
baseURL: "<http://localhost:8080>",
withCredentials: true, // ์ฟ ํค ๊ธฐ๋ฐ ์ธ์ฆ์ ์ฌ์ฉํ ๊ฒฝ์ฐ ํ์
});
axiosWhitAuth.interceptors.request.use((config) => {
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
}, (error) => {
return Promise.reject(error);
});
// ์๋ต ์ธํฐ์
ํฐ ์ค์ (ํ ํฐ ์๋ ๊ฐฑ์ )
axiosWhitAuth.interceptors.response.use(
response => response,
async error => {
if (error.response && error.response.status === 401) { //&& error.response.data.error === "Invalid Access Token"
console.log("Access Token ๋ง๋ฃ, Refresh Token์ผ๋ก ์ฌ๋ฐ๊ธ ์๋");
const originalRequest = error.config;
try {
const response = await axios.post("/user/api/auth/refresh", {}, { withCredentials: true });
const newAccessToken = response.data.accessToken;
localStorage.setItem("accessToken", newAccessToken);
// ์๋ ์์ฒญ์ ์๋ก์ด ํ ํฐ ์ถ๊ฐ ํ ์ฌ์๋
originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
return axiosWhitAuth(originalRequest);
} catch (refreshError) {
console.error("๋ฆฌํ๋ ์ ํ ํฐ ๋ง๋ฃ ๋๋ ์คํจ", refreshError);
alert("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
router.push({ name: "LoginPage" });
return;
}
}
return Promise.reject(error);
}
);
export default axiosWhitAuth;
โ 2-2. API Gateway์์ JWT ๊ฒ์ฆ ๋ฐฉ์
๋ณดํต Spring Cloud Gateway + Spring Security๋ฅผ ์ฌ์ฉํ์ฌ JWT๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
๐ API Gateway์์ JWT ํํฐ ์ ์ฉ (Spring Security ์ฌ์ฉ)
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain (ServerHttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.formLogin(login -> login.disable())
.httpBasic(basic -> basic.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
http.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange(exchanges -> exchanges
.pathMatchers("/user/api/auth/**", "/user/api/users/**").permitAll()
.pathMatchers("/order/**").hasRole("USER")
.anyExchange().authenticated() // ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์์ฒญ ์ธ์ฆ, ์ด๋ค ๊ถํ์ด๋ผ๋ ์์ผ๋ฉด ํต๊ณผ
);
http.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) // 401 ์๋ต ์ค์ , ์ด๊ฑฐ ์์ผ๋ฉด authenticated๊ฑธ๋ ธ์ ๋ ๋ก๊ทธ์ธ ํ์
์ด ๋ฌ๋ค.
);
return http.build();
}
}
๐ JWT ํํฐ์์ ์๋ต ํ ์๋ ์๋ค.
@Component
public class JwtAuthenticationFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Authorization ํค๋์์ JWT ์ถ์ถ
if (!request.getHeaders().containsKey("Authorization")) {
return onError(exchange, "Missing Authorization Header", HttpStatus.UNAUTHORIZED);
}
String token = request.getHeaders().getOrEmpty("Authorization").get(0).replace("Bearer ", "");
try {
// JWT ๊ฒ์ฆ (๋ง๋ฃ ์ฌ๋ถ ์ฒดํฌ)
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
} catch (ExpiredJwtException e) {
return onError(exchange, "Token Expired", HttpStatus.UNAUTHORIZED);
} catch (Exception e) {
return onError(exchange, "Invalid Token", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus status) {
exchange.getResponse().setStatusCode(status);
return exchange.getResponse().setComplete();
}
}
โ 2-3. ์ธ์ฆ ์๋น์ค (Auth Service)์์ Refresh Token ์ฒ๋ฆฌ
- Refresh token์ ์ฟ ํค์ DB(or ์บ์)์ ์ ์ฅ!!
- ์ฌ์ฉ์๊ฐ ์์ฒญํ๋ฉด ์ฟ ํค๋ ์๋์ผ๋ก ํฌํจ๋๊ณ , ์ด๋ฅผ db์ ์ ์ฅ๋ ๊ฒ๊ณผ ๋น๊ตํ์ฌ ๊ฐ์ ๋๋ง ์ ํจํ๊ฑธ๋ก ํ๋ค.
- ์ ์ฑ ์ ๋ฐ๋ผ refresh token๋ ์ผํ์ฉ์ผ๋ก ํ๋ค. (refresh ํ ๋ refresh token๋ ์ฌ๋ฐ๊ธo)
๐ ํ ํฐ ๊ฐฑ์ (cookie, redis์ ํ ํฐ ์ ์ฅ, refresh token ์ฌ๋ฐ๊ธ)
//AuthController.java
@PostMapping("/refresh")
public ResponseEntity<Map<String, String>> refreshToken(@CookieValue(value = "refreshToken", required = false) String refreshToken) {
Map<String, String> tokens = userService.refreshAccessToken(refreshToken);
saveRefreshToken(tokens.get("refreshToken"));
Map<String, String> response = new HashMap<>();
response.put("accessToken", tokens.get("accessToken")); // accessToken, refreshToken ํฌํจ
return ResponseEntity.ok(response);
}
private HttpHeaders saveRefreshToken(String refreshToken) {
//refreshToken์ HttpOnly์ฟ ํค์ ์ ์ฅ
String cookieHeader = "refreshToken=" + refreshToken + "; Path=/; HttpOnly; Secure";
HttpHeaders headers = new HttpHeaders();
headers.add("Set-Cookie", cookieHeader);
return headers;
}
๐ UserService.java
๋๋ ์บ์์๋ ์ ์ฅํด์ refresh ํ ํฐ ๋น๊ตํ๋๋ฐ ์บ์๋ง๊ณ ๊ทธ๋ฅ db์ ์ ์ฅํด๋ ๋จ.
๋๋ refresh token์ 1ํ์ฐ๋ฉด ์ฌ๋ฐ๊ธ ํ๋๋ก ํ์.
// UserService.java
public Map<String, String> refreshAccessToken(String refreshToken) {
// Refresh Token ๊ฒ์ฆ
if (!jwtTokenProvider.validateToken(refreshToken)) {
throw new InvalidCredentialsException("Invalid Refresh Token");
}
// ์ ์ฅ๋ Refresh Token๊ณผ ๋น๊ตํ์ฌ ๊ฒ์ฆ
String username = jwtTokenProvider.getUsernameFromToken(refreshToken);
String savedRefreshToken = cacheService.getRefreshToken(refreshToken);
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (!refreshToken.equals(savedRefreshToken)) {
throw new InvalidCredentialsException("Refresh Token mismatch");
}
// ํ ํฐ ์์ฑ
Map<String, String> tokens = jwtTokenProvider.generateTokens(user);
// refresh Token์ ์บ์์ ์ ์ฅ
cacheService.saveRefreshToken(username, tokens.get("refreshToken"));
return tokens;
}
'Backend > spring cloud (MSA)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ฐ์ดํฐ ํตํฉ ์กฐํ ๋ฐฉ๋ฒ ์ค๊ณ (0) | 2025.04.06 |
---|---|
์ค์๊ฐ ๋ฐ์ดํฐ ์ ์ก (0) | 2025.02.23 |
[Kafka] ๊ณ ๊ธ ์ค์ (0) | 2025.02.23 |
Kafka๋? (0) | 2025.02.23 |
Eureka๋? (0) | 2025.02.16 |