0. 들어가며
마이크로서비스 아키텍처에서 인증과 인가는 특히 중요한 과제다. 모놀리식 애플리케이션에서는 단일 세션 저장소를 사용하여 사용자 인증 상태를 관리할 수 있었지만, 분산 환경에서는 각 서비스가 독립적으로 인증 정보를 관리하기 어렵다. 이러한 문제를 해결하기 위한 대표적인 방법이 JWT(JSON Web Token)를 활용한 토큰 기반 인증이다.
이번 글에서는 앞서 구축한 User Service에 Spring Security와 JWT를 적용하여 안전한 인증 시스템을 구현할 것이다. 또한 API Gateway에서 JWT를 검증하는 AuthorizationFilter를 추가하여, 인증된 요청만 내부 서비스로 라우팅되도록 구성한다.
1. Users Microservice - 기능 추가
1.1. User Service에 주문 내역 포함하기
User Service가 사용자 정보를 반환할 때, 해당 사용자의 주문 내역도 함께 제공하도록 기능을 추가한다. 이를 위해 User Service와 Order Service 간의 통신이 필요하다.
ResponseUser.java (수정)
package com.example.userservice.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class ResponseUser {
private String email;
private String name;
private String userId;
private LocalDateTime createdAt;
// 주문 내역 추가
private List<ResponseOrder> orders;
}
ResponseOrder.java (User Service에 추가)
package com.example.userservice.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ResponseOrder {
private String productId;
private Integer quantity;
private Integer unitPrice;
private Integer totalPrice;
private String orderId;
private LocalDateTime createdAt;
}
1.2. User Service 수정
UserService.java (인터페이스 수정)
public interface UserService {
ResponseUser createUser(RequestUser user);
ResponseUser getUserByUserId(String userId);
List<ResponseUser> getAllUsers();
// 주문 내역을 포함한 사용자 정보 조회 메서드 추가
ResponseUser getUserWithOrders(String userId);
}
2. Users Microservice - 인증 처리 구현
2.1. JWT 의존성 추가
build.gradle (user-service)
dependencies {
// 기존 의존성들...
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
2.2. JWT 설정 프로퍼티 추가
application.yml (user-service)
# JWT 설정
token:
expiration_time: 86400000 # 1일 (밀리초)
secret: my-secret-key-which-should-be-very-long-and-secure-for-jwt-token-generation
2.3. AuthenticationFilter 추가
Spring Security의 인증 처리를 커스터마이징하기 위해 UsernamePasswordAuthenticationFilter를 상속받는 AuthenticationFilter를 생성한다.
AuthenticationFilter.java
package com.example.userservice.filter;
import com.example.userservice.dto.RequestLogin;
import com.example.userservice.entity.UserEntity;
import com.example.userservice.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final UserService userService;
private final Environment env;
public AuthenticationFilter(AuthenticationManager authenticationManager,
UserService userService,
Environment env) {
super(authenticationManager);
this.userService = userService;
this.env = env;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
try {
// 요청 본문에서 로그인 정보 추출
RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
// 인증 토큰 생성
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
new ArrayList<>()
)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException {
// 인증된 사용자 정보
String userName = ((User) authResult.getPrincipal()).getUsername();
UserEntity userDetails = userService.getUserDetailsByEmail(userName);
// JWT 토큰 생성
String token = Jwts.builder()
.setSubject(userDetails.getUserId())
.setExpiration(new Date(System.currentTimeMillis() +
Long.parseLong(env.getProperty("token.expiration_time"))))
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
.compact();
// 응답 헤더에 토큰과 사용자 ID 추가
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
log.info("User logged in successfully: {}", userDetails.getEmail());
}
}
2.4. RequestLogin DTO 생성
RequestLogin.java
package com.example.userservice.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RequestLogin {
@NotBlank(message = "이메일은 필수 입력값입니다.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
@NotBlank(message = "비밀번호는 필수 입력값입니다.")
private String password;
}
2.5. Security Configuration 수정
WebSecurityConfig.java (수정)
package com.example.userservice.config;
import com.example.userservice.filter.AuthenticationFilter;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final Environment env;
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
// AuthenticationManager 설정
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
// AuthenticationFilter 생성 및 설정
AuthenticationFilter authenticationFilter =
new AuthenticationFilter(authenticationManager, userService, env);
authenticationFilter.setFilterProcessesUrl("/login");
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/**").permitAll()
.anyRequest().authenticated()
)
.authenticationManager(authenticationManager)
.addFilter(authenticationFilter)
.addFilterBefore(new com.example.userservice.filter.AuthorizationFilter(),
UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.disable())
);
return http.build();
}
}
3. Users Microservice - 인증 기능 테스트
3.1. UserDetailsService 구현
UserService가 UserDetailsService를 구현하도록 수정한다.
UserService.java (수정)
public interface UserService extends UserDetailsService {
// 기존 메서드...
UserEntity getUserDetailsByEmail(String email);
}
UserServiceImpl.java (수정)
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
true, true, true, true,
new ArrayList<>());
}
@Override
public UserEntity getUserDetailsByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
}
// 기존 메서드들...
}
3.2. 로그인 테스트
로그인 요청
POST <http://localhost:8000/login>
Content-Type: application/json
{
"email": "test@example.com",
"password": "password123"
}
응답 헤더 확인
token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiw... (생략)
userId: 123e4567-e89b-12d3-a456-426614174000
4. API Gateway - Filter 추가
4.1. AuthorizationHeaderFilter 구현
API Gateway에서 JWT 토큰을 검증하는 필터를 구현한다.
AuthorizationHeaderFilter.java (apigateway-service)
package com.example.apigatewayservice.filter;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {
// 설정 정보가 필요한 경우 추가
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// Authorization 헤더 확인
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
String jwt = authorizationHeader.replace("Bearer ", "");
// JWT 토큰 유효성 검증
if (!isJwtValid(jwt)) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
};
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser()
.setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt)
.getBody()
.getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
5.2. API Gateway - Route 처리
application.yml (apigateway-service)
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
# User Service - 회원가입은 인증 없이 접근 가능
- id: user-service-signup
uri: lb://USER-SERVICE
predicates:
- Path=/users/**
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/users/(?<segment>.*), /$\\{segment}
# User Service - 로그인은 인증 없이 접근 가능
- id: user-service-login
uri: lb://USER-SERVICE
predicates:
- Path=/login
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/login, /login
# User Service - 그 외 요청은 인증 필요
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/users/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/users/(?<segment>.*), /$\\{segment}
- name: AuthorizationHeaderFilter
args:
# 필터 설정
# Catalog Service - 모든 요청 인증 필요
- id: catalog-service
uri: lb://CATALOG-SERVICE
predicates:
- Path=/catalogs/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/catalogs/(?<segment>.*), /$\\{segment}
- name: AuthorizationHeaderFilter
args:
# 필터 설정
# Order Service - 모든 요청 인증 필요
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/orders/**
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/orders/(?<segment>.*), /$\\{segment}
- name: AuthorizationHeaderFilter
args:
# 필터 설정
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: <http://localhost:8761/eureka/>
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
token:
secret: my-secret-key-which-should-be-very-long-and-secure-for-jwt-token-generation
5.3. API Gateway - Rewrite Path 처리
RewritePath 필터는 클라이언트가 요청한 경로를 실제 서비스의 경로로 변환한다. 예를 들어, 클라이언트가 /users/{userId}로 요청하면, 게이트웨이에서 이 요청을 받아 /{userId}로 변환하여 User Service로 전달한다. 이렇게 함으로써 각 마이크로서비스는 자신의 컨텍스트 패스(context path)를 신경 쓰지 않고 구현할 수 있다.
filters:
- RewritePath=/users/(?<segment>.*), /$\\{segment}
이 설정은 정규 표현식을 사용하여:
- /users/123 → /123 (User Service로 전달)
- /users → / (User Service로 전달)
6. 전체 테스트 시나리오
6.1. 회원가입
POST <http://localhost:8000/users>
Content-Type: application/json
{
"email": "user@example.com",
"name": "테스트 사용자",
"pwd": "password123"
}
응답: 201 Created
{
"success": true,
"message": "사용자가 성공적으로 생성되었습니다.",
"data": {
"email": "user@example.com",
"name": "테스트 사용자",
"userId": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-01-01T12:00:00",
"orders": null
}
}
6.2. 로그인
POST <http://localhost:8000/login>
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
응답 헤더:
token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxMjNlNDU2Ny1lODliLTEyZDMtYTQ1Ni00MjY2MTQxNzQwMDAiLCJleHAiOjE3MDQxMTUyMDB9...
userId: 123e4567-e89b-12d3-a456-426614174000
6.3. 인증이 필요한 API 호출
사용자 정보 조회
GET <http://localhost:8000/users/123e4567-e89b-12d3-a456-426614174000>
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
응답: 200 OK
{
"success": true,
"message": "Success",
"data": {
"email": "user@example.com",
"name": "테스트 사용자",
"userId": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2024-01-01T12:00:00",
"orders": [
{
"productId": "CATALOG-001",
"quantity": 2,
"unitPrice": 1500,
"totalPrice": 3000,
"orderId": "456e7890-e12f-34g5-h678-901234567890",
"createdAt": "2024-01-02T14:30:00"
}
]
}
}
상품 목록 조회
GET <http://localhost:8000/catalogs>
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
주문 생성
POST <http://localhost:8000/orders/123e4567-e89b-12d3-a456-426614174000>
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
Content-Type: application/json
{
"productId": "CATALOG-001",
"quantity": 1,
"unitPrice": 1500
}
7. 결론
이번 글에서는 JWT를 활용한 인증 시스템을 마이크로서비스 환경에 구현하는 방법을 살펴보았다.
구현된 기능:
- ✅ Spring Security와 JWT를 이용한 사용자 인증
- ✅ 로그인 성공 시 JWT 토큰 발급
- ✅ User Service에서 사용자 정보 조회 시 주문 내역 포함
- ✅ API Gateway에서 JWT 토큰 검증 필터 구현
- ✅ 경로 재작성(Rewrite Path)을 통한 유연한 라우팅
보안 고려사항:
- JWT 비밀키는 환경 변수나 설정 서버를 통해 안전하게 관리해야 한다.
- 토큰 만료 시간은 서비스 정책에 맞게 적절히 설정한다.
- HTTPS를 통해 통신을 암호화해야 한다.
- Refresh Token을 도입하여 토큰 갱신 메커니즘을 구현할 수 있다.
다음 글에서는 분산 환경에서의 설정 관리를 위한 Spring Cloud Config와 메시지 버스(Spring Cloud Bus)를 살펴볼 예정이다. 이를 통해 마이크로서비스의 설정을 중앙에서 관리하고 동적으로 변경하는 방법을 알아보겠다.
'MSA > MSA 기본' 카테고리의 다른 글
| [BASIC #7] Microservice 간 통신 전략 (0) | 2025.09.20 |
|---|---|
| [BASIC #6] Configuration Service와 중앙 설정 관리 (0) | 2025.09.20 |
| [BASIC #4][실습] E-commerce MSA 프로젝트 구조 설계 (0) | 2025.09.20 |
| [BASIC #3] API Gateway (0) | 2025.09.20 |
| [BASIC #2] Service Discovery (0) | 2025.09.20 |
