Ajax 인증 처리 흐름
- 보는 것 처럼 인증의 경우 AjaxAuthenticationFilter 로 인가의 경우 FilterSecurityIntercepter 로 요청이 전달 되는 것을 확인할 수 있다.
- ajax 방식으로 구현하기 위해서 각각의 클래스 구현해 줘야 하지만, 방식은 폼인증 방식과 크게 다르지 않다.
- AbstractAuthenticationProcessingFilter 상속해서 구현할 수 있다.
AjaxAuthenticationFilter 구현
- 폼인증 필터를 사용할 수 없기 때문에 AjaxLoginProcessingFilter 클래스 구현한다.
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private ObjectMapper objectMapper = new ObjectMapper();
public AjaxLoginProcessingFilter() {
super(new AntPathRequestMatcher("/api/login"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if(!isAjax(request)){
throw new IllegalStateException("Authentication is not supported");
}
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
if(StringUtils.isEmpty(accountDto.getUsername()) || StringUtils.isEmpty(accountDto.getPassword())){
throw new IllegalArgumentException("Username or Passoword is empty");
}
AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(accountDto.getUsername(), accountDto.getPassword());
return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
}
private boolean isAjax(HttpServletRequest request) {
if("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))){
return true;
}
return false;
}
}
new AntPathRequestMatcher("/api/login")
를 통해서 "/api/login" 로 오는 요청을 처리하도록 설정할 수 있다.- isAjax 메소드를 통해서 해더의 값을 분석해서 ajax 요청을 구분할 수 있다.
- 최종적으로 AuthenticationManager 에게 ajaxAuthenticationToken 넘기는 것을 확인할 수 있다.
- 이제 AuthenticationManager 는 ajaxAuthenticationToken 와 적절하게 매치될 Provider 를 찾아서 요청을 위임하게 될 것이고, 해당 Provider 가 인증을 처리하게 된다.
AjaxAuthenticationToken 구현
- UsernamePasswordAuthenticationToken 로 적절한 Provider를 불러올 수 없기 때문에 AjaxAuthenticationToken 을 따로 구현해 줘야 한다.
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public AjaxAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public AjaxAuthenticationToken(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
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
- AbstractAuthenticationToken 를 상속받아서 구현할 수 있다.
- 위의 코드는 UsernamePasswordAuthenticationToken 의 코드를 가져와 복붙해 준 코드이다.
AjaxAuthenticationProvider 구현
- AjaxAuthenticationProvider 클래스를 구현한다.
public class AjaxAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginId = authentication.getName();
String password = (String) authentication.getCredentials();
AccountContext accountContext = (AccountContext)userDetailsService.loadUserByUsername(loginId);
if (!passwordEncoder.matches(password, accountContext.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
return new AjaxAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(AjaxAuthenticationToken.class);
}
}
- AuthenticationProvider 인터페이스를 구현한다.
- 위의 코드는 FormAuthenticationProvider 코드와 거의 비슷하게 구현했다.
- AjaxAuthenticationToken 을 다룰 수 있도록 구현했다.
- 구현에 맞게 authenticate, support 메소드를 구현해 주면 된다.
- 최종적으로 사용자 정보와 권한 정보를 AjaxAuthenticationToken 에 담아서 AuthenticationManager 에게 전달해 준다.
- AuthenticationManager 는 다시 AjaxAuthenticationFilter 에게 전달한다.
AjaxAuthenticationSuccessHandler
- 인증이 성공한 후 처리를 구현하면 된다.
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Account account = (Account)authentication.getPrincipal();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
objectMapper.writeValue(response.getWriter(), account);
}
}
- AuthenticationSuccessHandler 인터페이스를 구현한다.
- response 에 Header 정보를 설정했다.
- objectMapper 를 이용해서 JSON 형식으로 변환하여 인증 객체를 리턴했다.
AjaxAuthenticationFailureHandler
- 인증이 실패한 후 처리를 구현한다.
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String errMsg = "Invalid Username or Password";
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if(exception instanceof BadCredentialsException) {
errMsg = "Invalid Username or Password";
} else if(exception instanceof DisabledException) {
errMsg = "Locked";
} else if(exception instanceof CredentialsExpiredException) {
errMsg = "Expired password";
}
objectMapper.writeValue(response.getWriter(), errMsg);
}
}
- AuthenticationFailureHandler 인터페이스를 구현한다.
- response 에 Header 정보를 설정했다.
- JSON 형식으로 변환하여 오류 메시지 리턴 했다.
AjaxLoginUrlAuthenticationEntryPoint
- ExceptionTranslationFilter 에서 인증 예외 시 호출한다.
- 익명사용자의 경우에 호출된다.
public class AjaxLoginAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"UnAuthorized");
}
}
- 위와 같이 response 에 인증 오류 메시와 401 상태 코드 반환했다.
AjaxAccessDeniedHandler
- ExceptionTranslationFilter 에서 인가 예외 시 호출
- 인증은 됐지만, 권한이 없을때 실행된다.
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access is denied");
}
}
- 위와 같이 response 에 인증 오류 메시와 401 상태 코드 반환했다.
AjaxSecurityConfig 구현
- Ajax 용으로 SecurityConfig 구성한다.
@Configuration
@Order(0)
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ajaxAuthenticationProvider());
}
@Bean
public AuthenticationProvider ajaxAuthenticationProvider() {
return new AjaxAuthenticationProvider();
}
@Bean
public AuthenticationSuccessHandler ajaxAuthenticationSuccessHandler(){
return new AjaxAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler ajaxAuthenticationFailureHandler(){
return new AjaxAuthenticationFailureHandler();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.antMatchers("/api/messages").hasRole("MANAGER")
.anyRequest().authenticated()
.and()
.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
http
.exceptionHandling()
.authenticationEntryPoint(new AjaxLoginAuthenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler());
http.csrf().disable();
}
public AccessDeniedHandler ajaxAccessDeniedHandler() {
return new AjaxAccessDeniedHandler();
}
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler());
ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler());
return ajaxLoginProcessingFilter;
}
}
REFERENCES
- 정수원님의 스프링 시큐리티
'Spring Security' 카테고리의 다른 글
메소드 시큐리티 (0) | 2022.02.28 |
---|---|
Custom DSL 적용 (0) | 2022.02.28 |
Custom Filter (0) | 2022.02.28 |
RememberMeAuthenticationFilter (0) | 2022.02.28 |
AccessDesicionManager 와 AccessDecisionVoter (0) | 2022.02.28 |