본문 바로가기

카테고리 없음

[Spring Boot] Security6 로그인 필터 구현 및 DB 로그인 검증(2)

 

https://docs.spring.io/spring-security/reference/servlet/architecture.html

 

Architecture :: Spring Security

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec

docs.spring.io

 

로직 흐름

더보기

사용자
  |
  v
POST /login (username, password)
  |
  v
LoginFilter
  |---------> attemptAuthentication() 
  |               |
  |               v
  |        AuthenticationManager.authenticate()  
  |               |
  |               v
  |<--------- 인증 검증
  |
  v
successfulAuthentication()
  |
  v
JwtService.generateToken()
  |
  v
응답 헤더에 JWT 추가
  |
  v
JWT 응답 반환

 

 

1. 로그인 요청 받기: 커스텀 UsernamePasswordAuthentication 필터 작성

  • LoginFilter
  • 클라이언트가 /login 엔드포인트로 POST 요청을 보낼 때 UsernamePasswordAuthentication 필터가 요청을 가로채어 검증을 진행합니다.
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    public LoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 클라이언트 요청에서 username, password 추출
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        // 스프링 시큐리티에서 username과 password를 검증하기 위해 token에 담아야 함
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);

        // token에 담은 검증을 위한 AuthenticationManager로 전달
        return authenticationManager.authenticate(authToken);
    }

    // 로그인 성공시 실행하는 메소드 (여기서 JWT를 발급하면 됨)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
        // JWT 발급 로직 구현
    }

    // 로그인 실패시 실행하는 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        // 로그인 실패 처리 로직 구현
    }
}

 

 

2. SecurityConfig 설정: 커스텀 로그인 필터 등록

  • SecurityConfig에서 커스텀 로그인 필터를 등록합니다.
  • AuthenticationManager Bean을 등록하고 LoginFilter에 전달합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // AuthenticationConfiguration 객체를 주입받아 필드에 저장
    private final AuthenticationConfiguration authenticationConfiguration;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    // AuthenticationManager 빈을 생성하여 스프링 컨테이너에 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    // BCryptPasswordEncoder 빈을 생성하여 스프링 컨테이너에 등록 (비밀번호 암호화에 사용)
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // SecurityFilterChain 빈을 생성하여 스프링 컨테이너에 등록 (HTTP 보안 설정 구성)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // CSRF 보호를 비활성화
            .csrf((auth) -> auth.disable())
            // 기본 제공되는 폼 로그인 방식을 비활성화
            .formLogin((auth) -> auth.disable())
            // HTTP Basic 인증 방식을 비활성화
            .httpBasic((auth) -> auth.disable())
            // 요청 경로에 따른 접근 권한 설정
            .authorizeHttpRequests((auth) -> auth
                .requestMatchers("/login", "/", "/join").permitAll() // 특정 경로는 인증 없이 접근 허용
                .anyRequest().authenticated()) // 나머지 모든 요청은 인증 필요
            // 커스텀 로그인 필터를 UsernamePasswordAuthenticationFilter 위치에 추가
            .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class)
            // 세션 정책을 STATELESS로 설정 (세션을 사용하지 않음)
            .sessionManagement((session) -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build(); // SecurityFilterChain 객체를 빌드하고 반환
    }
}

 

 

  • addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class): 커스텀 로그인 필터를 UsernamePasswordAuthenticationFilter 위치에 추가합니다. 이 필터는 로그인 요청을 처리합니다.
  • sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)): 세션 관리 정책을 STATELESS로 설정합니다. 이는 세션을 사용하지 않도록 설정하는 것입니다.

 

 

내부 SecurityFilterChain

 

 

3. 로그인 검증

UserRepository

UserRepository는 데이터베이스에서 사용자 정보를 관리하는 리포지토리입니다. 이 인터페이스는 Spring Data JPA의 JpaRepository를 확장하여 사용자 엔티티(UserEntity)를 관리합니다.

package com.example.loans_domain.auth.repository;

import com.example.loans_domain.auth.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, Long> {


    Boolean existsByUserEmail(String userEmail);

    UserEntity findByUserEmail(String userEmail);
}

 

UserDetailsService 커스텀 구현

UserDetailsService는 Spring Security에서 사용자의 인증 정보를 제공하는 인터페이스입니다. 커스텀 구현을 통해 데이터베이스에서 사용자 정보를 가져올 수 있습니다.

  • CustomUserDetailsService
package com.example.loans_domain.auth.service;

import com.example.loans_domain.auth.entity.CustomUserDetails;
import com.example.loans_domain.auth.entity.UserEntity;
import com.example.loans_domain.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

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

        UserEntity userData = userRepository.findByUserEmail(username);  // email 조회

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

        return null;
    }
}

 

설명

  1. CustomUserDetailsService 클래스는 UserDetailsService를 구현하여 사용자의 인증 정보를 제공하는 역할을 합니다.
  2. loadUserByUsername(String username) 메소드:
    • 주어진 사용자 이름(username)으로 데이터베이스에서 사용자 정보를 조회합니다.
    • 조회된 사용자 정보를 CustomUserDetails 객체로 감싸서 반환합니다.
    • 사용자 정보가 없으면 UsernameNotFoundException을 발생시킵니다.

 

UserDetails 커스텀 구현

UserDetails는 Spring Security에서 사용자 정보를 나타내는 인터페이스입니다. 이를 커스텀 구현하여 데이터베이스에서 가져온 사용자 정보를 포함할 수 있습니다.

  • CustomUserDetails
package com.example.loans_domain.auth.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

// CustomUserDetails 클래스는 UserDetails 인터페이스를 구현하여 사용자 정보를 제공
public class CustomUserDetails implements UserDetails {

    // UserEntity 인스턴스를 저장
    private final UserEntity userEntity;

    // 생성자에서 UserEntity를 주입 받음
    public CustomUserDetails(UserEntity userEntity){
        this.userEntity = userEntity;
    }

    // 사용자의 권한 목록을 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();

        // 사용자의 역할(role)을 GrantedAuthority로 변환하여 컬렉션에 추가
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userEntity.getUserRole().toString();
            }
        });

        return collection;
    }

    // 사용자의 비밀번호를 반환
    @Override
    public String getPassword() {
        return userEntity.getUserPassword();
    }

    // 사용자의 이메일(사용자 이름)을 반환
    @Override
    public String getUsername() {
        return userEntity.getUserEmail();
    }

    // 계정이 만료되지 않았는지 확인
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠기지 않았는지 확인
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 자격 증명이 만료되지 않았는지 확인
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정이 활성화되었는지 확인
    @Override
    public boolean isEnabled() {
        return true;
    }
}