认证是我们进行资源保护的第一步,在Spring Security中对认证过程可能涉及到的动作进行了抽象。
1、匹配正确的认证方式
要认证则首选需要从请求中获取认证数据。对于Http协议来讲,其请求的认证数据可能保存在Header或者Body中,一般情况我们都是通过参数名来获取认证数据,例如通过 username参数来获取登录的用户名,通过password来获取密码数据。
在一个系统中可能还存在多种认证方式,例如:用户名+密码、短信认证码、三方登录等。 对于不同的认证可能会使用相同的参数名,所以为了更好的区分用户使用的认证方式,可以通过请求的路径来进行匹配,不同的认证方式使用不同的认证路径。
在Spring Security中通过抽象Authentication来存储认证数据
- CredentialsContainer:指示实现对象包含敏感数据,可以使用 eraseCredentials 方法擦除这些数据,但仅供内部框架使用。编写自己的 AuthenticationProvider 实现的用户应在此Provider中返回已经减去任何敏感数据的Authentication,而不是使用此接口。
- Principal:表示主体的抽象概念,可用于表示任何实体,例如个人、公司和登录ID(如果是实体,记得实现hashCode和equals方法哦)。
- Authentication:表示身份验证请求的令牌,或者完成身份认证处理后的主体(认证人)的令牌。
- GrantedAuthority:表示授予 Authentication 对象的权限。
通过Filter来拦截请求,路径匹配成功后从请求中提取认证数据,并封装成了Authentication类型实例,接下来就是对数据进行认证了。
2、统一的认证逻辑
在SpringSecurity中,定义了一个基于浏览器的HTTP协议请求的身份验证抽象处理器AbstractAuthenticationProcessingFilter
,这里面定义了认证的一套基本流程,其源码摘录如下:
public abstract class AbstractAuthenticationProcessingFilter
extends GenericFilterBean
implements ApplicationEventPublisherAware,
MessageSourceAware {
//安全上下文持有策略
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
//认证管理器
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
//“记住我”的处理
private RememberMeServices rememberMeServices = new NullRememberMeServices();
//是否需要进行认证的请求匹配器
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
//Session认证策略
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
//是否允许创建session
private boolean allowSessionCreation = true;
//认证成功后的处理器
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
//认证失败后的处理器
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//安全上下文的存储
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//判断是否是需要开始进行认证处理的请求
if (!requiresAuthentication(request, response)) {
//如果不是需要进行认证的请求,则忽略本过滤器
chain.doFilter(request, response);
return;
}
try {
//尝试对请求进行认证操作
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
//认证完成后根据session策略进行处理
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功后的处理逻辑
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
//认证失败后的处理逻辑
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
//认证失败后的处理逻辑
unsuccessfulAuthentication(request, response, ex);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
this.securityContextHolderStrategy.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
其主体逻辑是
- 判断请求是否触发认证逻辑
- 从请求中获取认证数据并进行认证【子类实现】
- 认证成功后处理session会话
- 认证成功后处理后续成功逻辑
- 创建SecurityContext并设置Authentication
- 使用SecurityContext持有策略对SecurityContext存储
- 使用SecurityContext持久化策略对SecurityContext存储
- 执行“记住我”服务的登录成功逻辑
- 执行认证成功处理器逻辑
- 如果认证失败则执行失败逻辑
- 清空SecurityContext持有策略中的数据
- 执行“记住我”服务的登录失败逻辑
- 执行认证失败处理器逻辑
虽然这里定义了一套基本认证流程,但并非每种认证都必须严格这么做,根据实际情况决定
1)认证方法
当从请求中获取到认证数据并封装成Authentication类型对象后,Spring Security会调用AuthenticationManager的authenticate方法来完成认证(如果是继承自AbstractAuthenticationProcessingFilter),而在authenticate方法中会调用可以处理Authentication类型的AuthenticationProvider来完成最终的认证
- AuthenticationManager: 尝试对传递的身份验证对象进行身份验证,如果成功,则返回一个完全填充的身份验证对象(包括授予的权限)。同时AuthenticationManager必须遵守以下约定
- 当AuthenticationManager检测到帐户被禁用,则必须抛出DisabledException。
- 当AuthenticationManager检测到帐户被锁定,则必须抛出LockedException。
- 当AuthenticationManager检测到凭据错误,必须抛出BadCredentialsException。
应该按照上述的顺序对账号和认证数据进行检测,并抛出对应的异常。
- AuthenticationProvider:处理特定Authentication的认证实现。AuthenticationManager作为认证的入口,将具体的认证操作委托给可以识别特定Authentication的AuthenticationProvider去实现。
配置方法
在Spring Security中有一个存放共享对象的sharedObjects
实例,在Spring Security初始化过程中会预先构建一些实例并存放到里面,待其他对象在构建的过程中从sharedObjects
里面获取所需的实例。
以AuthenticationManager为例,在某些过滤器或对象中会使用到,但这个使用它的过滤器或对象不一定会被使用者启用,所以可以将构建好的AuthenticationManager实例放到sharedObjects
中,供其他需要的时候获取。
AuthenticationManager的初始化构建过程在 @EnableWebSecurity 注解上的@EnableGlobalAuthentication注解中,其会生成AuthenticationManagerBuilder对象存入sharedObjects
, HttpSecurity在执行过程中会获取AuthenticationManagerBuilder来创建AuthenticationManager实例,并存入sharedObjects
下面展示了如何对AuthenticationManager和AuthenticationProvider进行配置:
@Configuration
public class DemoConfiguration {
//这里会注册一个SecurityFilterChain类型实例,
//而WebSecurityConfiguration在通过HttpSecurity构建
//springSecurityFilterChain的时候会先从IOC中
//获取SecurityFilterChain, 所以该方法会先执行。
//然后如果获取到SecurityFilterChain后则不再使用ttpSecurity构建
//SecurityFilterChain了
@Bean
public SecurityFilterChain configure(HttpSecurity http,
AuthenticationManager authenticationManager,
AuthenticationProvider provider) throws Exception {
//方式1:添加到sharedObjects中已存在的AuthenticationManagerBuilder里
http.authenticationProvider(provider) ;
//方式2:直接设置(后面会替换掉sharedObjects中已存在的,但不是立刻)
http.authenticationManager(authenticationManager) ;
//方式3:直接立刻替换AuthenticationManager
http.setSharedObject(AuthenticationManager.class,authenticationManager);
//方式4:直接立刻替换Builder
http.setSharedObject(AuthenticationManagerBuilder.class,builder);
return http.build() ;
}
}
上面代码展示了如何设置AuthenticationManager和AuthenticationProvider,需要注意的是,如果设置了authenticationManager,则不应该调用authenticationProvider方法,而是先添加到authenticationManager中,否则你设置的authenticationProvider将无效(因为设置的authenticationManager会替换掉sharedObjects中的)
2)认证后的Session管理
允许在发生身份验证时对 HttpSession 相关行为提供可插入支持。 典型用途是确保会话存在或更改会话 ID 以防止会话固定攻击。
public interface SessionAuthenticationStrategy {
/**
* 在进行新的身份验证时执行与 Http 会话相关的功能。
* @throws SessionAuthenticationException 如果确定不允许对会话
* 进行身份验证(这通常是因为用户一次打开了太多会话)。
*/
void onAuthentication(Authentication authentication,
HttpServletRequest request,
HttpServletResponse response)
throws SessionAuthenticationException;
}
- CsrfAuthenticationStrategy
- ConcurrentSessionControlAuthenticationStrategy
- CompositeSessionAuthenticationStrategy
- RegisterSessionAuthenticationStrategy
- ChangeSessionIdAuthenticationStrategy
- SessionFixationProtectionStrategy
- NullAuthenticatedSessionStrategy
- SessionRegistry
现在很多项目开始不再依赖HttpSession,对于这样的项目,以上有关session的操作都每用了。
3)认证成功后处理
认证成功后需要进行后续的处理,一般会包括对认证结果进程存储、处理“记住我”逻辑、将数据写入cookie、让页面跳转到首页等
认证上下文持有
当认证完成后,会将认证主体的基本信息和权限等数据返回,而在后续的逻辑中可能会使用到这些数据,所以首先需要将这些数据进行存储,并提供后续逻辑可获取到正确数据的方法。
在Spring Security中会创建一个SecurityContext类型对象表示当前请求环境下的安全上下文。该Context包含两个方法:
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
显而易见其主要就是起到保存Authentication的作用。而SecurityContext的创建和保存是由具体的SecurityContextHolderStrategy实现的(也就是不同的策略可能使用到了不同的SecurityContext实现)。
以下类图展示了内置的一些策略实现,同时我们可通过SecurityContextHolder来访问正在使用的策略,简化编程。
- GlobalSecurityContextHolderStrategy:使用一个SecurityContext
类型的静态变量来保存当前的SecurityContext。这意味着 JVM 中的所有实例共享相同的一个SecurityContext。在web应用中基本不会用到。 - ThreadLocalSecurityContextHolderStrategy:使用ThreadLocal来存储SecurityContext,这意味着同一个线程在任何执行的地方都可以获取到。这也是最常用的策略,且是默认策略。
- InheritableThreadLocalSecurityContextHolderStrategy:使用InheritableThreadLocal来存储SecurityContext。InheritableThreadLocal扩展 ThreadLocal 以提供从父线程到子线程的值继承,这意味着子线程可以获取到父线程存储的SecurityContext
- ListeningSecurityContextHolderStrategy:用于在 SecurityContext 更改时发出通知的 API。其内部对Context的操作委托给了ThreadLocalSecurityContextHolderStrategy,自身专注事件通知的相关逻辑,在使用时传递实现了SecurityContextChangedListener接口的监听器,在Context变化时通知各个监听器
- SecurityContextHolder:初始化具体策略的地方,也是提供给开发人员快速设置和获取SecurityContext的工具。该Holder会根据系统参数spring.security.strategy的配置来初始化哪种策略,如果参数为空则默认使用ThreadLocalSecurityContextHolderStrategy。而其他使用到SecurityContextHolderStrategy的地方都是从SecurityContextHolder中获取的。
配置方法
通过系统参数spring.security.strategy
进行修改,默认为ThreadLocalSecurityContextHolderStrategy,可选的配置值有
- MODE_THREADLOCAL
- MODE_INHERITABLETHREADLOCAL
- MODE_GLOBAL
- MODE_PRE_INITIALIZED
认证上下文存储
上面SecurityContext的持有策略主要解决的是在代码执行的过程中可以方便的获取到上下文信息,从而简化代码,它不能跨请求。
而SecurityContext的存储是为了解决Http的每次请求都要重新获取用户信息的性能问题,具备跨请求能力。
- HttpSessionSecurityContextRepository:就是将SecurityContext保存在HttpSession中,显然该方法依赖会话的使用。
- RequestAttributeSecurityContextRepository:将SecurityContext保存在HttpServletRequest的Attribute中
- NullSecurityContextRepository:自身什么也不做,获取的时候从ContextHolderStrategy中获取
- DelegatingSecurityContextRepository:作为其他SecurityContextRepository的委托对象
建议大家根据需要自行实现SecurityContext的存储。在给SecurityContextHolder设值前也会尝试从SecurityContextRepository中获取SecurityContext实例
配置方法
默认为DelegatingSecurityContextRepository,里面包含了RequestAttributeSecurityContextRepository和HttpSessionSecurityContextRepository。
@Configuration
public class DemoConfiguration {
@Bean
public SecurityFilterChain configure(HttpSecurity http,
SecurityContextRepository repository) throws Exception {
//方法1
http.setSharedObject(SecurityContextRepository.class,repository);
//方法2:其实也是修改SharedObject的值
http.securityContext().securityContextRepository(repository) ;
}
}
记住我
无论是认证成功还是失败都会涉及”记住我“的操作。在Spring Security中主要由RememberMeServices负责,在RememberMeAuthenticationFilter过滤器中也会使用到。
记住我的实现主要依赖cookie,将需要记录的数据经过编码后存储到cookie,然后每次请求从cookie中恢复数据来自动完成登录。
其cookie中存储的内容可能是用户名和密码,也可能是对用户名创建成某种token(将token相应数据写入cookie,同时服务器端存储token)
- NullRememberMeServices
- TokenBasedRememberMeServices
- PersistentTokenBasedRememberMeServices
- PersistentTokenRepository
配置方法
@Configuration
public class DemoConfiguration {
@Bean
public SecurityFilterChain configure(HttpSecurity http,
RememberMeServices rememberMeServices) throws Exception {
//方法
http.rememberMe().rememberMeServices(rememberMeServices) ;
}
}
成功后续处理
上面讲到的上下文持有、上下文存储、记住我的操作都是认证后的操作中共性比较大的,后续的操作和业务的关系型会比较强些。
在Spring Security中定义了AuthenticationSuccessHandler接口来处理认证成功后的后续处理。
- ForwardAuthenticationSuccessHandler:提供一个forwardUrl,通过RequestDispatcher的forward方式实现跳转
- SimpleUrlAuthenticationSuccessHandler:从请求中获取要跳转的地址,并通过指定的RedirectStrategy策略来进行跳转。默认是DefaultRedirectStrategy,使用response的sendRedirect方法进行浏览器端的跳转。
- SavedRequestAwareAuthenticationSuccessHandler:继承自SimpleUrlAuthenticationSuccessHandler,从指定的RequestCache中获取存储的SavedRequest,并可从中获取要跳转的地址【默认】
- RedirectStrategy:重定向策略,默认实现为DefaultRedirectStrategy,使用response的sendRedirect方法进行浏览器端的跳转
现在有很多前后端分离的项目,这种项目在认证成功后往往都是返回token到前端,所以也会自定义AuthenticationSuccessHandler。
配置方法
1、继承AbstractAuthenticationProcessingFilter实现自己的认证过滤器,直接调用setAuthenticationSuccessHandler方法进行设置
2、使用内置的认证
@Configuration
public class DemoConfiguration {
@Bean
public SecurityFilterChain configure(HttpSecurity http,
AuthenticationSuccessHandler successHandler) throws Exception {
//方法:使用内置的用户名和密码验证时
http.formLogin().successHandler(successHandler);
}
}
4)认证失败后
认证失败后首先要做的就是清理掉SecurityContext,以免被错误的获取。其次就是对”记住我“的数据进行清理。
在AbstractAuthenticationProcessingFilter中通过执行使用的ContextHolderStrategy来清理SecurityContext,使用RememberMeServices来清理”记住我“的数据,然后执行AuthenticationFailureHandler的实现类来进行后续的错误逻辑处理。
AuthenticationFailureHandler的默认实现如下图所示
- AuthenticationEntryPointFailureHandler:由ExceptionTranslationFilter用于启动身份验证方案,具体哪种方案依赖于内部使用的AuthenticationEntryPoint,大体包含了响应状态码、跳转到指定的认证页面等
- SimpleUrlAuthenticationFailureHandler:跳转到指定的地址,通过RedirectStrategy的实现来执行跳转【默认】
- ExceptionMappingAuthenticationFailureHandler:继承自SimpleUrlAuthenticationFailureHandler,使用异常类型到URL的内部映射来确定身份验证失败时的目标
- ForwardAuthenticationFailureHandler:通过RequestDispatcher的forward来跳转到指定的forwardUrl
- DelegatingAuthenticationFailureHandler:它根据AuthenticationException参数的类型委托给其他AuthenticationFailureHandler实例
- AuthenticationEntryPoint
- Http403ForbiddenEntryPoint:调用response的sendError方法来响应403【Access Denied】
- LoginUrlAuthenticationEntryPoint:使用RedirectStrategy跳转到指定的认证页面
- HttpStatusEntryPoint:使用response的setStatus设置状态码
- BasicAuthenticationEntryPoint:由 ExceptionTranslationFilter 用于通过 BasicAuthenticationFilter 开始身份验证。其会设置WWW-Authenticate头部,并通过response的sendError方法进行响应(401)
- DigestAuthenticationEntryPoint:设置经过Digest处理的WWW-Authenticate响应头,并通过response的sendError方法进行响应(401)。 由SecurityEnforcementFilter用于通过 DigestAuthenticationFilter开始身份验证。
- DelegatingAuthenticationEntryPoint:根据RequestMatcher映射到具体AuthenticationEntryPoint的委托对象
配置方法
1、继承AbstractAuthenticationProcessingFilter实现自己的认证过滤器,直接调用setAuthenticationFailureHandler方法进行设置
2、使用内置的认证
@Configuration
public class DemoConfiguration {
@Bean
public SecurityFilterChain configure(HttpSecurity http,
AuthenticationFailureHandler failureHandler) throws Exception {
//方法:使用内置的用户名和密码验证时
http.formLogin().failureHandler(failureHandler);
}
}
上面对认证的整体逻辑进行了阐述,后面将对鉴权部分的逻辑进行说明。