상세 컨텐츠

본문 제목

Spring Security + JWT 인증 (2/2) - Spring Security 설정

Spring/Spring Security

by Chan.94 2024. 8. 21. 15:24

본문

반응형

SpringBoot환경에서 Spring Security기반에 JWT 인증 방식을 구현하도록 하겠다.

이번 포스팅에서는 Spring Security에 대한 내용이다.

 

Spring Security, JWT 개념과 JWT 구현에 대한 내용은 아래 포스팅을 참고하기 바란다.

 

Spring Security 개념 및 Architecture

JWT(Json Web Token) 정리

Spring Security + JWT 인증 (1/2) - JWT 구현

 


 

dependency 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

application.yml

permit:
  url:
    /favicon.ico,
    /auth/**

 

Spring Security와 JWT인증 로직에서 제외되어야 하는 URL 패턴을 정의한다.


CustomUserDetailsService.java

import org.springframework.beans.BeanUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.devlog.common.entity.User;
import com.devlog.repository.UserRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;
    
    /**
     * DaoAuthenticationProvider에 의해 실행됨.
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("CustomUserDetailsService loadUserByUsername : " + username);
        
        User user = userRepository.findByEmail(username);
        
        if(user == null) {
            throw new UsernameNotFoundException("Check username");
        }
        
        //UserDetails 생성
        PrincipalDetails principalDetails = new PrincipalDetails(user);
        BeanUtils.copyProperties(user, principalDetails);
        
        return principalDetails;
    }

}

 

UserDetailsService를 상속받아 구현한다.

UserDetailsService의 구현체인 CustomUserDetailsService은 로그인 인증처리에서 실행되어 사용자 정보를 조회하여 반환한다.

 

loadUserByUsername

Spring Security의 인증 과정에서 사용자의 세부 정보를 조회하는 중요한 역할을 한다.

username은 사용자 명을 뜻하는 것이 아닌 인증 전 객체 생성 시 사용된 접근 주체(principal)의 값이다. 

즉, 사용자의 key 값이다.

UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(requestBody.get("email"), requestBody.get("password"));

 

 

인증 전 객체를 생성할때 email이 접근주체(principal)로 사용되고 있다.


SecurityConfiguration.java

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.devlog.common.filter.JwtAuthenticationFilter;
import com.devlog.common.jwt.JwtAccessDeniedHandler;
import com.devlog.common.jwt.JwtAuthenticationEntryPoint;
import com.devlog.common.jwt.JwtProvider;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfiguration {

    @Value("${permit.url}")
    private final String[] PERMIT_ALL_RESOURCES;
    
    private final JwtProvider jwtProvider;
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            // JWT를 사용할 것이기 때문에 HTTP Basic 인증 disable
            .httpBasic(httpBasic -> httpBasic.disable())    
            // security의 기본 로그인 화면을 비활성화
            .formLogin(formLogin -> formLogin.disable())    
            // 모든요청에 JWT 인증을 하므로, CSRF 보호가 필요하지 않다.
            .csrf(csrf -> csrf.disable())        
            // JWT 인증방식으로 Session은 필요 없음
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // request 사용권한 체크
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers(this.getPermitAllResources()).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/admin/**")).hasAuthority("ADMIN")
                    .requestMatchers(new AntPathRequestMatcher("/api/v1/**")).hasAnyAuthority("USER", "ADMIN")
                    .anyRequest().authenticated())
            // 인증, 인가 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정
            .exceptionHandling(exception -> exception
                    .accessDeniedHandler(new JwtAccessDeniedHandler())                //인가(Authorization) 실패시
                    .authenticationEntryPoint(new JwtAuthenticationEntryPoint()))    //인증(Authentication) 실패시
            // JWT 인증 필터를 id/password 인증 필터 이전에 추가
            .addFilterBefore(new JwtAuthenticationFilter(jwtProvider, PERMIT_ALL_RESOURCES), UsernamePasswordAuthenticationFilter.class);
            ;
        
        return httpSecurity.build();
    }

    private AntPathRequestMatcher[] getPermitAllResources() {
        AntPathRequestMatcher antPathRequestMatcherArr[] = new AntPathRequestMatcher[PERMIT_ALL_RESOURCES.length];
        for(int nIdx = 0 ; nIdx < PERMIT_ALL_RESOURCES.length ; nIdx++) {
            antPathRequestMatcherArr[nIdx] = new AntPathRequestMatcher(PERMIT_ALL_RESOURCES[nIdx]);
        }
        return antPathRequestMatcherArr;
    }
}

 

@EnableWebSecurity

  • Spring Security에서 제공하는 어노테이션으로 Spring Security의 기본 보안 설정이 적용된다.
    기본 설정에는 SecurityFilterChain이 포함된다.
  • @EnableWebSecurity이 있는 클래스는 Spring Security의 보안 설정을 정의하는 데 사용한다.
  • SecurityFilterChain을 직접 정의하여 커스터마이징 할 수 있다.

PasswordEncoder

  • Spring Security에서 비밀번호를 안전하게 저장하고 비교하기 위해 사용되는 인터페이스
  • 주로 BCryptPasswordEncoder가 사용
    NoOpPasswordEncoder : 비밀번호를 암호화하지 않고 그대로 저장
    BCryptPasswordEncoder : BCrypt 해싱 알고리즘을 사용하는 가장 일반적인 PasswordEncoder 구현체
    Pbkdf2PasswordEncoder, SCryptPasswordEncoder, Argon2 PasswordEncoder, DelegatingPasswordEncoder
    이 있다.

  • 보통 Spring Security의 설정 클래스에서 PasswordEncoder를 Bean으로 등록


SecurityFilterChain

  • Spring Security 5.4 이후 도입된 개념으로, 보안 필터 체인을 구성하는 인터페이스이다.
    WebSecurityConfigurerAdapter는 더 이상 권장되지 않으며, 대신 SecurityFilterChain을 명시적으로 정의하여 보안 설정을 관리하는 방식이 도입됨.
  • WebSecurityConfigurerAdapter를 사용한 방식과 유사하지만, 더 명확하고 유연한 구성을 가능하게 한다.
  • 함수형 프로그래밍 스타일로 설정을 작성할 수 있어 설정이 더 읽기 쉽고 명확하게 작성될 수 있다.
  • 필터들의 순서를 정의하고, 각각의 필터가 어떻게 동작할지 설정한다.

HTTP Basic

HTTP Basic인증 이란 클라이언트가 서버에 요청을 보낼 때 Authorization 헤더에 Basic이라는 접두사와 함께 사용자 이름과 비밀번호를 Base64로 인코딩한 문자열을 포함시킨가. 서버는 이 인코딩 된 문자열을 디코딩하여 인증하는 방식이다.
ex) Authorization: Basic YWRtaW46cGFzc3dvcmQ=

 

OAuth2, JWT 등의 다른 인증 방식을 사용하고자 할 때 HTTP Basic 인증을 비활성화한다.

.httpBasic(httpBasic -> httpBasic.disable())

 

CSRF

CSRF(Cross-Site Request Forgery)는 웹 애플리케이션에서 악의적인 사용자가 다른 사용자를 가장해 요청을 보내는 공격 기법이다.

CSRF 보호는 웹 애플리케이션에서 중요한 보안 기능으로, 클라이언트가 서버로 의도치 않은 요청을 보내는 것을 방지한다. 대부분의 경우, CSRF 보호를 활성화해 두는 것이 안전하지만, RESTful API와 같이 필요하지 않은 경우에는 비활성화할 수 있다.

 

CSRF 공격의 Example

  1. 사용자가 웹사이트 A에 로그인(여기서 쿠키나 세션에 의해 사용자가 인증된 상태)
  2. 사용자가 공격자가 만든 악성 사이트(웹사이트 B)를 방문
  3. 웹사이트 B는 사용자가 이미 인증된 웹사이트 A로 의도하지 않은 요청을 보냄.
    예를 들어, 사용자의 계정에서 돈을 송금하는 요청일 수 있음
  4. 웹사이트 A는 이 요청이 사용자가 의도한 것인지 확인하지 못한 채 요청을 처리할 수 있다.

sessionManagement

사용자의 세션을 관리하는 방법을 설정하는데 사용된다.

  • SessionCreationPolicy.ALWAYS : 항상 세션을 생성
  • SessionCreationPolicy.IF_REQUIRED : 필요할 때만 세션을 생성
  • SessionCreationPolicy.NEVER : Spring Security가 세션을 생성하지 않지만, 기존 세션을 사용할 수 있음
  • SessionCreationPolicy.STATELESS : 세션을 전혀 생성하지 않음

authorizeHttpRequests

HTTP 요청에 대한 권한을 설정하고 관리하는 데 사용되는 구성 메서드로 URL 패턴과 권한을 매핑하는 역할을 한다.

특정 URL은 인증된 사용자만 접근할 수 있도록 하거나, 특정 역할을 가진 사용자만 접근할 수 있도록 설정할 수 있다.

 

requestMatchers

특정 URL 패턴에 대해 권한을 설정한다.

  • permitAll()
    모든 사용자에게 접근을 허용. 인증되지 않은 사용자도 접근할 수 있다.
  • hasRole("역할명")
    특정 역할을 가진 사용자만 접근할 수 있음.
  • hasAuthority("권한명")
    특정 권한을 가진 사용자만 접근할 수 있음

    hasRole은 prefix로 "ROLE_"이 붙기 때문에 로그인 시 UserDetails의 구현체를 GrantedAuthority(권한)을 정의할 때 Spring Security에서 hasRole과 hasAuthority 중 어떤 것을 사용할지 결정한 후 작성하는 것이 좋음.

anyRequest().authenticated()

정의된 패턴을 제외한 모든 요청에 대해 인증을 요구

 

exceptionHandling

인증 및 권한 부여 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정하는 기능이다. 이를 통해 사용자에게 적절한 오류 메시지를 표시하고, 특정 오류 상황에 맞는 행동을 정의할 수 있다.

  • AccessDeniedHandler 
    사용자가 접근 권한이 없는 리소스에 접근하려 할 때 발생하는 예외를 처리
    인터페이스를 상속받아 CustomAccessDeniedHandler 구현 가능
  • AuthenticationFailureHandler 
    인증이 실패할 경우 발생하는 예외를 처리
    인터페이스를 상속받아 CustomAuthenticationFailureHandler 구현 가능
  • AuthenticationEntryPoint
    인증이 필요하지만 인증되지 않은 사용자 요청에 대해 처리
    인터페이스를 상속받아 Custom AuthenticationEntryPoint 구현 가능
//인증, 인가 과정에서 발생할 수 있는 예외를 처리하는 방법을 설정
.exceptionHandling(exception -> exception
    .accessDeniedHandler(new JwtAccessDeniedHandler())                //인가(Authorization) 실패시
    .authenticationEntryPoint(new JwtAuthenticationEntryPoint()))    //인증(Authentication) 실패시

 

JwtAccessDeniedHandler, JwtAuthenticationEntryPoint 객체는 JWT 구현시 작성하였다.

간단하게 로그만 찍었지만 상황에 맞게 특정 페이지로 리다이렉션 처리를 할 수 있다.

 

Spring Security + JWT 인증 (1/2) - JWT 구현

 

addFilterBefore

필터를 추가하는 데 사용되는 메서드로 특정 필터를 기존 필터 체인의 특정 위치에 삽입할 수 있다.

 

// JWT 인증 필터를 id/password 인증 필터 이전에 추가
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider, PERMIT_ALL_RESOURCES), UsernamePasswordAuthenticationFilter.class);

 

JWT 인증 필터를 UsernamePasswordAuthenticationFilter앞에 추가

 


테스트 시나리오 (1)

1. 로그인(USER 권한) 후 Access Token, Refresh Token 확인

AccessToken Header에서 추출하여 확인

 

RefreshToken DB값과 Response값 확인

 

2. /api/v1/** 요청 - 성공

 

3. /admin/** 요청 - 실패

 

ADMIN 권한이 없기 때문에 인가(Authorization)가 실패하여 ExceptionHandling으로 등록한 JwtAccessDeniedHandler이 실행된 것을 확인할 수 있다.

 

테스트 시나리오 (2)

1. 로그인(ADMIN권한) 후 Access Token, Refresh Token 확인

AccessToken Header에서 추출하여 확인

RefreshToken DB값과 Response값 확인

 

 

2. /api/v1/** 요청 - 성공

3. /admin/** 요청 - 성공

반응형

관련글 더보기

댓글 영역

>