[그리고 시큐리티를 곁들인] #4 : Spring Security 를 이용한 JWT 인증/인가
인트로
이전 글(3편)에서는 Jwt 토큰에 관련된 Util 클래스와 로그인, 회원가입 기능을 구현했다.
[그리고 시큐리티를 곁들인] #3 : ID/PW 방식의 회원가입과 로그인과 JWT 토큰 발급
인트로이전 글(2편)에서는 이번 시리즈의 주제와 글을 쓰게 된 배경을 소개했다. [그리고 시큐리티를 곁들인] #2 : 프로젝트 설계 + 유저 도메인 ERD 설계요구사항과 그에 따른 프로젝트 설계, 그
0woodev.tistory.com
이번 글에서는 발급된 JWT 토큰에 대해서 인증/인가 하기 위해 `Spring Security` 를 이용해보려고 한다.
필자는 단순히 따라하면서 하다가 어느 순간 내가 뭘 하고 있는지도 모르면서 복붙싸개가 되버리는건 아닌지, 이런 생각을 가진 적이 몇 번 있다. `Spring Security` 는 이런 생각을 가지게 했던 친구 중 하나였다.
이 번 포스트에서는 시큐리티에 대해서 완벽히 공부해서 쓰는 것은 아니지만, 다른 분들에게 좀 더 이해가 되기를 바라면서 적어본다.
과거의 필자처럼 낙오되지 않게 꽉 붙잡으시죠.
Spring Security (이하 시큐리티)
시큐리티는 프레임워크이다. 그것도 아주 덩치가 큰 녀석이다. 프레임워크? 라이브러리? 무슨 차이일까?
쉽게 표현하면,
용어 | 설명 |
프레임워크 | 특정한 목적을 달성하기 위해 프레임워크가 지정한 규칙에 맞춰 사용자가 코드를 작성했을 때 다양한 기능을 제공해주는 것 |
라이브러리 | 특정한 목적을 위한 다양한 기능을 제공해주고 사용자가 이를 적절히 활용하도록 도와주는 것 |
조금 더 디테일하게 얘기하면, 제어권이 사용자에게 있는지, 프레임워크에 있는지가 다르다.
시큐리티는 보안에 관련된 설정을 개발자가 손쉽게 할 수 있도록 도와준다. (손쉽게?)
Security 를 처음 써보면, 뭔놈의 설정이 이렇게 많은지, 또 뭘 넣어야하는지, 인터넷의 코드를 복사해서 넣었더니, 버젼차이로 메서드의 인자가 달라진다든가 하는 등 굉장히 어려워 보인다. 그래서 인터넷을 아무리 뒤져도 뭔가 속시원한 정보들이 잘 안보이다보니 금방 포기하거나, 대충 설정해두고 마무리하게 된다. (진짜, 인터넷을 찾아보면, 다 MVC 에 대한 개발, 즉 SpringBoot 에서 탬플릿엔진을 이용한 예시들만 차고 넘친다)
하지만, 이제는 우리에게 AI 가 있다. ChatGPT 가 알려준다. 물론, ChatGPT 를 쓴다고 해서 그냥 주는대로 쓰라는 것은 아니다.
그리고 AI 보단 못하지만, 필자도 어려움을 겪는 개발자들에게 알려주고 싶다. 이번 기회로, 필자가 쓴 이 글을 통해 조금은 각 설정들이 왜 필요한지, 어떤 설정들을 해줘야 하는지, 어떤 이유로 이렇게 설정하는지 완벽하게는 아니지만, 조금이라도 알고 넘어가보자.
시큐리티의 `HttpSecurity` 은 웹 애플리케이션의 HTTP 요청을 보호하는 데 사용하는 클래스이다. 이를 통해 애플리케이션에 적용할 보안 설정을 정의할 수 있는데, 다음과 같은 설정을 할 수 있다.
1. 인증 (Authentication)
formLogin( ) - 사용자에게 로그인 페이지를 제공하고, 로그인 폼을 통한 인증을 처리할 수 있다.
httpBasic( ) - 사용자의 Password 를 HTTP 헤더를 통해 서버에 전송(Authorization: Basic {EncodedPassword})하여 자격 증명에 대한 인증을 받기 위한 설정
oauth2Login( ) - 소셜 로그인을 위한 설정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
/* 만약, formLogin() 을 적용하고 싶다면, 아래와 같이 설정할 수 있고, logout() 과 같이 적용할 수 있다.
.formLogin()
.loginPage("/login") // 커스텀 로그인 페이지의 URL
.defaultSuccessUrl("/home", true) // 로그인 성공 후 이동할 URL
.permitAll() // 로그인 페이지 접근은 누구나 가능
.and()
.logout()
.logoutUrl("/logout") // 로그아웃 요청을 처리할 URL
.logoutSuccessUrl("/login?logout") // 로그아웃 성공 후 이동할 URL
.permitAll();
*/
.oauth2Login(oauth -> oauth
.userInfoEndpoint(c -> c.userService(oAuth2UserService))
.successHandler(oAuth2SuccessHandler));
return http.build();
}
2. 인가 (Authorization)
addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) - UsernamePasswordAuthenticationFilter.class 이전에 JwtAuthFilter 를 추가해주면 된다.
authorizeRequests(Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizationHttpRequestsCustomizer) - 특정 URL 에 대한 접근 권한을 설정할 수 있다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new JwtAuthFilter(customUserDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(request -> request
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated());
return http.build();
}
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
if (jwtUtil.validateToken(token)) {
Long userId = jwtUtil.getUserId(token);
UserDetails userDetail = customUserDetailsService.loadUserById(userId);
if (userDetail != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
package org.imgame.imgame.service.impl;
import lombok.RequiredArgsConstructor;
import org.imgame.imgame.common.ErrorCode;
import org.imgame.imgame.dto.CustomUserDetails;
import org.imgame.imgame.exception.CustomException;
import org.imgame.imgame.model.User;
import org.imgame.imgame.repository.UserRepository;
import org.modelmapper.ModelMapper;
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;
private final ModelMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
return mapper.map(user, CustomUserDetails.class);
}
public UserDetails loadUserById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
return mapper.map(user, CustomUserDetails.class);
}
}
3. 세션 관리
sessionManagement( ).sessionFixation( ) - 세션 고정 공격을 방지하는 설정
sessionManagement( ).maximumSessions( ) - 동일 사용자의 동시 세션 수 제한
sessionManagement( ).sessionCreatePolicy(SessionCreationPolicy.STATELESS) - API 서버에서 세션 사용하지 않도록 설정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
4. CSRF 보호 (Cross-Site Request Forgery Protection)
csrf( ) - CSRF 공격을 방지한다. 주로 웹 애플리케이션에서 폼 제출을 통해 발생할 수 있는 공격이다.
CSRF 란, 무엇이냐 도대체 왜 필요하냐. 뭔가 얼핏보면, cors( ) 로 해결할 수 있을 것 같은데, 뭘까?
우선, cors 부터 설명 하겠다. cors 란, Cross-Orgin-Resource-Sharing 으로, 브라우저 차원에서 javascript 를 통해 http 요청을 보낼 때, 동일한 오리진이 아닌, 다른 오리진에 대해서 허용이 되어있는지에 대한 설정을 의미한다.
즉, imgame.com 이라는 클라이언트 주소가 있다고 하자. api.imgame.com 이 서버 주소라고 했을 때, imgame.com 에서 api.imgame.com 에서 준 응답값을 브라우저가 믿고 신뢰할 수 있을지에 대해서 서버가 imgame.com 에 대해서 설정을 열어주었다면, 성장적으로 요청을 보낼 수 있다. 자, 그럼 여기서 설정을 열어주었는지를 어떻게 확인하는지 보자. 즉, preflight 를 요청해서 요청 결과가 허용 x 가 나오면, 브라우저에서 더 이상 백엔드에 요청하지 않도록 막는다.
CSRF 공격은 단순 요청인 form 요청을 이용한다. 그렇기에 Preflight 없이 서버에 바로 전송이 된다. 따라서, CORS 설정과 상관없이 요청이 동작한다.
예시를 보면, 좀 더 이해하기 쉬울것 같다.
만약, 공격자가 attacker.com 이라는 사이트를 만들고, 세션방식의 서버를 이용하고 있는 bank 서비스를 이용하고 있다고 가정하자. 이때, bank 서비스에 로그인을 하고, 새로운 탭을 켜서 (같은 세션내) 로그인을 한 후, 공격자가 의도한대로 attacker.com 에 접속을 했을 때, 다음과 같은 코드가 실행되는 상황을 CSRF 공격 이라고 한다. 이러한 상황은 요청주소에 따라 브라우저가 자동으로 쿠키의 값을 포함해서 요청하는 것 때문에 발생한다. 이를 해결하기 위한 방법으로 서버에서 csrf 에 관련된 토큰을 내려주거나, 쿠키에 httpOnly 설정을 키는 방법이 있다.
<form action="https://api.bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to_account" value="attacker_account">
</form>
<script>
// 사용자가 이 페이지를 방문하면 폼이 자동으로 제출됩니다.
document.forms[0].submit();
</script>
5. 예외 처리
exceptionHandling( ) -@Deprecated(since="6.1", forRemoval=true)
exceptionHandling(Customizer<ExceptionHandlingConfigurer<HttpSecurity>> exceptionHandlingCustomizer) throws Exception - ExceptionHandling 에 대한 Customizer 를 정의해야 한다.
ExceptionHandlingConfigurer
- authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) - 인증이 안된 익명의 사용자가 인증이 필요한 엔드포인트에 접근하게 되는 경우 401 과 스프링의 기본 오류페이지를 보게 된다. 커스텀된 오류 페이지를 보여주거나, 특정 응답로 변경해야 하는 경우, 해당 인터페이스를 구현한 구현체를 시큐리티에 등록하여 사용할 수 있다.
- accessDeniedPage(String accessDeniedUrl) - 에러상황에 보여줄 Error Page 를 빠르게 설정할 수 있는 기능
- accessDeniedHandler(AccessDeniedHandler accessDeniedHandler) - 인증이 완료되었으나, 해당 엔트포인트에 접근할 권한이 없는 경우, 403 Forbidden 오류를 스프링의 기본 오류페이지와 함께 응답한다. 해당 응답형태나 보여줄 페이지를 변경하고자 할때, AccessDeniedHandler 를 구현한 구현체를 시큐리티에 등록하여 사용할 수 있다.
- handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) 를 Override 해야한다.
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
private final CustomAccessDeniedHandler accessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
return http.build();
}
}
@AllArgsConstructor
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper mapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
CommonResponse<Void> errorResponse = CommonResponse.error(ErrorCode.FORBIDDEN);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(mapper.writeValueAsString(errorResponse));
}
}
@AllArgsConstructor
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper mapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
CommonResponse<Void> errorResponse = CommonResponse.error(ErrorCode.UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(mapper.writeValueAsString(errorResponse));
}
}
6. 로그아웃
사용자가 로그아웃을 요청했을 때, 다음과 같은 작업을 수행해야 한다.
- 세션 무효화
- 인증토큰 삭제
- SecurityContext 삭제
- 쿠키정보 삭제
- 로그인 페이지로 리다이렉트
와 같은 작업을 일반적으로 수행한다.
필자는 JWT 토큰을 사용하여, `SessionCreationPolicy.STATELESS` 이기 때문에, logout 설정이 따로 필요없다.
logout( ) - @Deprecated(since="6.1", forRemoval=true) in 7.0.
logout(LogoutConfigurer<HttpSecurity>> logoutCustomizer) - LogoutCustomizer 를 시큐리티에 등록할 수 있다.
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout(AbstractHttpConfigurer::disable)
return http.build();
}
}
LogoutCustomizer
- deleteCookies(String... cookieNamesToClear) - 로그아웃 후 삭제할 쿠키 지정
- logoutSuccessUrl(String logoutSuccessUrl) - 로그아웃 성공 후 이동할 Url
- logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) - logoutSucessUrl 과 함께 사용못함
- logoutUrl(String logoutUrl) - logoutRequestMatcher 와 중복 설정 x, 로그아웃 API Url 설정
- invalidateHttpSession(boolean invalidateHttpSession) - HttpSession 을 무효화해야하는 경우 true(기본값) 로 해야하며, 그렇지 않은 경우 false 로 설정하면 된다.
세션 무효화 처리는
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/logout") // 로그아웃 처리 URL (= form action url)
.addLogoutHandler((request, response, authentication) -> {
// 로그아웃 핸들러 추가
})
.logoutSuccessHandler((request, response, authentication) -> {
// 로그아웃 성공 핸들러
response.sendRedirect("/login");
})
.deleteCookies("remember-me"); // 로그아웃 후 삭제할 쿠키 지정
)
return http.build();
}
}
7. 소셜로그인
oauth2Login(Customizer<OAuth2LoginConfigurer<HttpSecurity>> oauth2LoginCustomizer) 를 통해 설정할 수 있습니다. 자세한 내용에 대해서는 이후 시리즈 글에서 다뤄보고자 한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth -> oauth
.userInfoEndpoint(c -> c.userService(oAuth2UserService))
.successHandler(oAuth2SuccessHandler));
return http.build();
}
최종 코드
SecurityConfig.java
package org.imgame.imgame.config;
import lombok.RequiredArgsConstructor;
import org.imgame.imgame.service.impl.CustomOAuth2UserService;
import org.imgame.imgame.service.impl.CustomUserDetailsService;
import org.imgame.imgame.util.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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;
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;
private final CustomAccessDeniedHandler accessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final CustomOAuth2UserService oAuth2UserService;
private static final String[] AUTH_WHITELIST = {"/auth/**"};
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().
requestMatchers("/error", "/favicon.ico");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthFilter(customUserDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.authorizeHttpRequests(request -> request
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated());
// 다음 블로그 포스트에서 다룰 예정
http
.oauth2Login(oauth -> oauth
.userInfoEndpoint(c -> c.userService(oAuth2UserService))
.successHandler(oAuth2SuccessHandler));
return http.build();
}
}
JwtAuthFilter.java
package org.imgame.imgame.config;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.imgame.imgame.service.impl.CustomUserDetailsService;
import org.imgame.imgame.util.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
if (jwtUtil.validateToken(token)) {
Long userId = jwtUtil.getUserId(token);
UserDetails userDetail = customUserDetailsService.loadUserById(userId);
if (userDetail != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
CustomUserDetailsService.java
package org.imgame.imgame.service.impl;
import lombok.RequiredArgsConstructor;
import org.imgame.imgame.common.ErrorCode;
import org.imgame.imgame.dto.CustomUserDetails;
import org.imgame.imgame.exception.CustomException;
import org.imgame.imgame.model.User;
import org.imgame.imgame.repository.UserRepository;
import org.modelmapper.ModelMapper;
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;
private final ModelMapper mapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
return mapper.map(user, CustomUserDetails.class);
}
public UserDetails loadUserById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
return mapper.map(user, CustomUserDetails.class);
}
}
CustomAccessDeniedHandler.java
package org.imgame.imgame.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.imgame.imgame.common.ErrorCode;
import org.imgame.imgame.util.CommonResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@AllArgsConstructor
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper mapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
CommonResponse<Void> errorResponse = CommonResponse.error(ErrorCode.FORBIDDEN);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(mapper.writeValueAsString(errorResponse));
}
}
CustomAuthenticationEntryPoint.java
package org.imgame.imgame.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.imgame.imgame.common.ErrorCode;
import org.imgame.imgame.util.CommonResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@AllArgsConstructor
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper mapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
CommonResponse<Void> errorResponse = CommonResponse.error(ErrorCode.UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(mapper.writeValueAsString(errorResponse));
}
}
마침
다음 시리즈는 첫 스프링부트 프로젝트(2022)부터 지금(2024)까지의 소셜로그인 방식에 대한 삽질 과정을 다뤄보고자 한다.
[그리고 시큐리티를 곁들인] #5 : 소셜로그인 방식에 대한 삽질기
// TODO
0woodev.tistory.com