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;
}
}
설명
- CustomUserDetailsService 클래스는 UserDetailsService를 구현하여 사용자의 인증 정보를 제공하는 역할을 합니다.
- 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;
}
}