OAuth2 + JWT 실습

Gateway인증
2024.11.26
·
5 min read

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();
    }
}

- 흐름

  1. 요청이 들어오면 SecurityFilterChain이 활성화된다

  2. CSRF 보호는 비활성화

  3. 요청 경로가 /auth/signIn이면 인증 없이 허용하며 그 외의 모든 요청은 인증이 필요하다

  4. 세션을 사용하지 않는다 (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를 생성해서 반환한다

최종 흐름

  1. 클라이언트가 게이트웨이로 요청을 보낸다

  2. 게이트웨이는 요청을 받아 인증 처리를 진행한다

    a. 이때 /auth/signIn 경로의 요청이면 별도 인증 처리 없이 필터를 넘긴다

    b. 필터가 없다면 auth가 요청을 받아 JWT를 생성하여 반환한다

  3. 그외 경로의 요청이라면 JWT 토큰을 검증한다

    • 유효한 토큰이면 다음 필터로 넘긴다

    • 유효하지 않은 토큰이면 상태코드를 설정하고 요청을 종료한다







- 컬렉션 아티클