Java/Spring Boot

[MSA] Spring Cloud로 MSA를 개발해보자 4편 [JWT 인증]

누리는 귀여워 2024. 8. 25. 00:46

1. AuthenticationFilter란?

- Spring Security를 이용한 로그인 요청 발생 시 작업을 처리해 주는 Custom Filter 클래스

 

2. User-Service -> AuthenticationFilter.java

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    //    final AuthenticationManager authenticationManager;
    private final UserService userService;
    private final Environment env;

    public AuthenticationFilter(AuthenticationManager authenticationManager,
                                UserService userService,
                                Environment env) {
        super.setAuthenticationManager(authenticationManager);
        this.userService = userService;
        this.env = env;
    }
    
   // 1. 로그인 요청
   // 2. 본문에 JSON 데이터를 RequestLogin 객체로 변환
   // 3. 이메일과 비밀번호를 UsernamePasswordAuthenticationToken로 감싸서 인증 시도

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
        	// RequestLogin 객체로 요청 데이터를 변환
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

			// email과 password를 UsernamePasswordAuthenticationToken에다가 저장하고 인증시도
            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, ServletException {
        // 인증된 사용자 이메일 가져오기
        String userName = ((User)authResult.getPrincipal()).getUsername();
        // UserService를 통해 사용자 정보(UserDto) 가져오기
        UserDto userDetails = userService.getUserDetailsByEmail(userName);
        // JWT 토큰 생성
        String token = Jwts.builder()
        		// 토큰의 subject로 사용자 ID 설정
                .setSubject(userDetails.getUserId())
                .setExpiration(new Date(System.currentTimeMillis() +
                		// 토큰 만료 시간 설정
                        Long.parseLong(env.getProperty("token.expiration_time"))))
                 // 토큰을 HS512 알고리즘 만들기
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
                // 토큰 생성 완료
                .compact();

		//JWT 토큰과 사용자 ID를 HTTP 응답 헤더에 추가
        response.addHeader("token", token);
        response.addHeader("userId", userDetails.getUserId());
    }
}

 

3. User-Service -> WebSecurity.java

// 권한 설정
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
    private UserService userService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;

    public WebSecurity(UserService userService, BCryptPasswordEncoder bCryptPasswordEncoder, Environment env) {
        this.env = env;
        this.userService = userService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// CSRF 보호 비활성화
        http.csrf().disable();
        
        // 요청에 대한 권할 설정
        http.authorizeRequests()
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(getAuthenticationFiler());

        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFiler() throws Exception {
        AuthenticationFilter authenticationFilter =
                new AuthenticationFilter(authenticationManager(), userService, env);
//        authenticationFilter.setAuthenticationManager(authenticationManager());

        return  authenticationFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	// 로그인했던 password를 암호화
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
}

 

 

4. Gateway-Service -> application.yml

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
#        - id: user-service
#          uri: lb://USER-SERVICE
#          predicates:
#            - Path=/user-service/**
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
#			  /user-service/login에 POST 요청만 규칙 적용
            - Path=/user-service/login
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
 #           기존 = /user-service/login
 #			 변경 = /login
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}

 

5. GateWay-Service -> AuthorizationHeaderFilter.java

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {

    Environment env;

    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    public static class Config {

    }

    // login -> token -> users (with token) -> header(include token)
    @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);
            }

			// Authorization 헤더에서 JWT 추출
            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);
        });
    }

    // Mono, Flux -> Spring WebFlux
    // 에러 처리 메서드: 요청을 차단하고 오류 응답을 반환
    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        log.error(err);

        return response.setComplete();
    }

	// JWT 유효성 검사
    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;

        String subject = null;

		// JWT 파싱 검증
        try {
            subject = Jwts
                    .parser()
                    // 비밀키를 통해 검증
                    .setSigningKey(env.getProperty("token.secret"))
                    // JWT 파싱
                    .parseClaimsJws(jwt)
                    // 클레임, subject 추출
                    .getBody()
                    .getSubject();
        } catch (Exception ex) {
            returnValue = false;
        }

        if (subject == null || subject.isEmpty()) {
            returnValue = false;
        }

        return returnValue;
    }

}

 

6. 테스트

1). 회원가입

 

2). 로그인

 

3). 조회

(1). 토큰 미적용

 

(2). 토큰 적용