1. ๊ฐ์
MSA๋ฅผ ๋์ ํ๋ฉด์, ํน์ ๊ณ ๋ คํ๋ฉด์ ๋๋ถ๋ถ์ ์ฌ๋๋ค์ด ์ธ์ฆ ์ธ๊ฐ์ ๋ํ ๊ณ ๋ฏผ์ ํ๊ฒ ๋ ๊ฑฐ๋ผ ์๊ฐํ๋ค.
๋๋ Spring Cloud๋ฅผ ์ฌ์ฉํ MSA ์ํคํ ์ฒ ๊ตฌ์กฐ๋ก ๊ฐ๋ฐ์ ์งํํ๋ฉด์ ์ธ์ฆ, ์ธ๊ฐ์ ๋ํ ๊ณ ๋ฏผ์ ํ๊ฒ ๋๋ค. ๊ตฌ๊ธ๋ง ํด๋ณด๋ฉด ํฌ๊ฒ 2๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๋๋์๋ค.
1. Gateway์์ ๋ชจ๋ ์ธ์ฆ, ์ธ๊ฐ ์ฒ๋ฆฌ
2. ๊ฐ Micro Service ์์ ์ธ์ฆ, ์ธ๊ฐ ์ฒ๋ฆฌ
์ด ๋ ๊ฐ์ง ๋ฐฉ๋ฒ ์ค ์ด๋ค๊ฑธ ์ ํํด์ผ ํ ์ง์ ๋ํด ๋ง์ ๊ณ ๋ฏผ์ ํ๋ค. ๊ฒฐ๋ก ๋ถํฐ ์๊ธฐํ๋ฉด ํ์ฌ๋ Gateway์์ ์ธ์ฆ ์ฒ๋ฆฌ, ๊ฐ Micro Service์์ ์ธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ฒ์ผ๋ก ๊ฒฐ์ ํ๋ค.
ํ์ง๋ง, ์๋น์ค๋ฅผ ์ด์ํด๋ณธ๊ฒ ์๋๊ณ ํ์ตํ๋ ๋จ๊ณ์ด๊ธฐ ๋๋ฌธ์ ๋ด ์๊ฐ์ด ํ๋ฆด ์๋ ์๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฐ์ ๊ฐ๋ฐํ๋ ์๋น์ค์ ๋๋ฉ์ธ, ์ฑ๊ฒฉ, ํธ๋ํฝ ๋ฑ ๋ง๊ณ ๋ค์ํ ์์ธ๋ค์ ๋ฐ๋ผ ๊ฒฐ์ ์ ๋ฌ๋ผ์ง ๊ฒ์ด๋ค. ๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ๋ง์ฝ ์ด๋ค ์ด์ ๋๋ฌธ์ ๋ด ์๊ฐ์ด ํ๋ ธ๋ค ์๊ฐ์ด ๋ค๋ฉด ๋ค์ ๋์์์ ๊ธ์ ์ด์ด์ ์์ฑํ ์์ ์ด๋ค.
2. Gateway์์ ์ธ์ฆ, ์ธ๊ฐ ์ฒ๋ฆฌ
spring security ์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ง์ฐฌ๊ฐ์ง๋ก, spring api gateway๋ ํํฐ๊ฐ ๋ง์ด ํ์ฉ๋๋ค. ์ฌ๋ฌ ํํฐ๋ค์ ์ ์ธ ํด๋๋ฉด, ํด๋น ํํฐ๋ค์ ๊ฑฐ์ณ์ ์ค์ ์๋น์ค๋ก ๋ผ์ฐํ ์ด ๋๋ค.
์ฌ๋ฌ Filter๋ค์ ๊ตฌํํ๊ณ Bean์ผ๋ก ๋ฑ๋ก๋ง ํด์ฃผ๋ฉด ๋์ํ ์ ์๋๋ฐ ์์ ๋ํ ์กฐ์ ํ ์ ์๋ค.
ํํฐ๊ฐ ํธ์ถ๋๋ ๋ฐฉ์์ ์ผ๋ฐ์ ์ธ ๋ค๋ฅธ ํํฐ์ ๋ง์ฐฌ๊ฐ์ง์ด๋ค. ๋ง์ฝ ์์๋ฅผ ์ง์ ํ์ง ์์ผ๋ฉด ๋ง์๋๋ก ๋์ํ๊ฒ ๋๋ค.
Spring Cloud์์๋ ์ด๋ฐ Filter๋ค์ ๊ตฌํํ๊ธฐ ์ํด AbstractGatewayFilterFactory ๋ผ๋ ์ถ์ ํด๋์ค๋ฅผ ์ ๊ณตํด์ค๋ค. ์ด ํด๋์ค๋ฅผ ์์ ๋ฐ์ overriding์ ํตํด JwtFilter๋ฅผ ๊ตฌํํ ์ ์๋ค.
Gateway์์ ์ธ์ฆ ์ฒ๋ฆฌ
@Slf4j
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
private final JwtTokenProvider jwtProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtProvider) {
super(Config.class);
this.jwtProvider = jwtProvider;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String authorizationHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7);
// ๋ง๋ฃ ์ฒดํฌ & ํ ํฐ ๊ฒ์ฆ
if (jwtProvider.isExpiration(token)) {
log.info("access token ๋ง๋ฃ");
throw new DelegationTokenExpiredException("ํ ํฐ ๋ง๋ฃ");
}
String loginId = jwtProvider.getLoginId(token);
String role = jwtProvider.getRoles(token);
// ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํค๋์ ์ถ๊ฐ
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", loginId)
.header("X-User-Roles", role)
.build();
// ์ ์์ฒญ์ ํฌํจํ Exchange ์์ฑ
exchange = exchange.mutate().request(modifiedRequest).build();
}
return chain.filter(exchange);
};
}
public static class Config {
}
}
๊ธฐ์กด ๋ชจ๋๋ก์ ์ํคํ ์ฒ์์ JWT๋ฅผ ์ฌ์ฉํ ๋ OncePerRequestFilter๋ฅผ ์์๋ฐ์ ๊ตฌํํ JwtFilter์ ๊ต์ฅํ ์ ์ฌํ๋ค.
ํค๋์ ํ ํฐ์ด ์๋ค๋ฉด ๊ฒ์ฆํ๊ณ , ๋ค์ ํํฐ๋ฅผ ์คํํ๋ค. ๋๋ ๋ค๋ฅธ micro service์์ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ฅผ ์ป๊ธฐ ์ํด header์ ๊ฐ์ ์ถ๊ฐ๋ก ๋ด์์คฌ๋ค.
์ด๋ ๊ฒ ์ค์ ํ Filter๋ application.yml ์์ ์ ์ฉ์ํฌ ์ ์๋ค.
- api gateway ์ application.yml
spring:
application:
name: apigateway
cloud:
gateway:
routes:
- id: todo-app
uri: lb://TODO-APP
predicates:
- Path=/todo-app/**
- id: temp-server
uri: lb://TEMP-SERVER
predicates:
- Path=/temp-server/**
- id: member-server
uri: lb://MEMBER-SERVER
predicates:
- Path=/member-server/**
default-filters:
- JwtAuthenticationFilter # ๋ชจ๋ ๋ผ์ฐํธ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ฉ
Gateway์์ ์ธ๊ฐ ์ฒ๋ฆฌ
@Slf4j
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
private final JwtTokenProvider jwtProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtProvider) {
super(Config.class);
this.jwtProvider = jwtProvider;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String authorizationHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7);
// ๋ง๋ฃ ์ฒดํฌ
if (jwtProvider.isExpiration(token)) {
log.info("access token ๋ง๋ฃ");
throw new DelegationTokenExpiredException("ํ ํฐ ๋ง๋ฃ");
}
// ํ ํฐ ๊ฒ์ฆ
String loginId = jwtProvider.getLoginId(token);
String role = jwtProvider.getRoles(token);
/* ์ถ๊ฐ */
// ๊ถํ ์ฒดํฌ
if (config.getRequiredRole() != null && !role.contains(config.getRequiredRole())) {
log.info("๊ถํ ๋ถ์กฑ: ํ์ํ ์ญํ = {}, ์ฌ์ฉ์์ ์ญํ = {}", config.getRequiredRole(), role);
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "ํด๋น ๋ฆฌ์์ค์ ์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.");
}
// ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํค๋์ ์ถ๊ฐ
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", loginId)
.header("X-User-Roles", role)
.build();
// ์ ์์ฒญ์ ํฌํจํ Exchange ์์ฑ
exchange = exchange.mutate().request(modifiedRequest).build();
}
return chain.filter(exchange);
};
}
/* ์ถ๊ฐ */
@Data
public static class Config {
// ํํฐ ๊ตฌ์ฑ์ ํ์ํ ์ค์ ์ ๋ณด๋ฅผ ์ถ๊ฐ
private String requiredRole;
}
}
spring:
cloud:
gateway:
routes:
- id: todo-app
uri: lb://TODO-APP
predicates:
- Path=/todo-app/**
filters:
- name: JwtAuthenticationFilter
args:
requiredRole: MEMBER
- id: temp-server
uri: lb://TEMP-SERVER
predicates:
- Path=/temp-server/**
filters:
- name: JwtAuthenticationFilter
args:
requiredRole: ADMIN
- id: member-server
uri: lb://MEMBER-SERVER
predicates:
- Path=/member-server/**
filters:
- name: JwtAuthenticationFilter
args:
requiredRole: MEMBER
Config ํด๋์ค์์ ํํฐ์ ํ์ํ ์ค์ ์ ๋ณด๋ฅผ ๋ฐ์ ์ ์๋ค. application yml๋ก ๋ถํฐ ์ด๋ค ๊ถํ์ด ํ์ํ์ง requiredRole ์ ๋ณด๋ฅผ ๋ฐ๊ณ ํ ํฐ์ ๊ถํ ์ ๋ณด์ ๋น๊ตํ์ฌ ๊ถํ ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ์๋ค.
3. ๊ฐ Micro Service์์ ์ธ์ฆ, ์ธ๊ฐ ์ฒ๋ฆฌ
๋ Multi Module์ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ ๊ณตํต ๋ชจ๋์ ๋ง๋ค์ด ๊ฐ Micro Service์ ์ ์ฉ๋ Security ์ค์ ์ ํด์ฃผ์๋ค.
Common ๋ชจ๋์์ Security ๊ด๋ จ ์ค์
Security Context์ ์ ์ ์ ๋ณด๋ฅผ ๋ฃ์ด์ค์ผ ํ๊ธฐ ๋๋ฌธ์ jwtFilter๋ง ํํฐ๋ก ๋ฑ๋ก์์ผ์ฃผ์๋ค.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll()
.anyRequest().permitAll()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String authorizationHeader = request.getHeader("Authorization");
String token;
// ํค๋๊ฐ null ์ด ์๋๊ณ ์ฌ๋ฐ๋ฅธ ํ ํฐ์ด๋ผ๋ฉด
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// ํ ํฐ ์ถ์ถ
token = authorizationHeader.substring(7);
// ๋ง๋ฃ ์ฒดํฌ
if (jwtProvider.isExpiration(token)) {
log.info("access token ๋ง๋ฃ");
throw new DelegationTokenExpiredException("token ๋ง๋ฃ");
}
String loginId = jwtProvider.getLoginId(token);
Role role = jwtProvider.getRoles(token);
PrincipalDetails principalDetails = new PrincipalDetails(new MemberDto(loginId, role));
// ์ธ์ฆ ์ ๋ณด ์์ฑ
Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null,
principalDetails.getAuthorities());
// SecurityContextHolder์ ์ธ์ฆ ์ ๋ณด ์ค์
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
} catch (Exception e) {
// response ์ธํ
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8"); // JSON ์๋ต์ UTF-8๋ก ์ค์
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter()
.write(e.getMessage());
}
}
}
๊ถํ ์ค์ ์ ์ด๋ ธํ ์ด์ ์ผ๋ก ํ ์๊ฐ์ด๊ธฐ ๋๋ฌธ์ Security Config์ ๋ฐ๋ก ๋ฃ์ง ์์๋ค.
์ด๋ ธํ ์ด์ ๊ถํ ์ค์ ์ ์ฌ๊ธฐ๋ฅผ ์ฐธ๊ณ
์ธ์ฆ, ์ธ๊ฐ๊ฐ ํ์ํ ๋ชจ๋์์ Common ๋ชจ๋ ์์กด์ฑ ์ค์
dependencies {
implementation project(':common')
}
4. ๊ฐ ๋ฐฉ๋ฒ์ ์ฅ๋จ์
Gateway
- ์ฅ์
- ๊ฐ ๋ง์ดํฌ๋ก ์๋น์ค์์ ์ธ์ฆ, ์ธ๊ฐ ๋ก์ง์ด ๋น ์ ธ๋๊ฐ๋ฏ๋ก, ์๋น์ค๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ง์คํ ์ ์๋ค.
- ๊ฐ ์์ฒญ์ Gateway์์ ์ธ์ฆํ๊ณ ๋๋ฉด ์ดํ ๋ง์ดํฌ๋ก์๋น์ค ๊ฐ์ ์์ฒญ์๋ ๋ณ๋์ ์ธ์ฆ ๊ฒ์ฆ์ด ํ์ํ์ง ์์ผ๋ฏ๋ก ์ฑ๋ฅ์ด ํฅ์๋ ์ ์๋ค.
- ๋ชจ๋ ํธ๋ํฝ์ ๋ํด ์ผ๊ด๋ ๋ณด์ ์ ์ฑ ์ ์ ์ฉํ ์ ์๋ค.
- ๋จ์
- ๋จ์ผ ์ฅ์ ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. Gateway์ ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋ชจ๋ ์ธ์ฆ์ด ๋ถ๊ฐ๋ฅํด์ง๊ณ , ๋ฐ๋๋ก ์ธ์ฆ์ ๋ฌธ์ ๊ฐ ์๊ธฐ๋ฉด ๋ผ์ฐํ ๊ธฐ๋ฅ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
- ๋ชจ๋ ์๋น์ค์ ๋ค์ํ ์ธ์ฆ ์๊ตฌ์ฌํญ์ Gateway์ ๋ฐ์ํด์ผ ํ๋ฏ๋ก, Gateway์ ๋ก์ง์ด ๋ณต์กํด์ง ์ ์๋ค.
- Gateway๋ฅผ ํตํ์ง ์๊ณ ๊ฐ ์๋น์ค๊ฐ ํธ์ถ์ ๋ํด์ ๊ถํ ์ฒดํฌ๋ฅผ ํ ์ ์๋ค.
๊ฐ Micro Service
- ์ฅ์
- ๊ฐ ์๋น์ค๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์ธ์ฆ, ์ธ๊ฐ๋ฅผ ์งํํ๋ฏ๋ก, ๊ฐ๋ณ ์๋น์ค์ ๊ถํ ์๊ตฌ์ฌํญ์ ๋ง์ถฐ ์ธ๋ถ์ ์ธ ์ธ๊ฐ ์ ์ฑ ์ ์ธ์ธ ์ ์๋ค.
- ์๋น์ค ๊ฐ ๊ฒฐํฉ๋๋ฅผ ์ค์ฌ MSA์ ์ฅ์ ์ ์ด๋ฆด ์ ์๋ค.
- ์ธ๋ถ์ ์ธ ๊ถํ ์ค์ ์ด ๊ฐ๋ฅํ๋ค.
- ๋จ์
- ์ธ์ฆ, ์ธ๊ฐ์ ํ์ํ ํํฐ๋ฅผ ์ฌ๋ฌ๋ฒ ํ๊ฒ ๋๋ค.
- ๊ฐ ์๋น์ค์ ๋ฐ๋ผ ๋ณด์ ์ ์ฑ ์ ์ผ๊ด๋๊ฒ ๊ฐ์ ธ๊ฐ์ง ๋ชปํ ์๋ ์๋ค.
- ์ธ์ฆ, ์ธ๊ฐ ์ฝ๋๊ฐ ์ค๋ณต๋๋ค.
๋ ๋ฐฉ๋ฒ์ ์ฅ๋จ์ ์ ๋น๊ตํด๋ณด๋ฉด ์์ ๊ฐ์ด ์ ๋ฆฌํ ์ ์์ ๊ฒ ๊ฐ๋ค.
5. ์ต์ข ๊ฒฐ๋ก
์ต์ข ์ ์ผ๋ก ์ธ์ฆ(ํ ํฐ ๊ฒ์ฆ)์ Gateway์์ ํ๊ณ , ์ธ๊ฐ(๊ถํ ์ ์ด)๋ ๊ฐ ๋ง์ดํฌ๋ก ์๋น์ค์์ ์งํํ๊ธฐ๋ก ํ๋ค.
API Gateway์์ ์ธ๊ฐ๋ฅผ ์งํํ์ ๊ฒฝ์ฐ ๊ฐ์ฅ ๋จ์ ์ด ์ธ๊ฐ์ ๋ํ ์ค์ ์ ์ธ์ธํ๊ฒ ํ ์ ์๊ณ ๊ฐ๋ฐ์ ํ๋ ์ฌ๋ ์ ์ฅ์์ ๊ฐ๋ ์ฑ ๋ํ ์ข์ง ๋ชปํ๋ค ์๊ฐ์ด ๋ค์๋ค.
URI ๊ท์น์ ๋ฐ๋ผ ๊ถํ์ ์ค์ ํ๊ฒ ๋๋๋ฐ ์ฌ๋ฌ ๋ง์ดํฌ๋ก ์๋น์ค์ API๋ค์ด ์ ์ ํ ๊ถํ ์ค์ ์ด ๋๋์ง ๋น๊ตํ๊ธฐ ์ํด์ ํ๋ํ๋ ๋น๊ตํด๋ด์ผ ํ ๊ฒ์ด๋ค.
๋ฟ๋ง ์๋๋ผ, service -> service์ ์์ฒญ์์ ๊ฒ์ฆ์ด ๋์ง ์์ผ๋ ๋ณด์์ ์ผ๋ก๋ ์ข์ง ๋ชปํ๋ค๊ณ ์๊ฐ์ด ๋ค์๋ค.
๋ฌผ๋ก ๊ฐ ๋ง์ดํฌ๋ก ์๋น์ค์์ ์ธ์ฆ, ์ธ๊ฐ ๋ก์ง์ ์ํํ๋ฉด ์ค๋ณต๋ ์ฝ๋, ์ค๋ณต ๊ฒ์ฆ์ผ๋ก ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ๊ธฐ๋ ํ๋ค.
์ฝ๋ ์ค๋ณต์ ๊ณตํต ๋ชจ๋์ ์ฌ์ฉํ์ฌ ํด๊ฒฐํ๊ณ ,
์ค๋ณต ๊ฒ์ฆ์ผ๋ก ๋ฐ์ํ๋ ์ฑ๋ฅ ์ ํ๋ ๋ฏธ๋ฏธํ ์์ค์ด๋ผ ํ๋จํ๋ค. ๋ง์ฝ ์์ญ๋ฒ์ ์ค๋ณต ๊ฒ์ฆ์ผ๋ก ์ฑ๋ฅ ์ ํ๊ฐ ์ผ์ด๋๋ค๋ฉด ํด๋น ์๋น์ค๋ ์ค๊ณ๊ฐ ์๋ชป๋์ ๊ฐ๋ฅ์ฑ์ด ๋๋ค๊ณ ์๊ฐํ๋ค.