이전 포스팅 부터 내용을 이것 저것 넣어 통합 로그인에 대한 글을 작성하려다 보니, 가독성이 매우 떨어지는 부분이 있습니다. 감안하고 봐주시면 감사하겠습니다.
아직 내용이 너무 많습니다. 상세한 설명은 좀 생략하고, 코드 위주로 빠르게 진행해보겠습니다.
이 포스팅에서는 스프링 부트 3.2.4 버전을 사용하고, 스프링 시큐리티 6.2.3 버전을 사용합니다.
저번 포스팅에 이어, 이번에는 시큐리티 필터를 직접 커스터마이징 해보겠습니다.
RESTFUL API 기반의 로그인을 구현할것이 때문에, Json을 처리할 수 있는 필터를 구현해보겠습니다.
public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/api/member/login"; // /login/oauth2/ + ????? 로 오는 요청을 처리할 것이다
private static final String HTTP_METHOD = "POST"; //HTTP 메서드의 방식은 POST 이다.
private static final String CONTENT_TYPE = "application/json";//json 타입의 데이터로만 로그인을 진행한다.
private final ObjectMapper objectMapper;
private static final String MEMBER_ID_KEY="memberId";
private static final String PASSWORD_KEY="password";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD); //=> /login 의 요청에, POST로 온 요청에 매칭
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER); // 위에서 설정한 /api/member/login 의 요청에, POST 온 요청을 처리하기 위해 설정
this.objectMapper = objectMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if(request.getContentType() == null || !request.getContentType().equals(CONTENT_TYPE) ) {
throw new AuthenticationServiceException("Authentication Content-Type not supported: " + request.getContentType());
}
String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
Map<String, String> usernamePasswordMap = objectMapper.readValue(messageBody, Map.class);
String memberId = usernamePasswordMap.get(MEMBER_ID_KEY);
String password = usernamePasswordMap.get(PASSWORD_KEY);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(memberId, password);//principal 과 credentials 전달
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/api/member/login
으로 들어오는 "Post"인 "JSON"형식의 요청에 대해서만 작동하도록 설정했습니다.attemptAuthentication
메서드발급받은 토큰을 헤더에 실어서 보내면, 토큰을 기반으로 인증을 진행해주는 필터입니다.
OncePerRequestFilter
을 상속받아 구현해보겠습니다.
OncePerRequestFilter
는 모든 서블릿 컨테이너에서 요청 디스패치당 단일 실행을 보장하는 것을 목표로 하는 필터 기본 클래스 입니다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final MemberRepository memberRepository;
private ObjectMapper objectMapper = new ObjectMapper();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();//5
private static final String NO_CHECK_URL = "/api/member/login";//1
private static final String NO_CHECK_URL_2 = "/api/member/login/oauth2";
private static final String NO_CHECK_URL_3 = "/api/member/login-extension";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(request.getRequestURI().equals(NO_CHECK_URL) || request.getRequestURI().contains(NO_CHECK_URL_2) ||
request.getRequestURI().equals(NO_CHECK_URL_3)) {
filterChain.doFilter(request, response);
return;//안해주면 아래로 내려가서 계속 필터를 진행해버림
}
checkAccessTokenAndAuthentication(request, response, filterChain);
}
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
jwtService.extractAccessToken(request).filter(jwtService::isTokenValid).flatMap(jwtService::extractMemberId)
.flatMap(memberRepository::findByMemberId).ifPresent(this::saveAuthentication);
filterChain.doFilter(request, response);
} catch (NullPointerException e) {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setStatus(ErrorStatus.MEMBER_AUTHORIZATION_NOT_VALID.getHttpStatus().value());
response.setContentType("application/json");
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus.MEMBER_AUTHORIZATION_NOT_VALID.getCode(),
ErrorStatus.MEMBER_AUTHORIZATION_NOT_VALID.getMessage(), e.getMessage())));
log.info("Authentication failed: " + e.getClass().toString() + " : " + e.getMessage());
}
}
private void saveAuthentication(Member member) {
CustomUserDetails userDetails = CustomUserDetails.create(member);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null,authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
SecurityContext context = SecurityContextHolder.createEmptyContext();//5
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
log.info("Authentication success: memberId = {}", member.getMemberId());
}
}
이는 스프링 시큐리티에서 제공해주는 것입니다.
쿠키에 있는 RefreshToken을 바탕으로, 사용자가 눈치채지 못하게 AccessToken을 재발급 해주어 로그인을 연장해주는 기능을 수행하는 필터입니다.
/api/member/login-extension
로 오는 POST 동작에 대해 동작합니다.
@Slf4j
@RequiredArgsConstructor
public class SilentReAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final MemberRepository memberRepository;
private ObjectMapper objectMapper = new ObjectMapper();
private static final String MATCH_URL = "/api/member/login-extension";//1
private static final String REFRESH_TOKEN = "refreshToken";
private static final String HTTP_METHOD = "POST";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(!request.getRequestURI().equals(MATCH_URL)) {
filterChain.doFilter(request, response);
return;//안해주면 아래로 내려가서 계속 필터를 진행해버림
}
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
if (!request.getMethod().equals(HTTP_METHOD)) {
response.setStatus(ErrorStatus._BAD_REQUEST.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus._BAD_REQUEST.getCode(),
ErrorStatus._BAD_REQUEST.getMessage(), null)));
return ;
}
String refreshToken = resolveRefreshToken(request);
if (refreshToken == null) {
response.setStatus(ErrorStatus.MEMBER_COOKIE_NOT_FOUND.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus.MEMBER_COOKIE_NOT_FOUND.getCode(),
ErrorStatus.MEMBER_COOKIE_NOT_FOUND.getMessage(), null)));
log.info("Access Denied : RefreshToken이 없습니다. serverName : {}", request.getServerName());
}
if (refreshToken != null && jwtService.isTokenValid(refreshToken)) {
checkRefreshTokenAndReIssueAccessToken(request, response, refreshToken);
}
}
private void checkRefreshTokenAndReIssueAccessToken(HttpServletRequest request, HttpServletResponse response, String refreshToken) throws IOException {
Optional<Member> member = memberRepository.findByRefreshToken(refreshToken);
if (member.isEmpty()) {
// 유효하지만, DB에 저장된 정보와 다른 경우
return;
}
log.info("RefreshToken을 재발급합니다. memberId : {} refreshToken : {}", member.get().getMemberId(), refreshToken);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
String newRefreshToken = jwtService.reIssueAndSaveRefreshToken(member.get().getMemberId());
String reIssuedAccessToken = jwtService.reIssueAccessToken(member.get().getMemberId());
if (request.getServerName().equals("localhost")) {
setCookieForLocal(response, newRefreshToken);
} else {
setCookieForProd(response, newRefreshToken);
}
LoginResponseDto.LoginDto loginDto = LoginResponseDto.LoginDto.builder()
.memberId(member.get().getMemberId())
.accessToken(reIssuedAccessToken)
.role(member.get().getRole().getTitle())
.build();
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onSuccess(loginDto)));
}
private void setCookieForLocal(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN, refreshToken);
cookie.setPath("/"); // 모든 곳에서 쿠키열람이 가능하도록 설정
cookie.setMaxAge(60 * 60 * 24); //쿠키 만료시간 24시간
response.addCookie(cookie);
}
private void setCookieForProd(HttpServletResponse response, String refreshToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN, refreshToken);
cookie.setHttpOnly(true); //httponly 옵션 설정
cookie.setSecure(true); //https 옵션 설정
cookie.setPath("/"); // 모든 곳에서 쿠키열람이 가능하도록 설정
cookie.setMaxAge(60 * 60 * 24); //쿠키 만료시간 24시간
response.addCookie(cookie);
}
private String resolveRefreshToken(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
if (Objects.isNull(cookies)) return null;
for (Cookie cookie : cookies) {
if (REFRESH_TOKEN.equals(cookie.getName())) {
// log.info("cookie value = {}, {}",cookie.getValue(), cookie.getName());
if (cookie.getValue().equals("undefined"))
continue;
return cookie.getValue();
}
}
return null;
}
}
RefreshToken이 유효하다면, AccessToken과 RefreshToken 모두 재발급합니다. (RTR 기법. 이전 포스팅 참고)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Lazy
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final CustomUserDetailsService userDetailsService;
private final JwtService jwtService;
private final MemberRepository memberRepository;
private final CorsProperties corsProperties;
private final AccessTokenAuthenticationProvider provider;
// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
return (web -> web.ignoring()
// .requestMatchers(toH2Console())
.requestMatchers("/fcm", "/static/**", "/h2-console/**",
"/favicon.ico", "/error", "/swagger-ui/**",
"/swagger-resources/**", "/v3/api-docs/**")
);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http .csrf(AbstractHttpConfigurer::disable)
.cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer
.configurationSource(corsConfigurationSource()))
.headers(headersConfigurer -> headersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // For H2 DB
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers( "/api/member/signup", "/", "/api/member/isDuplicated", "/api/member/login/oauth").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
;
http
.exceptionHandling(ex -> ex
.accessDeniedHandler(new CustomAccessDeniedHandler(jwtService))
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()))
.addFilterAfter(silentReAuthenticationFilter(), LogoutFilter.class)
.addFilterAfter(jsonUsernamePasswordLoginFilter(), LogoutFilter.class)
.addFilterBefore(jwtAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(auth2AccessTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {//2 - AuthenticationManager 등록
DaoAuthenticationProvider provider = daoAuthenticationProvider();//DaoAuthenticationProvider 사용
return new ProviderManager(provider);
}
@Bean
public LoginSuccessJWTProvideHandler loginSuccessJWTProvideHandler(){
return new LoginSuccessJWTProvideHandler(jwtService, memberRepository);
}
@Bean
public LoginFailureHandler loginFailureHandler(){
return new LoginFailureHandler();
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
jsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
jsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessJWTProvideHandler());
jsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return jsonUsernamePasswordLoginFilter;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationProcessingFilter(){
return new JwtAuthenticationFilter(jwtService, memberRepository);
}
@Bean
OAuth2AccessTokenAuthenticationFilter auth2AccessTokenAuthenticationFilter() {
return new OAuth2AccessTokenAuthenticationFilter(provider, loginSuccessJWTProvideHandler(), loginFailureHandler());
}
@Bean
public SilentReAuthenticationFilter silentReAuthenticationFilter() {
return new SilentReAuthenticationFilter(jwtService, memberRepository);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders().split(",")));
corsConfiguration.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods().split(",")));
corsConfiguration.setAllowedOrigins(Arrays.asList(corsProperties.getAllowedOrigins().split(",")));
corsConfiguration.setExposedHeaders(Arrays.asList("Authorization", "Authorization-refresh"));
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(corsConfiguration.getMaxAge());
UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource();
corsConfigSource.registerCorsConfiguration("/**", corsConfiguration);
return corsConfigSource;
}
}
.addFilterAfter(jsonUsernamePasswordLoginFilter(), LogoutFilter.class)
메서드를 통해 Logout필터 이후에 커스텀 필터를 추가했습니다.이제 UserDetails를 반환하는 UserDetailsService 를 상속받은 클래스를 만들어 보겠습니다.
@Builder
public class CustomUserDetails implements UserDetails {
private String id;
private String email;
private String password;
private String name;
private SocialType socialType;
private Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
public static CustomUserDetails create(Member member) {
return CustomUserDetails.builder()
.id(member.getMemberId())
.email(member.getEmail())
.password(member.getPassword())
.socialType(member.getSocialType())
.authorities(AuthorityUtils.createAuthorityList(member.getRole().toString()))
.build();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void updateAuthorities(Member member) {
this.authorities = AuthorityUtils.createAuthorityList(member.getRole().toString());
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.id;
}
public String getMemberId() {
return this.id;
}
public String getMemberName() {
return this.name;
}
public String getEmail() {
return this.email;
}
public SocialType getSocialType() {
return this.socialType;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
Member member = memberRepository.findByMemberId(memberId)
.orElseThrow(() -> new UsernameNotFoundException("해당하는 회원을 찾을 수 없습니다."));
return CustomUserDetails.create(member);
}
}
DB에서 email에 해당하는 유저를 찾아 반환해줍니다.
비밀 번호 검증은 DaoAuthenticationProvider에서 해줍니다. 더 정확히는 AbstractUserDetailsAuthenticationProvider 에서 진행합니다.
additionalAuthenticationChecks도 retriveUser와 마찬가지로 추상 메서드이므로 구현 클래스에게 처리를 맡깁니다.
구현클래스인 DaoAuthenticationProvider에서 구현 되어있는 것을 아래 코드에서 볼 수 있습니다.
비밀번호 (Credential)의 일치 여부를 판단합니다.
userDetails는 UserDetailsService에서 만들어준 Member 객체이고, 이것의 password 와JsonUsernamePasswordAuthenticationFilter (정확히는 AbstractAuthenticationProcessingFilter)에서 Request의 정보를 통해 인자로 전달해준 Authentication 객체(여기서는 UsernamePasswordAuthenticationToken)의 Credentials (우리는 password를 넣어주었다)를 비교합니다.
그렇기 때문에 저희는 DB에서 유저 정보만 조회해서 반환만하면 됩니다.
이제 성공 처리와 실패 처리를 할 Handler을 구현해보겠습니다.
@Slf4j
@RequiredArgsConstructor
@Transactional
public class LoginSuccessJWTProvideHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtService jwtService;
private final MemberRepository memberRepository;
private ObjectMapper objectMapper = new ObjectMapper();
private static final String REFRESH_TOKEN = "refreshToken";
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String memberId = extractMemberId(authentication);
JwtToken jwtToken = jwtService.createJwtToken(authentication);
Member member = memberRepository.findByMemberId(memberId).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));
member.updateRefreshToken(jwtToken.getRefreshToken());
jwtService.sendAccessToken(response, jwtToken);
log.info( "로그인에 성공합니다. memberId: {}" , memberId);
log.info( "AccessToken 을 발급합니다. AccessToken: {}" ,jwtToken.getAccessToken());
log.info( "RefreshToken 을 발급합니다. RefreshToken: {}" ,jwtToken.getRefreshToken());
SecurityContext context = SecurityContextHolder.createEmptyContext();//5
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
LoginDto loginDto = LoginDto.builder()
.memberId(memberId)
.accessToken(jwtToken.getAccessToken())
.role(member.getRole().getTitle())
.build();
setCookieForLocal(response, jwtToken); // 개발단계에서 사용
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onSuccess(loginDto)));
// if(request.getServerName().equals("localhost")){
// setCookieForLocal(response, jwtToken);
// }
// else{
// setCookieForProd(response, jwtToken);
// }
}
private void setCookieForLocal(HttpServletResponse response, JwtToken jwtToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN, jwtToken.getRefreshToken());
cookie.setHttpOnly(true); //httponly 옵션 설정
cookie.setPath("/"); // 모든 곳에서 쿠키열람이 가능하도록 설정
cookie.setMaxAge(60 * 60 * 24); //쿠키 만료시간 24시간
response.addCookie(cookie);
}
private void setCookieForProd(HttpServletResponse response, JwtToken jwtToken) {
Cookie cookie = new Cookie(REFRESH_TOKEN, jwtToken.getRefreshToken());
cookie.setHttpOnly(true); //httponly 옵션 설정
cookie.setSecure(true); //https 옵션 설정
cookie.setPath("/"); // 모든 곳에서 쿠키열람이 가능하도록 설정
cookie.setMaxAge(60 * 60 * 24); //쿠키 만료시간 24시간
response.addCookie(cookie);
}
private String extractMemberId(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
private String extractPassword(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getPassword();
}
}
SimpleUrlAuthenticationSuccessHandler
을 상속받아, 로그인 성공시 JWT를 발급하도록 합니다.
Body에 직접 response.write()
메서드를 통해 응답 형식을 작성하고, 쿠키에 refreshToken을 넣어 반환합니다.
@Slf4j
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
if (exception.getMessage().contains("Content-Type")){
response.setStatus(ErrorStatus.MEMBER_LOGIN_NOT_SUPPORT.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus.MEMBER_LOGIN_NOT_SUPPORT.getCode(),
ErrorStatus.MEMBER_LOGIN_NOT_SUPPORT.getMessage(), exception.getMessage())));
} else {
response.setContentType("application/json");
response.setStatus(ErrorStatus.MEMBER_EMAIL_PASSWORD_NOT_MATCH.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus.MEMBER_EMAIL_PASSWORD_NOT_MATCH.getCode(),
ErrorStatus.MEMBER_EMAIL_PASSWORD_NOT_MATCH.getMessage(), exception.getMessage())));
}
log.info("Authentication failed: " + exception.getMessage());
}
}
SimpleUrlAuthenticationFailureHandler
을 상속받은 핸들러입니다.
상태 코드는 인증을 실패하였다는 의미로, 401 Unauthorized를 반환하도록 하였습니다.
이 핸들러는 Security Config에 설정 되어있습니다.
인증이 되지 않은 사용자가 경로에 접근하려고 시도할 경우, 401에러를 반환하도록 하는 AuthenticationEntryPoint를 상속받아 커스터마이징한 클래스입니다. 원래 스프링 시큐티에서 정해진 규약대로 동작하는 것을, 제가 임의로 401 에러를 발생시키도록 하였습니다.
@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(ErrorStatus._UNAUTHORIZED.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus._UNAUTHORIZED.getCode(),
ErrorStatus._UNAUTHORIZED.getMessage(), null)));
response.setStatus(HttpStatus.UNAUTHORIZED.value());
log.info("Access denied : {}, requestURI = {} " ,ErrorStatus._UNAUTHORIZED.getMessage(), request.getRequestURI());
}
}
권한이 부족한 사용자가 접근을 시도했을때 403에러를 반환하도록 하였습니다.
@Slf4j
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private ObjectMapper objectMapper = new ObjectMapper();
private final JwtService jwtService;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
Optional<String> accessToken = jwtService.extractAccessToken(request);
if (accessToken.isPresent()) {
Optional<String> memberId = jwtService.extractMemberId(accessToken.get());
log.info("Access denied: memberId = " + memberId.get());
}
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(ErrorStatus.MEMBER_NOT_AUTHENTICATED.getHttpStatus().value());
response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus.MEMBER_NOT_AUTHENTICATED.getCode(),
ErrorStatus.MEMBER_NOT_AUTHENTICATED.getMessage(), null)));
response.setStatus(HttpStatus.FORBIDDEN.value());
}
}
이제 일반적인 Spring Security 설정은 얼추 다 추가한 것 같습니다. 다음 포스팅에서는 이렇게 설정한 스프링 시큐리티에, 구글 카카오 네이버 모두 동작하는 소셜 로그인 로직을 추가해 보겠습니다.
언급되지 않은 코드는 아래 깃허브에서 보실 수 있습니다.
출처 및 참고