기본
최근 자바와 스프링으로 많은 웹 애플리케이션을 만들어지고 있다. 그리고 웹 애플리케이션은 보안이 중요하다는 것은 누구나 이해하면서도 공감하고 있다.
이번 포스팅에서는 자바의 보안과 관련된 프레임워크 중 스프링 시큐리티 프레임워크의 원리에 대해서 설명해보려고 한다.
먼저 기존 서블릿에서 인증과 권한이 어떻게 이루어졌는지 간단히 한번 살펴보겠다.
단순한 기존 인증 형태
String username = request.getParameter("username");
String password = request.getParameter("password");
// 상세한 부분 생략
// 사용자 정보 조회
Member member = memberService.find(username);
if ( member == null ) {
// 실패 처리 생략
}
// 정말 단순하게 구현 ( 사실은 패스워드는 암호화한 후 알고리즘에 따라서 비교해야 함 )
if (!password.equals(member.getPassword())) {
// 실패 처리 생략
}
// 로그인 성공! 세션에 저장
request.getSession().setAttribute("member", member);
// 로그인 성공 후 이동할 페이지
response.sendRedirect("/");
권한 처리
기존 권한 처리 방법도 여러 가지가 있다.
- 페이지마다 기술하는 형태
- 필터 또는 인터셉터 ( 프레임워크에서 지원하는... )
간단하게 권한을 체크하는 필터를 작성해보면
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String uri = request.getRequestURI();
HttpSession session = request.getSession(false);
if (session == null) {
// 세션이 없는 사용자 처리
response.sendRedirect("/login-page");
return;
}
Object sessionMember = session.getAttribute("member");
if (sessionMember == null || !(sessionMember instanceof Member)) {
// 세션이 없는 사용자 처리
response.sendRedirect("/login-page");
return;
}
Member member = (Member) sessionMember;
// 권한 처리 로직
boolean isAuthority = false;
if (uri.startsWith("/member/")) {
if (member.hasRole("ROLE_USER") ||
member.hasRole("ROLE_ADMIN")) {
isAuthority = true;
}
}
if (uri.startsWith("/dev/")) {
if (member.hasRole("ROLE_USER") ||
member.hasRole("ROLE_TEST"))
isAuthority = true;
}
if (isAuthority) {
chain.doFilter(req, res);
}
// 권한이 없을 때 사용자 처리
response.sendRedirect("/access_denied");
}
( 구체적으로 처리해야 되는 부분이 많지만 생략했다. )
자세히 보면 알겠지만 세션정보의 존재 여부에 따라 인증을 판단하고, 인증된 정보에서 가지고 있는 Role에 따라 권한을 판단하는 형태이다.
이렇게 진행되는 인증(Authentication) 권한(Authority) 형태를 재활용할 수 있으며 다양한 환경에서 확장하여 사용할 수 있도록 정리하여 만든 프레임워크가 스프링 시큐리티이다.
이제 스프링 시큐리티에 대해서 이야기해보자.
먼저 스프링 시큐리티는 Acegi Security란 이름으로 2003년에 나온 프로젝트로 이후 2007년에 스프링 프로젝트에 합쳐지면서 현재의 스프링 시큐리티(이하 시큐리티)로 바뀌게 되었다.
( 10년이 넘게 프로젝트가 유지되면서 기본 기존에 다양한 기능이 추가되면서 복잡도가 늘어난 것으로 생각된다. )
기본적으로는 자바에서 서블릿 필터를 이용해서 서블릿 보안을 기본으로 하지만 기본적으로 서블릿 없는 자바 시스템도 ( 보통 Swing 같은 GUI 형태 앱 ) 보안도 지원한다.
보통 웹 시스템 구성시에는 세션에 인증정보를 직접 저장하고 그 정보를 가지고 권한을 확인하는 형태를 취하고 있다. 하지만 시큐리티 같은 경우 기본적으로 세션을 사용하고 있지만 SecurityContext 객체를 통해서 전달하는 간접적으로 인증정보를 전달하고 있다.
// SecurityContext 가져오는 방법
SecurityContext securityContext = SecurityContextHolder.getContext();
이렇게 정적 메서드를 통해서 SecurityContext 객체를 가져오는데 내부 구현을 모르면 이상하게 생각할지도 모르겠다.
정적 메서드를 가져오면 멀티스레드에서 인증정보가 섞이지 않을까 하는 문제인데 그 부분은 기본적으로 ThreadLocal(이하 스레드 로컬)이라는 방식을 사용해서 해결하고 있다.
스레드 로컬은 자바 애플리케이션에서 내부적으로 변수를 전달 시에 많이 사용하는 방법인데 최근 프레임워크에서는 많이 사용하는 방법이다.
간단히 이야기하자면 자바 애플리케이션의 모든 처리는 스레드를 통해서 이루어진다. main() 메서드 조차 스레드로 시작되며, 알다시피 톰캣 같은 WAS는 멀티스레드를 이용해서 처리된다.
그 스레드 안에서만 변수를 사용할 수 있도록 저장할 수 있도록 하는 공간이 바로 스레드 로컬이라고 불리는 저장 장소이다.
스레드 로컬에 대한 자세한 정보는 최범균 님 블로그에 설명된 글을 추천한다.
여기까지 이야기를 정리하자면 기존 웹에서는 사용자 인증정보는 세션을 통해 직접 관리했지만 시큐리티에서는 한 번 더 추상화한 스레드 로컬을 사용한 SecurityContext를 사용해서 인증정보를 사용한다.
그런데 왜 시큐리티는 세션을 직접 사용하지 않고 SecurityContext를 사용해서 인증 정보를 관리할까?
몇 가지 이유가 있다.
Session 즉 HttpSession은 서블릿을 의존하고 있다. 만약 세션에서 사용자 정보를 직접 관리하게 되면 내부적으로 파라미터로 HttpSession을 전달하거나 앱 자체에 서블릿 클래스에 의존하는 형태가 많이 만들어진다. ( 앞서서 이야기했지만 서블릿 애플리케이션뿐만 아니라 일반 자바 애플리케이션에서 사용할 수 있도록 설계되어 있다. )
AOP 및 인터셉터 등 공통 프레임워크를 만들 시에 유용하다. 심지어 Log4J 및 Slf4j(Logback) 등을 사용했을 시에 인증 정보 전달 시 유용하다. ( MDC 이용 등 )
HttpSession에 저장하지 않고 외부로 인증 정보를 저장하는 형태도 구현이 가능하다. (기본 클래스를 확장하여 DB 또는 Redis, Memcached, MongoDB 등으로 인증정보를 저장이 가능하다.)
자 그럼 시큐리티의 인증정보를 플로우를 한번 살펴보겠다.
이 플로우의 핵심은 직접 서비스로 가기 전에 필터에서 세션(기본)을 조회해서 사용자 정보를 SecurityContext로 저장해서 전달하는 것과 요청이 끝나면 기존 사용된 SecurityContext에서 변경된 정보를 원래 읽어왔던 곳(여긴 세션)으로 저장 후 제거한다는 것이다.
즉 SecurityContext는 한번 요청에서 사용되고 난 이후 없어진다. 즉 다음 요청이 오면 그때 새로 인증 저장소(기본은 세션)에서 가져와서 만들어지는 형태인 것이다.
사실 SecurityContext 인터페이스를 살펴보면
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
Authentication이라는 이름의 클래스의 객체를 저장하고 가져오는 역할만 하고 있다.
사실 중요한건 Authentication 객체에 들어가 있는 것이다. 그걸 SecurityContext는 감싸고 있을 뿐이다.
그럼 다음 글에서는 Authentication에 대해 구체적으로 알아보려고한다.