验证

架构组件

  • SecurityContextHolde
  • SecurityContext
  • Authentication
  • GrantedAuthority
  • AuthenticationManager
  • ProviderManager
  • AuthenticationProvider
  • Request Credentials with AuthenticationEntryPoint
  • AbstractAuthenticationProcessingFilter

SecurityContextHolder

SecurityContextHolder是Spring Security验证模型的核心,它包含SecurityContext

SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context);
  • SecurityContextHolder.createEmptyContext() 可以避免线程竞争带来的问题:当重新使用池中的线程时,线程中仍可能存在任何线程本地数据
  • SecurityContextHolder.setContext(context)SecurityContext保存在SecurityContextHolder中,之后用于授权

访问当前已验证的用户

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
  • SecurityContextHolder.MODE_THREADLOCAL :默认情况下,SecurityContextHolder使用ThreadLocal保存, FilterChainProxy确保SecurityContext总是被清除
  • SecurityContextHolder.MODE_GLOBAL:Java虚拟机中的所有线程使用相同的安全上下文,如:Swing客户端
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 子线程继承父线程安全标识

配置:-Dspring.security.strategy=MODE_GLOBAL

SecurityContext

SecurityContext包含 Authentication 对象

Authentication

是授予用户的高级许可,如:roles、scopes

GrantedAuthority

AuthenticationManager

AuthenticationManager是 Spring Security Filter进行验证的抽象,它只定义了一个方法:

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

如果不集成 Spring Security Filter,可以绕过AuthenticationManager,直接设置SecurityContextHolder。Spring Security提供了通用的实现:ProviderManager

ProviderManager

ProviderManager委托给一组AuthenticationProvider,每个AuthenticationProvider都有机会表明验证成功、失败,或者无法验证,并交由下游。如果没有配置AuthenticationProvider则抛出ProviderNotFoundException

每个AuthenticationProvider执行特定类型的验证,因此AuthenticationManager可以支持多种类型验证

ProviderManager可以配置父AuthenticationManager,当无法身份验证时,可以咨询父AuthenticationManager。通常它是一个ProviderManager实例。

ProviderManager多个实例可以共享一个父AuthenticationManager,这在多个SecurityFilterChain实例的场景中比较常见。

默认情况下,ProviderManager将尝试从成功的身份验证请求返回的身份验证对象中清除任何敏感凭据信息。当使用缓存时,那么将不再可能根据缓存的值进行身份验证。解决方法:

  • 提前做数据备份,在缓存实现中,或者在AuthenticationProvider创建返回Authentication对象中
  • 禁用ProviderManager的属性eraseCredentialsAfterAuthentication

AuthenticationProvider

多个AuthenticationProvider被注入到ProviderManager,每个可以执行特定类型的验证。如:DaoAuthenticationProvider JwtAuthenticationProvider

AuthenticationEntryPoint

被用于从客户端请求用户凭证,AuthenticationEntryPoint可能是跳转到登录页,或者响应WWW-Authenticate

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter是个基础的Filter,用来验证客户端提交的用户凭证。

  1. AbstractAuthenticationProcessingFilterHttpServletRequest中创建Authentication,具体依赖子类实现。如:UsernamePasswordAuthenticationFilter创建UsernamePasswordAuthenticationToken
  2. Authentication传递给AuthenticationManager去验证
  3. 如果验证失败
    • SecurityContextHolder被清除
    • RememberMeServices.loginFail被调用
    • AuthenticationFailureHandler被调用
  4. 如果验证成功
    • SessionAuthenticationStrategy会话验证策略被通知有个新登录
    • Authentication保存到SecurityContextHolder,之后 SecurityContextPersistenceFilter 保存 SecurityContextHttpSession
    • RememberMeServices.loginSuccess
    • ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent事件

验证机制

  • Username and Password
  • OAuth 2.0 Login
  • SAML 2.0 Login
  • Central Authentication Server (CAS
  • Remember Me
  • JAAS Authentication
  • OpenID
  • Pre-Authentication Scenarios
  • X509 Authentication

Username and Password Authentication

Reading the Username & Password

读取用户名和密码机制

  • Form Login
  • Basic Authentication
  • Digest Authentication

Form Login

  1. 未验证请求私密资源
  2. FilterSecurityInterceptor抛出异常AccessDeniedException
  3. ExceptionTranslationFilter通过AuthenticationEntryPoint跳转到登录页,通常是LoginUrlAuthenticationEntryPoint

UsernamePasswordAuthenticationFilter 继承自( extend)AbstractAuthenticationProcessingFilter

默认开启,可以禁用:

protected void configure(HttpSecurity http) throws Exception {
    http.formLogin().disable();
}

Basic Authentication

配置:

protected void configure(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
}

Digest Authentication

Spring 提供DigestAuthenticationFilter

存储机制

  • In-Memory Authentication
  • JDBC Authentication
  • UserDetailsService
  • LDAP Authentication

In-Memory

InMemoryUserDetailsManager

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

JDBC

DaoAuthenticationProvider

  1. UsernamePasswordAuthenticationToken
  2. ProviderManager
  3. DaoAuthenticationProvider 通过UserDetailsService查找UserDetails
  4. DaoAuthenticationProvider使用PasswordEncoder验证UserDetails返回的密码
  5. 验证成功后,UsernamePasswordAuthenticationToken将被返回

Session Management

Http会话由接口SessionManagementFilter SessionAuthenticationStrategy共同处理

并发会话控制

单一用户登陆,失效之前session,或者阻止后续登陆

http.sessionManagement()
                .maximumSessions(1).maxSessionsPreventsLogin(true);

固定会话攻击

攻击者访问网站创建会话,被攻击者使用同一个会话登陆。

  • none 不做任何处理
  • newSession 创建一个新的session,Spring Security相关属性仍会被复制
  • migrateSession 创建新的session,从session中复制所有属性,默认
  • changeSessionId 不创建新的session,Servlet 3.1以上版本支持

SessionManagementFilter

SessionManagementFilter对比SecurityContextRepositorySecurityContextHolder的内容,来检查用户是否登陆,如:pre-authentication 、remember-me

Session验证策略:

if (!securityContextRepository.containsContext(request)) {
	Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication != null && !trustResolver.isAnonymous(authentication)) {
	    // The user has been authenticated during the current request, so call the ession strategy
	    sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
    }
}

无效session策略,默认SimpleRedirectInvalidSessionStrategy

if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
        invalidSessionStrategy.onInvalidSessionDetected(request, response);
}

SessionAuthenticationStrategy

同时被SessionManagementFilter AbstractAuthenticationProcessingFilter用到,自定义时需要同时注入

并发控制

Spring Security 能够阻止主体同时对同一个 application 进行多次身份验证。常用的实现:ConcurrentSessionControlAuthenticationStrategy

调用SessionRegistry.getAllSessions()获取用户所有session,检查是否大于配置并发数,如果不允许超过,则抛出异常SessionAuthenticationException, 如果不允许超过,则过期之前的session。SessionRegistry默认实现:SessionRegistryImpl

Remember-Me Authentication

在会话中保存认证状态,Spring提供了两种实现,基于Cookie,基于数据库

RememberMeServices

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication successfulAuthentication);

RememberMeAuthenticationFilter 调用RememberMeServicesautoLogin方法

#### TokenBasedRememberMeServices TokenBasedRememberMeServices生成 RememberMeAuthenticationToken,它由RememberMeAuthenticationProvider处理。密钥在TokenBasedRememberMeServicesRememberMeAuthenticationProvider之间共享。TokenBasedRememberMeServices需要从UserDetailsService获取用户名密码,用于签名比较。TokenBasedRememberMeServices也要实现LogoutHandler自动清除cookie。

PersistentTokenBasedRememberMeServices

TokenBasedRememberMeServices一样,此外,还要配置PersistentTokenRepository

  • InMemoryTokenRepositoryImpl
  • JdbcTokenRepositoryImpl

Anonymous Authentication

采用“默认拒绝”通常被认为是良好的安全实践,这就是我们所说的匿名身份验证。

  • AnonymousAuthenticationToken实现Authentication,存储GrantedAuthority
  • AnonymousAuthenticationProvider被链接到ProviderManager,所以AnonymousAuthenticationToken可以被接受
  • AnonymousAuthenticationFilter在正常验证机制之后,在没有Authentication时,把AnonymousAuthenticationToken添加到SecurityContextHolder

AuthenticationTrustResolver

public interface AuthenticationTrustResolver {
    boolean isAnonymous(Authentication var1);

    boolean isRememberMe(Authentication var1);
}

它有一个默认实现类AuthenticationTrustResolverImpl,Spring Security就是使用它来判断一个SecurityContextHolder持有的Authentication是否AnonymousAuthenticationToken或RememberMeAuthenticationToken

AuthenticatedVoter方法更强大,因为它允许您区分匿名用户、remember-me用户和完全通过身份验证的用户。如果您不需要这个功能,那么您可以使用ROLE_ANONYMOUS,它将由Spring Security的标准RoleVoter处理。

Pre-Authentication Scenarios

只使用Spring Security的授权功能,某些外部系统已经对用户进行了可靠的身份验证。

CAS Authentication

JA-SIG生成了企业范围的单点登录系统,称为CAS. 与其他计划不同,JA-SIG的中央身份验证服务是开放源代码,广泛使用,易于理解,独立于平台并支持代理功能.

Run-As Authentication Replacement

在安全对象回调阶段, AbstractSecurityInterceptor能够临时替换SecurityContext和SecurityContextHolder中的Authentication对象. 仅当AuthenticationManager和AccessDecisionManager成功处理了原始Authentication对象时,才会发生这种情况. RunAsManager将指示在SecurityInterceptorCallback期间应使用的替换Authentication对象(如果有).

public interface RunAsManager {
    Authentication buildRunAs(Authentication var1, Object var2, Collection<ConfigAttribute> var3);

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);
}

AbstractSecurityInterceptor的实现FilterSecurityInterceptor调用RunAsManagerImpl

Handling Logouts

WebSecurityConfigurerAdapter ,将自动应用注销功能. 默认是访问URL /logout将通过以下方式/logout用户:

  • 使HTTP会话无效
  • 清理配置的所有RememberMe身份验证
  • 清除SecurityContextHolder
  • 重定向到/login?logout

LogoutHandler

内置实现

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler
  • HeaderWriterLogoutHandler

LogoutSuccessHandler

LoginFilter成功退出后,LogoutSuccessHandler将会被调用,以处理例如重定向或转发到适当的目标。

  • SimpleUrlLogoutSuccessHandler 默认跳转到/login?logout
  • HttpStatusReturningLogoutSuccessHandler 默认返回状态码200

    Authentication Events

对于成功或失败的每个身份AuthenticationFailureEvent ,分别触发AuthenticationSuccessEvent或AuthenticationFailureEvent .

首先发布AuthenticationEventPublisher

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}

其次@EventListener

@Component
public class AuthenticationEvents {
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent success) {
        // ...
    }

    @EventListener
    public void onFailure(AuthenticationFailureEvent failures) {
        // ...
    }
}

添加异常映射

DefaultAuthenticationEventPublisher默认会发布以下事件:

Exception Event
BadCredentialsException AuthenticationFailureBadCredentialsEvent
UsernameNotFoundException AuthenticationFailureBadCredentialsEvent
AccountExpiredException AuthenticationFailureExpiredEvent
ProviderNotFoundException AuthenticationFailureProviderNotFoundEvent
DisabledException AuthenticationFailureDisabledEvent
LockedException AuthenticationFailureLockedEvent
AuthenticationServiceException AuthenticationFailureServiceExceptionEvent
CredentialsExpiredException AuthenticationFailureCredentialsExpiredEvent
InvalidBearerTokenException AuthenticationFailureBadCredentialsEvent

setAdditionalExceptionMappings添加额外的事件

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    Map<Class<? extends AuthenticationException>,
        Class<? extends AuthenticationFailureEvent>> mapping =
            Collections.singletonMap(FooException.class, FooEvent.class);
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
    return authenticationEventPublisher;
}

setDefaultAuthenticationFailureEvent捕获所有的异常

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setDefaultAuthenticationFailureEvent
        (GenericAuthenticationFailureEvent.class);
    return authenticationEventPublisher;
}