WebSecurity로 Ajax로 JSON객체를 반환할 수 있는 API를 제작하는 법을 알아보겠습니다.
우선 화면단에서 Ajax로 데이터를 전송할 Script를 구성해야합니다
_csrf_header
, _csrf
를 가져와서 보내줘야하므로 meta에 선언하였고, js로 값을 가져와보내는 형식으로 만들었습니다. Ajax가 아닐때는 자동으로 csrf가 설정이 되었지만, 여기서는 xhe.setRequestHeader
에 값을 세팅해서 요청을 보내게 됩니다.
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>
<script>
function formLogin(e) {
var username = $("input[name='username']").val().trim();
var password = $("input[name='password']").val().trim();
var data= { "username": username, "password": password};
var csrfHeader = $("meta[name='_csrf_header']").attr('content');
var csrfToken = $("meta[name='_csrf']").attr('content');
$.ajax({
type: "POST",
url: "api/login",
data: JSON.stringify(data),
dataType: "json",
beforeSend : function(xhe){
xhe.setRequestHeader(csrfHeader, csrfToken);
xhe.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhe.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>
이때 Ajax통신이라는것을 서버에 알려줘야하기 때문에 다음과 같이 header에 내용을 하나 추가합니다.
xhe.setRequestHeader("X-Requested-With", "XMLHttpRequest");
이전 Provider 포스팅 에서는 AuthenticationProvider
를 이용하여 인증처리를 진행하였습니다.
AuthenticationProcessingFilter
을 통해 이러한 인증처리 과정을 Custom Filtering 하는 것이 가능합니다.
AntPathRequestMatcher
로 Url
을 특정하고, 추가적인 요청에대한 로직을 실행한 후 AuthenticationManager
로 인증된 Token
을 넘길 수 있습니다.
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private ObjectMapper objectMapper = new ObjectMapper();
public AjaxLoginProcessingFilter() {
// api/login에 대한 요청만 아래의 인증 과정을 실행해줍니다.
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");
}
//objectMapper를 이용하여 json으로 들어온 요청을 객체로 변환합니다.
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
if(StringUtils.isEmpty(accountDto.getUsername()) || StringUtils.isEmpty(accountDto.getPassword())){
throw new IllegalAccessError("Username or Password is empty");
}
// 그렇게 검사를 마친 객체를 token으로 생성해줍니다 (아래 소스 확인)
AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(accountDto.getUsername(), accountDto.getPassword());
return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
}
// 위에서 header에 요청이 'XMLHttpRequest' 맞는지를 검토하게 됩니다.
private boolean isAjax(HttpServletRequest request) {
if("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))){
return true;
}
return false;
}
}
요약을 해보면 받은 요청이 /api/login
이며, 헤더가 XMLHttpRequest
가 맞는지 확인후 인증정보를 셋팅해주면 됩니다. Token 객체는 Spring Security가 사용하는 UsernamePasswordAuthenticationToken
객체를 그대로 복붙하여 하나 만들었습니다만, 따로 추가할게 없어서 이걸 그대로 사용해도 될거같기는 합니다.
//UsernamePasswordAuthenticationToken 파일과 완전 동일 합니다.
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
private final Object principal;
private Object credentials;
public AjaxAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
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 {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
이제 WebSecurityConfigurerAdapter
를 상속받는 설정파일에 셋팅을 해주시면 됩니다.
앞서 말씀드린데로 AuthenticationManager
를 Bean
으로 가져와 우리가 만든 Ajax용 Filter
에 넣어주면 됩니다.
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {
//중략...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
;
// 아래에서 만들어낸 필터를 기존로그인에서 사용하는 AuthenticationFilter앞에서 실행하게 해줍니다.
// addFilter(맨뒤에), addFilterAfter(~앞에), addFilterAt(대체) 등 있습니다.
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter(); // 생성한 필터에...
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean()); // Manager를 가져와...
ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler);
ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler);
// 그 밖에 필요한 헨들러도 구성 가능합니다. http.successHandler(), failureHandler()와 같은 기능입니다. 번외참고
return ajaxLoginProcessingFilter; //설정을 마칩니다.
}
Provider가 무슨 역할을 하는지의 대한 설명은 제외하겠습니다 여기서 확인 가능합니다.
public class AjaxAuthenticationProvider implements AuthenticationProvider{
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password,accountContext.getAccount().getPassword())){
throw new BadCredentialsException("BadCredentialsException");
}
AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(
accountContext.getAccount(),
null,
accountContext.getAuthorities()
);
return ajaxAuthenticationToken;
}
@Override
public boolean supports(Class<?> authenticate) {
return AjaxAuthenticationToken.class.isAssignableFrom(authenticate);
}
}
Handler의 경우 Ajax통신이므로 Json형식으로 리턴해줘야합니다 ObjectMapper.writeValue()
를 이용하면 됩니다.
@Component
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper; // ObjectMapper는 Spring 빈에 등록되어있습니다. Autowired됩니다.
@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); //요렇게 선언해주시면 됩니다.
}
}
@Component
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
String errorMessage = "Invalid Username or Password";
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if(e instanceof BadCredentialsException) errorMessage = "Invalid Username or Password";
else if(e instanceof InsufficientAuthenticationException) errorMessage = "Invalid Secret Key";
objectMapper.writeValue(response.getWriter(), errorMessage);
}
}
Anonymous
의 경우 EntryPoint
를 설정해주고, Authentication
의 경우 Denied
를 지정해 줍니다.
public class AjaxLoginAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.sendError(response.SC_UNAUTHORIZED,"UnAuthorized");
}
}
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.sendError(response.SC_FORBIDDEN,"Access is denied");
}
}
Configure, http
에 ExceptionHandler
에 추가해주시면 됩니다.
//생략...
http
.exceptionHandling()
.authenticationEntryPoint(new AjaxLoginAuthenticationEntryPoint())
.accessDeniedHandler(new AjaxAccessDeniedHandler())
;
Java Security API를 만드는 법을 알아봤습니다.
antMatcher()
로 API Url의 범위를 지정해주고, 호출 부분은 @RestController
로 작성해주시면 됩니다.
단, 계정에 대한 인증 권한을 검사하는 단계는 Filter를 이용해서 새로 구성해야합니다.
Spring과 Security의 틀에서 벗어난게 없어서 이해하는데 어려움은 없었습니다.