본문 바로가기
SI 업무/나만의 프로젝트 만들기

11-3.JWT- SpringSecurity/ Filter chain

by 새로운걸 배우는게 너무 싫은 IT 복붙러 2024. 12. 14.
728x90

11-2 에서 token 을 만드는 JwtUtils.java를 만들었다.

파일명으로도 알수 있듯 Util class 인데 이걸 누가 어떻게 쓸가?

 

쓰는 사람은 Spring Security 의 Filter chain 이다.

 

Spring Security?

Filter chain?


Spring Security 는 Spring쪽에서 사용하는 사용자인증, 사용자 권한 등을 편하게 쓸수 있게 해주는

Framework 이다.

(11-1 에서 의존성을 gradle.build 에 넣었으므로 Spring Security는 쓸수 있는 상태임.)

Spring Security 의 여러 기능중 Filter chain 이라는 개념이 있다.

Spring Security는 UI 가 보내는 RESTFul 요청이 애플리케이션에 도달하기 전에

다수의 Security Filter를 거치도록 설계되어 있다. 

( 여러개의 chain들이 존재 하고 순차적으로 앞 chaing이 끝나면 -> 그 다음 chain -> 그 다음 chain 으로 넘어가는 구조)

이를 Filter Chain이라 부르며, 각 Filter는 요청을 가로채 특정 작업(인증, 권한 확인 등)을 수행한다.

 

Filter Chain의 주요 역할

  1. 요청을 가로채서 처리(intercept).
  2. 사용자 인증(authentication) 수행.
  3. 인증된 사용자의 권한(authorization) 검증.
  4. 검증이 성공하면 요청을 컨트롤러로 전달, 실패하면 에러 응답을 반환.

예)

Client ---intercept(요청 가로채기) --> Filter Chain (1번 chain-2번 chain-3번 chain-...)-----> Controller

 

Filter chain 에 default 로 포함된 filter들 중 중요 항목들

  • SecurityContextPersistenceFilter: SecurityContext를 유지.
  • UsernamePasswordAuthenticationFilter: 폼 로그인 인증 처리.
  • BasicAuthenticationFilter: 기본 인증(Basic Auth) 처리.
  • BearerTokenAuthenticationFilter: JWT 또는 OAuth2 토큰 기반 인증 처리.
  • ExceptionTranslationFilter: 예외 처리 및 오류 메시지 반환.
  • FilterSecurityInterceptor: 최종 권한 검증.

그러면 default 로 제공 되는 filter 들을 그냥 써도 될까?

된다.

아래는 그냥 기본 제공 filter를 쓴 예이다. ( 100% 이해는 못하고 개념 이해 하고 어찌 쓴다는 정보만 알자)

더보기

Spring Security의 기본 필터 사용 방식

Spring Security는 다음과 같은 기본 제공 필터들을 포함하고 있으며, 이들을 수정하지 않아도 Spring Security 설정만으로 적절히 동작하게 할 수 있습니다:

1. UsernamePasswordAuthenticationFilter

  • 기능: 폼 기반 로그인 요청 처리.
  • 사용 예: 사용자가 /login 엔드포인트로 아이디와 비밀번호를 POST하면 이를 처리.
  • 수정 없이 사용하기: Spring Security의 기본 로그인 설정을 활성화하면 바로 사용 가능합니다.
http.formLogin(); // 기본 UsernamePasswordAuthenticationFilter 활성화​

 


2. BasicAuthenticationFilter

  • 기능: HTTP Basic 인증 처리.
  • 사용 예: 클라이언트가 Authorization: Basic base64encoded(username:password) 헤더를 포함한 요청을 보낼 때 이를 처리.
  • 수정 없이 사용하기: Spring Security 설정에서 HTTP Basic을 활성화.
http.httpBasic(); // BasicAuthenticationFilter 활성화

 


3. BearerTokenAuthenticationFilter (Spring Security 5.1 이상)

  • 기능: OAuth2 또는 JWT 토큰 기반 인증 처리.
  • 사용 예: 요청에 Authorization: Bearer <token> 헤더를 포함할 경우 토큰 검증.
  • 수정 없이 사용하기: Spring Security에서 OAuth2 및 JWT 관련 라이브러리를 추가하고, 기본 구성 사용.
 
http.oauth2ResourceServer().jwt(); // BearerTokenAuthenticationFilter 활성화
 

4. SecurityContextPersistenceFilter

  • 기능: SecurityContext 객체를 생성, 유지, 복원.
  • 사용 예: 인증 상태를 요청 간 유지하거나 기존 인증 정보를 재사용.
  • 수정 없이 사용하기: Spring Security가 자동으로 활성화.

5. ExceptionTranslationFilter

  • 기능: 인증 또는 권한 실패 시 적절한 예외 처리 및 에러 응답 반환.
  • 사용 예: 인증 실패 시 401 Unauthorized, 권한 실패 시 403 Forbidden 응답.
  • 수정 없이 사용하기: Spring Security의 기본 예외 처리 기능 사용.

6. CsrfFilter

  • 기능: CSRF(Cross-Site Request Forgery) 공격 방지.
  • 사용 예: POST, PUT, DELETE 요청 시 CSRF 토큰이 유효한지 확인.
  • 수정 없이 사용하기: CSRF 보호는 기본 활성화 상태입니다.

수정 없이 사용할 수 있는 필터 활성화 예제

Spring Security는 기본 필터를 자동으로 활성화하므로, 간단히 설정만 하면 수정 없이 사용할 수 있습니다:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf() // CSRF 보호 (기본 활성화)
                .and()
            .authorizeRequests()
                .antMatchers("/public/**").permitAll() // 공개 URL
                .anyRequest().authenticated() // 나머지는 인증 필요
                .and()
            .formLogin() // UsernamePasswordAuthenticationFilter 활성화
                .loginPage("/login") // 커스텀 로그인 페이지 설정 (옵션)
                .permitAll()
                .and()
            .httpBasic(); // BasicAuthenticationFilter 활성화
    }
}
 


아마도 실제 SI 프로젝트에는 그대로 Filter chain 을 쓸 일은 없을 것이다.

 

나도 아래처럼 기본 제공 하는 Filter chain을 override 해서 사용 했다.

 

첫번째 filter ( CustomAuthenticationFilter.java)

두번째 filter ( JwtAuthorizationFilter.java)

 

첫번째 filter는 최초 로그인시  id,password 가 db 정보와 일치 하면 JWT 로 Token을 만드는 filter

두번째 filter는 로그인 후 다른 ui 에서 요청한 RESTFul 의 payload 에 header 부분의 token 이 맞는 token 인지 확인 하는 filter

 

package com.example.myproject.biz.com.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        setFilterProcessesUrl("/api/login"); // 필터가 처리할 URL
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        try {
            // 요청 본문에서 JSON 데이터 읽기
            String requestBody = request.getReader().lines().collect(Collectors.joining());
            ObjectMapper objectMapper = new ObjectMapper();

            // JSON 데이터를 파싱하여 ID와 Password 추출
            Map<String, String> authRequest = objectMapper.readValue(requestBody, Map.class);
            String username = authRequest.get("id");
            String password = authRequest.get("password");

            System.out.println("username:" + username);
            System.out.println("password:" + password);

//            // 요청에서 ID와 Password 추출
//            String username = request.getParameter("id");
//            String password = request.getParameter("password");

            // Oracle DB에서 사용자 확인
            if (!validateUserFromDB(username, password)) {
                throw new BadCredentialsException("Invalid username or password");
            }

            // 인증 객체 생성
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password);

            // AuthenticationManager로 인증 진행
            //return this.getAuthenticationManager().authenticate(authenticationToken);
            //return authenticationManager.authenticate(authenticationToken);
            return authenticationToken;

        } catch (Exception e) {
            throw new RuntimeException("Authentication failed");
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException {
        // JWT 생성
        String token = generateJwtToken(authResult);

        // 응답 헤더 또는 바디에 JWT 추가
        response.setContentType("application/json");
        response.getWriter().write("{\"token\": \"" + token + "\"}");
    }

    private String generateJwtToken(Authentication authResult) {
        String username = authResult.getName();

        return JwtUtils.generateToken(username);


//        List<String> roles = authResult.getAuthorities().stream()
//                .map(GrantedAuthority::getAuthority)
//                .toList();
//
//        // JWT 생성 (예제: HMAC SHA256)
//        return Jwts.builder()
//                .setSubject(username)
//                .claim("roles", roles)
//                .setIssuedAt(new Date())
//                .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24시간
//                .signWith(Keys.secretKeyFor(SignatureAlgorithm.HS256)) // 비밀키 설정
//                .compact();
    }

    private boolean validateUserFromDB(String username, String password) {
        String sql = "SELECT COUNT(*) FROM USER WHERE ID = ? AND PASSWORD = ?";
//        try (Connection connection = dataSource.getConnection();
//             PreparedStatement statement = connection.prepareStatement(sql)) {
//            statement.setString(1, username);
//            statement.setString(2, password);
//            ResultSet rs = statement.executeQuery();
//            if (rs.next()) {
//                return rs.getInt(1) > 0;
//            }
//        } catch (SQLException e) {
//            e.printStackTrace();
//        }
//        return false;

        return true;
    }
}
package com.example.myproject.biz.com.auth;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import java.io.IOException;

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }
        String token = header.replace("Bearer ", "");
        String username = JwtUtils.validateTokenAndGetUsername(token);
        if (username != null) {
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(username, null, null)
            );
        }
        chain.doFilter(request, response);
    }
}

 


Custom 으로 사용할 filter 를 두개 만들었다.

이제 해당 filter를 filter chain 에서 작동 하도록 추가를 해야 한다.

추가는 아래처럼 한다.

SecurityConfig.java 파일에 보면 

.addFilter(new Customxxxx)

.addFilter(new JwtAuxxx)  로 해서 chain을 건걸 볼수 있다.

package com.example.myproject.biz.com.auth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        System.out.println("filterChain start");
        return http.csrf(csrf -> csrf.disable()) // CSRF 비활성화
                .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/login").permitAll()
                        .anyRequest().authenticated()
                )
                .addFilter(new CustomAuthenticationFilter(authenticationManager))
                .addFilter(new JwtAuthorizationFilter(authenticationManager))
                .build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedOriginPattern("*"); // 모든 도메인 허용
        configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
        configuration.addAllowedHeader("*"); // 모든 헤더 허용
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
728x90