MSA 인증 실습
Gateway + Auth + Product
간단한 로그인 실습
Gateway와 Product는 기존 실습 프로젝트 그대로 사용 Auth 프로젝트 생성 후 진행
Gateway 필터
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// /auth/signIn 경로로 요청이 들어오면 다음 필터로 넘긴다
if ((exchange.getRequest().getURI().getPath()).equals("/auth/signIn")) {
return chain.filter(exchange);
}
// 그 외 요청이라면 토큰을 추출한다
String token = extractToken(exchange);
// 추출한 토큰이 null 또는 유효하지 않으면 상태 코드를 설정하고 요청 처리를 종료한다
if(token == null || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 유효한 토근이면 다음 필터로 넘긴다
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange) {
// Authorization 헤더의 값 추출
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
// null이 아니고 "Bearer "로 시작한다면
if(authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7); // "Bearer "를 제외한 실제 토큰 부분을 반환
}
// null이거나 형식이 맞지 않으면 null 반환
return null;
}
private boolean validateToken(String token) {
try {
// SecretKey 생성 및 JWT 서명 검증
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(key)
.build().parseSignedClaims(token);
log.info("##payload {}", claimsJws.getPayload().toString());
//추가 검증
return true;
} catch (Exception e) {
return false;
}
}
}
Auth - build.gradle 및 yml 파일
gradle 파일에 jwt 의존성 추가
yml 파일에 eureka 서버 등록 설정 및 jwt 관련 내용 추가
AuthConfig
@Configuration
@EnableWebSecurity // Security 활성화
public class AuthConfig {
// SecurityFilterChain 빈 정의 및 보안 필터 체인을 설정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF(Cross-Site Request Forgery) 보호를 비활성화
.csrf(csrf -> csrf.disable())
// HTTP 요청에 대한 접근 권한을 설정
.authorizeHttpRequests(authorize -> authorize
// /auth/signIn 경로에 대해 인증 없이 접근을 허용
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증 필요
.anyRequest().authenticated()
)
// 세션 관리 정책 설정 (세션 사용하지 않음)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 설정된 보안 필터 체인을 반환
return http.build();
}
}
- 흐름
요청이 들어오면 SecurityFilterChain이 활성화된다
CSRF 보호는 비활성화
요청 경로가
/auth/signIn
이면 인증 없이 허용하며 그 외의 모든 요청은 인증이 필요하다세션을 사용하지 않는다 (JWT를 통해 인증)
AuthController
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthToken(@RequestParam String userId) {
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(userId)));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String accessToken;
}
}
AuthService
@Service
public class AuthService {
@Value("${spring.application.name}")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}
public String createAccessToken(String userId) {
return Jwts.builder()
.claim("user_id", userId)
.claim("roles", "ADMIN")
.issuer(issuer)
.issuedAt(new Date((System.currentTimeMillis())))
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
}
signIn 요청이 들어오면 JWT를 생성해서 반환한다
최종 흐름
클라이언트가 게이트웨이로 요청을 보낸다
게이트웨이는 요청을 받아 인증 처리를 진행한다
a. 이때 /auth/signIn 경로의 요청이면 별도 인증 처리 없이 필터를 넘긴다
b. 필터가 없다면 auth가 요청을 받아 JWT를 생성하여 반환한다
그외 경로의 요청이라면 JWT 토큰을 검증한다
유효한 토큰이면 다음 필터로 넘긴다
유효하지 않은 토큰이면 상태코드를 설정하고 요청을 종료한다