Backend/spring cloud (MSA)

MSA ํ™˜๊ฒฝ์—์„œ ์ธ์ฆ/ํ† ํฐ ์žฌ์š”์ฒญ

dddzr 2025. 4. 6. 21:53

๐Ÿ“Œ ๋ฌธ์ œ

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