사용자 인증 및 권한 처리를 쉽게 제공하는 Spring Security 프레임워크에 대해 공부해보자 ❕
1. 스프링 시큐리티 (Spring Security) ?
Spring Security는 인증, 권한 관리, 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 사용자 관리 기능을 구현하는데 도움을 주는 Spring의 하위 프레임워크이다.
대부분의 시스템들은 회원을 관리하기 위해 *인증(Authentication)과 *인가(Authorization)에 대한 처리를 진행하는데, Spring에서는 SpringSecurity라는 프레임워크에서 관련된 기능을 제공해 보다 쉽게 관리할 수 있도록 한다.
*인증, 인가?
인증(Authentication) : 유저가 누구인지 확인하는 것
인가(Authorization) : 유저에 대한 권한을 확인하는 것
⇒ 사이트에 접속해서 아이디와 비밀번호를 입력해 로그인을 진행하는 것을 인증, 로그인 후 나의 권한에 따라 접근이 가능한지에 대해 확인하는 것은 인가. 따라서 인증이 완료된 주체에 대해 인가를 진행한다.
2. 아키텍처
먼저 Client에서 요청이 어떻게 들어오는지 보자
Client에서 Server에게 요청을 보내면 먼저 Servlet Container가 받는다. 그리고 이 모든 요청을 Servlet Container에서 관리하는 Dispatcher Servlet이 받아 공통적인 작업을 마친 후 해당 요청을 처리해야 하는 Spring Container의 컨트롤러를 찾아서 작업을 위임한다.
이제 Spring Security의 아키텍처를 확인해 보자
SpringSecurity는 Filter를 기반으로 동작한다.
Client에서 Server에게 요청을 보내면 먼저 Servlet Container가 받는다고 했는데, Servlet Container에서 Filter가 실행된다. Filter가 순차적으로 실행되면서 DelegatingFilterProxy라는 Filter가 실행되고, 이 DelegatingFilterProxy가 스프링 빈으로 등록된 Filter(SecurityFilterChain)들을 실행시킨다. (^SpringSecurity는 Filter를 기반으로 동작하기 때문에 Spring MVC와 분리되어 동작한다.^)
DelegatingFilterProxy로 어떻게 Spring에 등록된 Filter가 실행되는지 살펴보자
스프링 빈은 스프링 컨테이너에서 관리하고, Servlet Filter는 서블릿 컨테이너에서 관리하기 때문에 서로 실행되는 위치가 다르다. 따라서 Servlet Filter는 스프링에서 생성된 Bean을 주입해서 사용할 수 없는데 DelegatingFilterProxy가 이를 가능하게 하게 해준다. DelegatingFilterProxy가 스프링 컨테이너에 특정 Bean(springSecurityFilterChain)을 찾아 요청을 위임하기 때문이다.
DelegatingFilterProxy 필터를 통해 Servlet Container와 Spring Container가 연결된다고 생각하면 된다.
이제 Spring Container에서 Filter들이 어떻게 동작하는지 보자
DelegatingFilterProxy가 요청을 받게 될 경우 자신이 요청받은 객체를 delegate request로 요청 위임을 하고, 요청 객체는 Spring Container의 springSecurityFilterChain에서 받게 된다. springSecurityFilterChain 필터를 가지고 있는 빈이 FilterChainProxy인데, FilterChainProxy는 자신이 가진 필터들을 차례로 수행한다. 여기서 인증에 대한 처리를 진행하는 것이다. 처리가 완료되면 최종 자원에 요청을 전달해 다음 로직이 수행된다.
3. 주요 모듈
Spring Security의 주요 모듈을 다음과 같이 구성된다.
1) SecurityContextHolder : SecurityContext를 저장하고 감싸고 있는 객체. 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보도 저장된다. 3가지 전략에 따라 SecurityContext을 저장하는 방법이 달라진다.
- `MODE_THREADLOCAL` : default 전략. 스레드당 SecurityContext 할당
- `MODE_INHERITABLETHREADLOCAL` : 자식 스레드가 생성되면 SecurityContext 공유
- `MODE_GLOBAL` : 애플리케이션 전체에서 SecurityContext 공유
public class SecurityContextHolder {
...
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
...
}
public static SecurityContext getContext() {
return strategy.getContext();
}
...
}
2) SecurityContext : Authentication를 저장하고 감싸고 있는 객체
- 선택한 전략에 따라 SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
3) Authentication : 현재 접근하는 주체의 정보와 권한을 담는 인터페이스
- Principal (접근 주체) : 보호 받는 리소스에 접근하는 대상
- Credential (비밀번호) : 리소스에 접근하는 대상의 비밀번호
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
4) UsernamePasswordAuthenticationToken : Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스
- Authentication 객체를 생성하는데 사용된다.
- 첫번째 생성자는 인증 전의 Authentication 객체를 생성하고, 두번째 생성자는 인증이 완료된 Authentication 객체를 생성한다
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
private final Object principal; // 보호받는 리소스에 접근하는 대상
private Object credentials; // principal의 비밀번호
// 인증 전 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// 인증 완료된 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
...
}
5) AuthenticationManager : 인증 처리를 위한 인터페이스
- `authenticate()`라는 메서드만 정의되어 있다.
- 실제 인증을 담당하는 AuthenticationProvider을 AuthenticationManager에 등록해 사용한다.
- AuthenticationManager는 등록된 AuthenticationProvider들을 돌면서 `authenticate()`를 실행시키고 인증을 진행한다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
6) AuthenticationProvider : 실제 인증에 대한 부분을 처리하는 곳
- `authenticate()`에서 실제 인증에 대한 부분을 구현한다.
- 인증 전의 Authentication 객체를 받아서 인증이 완료된 Authentication 객체를 반환한다.
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
7) UserDetails : 현재 접근하는 주체의 정보를 저장하기 위해 사용하는 인터페이스. 사용자 정보를 불러오기 위해 구현해야하는 인터페이스이다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 권한
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
8) UserDetailsService : 현재 접근하는 주체의 정보를 조회하는 인터페이스. 사용자의 정보를 조회하기 위해 구현해야하는 인터페이스이며, DB에서 유저를 조회하는 로직이 들어간다.
- `loadUserByUsername()` 하나의 메서드만 존재하며, UserDetailsService를 구현하는 클래스를 만들어 오버라이드 해 구현한다.
- 오버라이딩 한 `loadUserByUsername()`에서 DB에서 유저를 조회하고 UserDetails를 반환해야 한다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
4. 인증 처리 과정
- 사용자가 로그인 정보를 입력해 인증 요청을 보낸다.
- AuthenticationFilter가 HttpServletRequest에서 사용자가 보낸 아이디와 패스워드를 인터셉트한다.
HttpServletRequest에서 꺼내온 사용자 아이디와 패스워드로 UsernamePasswordAuthenticationToken을 만든다.
(UsernamePasswordAuthenticationToken의 첫번째 생성자로 인증 전의 Authentication 객체를 생성) - AuthenticationManager에게 UsernamePasswordAuthenticationToken을 넘긴다.
- AuthenticationManager는 실제 인증을 담당할 AuthenticationProvider들에게 Authentication객체(UsernamePasswordAuthenticationToken)을 다시 전달한다.
- AuthenticationProvider는 입력받은 아이디정보로 UserDetailsService를 통해 DB에서 사용자를 조회한다.
(AuthenticationProvider에서 실제 인증에 대한 부분은 `authenticate()`를 오버라이드해서 구현한다) - UserDetailsService에서 조회된 객체는 UserDetails라는 객체로 전달받는다.
- AuthenticationProvider는 UserDetails 객체와 사용자 입력정보를 비교해 인증을 시도한다.
- AuthenticationProvider에서 인증이 완료되면 Authentication 객체를 AuthenticationFilter로 반환한다. (UsernamePasswordAuthenticationToken의 두번째 생성자로 인증이 완료된 Authentication 객체를 생성)
- AuthenticationFilter에서 Authentication 객체를 SecurityContext에 담은 후 인증이 성공하면 `successfulAuthentication()`를 실행하고, 인증이 실패하면 `unsuccessfulAuthentication()`를 실행한다.
- 이 Authentication 객체는 SecurityContext에 저장되며, 언제든지 SecurityContextHolder를 통해 SecurityContext에 접근하고 Authentication를 조회할 수 있다.
5. 구현
아래 실습에서는 JwtAuthenticationFilter라는 커스텀 필터를 만들어 인증 과정에 대한 구현을 정리했다. SpringSecurity에서 기본적으로 제공하는 form 로그인이 아닌 api 요청을 통한 로그인 인증을 진행했고, 기본 유저 저장 방법인 세션을 사용하지 않고 JWT를 이용해 인증 유저를 저장했다.
build.gradle
- 먼저 SpringSecurity dependency를 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-security’
(버전은 spring-boot-starter-security:2.7.6 사용)
1. 시큐리티 설정 파일(SecurityConfig)을 만든다.
@EnableWebSecurity
어노테이션을 추가해 SpringSecurity를 활성화한다.- SecurityFilterChain을 빈으로 등록하고, 인증에 대한 전반적인 설정을 해준다.
- 인증을 담당할 커스텀 필터 JwtAuthenticationFilter에 AuthenticationManager를 전달해준다.
- 실제 인증을 담당할 AuthenticationProvider의 구현체 JwtAuthenticationProvider에게 UserDetailsService와 PasswordEncoder의 구현체를 전달한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// UserDetailsService interface를 구현하는 클래스
private final CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = authenticationManager(http.getSharedObject(AuthenticationConfiguration.class));
JwtAuthenticationFilter jwtAuthenticationFilter = jwtAuthenticationFilter(authenticationManager);
http
.cors().disable()
.csrf().disable()
.formLogin().disable() // form, httpBasic 로그인 방식 끄기
.httpBasic().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // jwt사용으로 세션관리 해제
http
.authorizeRequests()
.antMatchers("/api/admin/**")
.access("hasRole('ROLE_ADMIN')")
.antMatchers("/api/test/**")
.access("hasRole('ROLE_USER')")
.antMatchers("/api/login")
.permitAll()
.anyRequest().authenticated();
return http.build();
}
// 유저 패스워드 암호화에 사용되는 PasswordEncoder 구현체 빈 등록
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// JwtAuthenticationProvider 빈 등록
@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider() throws Exception {
return new JwtAuthenticationProvider(customUserDetailsService, passwordEncoder());
}
// AuthenticationManager 빈 등록
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// 인증(Authentication)과정을 담당할 필터 등록
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(AuthenticationManager authenticationManager) {
JwtAuthenticationFilter authenticationFilter = new JwtAuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManager);
return authenticationFilter;
}
}
2. UserDetails를 implements하는 CustomUserDetails를 생성한다.
- User 정보를 필드로 구성한다. (예시 코드에서는 User 엔티티(User)를 필드로 구성했다)
- boolean 타입 필드들은 true로 바꾼다.
@Getter
@ToString
public class CustomUserDetails implements UserDetails {
private User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
List<String> roleList = Arrays.asList(user.getRoles().split(","));
roleList.forEach(role -> {
authorities.add(() -> role.strip());
});
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3. UserDetailsService를 implements하는 클래스를 생성해 `loadUserByUsername()`을 오버라이드한다.
- 여기서 DB조회가 이루어진다.
- 조회된 User는 UserDetails 객체로 생성해 반환된다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> userOpt = userRepository.findByUsername(username);
if(userOpt.isPresent()) {
return new CustomUserDetails(userOpt.get());
}else {
throw new UsernameNotFoundException("로그인 정보가 올바르지 않습니다.");
}
}
}
4. UsernamePasswordAuthenticationFilter를 상속받는 커스텀 필터 JwtAuthenticationFilter 생성
- UsernamePasswordAuthenticationFilter는 SpringSecurity에서 fromLogin 방식으로 인증을 진행할 때 아이디 & 패스워드를 파싱하여 인증 요청을 위임하는 필터이다. (지금 예시에서는 fromLogin 방식을 이용하지 않으므로 커스텀 로그인이 실행될 url을 설정하고 UsernamePasswordAuthenticationFilter를 상속받아 인증에 대한 로직을 구현했다. 따라서 해당 클래스를 참고해 JwtAuthenticationFilter를 구현했다)
- 생성자에서 인증을 담당할 url을 설정한다.
- 사용자가 입력한 아이디와 패스워드로 UsernamePasswordAuthenticationToken을 만든다. (첫 번째 생성자를 이용해 인증 전 Authentication 객체 생성)
- 그 다음 AuthenticationManger에게 인증을 위임한다.
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JwtAuthenticationFilter() {
// form 로그인이 아닌 커스텀 로그인에서 api 요청시 인증 필터를 진행할 url
this.setFilterProcessesUrl("/api/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
System.out.println("#=====AUTHENTICATION FILTER=====#");
try {
// 넘어온 값으로 user 객체를 생성
User user = new ObjectMapper().readValue(request.getReader(), User.class);
// 인증 전 객체 생성
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
this.setDetails(request, userToken);
// AuthenticationManager에게 인증 위임
return this.getAuthenticationManager().authenticate(userToken);
} catch (IOException e) {
throw new AuthenticationServiceException("아이디와 비밀번호를 올바르게 입력해주세요.");
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
...
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
...
}
}
- UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter의 하위 클래스이다. AbstractAuthenticationProcessingFilter의 `attemptAuthentication()`을 구현하며, AuthenticationManager에게 인증에 대한 처리를 넘긴다.
- JwtAuthenticationFilter는 AbstractAuthenticationProcessingFilter와 UsernamePasswordAuthenticationFilter를 참고해서 구현한 것이다.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
...
}
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
5. AuthenticationProvider를 implements 하는 커스텀 JwtAuthenticationProvider 생성
- AuthenticationManger는 AuthenticationProvider로 등록된 Provider들을 돌면서 실제 인증을 진행한다.
- JwtAuthenticationProvder는 AuthenticationProvider를 implements하고, `authenticate()`를 오버라이드해 구현한다. 여기서 UserDetailsService의 `loadUserByUsername()`를 실행시켜 DB 조회를 실행한다.
- 인증이 완료되면 UsernamePasswordAuthenticationToken를 생성해 반환한다. (두 번째 생성자를 이용해 인증이 완료된 Authentication 객체 생성)
@RequiredArgsConstructor
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService customUserDetailsService;
private final BCryptPasswordEncoder passwordEncoder;
// 실제 인증을 담당
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("#=====AUTHENTICATION PROVIDER=====#");
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
String password = token.getCredentials().toString();
CustomUserDetails savedUser = (CustomUserDetails) customUserDetailsService.loadUserByUsername(username);
UUID salt = savedUser.getUser().getSalt();
// salt된 password를 구한다
String saltedPassword = password + salt;
if(!passwordEncoder.matches(saltedPassword, savedUser.getPassword())) {
throw new BadCredentialsException("로그인 정보가 올바르지 않습니다.");
}
// 인증 완료된 객체 생성
return new UsernamePasswordAuthenticationToken(savedUser, password, savedUser.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
6. Provider에서 인증이 완료되면 Authentication 객체를 AuthenticationFilter로 반환하고, 이를 AuthenticationFilter에서 SecurityContext에 담고 인증 성공 여부에 따라 성공/실패 메서드가 실행된다.
- 인증이 성공하면 `successfulAuthentication()` 실행
- 인증이 실패하면 `unsuccessfulAuthentication()` 실행
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JwtAuthenticationFilter() {
// form 로그인이 아닌 커스텀 로그인에서 api 요청시 인증 필터를 진행할 url
this.setFilterProcessesUrl("/api/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// 넘어온 값으로 user 객체를 생성
User user = new ObjectMapper().readValue(request.getReader(), User.class);
// 인증 전 객체 생성
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
this.setDetails(request, userToken);
// AuthenticationManager에게 인증 위임
return this.getAuthenticationManager().authenticate(userToken);
} catch (IOException e) {
throw new AuthenticationServiceException("아이디와 비밀번호를 올바르게 입력해주세요.");
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("-----successfulAuthnentication-----");
...
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("-----unsuccessfulAuthnentication-----");
...
}
}
이렇게 인증이 완료된다면 `successfulAuthentication()`에서 인증 토큰을 발급하는 형태로 구현했다.
인증 성공 시, 실행된 순서를 보면 AuthenticationFilter -> AuthenticationProvider(DB에서 유저 조회) -> successfulAuthentication() 으로 동작하는 것을 확인할 수 있다.
+ 실패 시 예외 처리
보통 예외 처리할 때, 전역 예외처리 핸들러를 만들어 @RestControllerAdvice와 @ExceptionHandler를 설정하여 사용하는데, 이 방법은 SpringSecurity Filter에서 발생하는 예외를 처리해주지 못한다.
Filter에서 발생한 예외는 스프링의 DispatcherServlet에 도달하기 전에 발생하고, @ExceptionHandler는 스프링의 DispathcerServlet이 요청을 처리하는 동안 동작한다. 따라서 @ExceptionHandler는 Filter단에서 발생한 예외를 감지하고 처리할 수 없다.
다음과 같이 필터에서 발생한 예외를 직접 처리해주는 과정이 필요하다. (물론 예외 처리 방법은 다양하다)
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
...
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("-----unsuccessfulAuthentication-----");
// 예외에 따른 response 세팅
Message message = new Message();
message.setStatus(HttpStatus.UNAUTHORIZED);
message.setMessage("auth_fail");
message.setMemo(failed.getLocalizedMessage());
this.createResponseMessage(response, message);
}
// response message 설정
private void createResponseMessage(HttpServletResponse response, Message message) throws StreamWriteException, DatabindException, IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON.toString());
new ObjectMapper().writeValue(response.getOutputStream(), message);
}
}
참고자료 😃
https://docs.spring.io/spring-security/reference/servlet/architecture.html
https://spring.io/projects/spring-security
https://mangkyu.tistory.com/76
https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0
https://catsbi.oopy.io/c0a4f395-24b2-44e5-8eeb-275d19e2a536
https://cjw-awdsd.tistory.com/45
'Spring' 카테고리의 다른 글
[Spring] SpringBoot에서 Redis 적용하기 (+부하 테스트) (8) | 2024.11.08 |
---|---|
[Spring] Spring에서 동시성 이슈를 해결하는 방법 (1) | 2024.10.02 |
[Spring] Apache POI (+ Multipart, Spring 구현) (0) | 2024.05.14 |
[Spring] V2. OAuth2.0으로 소셜로그인 구현하기 (spring-security-oauth2-client 사용) (0) | 2024.03.23 |
[Spring] V1. OAuth2.0으로 소셜로그인 구현하기 (0) | 2024.03.16 |