스프링 시큐리티는 스프링 기반의 애플리케이션에서 인증과 권한을 통해 접근을 필터로 제어하는 프레임워크로 개발자가 보안 관련 로직을 일일히 작성하지 않아도 된다는 장점이 있다.
인증, 인가?
인증(Authentication) : 해당 사용자가 본인이 맞는지 확인하는 과정
인가(Authorization) : 해당 사용자가 요청하는 자원을 실행할 수 있는 권한이 있는가 확인하는 과정
인증을 로그인을, 인가는 로그인이 필요한 서비스에 접근하는 것이라고 보면 된다.
Spring Security에서는 인증, 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.
Principal(접근 주체) : 보호받는 리소스에 접근하는 대상
Credential(비밀번호) : 리소스에 접근하는 대상의 비밀번호
Spring Security FilterChain
Servlet Container 위에 Spring Dispatcher Servlet이 떠있는 형태지만 순서를 나타내기 위해 위와 같이 그림을 그렸다.
WAS와 Spring Dispatcher Servlet 사이에 필터는 원래 Servlet Container가 생성해서 스프링 컨테이너에 접근할 수 없지만 스프링 시큐리티는 필터의 생성을 Spring Dispathcer Servlet에 위임해서 스프링 컨테이너에 접근가능한 필터들을 생성한다.
인증이든 인가든 몇겹으로 걸친 스프링 시큐리티 필터체인이 요청을 걸러서 넘겨주는 방식이다.
인증
1. AuthenticationFilter
Security Config에서 설정한 FilterChain중 하나로 인증 HTTP Request를 처리한다.
Default는 UsernamePasswordAuthenticationFilter이다.
2. AuthenticationToken
AuthenticationFilter에 접근한 Request에서 Username(Principal), Password(Credential)을 추출해서 Authentication 인터페이스의 구현체인 AuthenticationToken을 생성한다.
Default는 UsernamePasswordAuthenticationToken이고 생성자는 두개인데 로그인 요청으로 넘어온 Principal, Password를 String으로 넣어만드는 것과 (5~7번의 과정)해당 유저의 정보를 DB에서 찾아와 만들기 위해서이다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 570L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
}
3. AuthenticationManager
Authentication Filter에서 만든 AuthenticationToken을 AuthenticationManager로 넘겨 인증을 진행한다.
AuthenticationManager는 ProviderManager를 통해서 넘겨받은 토큰을 처리할 수 있는 AuthenticationProvider를 찾는다.
ProviderManager에서는 가능한 Provider 리스트를 돌면서 해당 토큰을 처리할 수 있는지 Provider들의 support 메소드를 통해 확인하고 처리할 수 있다면 해당 Provider에게 토큰의 처리를 위임한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) { // 해당 토큰 처리 불가하면 넘김
continue;
}
try {
result = provider.authenticate(authentication); // 토큰 처리가능하면 Provider에게 authenticate 위임
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
}
커스텀이 필요할 경우 Spring Security 5버전 아래서는 WebSecurityConfiguererAdapter에서 빈으로 등록해줘서 바로 주입받아 사용하면 되지만 최신버전에서는 AuthenticationConfiguration을 통해 빈으로 등록 후 주입받아야 한다.
4. AuthenticationProvider
Manager에게 위임받은 토큰의 authenticate를 맡는다.
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
// ...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try { // 1. 애플리케이션 외부에서 User 정보 찾아오기
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
try {
this.preAuthenticationChecks.check(user);
//2. 토큰 비교
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 3.Manager에게 Authentication return
return createSuccessAuthentication(principalToReturn, authentication, user);
}
//...
}
- Service를 통해 애플리케이션 밖의 유저 정보를 찾아서 토큰으로 만든다.
- Manager에게 위임받은 토큰(아이디, 비밀번호), Service에서 찾아온 토큰(User정보, 비밀번호)를 비교하고 인증 결과를 Manager에게 리턴한다. 인증 실패시 BadCredentialsException이 발생한다.
- 인증 성공 시 AuthenticationManager에게 Authentication 리턴
Default는 UserDetails를 사용하는 DaoAuthenticationProvider이다.
5. UserDetailsService
Repository를 통해 애플리케이션 외부(DB)에서 2번에서 만든 Token의 Principal에 해당하는 User의 정보를 가져온다.
UserDetails의 구현체의 상태로 User의 정보를 가져오는 UserDetails, UserDetailsSerivce가 Default이다.
10. AuthenticationFilter -> SecurityContext
Authentication Filter에서 AuthenticationManager에게 받은 Authentication을 Spring Context Holder를 통해 시큐리티 전역 저장소인 Spring Context에 접근해 저장한다.
인증의 흐름을 정리하자면
1. Filter or Controller에서로그인 정보로 토큰을 만든다.
2. AuthenticationManger에게 토큰을 넘겨 인증을 맡긴다.
3. AuthenticationManager의 ProviderManager가 해당 토큰을 처리가능한 AuthenticationProvider를 찾아 토큰을 넘긴다.
4. AuthenticationProvider는 넘겨받은 토큰의 Principal(Username)을 Service로 넘긴다.
5. Service는 해당 User의 정보(접근권한 포함)를 반환한다.
6. AuthenticationProvider는 넘겨받은 User정보로 토큰을 만들고 앞서 만들어진 토큰과 비교한다.
7. 인증이 실패하면 BadCredentailException 발생, 성공하면 Provider -> Manager -> Filter or Controller로 토큰을 반환한다.
8. Filter or Controller는 해당 토큰을 Security Context 에 저장한다.
9. 이후에 필요한 User 정보는 Security Context에 접근해서 사용한다.
인증이 이루어지는 6번에서 중요한 것은 토큰의 비밀번호는 암호화가 안되어있기 때문에 암호화 후 토큰과 비교해야한다.
인가
FilterSecurityInterceptor
접근 권한이 있는 페이제 접속할 때 가능여부를 판단하는 필터이기에 필터체인의 가장 마지막에 위치한다.
요청정보, 권한정보, 인증정보를 획득해 AccesssDecisionManager에게 인가처리를 위임한다.
요청정보
요청의 URL
FilterInvocation을 통해 획득
권한정보
사용자가 접속하려는 요청정보의 권한읽기
Spring Security Config에서 설정한 URL과 권한정보는 ExpressBasedFilterInvocationSecurityMetadataSource의 requestToExpressionAttributesMap에 저장되있는 것을 FilterInvocationMetadatSource로 획득
인증정보
SecurityContextHolder를 통해 SpringContext에 접근해 현재 Authentication을 획득
AccessDecisionManager
Access Control 결정을 내리는 인터페이스로, 구현체 3가지를 기본으로 제공한다.
AffirmativeBased : 여러 Voter 중에 하나라도 허용하면 허용, 기본전략
ConsensusBased : 과반수 Voter가 허용
UnanimousBased : 모든 Voter가 허용
AccessDecisionVoter
각각의 Voter는 필터에서 넘겨받은 요청정보, 권한정보, 인증정보로 기반으로 승인(ACCESS_GRANTED), 거절(ACCESS_DENIED), 보류(ACCESS_ABSTAIN)을 반환한다.WebExpressionVoter : Web Security에서 사용하는 기본 구현체 , SpEl표현식에 따른 웹 접근제어 처리(Security Config 설정 중 경로, 접근권한 설정 등)
인증, 인가 예외처리
보안 필터 중 마지막인 FilterSecurityInterceptor에서 인증, 인가 예외처리를 위해 ExceptionTranslationFilter를 발생시킨다.ExceptionTranslationFilter에서는 인증, 인가 예외처리 throws하고 Security Config에서 exceptionHandling()으로 관리한다.
AuthenticationExcepion 인증 예외처리
AuthenticationEntryPoint에서 예외처리 하도록 넘긴다.handle() 메소드를 오버라이딩해서 Response를 조작할 수 있다.
AccessDeniedException 인가 예외처리
AccessDeniedHandler에서 예외처리 하도록 넘긴다.commence() 메소드를 오버라이딩해서 Response를 조작할 수 있다.
정리
보안을 위해 많은 기능을 제공하기에 개발자가 일일히 보안을 위한 코드를 안짜도 되는 장점이 있다.
디스패쳐 서블릿에게 필터의 생성을 위임해서 스프링 컨테이너에 접근가능한 필터체인으로 구성된다
인증/ 인가를 담당하는 필터가 있다
예외처리를 위한 필터도 있다 => ControllerAdvice로 ExceptionHandler 안해도 된다
참고
https://mangkyu.tistory.com/77
https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-requests.html
https://fenderist.tistory.com/344
https://sloth.tistory.com/m/46