이 글은 정수원님의 Infrean 강의를 학습한 내용을 정리하여 작성합니다.
Ajax 인증 - AjaxAuthenticationFilter
- AbstractAuthenticationProcessingFilter 상속
- 필터 작동 조건
- AntPathRequestMatcher("/api/login")로 요청정보와 매칭하고 요청 방식이 Ajax이면 필터 작동
- AjaxAuthenticationToken을 생성하여 AuthenticationManager에게 전달하여 인증처리
- Filter 추가
- http.addFilterBefore(AjaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
구현
Ajax 인증 토큰 생성
- UsernamePasswordAuthenticationToken의 내용을 복사해 구현하도록 한다.
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* 인증을 받기 전 사용자가 입력하는 정보를 담는 생성자
* (username, password)
*/
public AjaxAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
/**
* 인증 이후 인증에 성공한 결과를 담는 생성자
* @param principal 인증에 성공한 user 객체
* @param credentials 패스워드
* @param authorities 권한 정보
*/
public AjaxAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
- 첫 번째 생성자는 인증을 받기 전 사용자의 아이디와 패스워드를 담는 생성자
- 두 번째 생성자는 인증 후 인증에 성공한 결과를 담는 생성자이다.
필터 생성
AjaxLoginProcessingFilter
- AbstractAuthenticationProcessingFilter 상속
- 필터 발동 조건
- AntPathRequestMatcher("/api/login")로 요청정보와 매칭하고 요청 방식이 Ajax이면 필터 작동
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.hasText(accountDto.getUsername()) || !StringUtils.hasText(accountDto.getPassword())) {
throw new IllegalArgumentException("Username or Password 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;
}
}
- 요청정보 매칭
- 요청방식 검사
- 이때, 요청방식이 Ajax인지 검사하는 기준은 자유롭게 하면 된다.
- 여기서는 사용자가 HttpHeader로 "XMLHTTPRequest" 정보를 담아 보내준다 가정하였다.
- isAjax 함수를 호출해 요청방식이 Ajax 방식이 맞다면 필터가 동작하도록 한다.
- 아니라면 예외를 발생시킨다.
- AuthenticationManager에게 인증 객체(AjaxAuthenticationToken) 전달
- 인증 받기 전 이므로 AjaxAuthenticationToken의 첫 번째 생성자를 사용해 AjaxAuthenticationToken 객체를 생성해 AuthenticationManager에게 인증 처리를 위임한다.
참고
위에서 사용하는 getAuthenticationManager()는 상속받은 AbstractAuthenticationProcessingFilter가 제공하는 함수이다.
설정 추가
- 직접 생성한 AjaxLoginProcessingFilter가 동작하도록 설정을 추가해야 한다.
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
ProviderManager authenticationManager = (ProviderManager)authenticationConfiguration.getAuthenticationManager();
authenticationManager.getProviders().add(ajaxAuthenticationProvider());
return authenticationManager;
}
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return ajaxLoginProcessingFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.antMatchers("/", "/users", "user/login/**", "/login*").permitAll()
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/messages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login_proc")
.authenticationDetailsSource(authenticationDetailsSource)
.defaultSuccessUrl("/")
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.permitAll();
http
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http
.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
http.csrf().disable();
return http.build();
}
...
}
- Filter 추가
주의!
SecurityConfig 파일에서 다음과 같이 AjaxLoginProcessingFilter를 Bean으로 등록하고자 할 때, AuthenticationManager를 설정하지 않으면 오류가 발생한다.
이러한 문제를 해결하기 위해서는 다음과 같이 AuthenticationManager를 등록해주어야 한다.
AuthenticationManager 는 초기화 때 생성되어 기본적으로 DaoAuthenticationProvider 와 같은 객체를 가지고 있다.
그리고 UsernamePasswordAuthenticationFilter 와 같은 클래스에서 참조하고 있다.
그렇다면 ajaxAuthenticationProvider 도 초기화때 생성된 AuthenticationManager 에서 추가해 주어야 한다.
그렇기 때문에 AjaxLoginProcessingFilter 에서 참조하고 있는 AuthenticationManager 에 ajaxAuthenticationProvider 를 추가해 주어야 정상 동작하게 된다.
참고: https://www.inflearn.com/questions/667022/ajaxauthenticationprovider%EA%B0%80-providermanager%EC%97%90%EA%B2%8C-%EB%93%B1%EB%A1%9D%EC%9D%B4-%EC%95%88%EB%90%A9%EB%8B%88%EB%8B%A4
주의!
CSRF 기능은 기본적으로 활성화 되어 있다.
HTTP 메소드 중 PATCH, POST, PUT, DELETE로 요청 시 반드시 CSRF 토큰명과 토큰값으로 요청해야 한다.
하지만 현재 포스트맨으로 테스트를 수행하므로 CSRF 토큰을 요청할 수 없다.
그러므로 잠시 CSRF 기능을 disable 상태로 변경한다.
CSRF 참조: https://yenjjun187.tistory.com/581
참고
필터를 추가할 때 위에서 사용한 addFilterBefore() 함수 말고도 다른 함수들이 존재한다.
- addFilterBefore() : 추가하고자 하는 필터를 기존 필터 앞에 위치시키고자 할 때
- addFilter() : 추가하고자 하는 필터를 마지막에 위치시킬 때
- addFilterAfter() : 기존 필터 다음에 위치시키고자 할 때
- addFilterAt() : 기존 필터 위치를 대체하고자 할 때
실행
- 실제 ajax 방식으로 인증 요청을 위해서는 화면을 구성해 자바스크립트의 JQuery 같은 기술을 사용해야 한다.
- 여기서는 간단하게 포스트맨을 사용해 테스트를 수행한다.
포스트맨 사용
결과
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Home</title>
<script src="/js/jquery-2.1.3.min.js"></script>
<link rel="stylesheet" href="/css/base.css" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" />
</head>
<body>
<div>
<nav class="navbar navbar-dark sticky-top bg-dark ">
<div class="container">
<a class="text-light" href="#">
<h4>Core Spring Security</h4>
</a>
<ul class="nav justify-content-end">
<li class="nav-item"><a class="nav-link text-light" href="/login">로그인</a></li>
<li class="nav-item"><a class="nav-link text-light" href="/users">회원가입</a></li>
<li class="nav-item"><a class="nav-link text-light" href="/">HOME</a></li>
</ul>
</div>
</nav>
</div>
<div class="login-form d-flex justify-content-center">
<div class="col-sm-5" style="margin-top: 30px;">
<div class="panel">
<p>아이디와 비밀번호를 입력해주세요.</p>
</div>
<form action="/login_proc" class="form-signin" method="post"><input type="hidden" name="_csrf" value="4b143d32-e041-4610-9f59-c629a6e12e8b"/>
<input type="hidden" value="secret" name="secret_key">
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="아이디" required="required" autofocus="autofocus">
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="비밀번호" required="required">
</div>
<button type="submit" class="btn btn-lg btn-primary btn-block">로그인</button>
</form>
</div>
</div>
</body>
</html>
정리
- 현재 Ajax 방식을 위한 필터를 직접 생성하였다.
- 하지만 필터 안에서 인증 처리를 담당하게 될 AuthenticationProvider를 생성하지는 않았다.
- 현재 AuthenticationProvider는 Ajax 방식이 아닌 Form 인증 방식으로 동작하므로 우리가 전달해주는 AjaxAuthenticationToken 타입과 맞지 않아 실제 동작하지 않는다.
- 즉, 인증 처리가 수행되지 않는다.
- 최종적으로 인증 예외가 발생한다.
- 인증 예외를 필터가 받게된다.
'스프링 시큐리티 > 실전프로젝트 - 인증 프로세스 Ajax 인증 구현' 카테고리의 다른 글
AjaxCustomDSLs구현하기 (0) | 2023.02.14 |
---|---|
인증 및 인가 예외 처리 - AjaxLoginUrlAuthenticationEntryPoint, AjaxAccessDeniedHandler (0) | 2023.02.14 |
인증 핸들러 - AjaxAuthenticationSuccessHandler, AjaxAuthenticationFailureHandler (0) | 2023.02.14 |
인증 처리자 - AjaxAuthenticationProvider (0) | 2023.02.14 |
Ajax 인증 - 흐름 및 개요 (0) | 2023.02.12 |