인프런 스프링 시큐리티 강의 학습-15
Ajax Custom DSLs 구현하기, Ajax 로그인 구현 & CSRF 설정를 정리한 포스트입니다.
출처는 인프런의 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security강의를 바탕으로 이 포스트를 작성하고 있습니다.
강의의 세션 4의 6,7번 강의내용에 대한 정리입니다.
Ajax Custom DSLs 구현하기
Custom DSLs를 구현하기 위해서 1개의 클래스와 1개의 메서드를 사용합니다.
- AbstractHttpConfigurer은 스프링 시큐리티 초기화 설정 클래스로써 필터, 핸들러, 메서드, 속성 등을 한 곳에 정의해 처리할 수 있도록 합니다.
- HttpSecurity의 apply(C configurer) 메서드를 사용해 구현한 구현체를 설정해 DSLs를 작동시킵니다.
실제 코드
AjaxLoginConfigurer.java
public final class AjaxLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, AjaxLoginConfigurer<H>, AjaxLoginProcessingFilter> {
private AuthenticationSuccessHandler successHandler;
private AuthenticationFailureHandler failureHandler;
private AuthenticationManager authenticationManager;
public AjaxLoginConfigurer() {
super(new AjaxLoginProcessingFilter(), null);
}
@Override
public void init(H http) throws Exception {
super.init(http);
}
@Override
public void configure(H http) {
if (authenticationManager == null) {
authenticationManager = http.getSharedObject(AuthenticationManager.class);
}
getAuthenticationFilter().setAuthenticationManager(authenticationManager); // 1
getAuthenticationFilter().setAuthenticationSuccessHandler(successHandler); // 1
getAuthenticationFilter().setAuthenticationFailureHandler(failureHandler); // 1
SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
if (sessionAuthenticationStrategy != null) {
getAuthenticationFilter().setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
getAuthenticationFilter().setRememberMeServices(rememberMeServices);
}
http.setSharedObject(AjaxLoginProcessingFilter.class, getAuthenticationFilter());
http.addFilterBefore(getAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
public AjaxLoginConfigurer<H> successHandlerAjax(AuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
return this;
}
public AjaxLoginConfigurer<H> failureHandlerAjax(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureHandler = authenticationFailureHandler;
return this;
}
public AjaxLoginConfigurer<H> setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
return this;
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
}
상속받은 AbstractAuthenticationFilterConfigurer 클래스의 코드를 가져와 적절하게 변경하였습니다.
- 원래는 AjaxSecurityConfig에서 ajaxLoginProcessingFilter 메서드로 설정했지만 getAuthenticationFilter를 통해 메니저와 핸들러를 Filter에 저장하고 있습니다.
AjaxSecurityConfig.java
@Bean
public SecurityFilterChain FilterChain(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.antMatchers("/api/message").hasRole("MANAGER")
.anyRequest().authenticated();
http
.exceptionHandling()
.authenticationEntryPoint(new AjaxLoginAuthenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler());
http.csrf().disable();
customConfigurerAjax(http); // 1
return http.build();
}
public void customConfigurerAjax(HttpSecurity http) throws Exception {
http // 2
.apply(new AjaxLoginConfigurer<>())
.successHandlerAjax(ajaxAuthenticationSuccessHandler())
.failureHandlerAjax(ajaxAuthenticationFailureHandler())
.setAuthenticationManager(authenticationManager(authenticationConfiguration))
.loginProcessingUrl("/api/login");
}
// @Bean
// public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
// AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
// ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
// ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler());
// ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler());
// return ajaxLoginProcessingFilter;
// }
- Custom DSLs를 사용하기 위한 메서드를 호출합니다.
- AjaxLoginConfigurer를 불러오고, success, failure, Manager를 설정합니다.
실제 실행 화면
configurers에 저장된 AjaxLogin 위에서 제작한 AjaxLoginConfigurer가 정상적으로 configurers에 등록이 되었습니다.
configure의 실행 이 사진은 AjaxLoginConfigurer의 configure 메서드가 정상적으로 실행되 Filter 저장까지 진행된 것을 확인할 수 있습니다.
이후에 ajax.http를 실행한 결과 다 정상적으로 실행이 되는 것을 확인할 수 있었습니다.
해당 코드는 깃허브에 올려놨습니다.
Ajax 로그인 구현 & CSRF 설정
위 구현을 위해서 전송 방식이 Ajax인지 여부를 확인하기 위한 헤더를 설정하고, CSRF 헤더를 설정해야 합니다.
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta name="_csrf" th:content="${_csrf?.token}" th:if="${_csrf} ne null"> <!-- 1 -->
<meta name="_csrf_header" th:content="${_csrf?.headerName}" th:if="${_csrf} ne null"> <!-- 1 -->
<head th:replace="layout/header::userHead"></head>
<script>
function formLogin(e) {
let username = $("input[name='username']").val().trim();
let password = $("input[name='password']").val().trim();
let data = {"username" : username, "password" : password};
let csrfHeader = $('meta[name="_csrf_header"]').attr('content') // 2
let csrfToken = $('meta[name="_csrf"]').attr('content') // 2
$.ajax({
type: "post",
url: "/api/login",
data: JSON.stringify(data),
dataType: "json",
beforeSend : function(xhr){
xhr.setRequestHeader(csrfHeader, csrfToken);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.setRequestHeader("Content-type","application/json");
},
success: function (data) {
console.log(data);
window.location = '/';
},
error : function(xhr, status, error) {
console.log(error);
window.location = '/login?error=true&exception=' + xhr.responseText;
}
});
}
</script>
<body>
<div th:replace="layout/top::header"></div>
<div class="container text-center">
<div class="login-form d-flex justify-content-center">
<div class="col-sm-5" style="margin-top: 30px;">
<div class="panel">
<p>아이디와 비밀번호를 입력해주세요</p>
</div>
<div th:if="${param.error}" class="form-group">
<span th:text="${exception}" class="alert alert-danger">잘못된 아이디나 암호입니다</span>
</div>
<form th:action="@{/login_proc}" class="form-signin" method="post">
<input type="hidden" th: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="button" onclick="formLogin()" id="formbtn" class="btn btn-lg btn-primary btn-block">로그인</button>
<!--<button type="submit" class="btn btn-lg btn-primary btn-block">로그인</button>-->
</form>
</div>
</div>
</div>
</body>
</html>
- 전송 방식이 Ajax인지 여부를 확인하기 위한 헤더를 설정합니다.
- CSRF 헤더를 설정합니다.
login.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/header::userHead"></head>
<html xmlns:th="http://www.thymeleaf.org">
<meta name="_csrf" th:content="${_csrf?.token}" th:if="${_csrf} ne null">
<meta name="_csrf_header" th:content="${_csrf?.headerName}" th:if="${_csrf} ne null">
<head th:replace="layout/header::userHead"></head>
<script>
function messages() {
let csrfHeader = $('meta[name="_csrf_header"]').attr('content')
let csrfToken = $('meta[name="_csrf"]').attr('content')
$.ajax({
type: "post",
url: "/api/messages",
//dataType: "json",
beforeSend : function(xhr){
xhr.setRequestHeader(csrfHeader, csrfToken);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.setRequestHeader("Content-type","application/json");
},
success: function (data) {
console.log(data);
window.location = '/messages';
},
error : function(xhr, status, error) {
console.log(error);
if(xhr.responseJSON.status === '401'){
window.location = '/api/login?error=true&exception=' + xhr.responseJSON.message;
}else if(xhr.responseJSON.status === '403'){
window.location = '/api/denied?exception=' + xhr.responseJSON.message;
}
}
});
}
</script>
<a href="#" onclick="messages()" style="margin:5px;" class="nav-link text-primary">메시지</a>
LoginController.java
@Controller
public class LoginController {
@GetMapping(value = {"/login", "/api/login"}) // 1
public String login(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "exception", required = false) String exception,
Model model) {
// 코드는 이전과 동일
}
@GetMapping(value={"/denied","/api/denied"}) // 1
public String accessDenied(@RequestParam(value = "exception", required = false) String exception, Principal principal, Model model) throws Exception {
// 코드는 이전과 동일
}
}
- 각 매핑 값들에 api 값들 또한 넣어 정상적으로 url이 동작하도록 설정했습니다.
AjaxLoginAuthenticationEntryPoint.java
public class AjaxLoginAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "UnAuthorized");
}
}