Spring Security를 이용한 REST API를 개발할 때, 외부 사이트와의 데이터 연동을 위해 CORS를 적용하는 것도 매우 좋은 선택이다.
일반적으로 Cross domain 문제를 해결하기 위해서, JSONP를 이용하는 경우가 많지만 이 방법은 두가지 이유때문에 개인적으로는 추천하지 않는다.
1. GET method만을 이용 가능 - 데이터를 많이 보내야지 되는 경우가 발생할 수 있고, 무엇보다 REST한 API를 만드는데 제약사항이 발생하게 된다.
2. 인증 문제 - 인증을 받아서 처리를 해줘야지 되는 API의 경우에는 JSONP를 이용할 수 없다. 모든 정보를 API서버에만 위치하고, HTML + javascript로만 동작하는 web application을 작성하는 것이 목적이라면 인증을 처리하기 위해서라도 JSONP가 아닌 CORS를 적용해야지 된다.
그러나 CORS 역시 많은 제약사항을 가지고 있다. 기본적인 제약사항들은 다음과 같다.
1. GET, HEAD, POST 만 사용 가능하다.
2. POST의 경우에는 다음과 같은 조건이 경우에만 사용가능하다.
1) content-type이 application/x-www.form-urlencoded, multipart/form-data, text/plain의 경우에만 사용 가능하다.
2) customer Header가 설정이 된 경우에는 사용 불가하다. (X-Modified etc...)
3. Server에서 Access-Control-Allow-Origin 안에 허용여부를 결정해줘야지 된다.
큰 제약사항은 위 3가지지만, 세부적으로는 preflight 문제가 발생하게 된다. preflight란, POST로 외부 site를 call 할때, OPTIONS method를 이용해서 URL에 접근이 가능한지를 다시 한번 확인하는 절차를 거치게 된다. 이때, 주의할 점이 WWW에서 제약한 사항은 분명히 content-type이 application/xml, text/xml의 경우에만 preflight가 발생한다고 되어있으나, firefox나 chrome의 경우에는 text/plain, application/x-www-form-urlencoded, multipart/form-data 모두에서 prefligh가 발생하게 된다.
위 이론을 먼저 알고, Spring Security를 적용한 REST API Server를 구축하기 위해서는 다음과 같은 절차가 우선 필요하다.
1. Spring Security Form authentication endPoint의 변경
: Spring Security Form authentication은 인증되지 않은 Request가 접근한 경우, login page로 302 redirect를 발생시킨다. 이렇게 되면 API를 이용해서 로그인의 실패등을 확인하기 힘들기 때문에, 인증되지 않은 Request를 처리하는 방법을 달리 해줘야지 된다. 기본적으로 Digest Authentication 역시 www는 지원하기 때문에 Digest 인증 방식의 end point를 이용해서 인증되지 않은 request가 접근한 경우 302가 아니라 401(NotAuthenticated)를 반환할 수 있도록 Spring Configuration을 변경하도록 한다.
2. CSRF disable
: 다른 domain에서의 API call이 발생하기 때문에 CSRF salt cookie값을 얻어내는 것은 불가능하다. 따라서, CSRF를 disable시켜야지 된다. (이 부분은 해결방법이 다른 것이 있는지 확인이 필요)
3. Login Processing을 재구현
: Spring Security를 이용한 Form Authentication의 경우, login success의 경우에도 마찬가지로 redirect가 발생하게 된다. AuthenticationSuccessHandler interface를 이용해서 변경시켜주거나, Login / Logout을 아애 새로 만들어주는 것이 필요하다. 개인적으로는 Login / Logout을 새로 만들어주는 것을 선호하는 편인데, Controller에 대한 Test code 역시 만들어주는 것이 가능하고, 좀더 깔끔해보이는 느낌이 든다.
4. CORS Filter의 적용
: 모든 response 에 Allow-Origin header를 삽입해주는 Filter 객체가 반드시 필요하다.
Spring Security의 Form 인증을 새로 만들어주기 위해서는 다음과 같이 Controller를 만들 필요가 있다.
@Controller
public class LoginController {
public static final String API_LOGIN = "/api/login";
public static final String API_LOGOUT = "/api/logout";
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@RequestMapping(value= API_LOGIN, method = {RequestMethod.GET, RequestMethod.OPTIONS})
@ResponseBody
@ResultDataFormat
public Object getAuthenticationStatus() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && !auth.getName().equals("anonymousUser") && auth.isAuthenticated()) {
User user = userService.findByUsername(auth.getName());
return new LoginStatus(true, auth.getName(), user.getName());
} else {
return new LoginStatus(false, null, null);
}
}
@RequestMapping(value= API_LOGIN, method = RequestMethod.POST)
@ResponseBody
@ResultDataFormat
public Object login(@RequestParam("username") String username, @RequestParam("password") String password) {
try {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
User user = userService.findByUsername(username);
token.setDetails(user);
Authentication auth = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
return new LoginStatus(auth.isAuthenticated(), user.getUsername(), user.getName());
} catch (Exception e) {
return new LoginStatus(false, null, null);
}
}
@RequestMapping(value = API_LOGOUT)
@ResponseBody
@ResultDataFormat
public Object logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null) {
session.invalidate();
}
SecurityContextHolder.clearContext();
return new LoginStatus(false, null, null);
}
@Getter
public class LoginStatus {
private final boolean isAuthenticated;
private final String username;
private final String name;
public LoginStatus(boolean loggedIn, String username, String name) {
this.isAuthenticated = loggedIn;
this.username = username;
this.name = name;
}
}
}
다음에는 CORS Filter 적용 및 Spring Security Configuration을 이용해서 REST Server 구성을 더 해보도록 하겠다.