안녕하세요.
이번 포스팅에서는 Spring Security에서 JSON 형식으로 로그인하는 방법에 대해서 알아보겠습니다.
아래의 내용에서 자신의 개발 방향에 맞게 커스텀하시면 될 듯 합니다.
1. UsernamePasswordAuthenticationFilter 살펴보기
로그인 요청이 들어오면, 인증 필터를 거쳐 UsernamePasswordAuthenticationFilter에서 ID와 PW를 처리합니다.
- SPRING_SECURITY_FORM_UESRNAME_KEY, SPRING_SECURITY_FORM_PASSWORD_KEY : username과 password 인자 값 설정
- DEFAULT_ANT_PATH_REQUEST_MATCHER : 로그인 요청 URL 설정
위의 소스는 UsernamePasswordAuthenticationFilter의 attemptAuthentication 메소드 부분인데,
기본적으로 POST를 이용한 요청만을 받을 수 있고, username과 password를 obtain이라는 함수를 이용해서 가져온다.
위의 obtatin 이라는 함수는 단순하게 request.getParameter를 이용해서 유저의 입력 값을 가져온다.
2. UsernamePasswordAuthenticationFilter에서 JSON도 받을 수 있도록 커스터마이징하기
CustomUsernamePasswordAuthenticationFilter.java
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.MimeTypeUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.stream.Collectors;
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("CustomUsernamePasswordAuthenticationFilter - attemptAuthentication START");
UsernamePasswordAuthenticationToken authenticationToken = null;
String userId = null;
String userPassword = null;
// JSON 요청일 경우
if (request.getContentType().equals(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
try{
// ObjectMapper를 이용해서 JSON 데이터를 dto에 저장 후 dto의 데이터를 이용
LoginDto loginDto = objectMapper.readValue(
request.getReader().lines().collect(Collectors.joining()), LoginDto.class);
userId = loginDto.getUserId();
userPassword = loginDto.getUserPassword();
logger.info("JSON 접속. USERID : " + userId + ", USERPW : " + userPassword);
} catch(IOException e){
e.printStackTrace();
}
// POST 요청일 경우 기존과 같은 방식 이용
} else if(request.getMethod().equals("POST")){
userId = obtainUsername(request);
userPassword = obtainPassword(request);
logger.info("POST 접속. USERID : " + userId + ", USERPW : " + userPassword);
}
else {
logger.error("POST / JSON 요청만 가능합니다.");
throw new AuthenticationServiceException("Authentication Method Not Supported : " + request.getMethod());
}
if(userId.equals("") || userPassword.equals("")){
System.out.println("ID 혹은 PW를 입력하지 않았습니다.");
throw new AuthenticationServiceException("ID 혹은 PW를 입력하지 않았습니다.");
}
authenticationToken = new UsernamePasswordAuthenticationToken(userId, userPassword);
this.setDetails(request, authenticationToken);
System.out.println("CustomUsernamePasswordAuthenticationFilter - attemptAuthentication END");
return this.getAuthenticationManager().authenticate(authenticationToken);
}
@Getter
@Setter
@ToString
private static class LoginDto {
private String userId;
private String userPassword;
}
}
3. 로그인 성공/실패 시, JSON 형태로 성공/실패 메시지 보내는 Handler 생성하기
로그인 성공 or 실패 시, JSON 형태로 다시 데이터를 보내줘서 Front 단에 메시지를 띄워줘야 하는 경우 SuccessHandler, FailureHandler에서 JSON 형식으로 응답해준다.
CustomSuccessHandler.java
import com.withus.withusApp.config.security.auth.PrincipalDetails;
import com.withus.withusApp.config.security.dto.JsonDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
requestCache.removeRequest(request, response);
clearAuthenticationAttributes(request);
}
String accept = request.getHeader("accept");
PrincipalDetails principalDetails = null;
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails) {
principalDetails = (PrincipalDetails) principal;
}
}
// 로그인 시, JSON 으로 반환
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
MediaType jsonMimeType = MediaType.APPLICATION_JSON;
String message = "로그인 성공";
JsonDto jsonDto = JsonDto.success(principalDetails, message);
if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
}
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
CustomFailureHandler.java
import com.withus.withusApp.config.security.dto.JsonDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("Exception Type : " + exception.getClass().getName());
logger.info("Exception Message : " + exception.getMessage());
// 로그인 시, JSON 으로 반환
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
MediaType jsonMimeType = MediaType.APPLICATION_JSON;
String message = "로그인에 실패하였습니다.";
JsonDto jsonDto = JsonDto.fail(message);
if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
}
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
}
추가) 로그아웃 시 성공했다는 메세지를 담은 Handler는 아래와 같다.
CustomLogoutSuccessHandler.java
import com.withus.withusApp.config.security.dto.JsonDto;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//super.onLogoutSuccess(request, response, authentication);
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
MediaType jsonMimeType = MediaType.APPLICATION_JSON;
String message = "로그아웃 완료";
JsonDto jsonDto = JsonDto.success(null, message);
if (jsonConverter.canWrite(jsonDto.getClass(), jsonMimeType)) {
jsonConverter.write(jsonDto, jsonMimeType, new ServletServerHttpResponse(response));
}
}
}
4. Spring Security Config 설정하기
import com.withus.withusApp.config.security.filter.*;
import com.withus.withusApp.config.security.oauth2.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity // Spring Security Filter 가 Spring Filter Chain 에 등록되도록
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* Spring Security Setting
* @param http
* @Author Seong-Mok Kim
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception{
// CORS 설정 및 CSRF 비활성화
http.cors().configurationSource(corsConfigurationSource()).and().csrf().disable();
http
// URL에 따른 접근 제한 처리
.authorizeRequests()
.antMatchers("/api/user/**").authenticated() // URL user : 인증이 되어야 함
.antMatchers("/api/admin/**").access("hasRole('ROLE_ADMIN')") // URL admin : 권한 'ROLE_ADMIN'이 있어야함
.anyRequest().permitAll() // 위에 명시되지 않은 URL 은 로그인 및 권한 검사 X
.and()
// 로그인 설정
.formLogin().disable() // 기존의 로그인 비활성화 후
.addFilterAt(getAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 새로 만들어준 CUSTOM 필터를 넣어줌
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler()) // LogoutSuccessHandler 설정
;
}
// JSON 방식으로도 로그인 가능하도록 설정한 Filter
protected CustomUsernamePasswordAuthenticationFilter getAuthenticationFilter() {
CustomUsernamePasswordAuthenticationFilter authFilter = new CustomUsernamePasswordAuthenticationFilter();
try {
authFilter.setFilterProcessesUrl("/api/login"); // login 주소 설정
authFilter.setAuthenticationManager(this.authenticationManagerBean());
authFilter.setUsernameParameter("userId"); // username 설정
authFilter.setPasswordParameter("userPassword"); // password 설정
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler()); // SuccessHandler 설정
authFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); // FailuerHandler 설정
} catch (Exception e) {
e.printStackTrace();
}
return authFilter;
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler(){
return new CustomSuccessHandler();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler(){
return new CustomFailureHandler();
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler(){
return new CustomLogoutSuccessHandler();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
참고 블로그
https://johnmarc.tistory.com/74
'BE > Spring' 카테고리의 다른 글
JdbcTemplate를 이용한 데이터 통신 (1) | 2024.09.19 |
---|---|
[Spring] Inteceptor prehandle 에서 response 넘기는 방법 (0) | 2023.04.06 |
[Spring Security] React(Front)와 Spring Security 설정 방법 (2) | 2022.07.19 |
[Spring Security] JWT - 4. JWT 로그인 및 권한 처리 (0) | 2022.06.19 |
[Spring Security] JWT - 3. JWT 임시 토큰 테스트 및 로그인 시 토큰 생성 (0) | 2022.06.18 |