[Security] Spring Security with JWT

SHINYEJI·2024년 1월 17일
0

Back-End

목록 보기
24/24

Spring Security 공식 문서
Spring Security Resource 공식 문서

Spring Security 6.0이상 공식 문서

Spring Security 코드로 구현해보기

Spring Boot 3.x.x부터 Spring Security 6.x.x를 사용해야 한다.
1. spring security의 의존성을 설정 후

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. AuthenticationConfig 클래스 파일에 @EnableWebSecurity어노테이션을 달아놓으면 모든 Spring Security가 API에 인증이 필요하다고 디폴트로 설정해 놓는다.
    그래서 별다른 조치를 하지 않는다면, API를 보낼 때 401(UnAuthorized)에러가 발생할 것이다.

권한 부여 규칙 정의

Filter Chain?
어떤 요청이 End-Point에 도달하기 전에 요청을 가로채서 특정 작업을 수행하는 컴포넌트를 Sevlet Filter라고 한다.
이 Filter에서는 주로 Security에 대한 전처리 과정을 진행한다.
Filter Chain은 이런한 Filter가 사슬처럼 엮여있는 것을 말한다.

그러나 모든 API에 인증이 필요한 것이 아니기 때문(회원가입 등)에 몇몇 API는 권한을 풀어야 한다.

config파일에 WebSecurityConfigurerAdapter를 사용하는 것은 Spring Security 5.4이후부터 Deprecated가 되었다.
그래서 security filter chain을 Bean등록해서 사용해야한다.
Spring blog 공식문서에서 확인

Spring Security lambda DSL과 HttpSecurity메서드를 사용하여 권한 부여 규칙을 정의한다.

"날; 숨" 프로젝트에서 Spring Security를 사용했는데, 이때 나는 Login Filter와 JWT Filter, Exception Handler를 Filter 사이에 추가했다.

 @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // cors
        http
                .cors((cors) -> cors.configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOriginPatterns(Collections.singletonList("http://localhost:5173"));
                        configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowCredentials(true);
                        configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setMaxAge(3600L);      // 1시간 동안 캐시하도록 설정

                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));
                        return configuration;
                    }
                }))
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers(HttpMethod.GET, "/comments/**").permitAll()
                        .requestMatchers("/", "/general/**", "/email/**", "/rehabilitation","/rehabilitation/{course_id}", "/rehabilitation/problem/{category_id}",
                                "/boards", "/boards/{board_id}", "/boards/search", "/articles/{article_id}", "/comments/list/{article_id}","/auth/kakao/**").permitAll()
                        .requestMatchers("/swagger-ui/**").permitAll()
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .requestMatchers("/users/**").hasRole("USER")
                        .anyRequest().authenticated())
                .addFilterBefore(new FilterExceptionHandler(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class)
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

권한 계층

계층을 등록할 때는 ROLE_을 사용하지 않아도 Spring Security가 자동으로 접두사를 붙인다.

http.requestMatchers("/admin").hasRole("ADMIN")

이와 같이 role을 설정할 때 계층만 정의하면 된다.

비밀번호 암호화

  • Spring Security에서 제공하는 클래스로 비밀번호 암호화를 진행 할 수 있다.
  1. SecurityConfig 클래스에 BCryptPasswordEncoder를 Bean 등록해야한다.
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }
  1. 사용할 때는 Bean 등록한 것을 주입받아 사용하면된다.
    나는 생성자 주입을 사용했다.
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public JoinService(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
    
	bCryptPasswordEncoder.encode(password);

Login 처리를 Filter에서!!

원래 Login Filter에서 사용자 인증을 진행하고, JWT 토큰과 Refresh token을 발급하려고 했지만, 로그인 response 데이터에 사용자 정보가 들어있어, 고민하다가 로그인 인증만 filter에서 처리하고 controller에서 token발급을 진행했다.
다 구현하고 보니... filter에서 모두 처리하는게 좋았을 듯하다.

프로젝트에서 로그인 url이 'general/login'이지만 filter의 default url은 '/login'이였다.
url을 커스텀하기 위해 생성자에 아래 코드를 추가했다.

        super.setFilterProcessesUrl("/general/login");
  • attemptAuthentication 함수에 AuthenticationManager에게 login data를 건네주면 AuthenticationManager가 내부적으로 UserDetailService와 UserDetail을 부른다. 그래서 serDetailService와 UserDetail를 커스텀하여 내 코드에 맞게 만들었다. 이곳에서는 login id, password의 유효성을 검사한다.

아래 코드는 CustomUserDetailsSevice의 일부인데, 이렇게 데이터베이스에 접근하여 id, password의 유효성을 검사한다.

    @Override
    public UserDetails loadUserByUsername(String login_id) throws UsernameNotFoundException {

        Member member = memberRepository.findByLoginIdAndWithdrawFalse(login_id);

        if(member!=null){
            return new CustomUserDetails(member);
        }

        return null;
    }

0개의 댓글