AbstractAuthenticationProcessingFilter를 상속받아서 필터를 구성하고자 한다 UsernamePasswordAuthenticationFilter도 이 추상클래스를 상속받아서 구성되어 있다. 대부분 인증 처리 기능을 이 추상클래스가 하고 잇다 .
필터 작동조건
- AntPathRequestMatcher("/api/login") 로 요청정보와 매칭하고 요청 방식이 Ajax 이면 필터 작동
- AjaxAuthenticationToken 생성하여 AuthenticationManager 에게 전달하여 인증처리
- Filter 추가
- http.addFilterBefore(AjaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
filter 패키지안에 AjaxLoginProcessingFiler라는 클래스를 만들어주자 클래스 이름은 자유지만 관례는 있다
누가봐도 이녀석이겠구나 하는 이름을 넣는것을 추천한다 (카멜케이스 잇지마시구여)
package io.security.corespringsecurity.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.security.corespringsecurity.domain.AccountDto;
import io.security.corespringsecurity.security.token.AjaxAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.thymeleaf.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#AbstractAuthenticationProcessingFilter 상속
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
#ObjectMapper 제이슨으로 넘어온 정보를 객체로 출해서 다시 다시 담는 역할을 한다
private ObjectMapper objectMapper = new ObjectMapper();
#필터 작동 조건
("/api/login") 요청 했을때 필터가 작동되고 요청방식이 ajax 인지 확인후에 필터가 작동여부를
판별할수 있도록 해준다.
public AjaxLoginProcessingFilter() {
super(new AntPathRequestMatcher("/api/login"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
#Ajax 이면 인증을 요청하고 아니면 예외를 발생시킨다.
if(!isAjax(request)){
throw new IllegalStateException("Authentication in not supported");
}
#인증조건이 2개를 만어줬고 2개가 모두 통과되고 나서 하는일을 정해준다
제이슨 방식으로 담아서 요청할건데 넘어온 정보를 객체로 더시 추출해서 담아야한다.
읽어온 정보를 AccountDto.class 로 담아서 받도록 한다
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
#username,password 를 받아서 인증처리를 하게 되는데 만약 null이라면 인증처리를 할수 없게 해줘야한다
if ( StringUtils.isEmpty(accountDto.getUsername()) ||StringUtils.isEmpty(accountDto.getPassword())){
throw new IllegalArgumentException("Username or password is empty");
}
# Token 클래스에 첫번째 생성자에게 정보들을 전달해주자 첫번째 생서자는
사용자의 username,password 를 전달받으니까 여기서 사용자의 username,password를 전달해주면된다.
AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(
accountDto.getUsername(),accountDto.getPassword());
#작성해준 토큰을 전달해주면된다.
return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
}
#Ajax 인지 아닌지 기준점을 정해준다
private boolean isAjax(HttpServletRequest request) {
#정보에 담겨 있는 값과 같은지 아닌지 판별해야하는데 서버에서 미리 약속을 정해줄수 있다.
if ( "XMLHttpRequest".equals(request.getHeader("X-Requested-With")) ){
return true;
}
#위에 값과 일치하지 않으면
return false;
}
}
여기가 통과 되면 인증처리가 되어야 하는데 토큰이 있어야한다 이유는 사용자의 아이디와 패스워드를 담아서 인증처리를 해야 하니까
AbstractAuthenticationToken 상속받고 이 토큰은 Form인증에 사용되는 AuthenticationToken이 있다 그래서 이 토큰을 참조해서 사용할수 있다 .
UsernamePasswordAuthenticationToken 클래스에 내용을 복사해오자 ! 위에 사진 4장중에 왼쪽 첫번째 사진이 UsernamePasswordAuthenticationToken 클래스에고 나머지 3장이 복사해온 코드들이다.
package io.security.corespringsecurity.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
#첫번째 생성자가 우리가 인증을 받기전에 사용자가 입력하는 username,password 를 담는 생성자이다
#현재 필자는 첫번째 생성자에게 아이디와 패스워드를 전달하고자 한다.
#다시 처음에 작성한 AjaxLoginProcessingFilter를 보자
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;
}
}
package io.security.corespringsecurity.security.configs;
import io.security.corespringsecurity.domain.Account;
import io.security.corespringsecurity.security.common.FormAuthenticationDetailsSource;
import io.security.corespringsecurity.security.filter.AjaxLoginProcessingFilter;
import io.security.corespringsecurity.security.handler.CustomAccessDeniedHandler;
import io.security.corespringsecurity.security.provider.CustomAuthenticationProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfigs extends WebSecurityConfigurerAdapter {
@Autowired
private FormAuthenticationDetailsSource authenticationDetailsSource;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#매니저 설정
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/users","user/login/**","/login*").permitAll()
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/massages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login_proc")
.defaultSuccessUrl("/")
.authenticationDetailsSource(authenticationDetailsSource)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and() #기존의 필터 앞에 위치하도록 해준다
기존필터는 UsernamePasswordAuthentication(폼인증) 앞에 위치
.addFilterBefore(ajaxLoginProcessingFilter(),UsernamePasswordAuthenticationFilter.class);
http.csrf().disable();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
accessDeniedHandler.setErrorPage("/denied");
return accessDeniedHandler;
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
return new CustomAuthenticationProvider(passwordEncoder());
}
#만들어준 필터를 빈으로 등록
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
#필터를 만들때 매니저도 설정을 해줘야한다.
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
return ajaxLoginProcessingFilter;
}
}
add 를 하게되면 4개의 api가 확인되는데 간단하게 보고가자
- addFilterBefore 실제 추가 하고자 하는 필터가 기존의 필터 앞에 위치할때
- addFilter 가장 마지막에 위치할때
- addFilterAfter추가하고자 하는 필터가 기존의 필터 뒤에 위치 할때
- addFilterAt 현재 기존의 필터 위치를 대체한다.
실제로 jquery 를 사용해서 ajax 방식으로 해야 하지만 일단 간하게 httpRequest api 테스틀 통해서 진행해보고자 한다
그리고 현재 필자는 인털레제이 커뮤니티 버전이라 지원이 안되서 포스트맨, TalendAPITester를 사용했다 .
보기좋게 전달되는 과정을 볼수 있다.
디버깅을 하면 현재 필터 안으로 들어왔다 그리고 설정해준 헤더값이랑 일치하는지 확인 하고 있는 과정이다.
지금 현제 해더에 X-Request-With 이값이 들어오 있는걸 확인할수 있다. 현재 동일하니 true를 리턴하게 되고
사용자가 입력한 아이디와 패스워드를 추출해서 accountDto 객체에 담고 있다 확인해보자
여기 보면 null 부분이 왜 비어 있지? 라고 생각할수 있는데 아까 필자는 첫번째 생성자에게 보낸다고 했다 첫번째 생성자에는 아이디와 패스워드만 전달받는다고 했다 그래서 아이디,패스워드 부분을 제외한 나머지는 널값으로 확인된다. 아직 두번째 생성자로 가지 않았다는 말이다. 이후에 authenticationManager 에게 인증객체를 전달한다 이후에 과정은 정상적으로 처리는 안된다 왜냐면 필터는 만들었지만 필터안에서 인증처리를 담당하게될 provider 클래스가 ajax 용으로 필요하다 현재는 form 인증방식 밖에 없기 때문에 실패한다 살펴보면
ProviderManger 클래스를 찾는데 여기서 전달받는 객체를 확인해보면
하나는 AnonyMousAuthenticationProvider 이고 하나는 CustomAuthenticationProvider이다 CustomAuthenticationProvider는 폼인증을 위해서 필자가 예전에 만들어준 클래스이다. 그렇기 때문에 형재는 Ajax 용 프로바이더가 없기 때문에 최종적으로 예외가 발생하고
필터가 예외를 받아서 인증에 실패한다.
그리고 위에 사진은 성공했을때 사진이고 성공하기 전에 요청실패가 한번 있었는데 보도록 하자
컴파일도 정상이고 서버도 정상적으로 구동된다 하지만 요청을 하게 되면 밑에 사진처럼 요류가 확인된다.
csrf 부분이 예외가 발생하는데 csrf기능이 기본적으로 활성화가 되어 잇기 때문에 포스트 방식으로 보낼때는 반드시 같이 가지고 서버쪽으로 가야한다 SpringSecurity가 포스트 방식으로 요청할때는 csrf토큰을 검사하기 때문에 그렇다 현재는 csrf값이 없기 때문에 예외가 발생한다 그래서 csrf 기능을 잠시 꺼두면 된다[http.csrf().disable();]
그리고 인털레제이 기능중에 .http 기능이 있는데 이기능은 체험판은 지원이 안되서 방법을 찾다가 한참을 구글링했다 1퍼센트의 희망을 가지고 해보다가 결국엔 커뮤니티에 글을 남겼더니 안된단다.. 지원안한다고 하더라 3시간을 날렸다
'SpringSecurity' 카테고리의 다른 글
SpringSecurity(AccessDenied) 인증거부처리 (0) | 2020.12.09 |
---|---|
SpringSecurity(인증실패핸들러) (0) | 2020.12.07 |
SpringSecurity(인증 성공 핸들러) (0) | 2020.12.04 |
SpringSecurity(Authentication) 인증 부가기능 (0) | 2020.12.02 |
SpringSecurity(로그아웃 및 인증에 따른 화면 보안처리) (0) | 2020.11.25 |