流程源码
核心组件
1、SecurityContextHolder:提供对 SecurityContext 的访问
2、SecurityContext:持有 Authentication 对象和其他可能需要的信息
3、AuthenticationManager:其中可以包含多个 AuthenticationProvider
4、ProviderManager:为 AuthenticationManager 接口的实现类
5、AuthenticationProvider:主要用来进行认证操作的类,调用其中的 authenticate() 方法去进行认证操作
6、Authentication:Spring Security 方式的认证主体,即认证信息封装类
7、GranteAuthority:对认证主体的应用层面的授权,含当前用户的权限信息,通常使用角色表示
8、UserDetails:构建 Authentication 对象的必须信息,可以自定义,可能需要访问数据库得到
9、UserDetailsService:通过 username 构建 UserDetails 对象,根据 username,通过 loadUserByUsername 获取 UserDetails 对象,可以基于自身业务进行自定义的实现,如:通过数据库,xml,缓存获取等
10、WebSecurityConfigurerAdapter(已弃用):自定义 Spring Security 安全框架的配置类,需要继承 WebSecurityConfigurerAdapter,重写其中的方法 configure
认证流程
1、第一次请求:UsernamePasswordAuthenticationFilter,Authentication(未认证)
2、AuthenticationManager(接口实现类:ProviderManager)管理多个 AuthenticationProvider,并委托对应的 AuthenticationProvider(接口实现类:DaoAuthenticationProvider),关联 UserDetailsService
3、查询数据库,返回 UserDetails
4、认证通过后,封装查询数据到 Authentication(已认证)
5、Authentication(已认证)封装到 SecurityContext(接口实现类:SecurityContextImpl)
6、SecurityContext 存入到 SecurityContextHolder,使用 SecurityContextPersistenceFilter(已弃用,使用 SecurityContextHolderFilter 代替)返回响应
UsernamePasswordAuthenticationFilter
1、父类:AbstractAuthenticationProcessingFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
2、调用父类 doFilter 方法
@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 {
//判断是否为POST请求
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//调用子类UsernamePasswordAuthenticationFilter的attemptAuthentication方法进行认证(即数据库查询用户密码),认证成功后,把认证信息封装到Authentication对象中
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
//策略处理,配置最大并发数
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
//认证成功,continueChainBeforeSuccessfulAuthentication == true,默认为false
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功,执行successfulAuthentication
successfulAuthentication(request, response, chain, authenticationResult);
}
//捕获异常,由异常处理器处理失败,认证失败,执行unsuccessfulAuthentication
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);
}
}
3、调用 attemptAuthentication 方法,进行认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//判断是否为POST请求,若是,继续执行,否则抛出异常
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取表单数据
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
//创建用户名密码认证令牌,标记为未认证状态
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
//允许子类将请求中的details属性设置给对象
setDetails(request, authRequest);
//调用authenticate方法进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
4、UsernamePasswordAuthenticationToken 的两个构造器,通过 this.setAuthenticated() 设置认证状态,true 表示认证成功,false 表示没认证
//这个构造函数可以被任何希望创建UsernamePasswordAuthenticationToken的代码安全使用,因为isAuthenticated()将返回false
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
//这个构造函数只能由AuthenticationManager或AuthenticationProvider实现使用,它们满足于产生一个可信的(即isAuthenticated() = true)认证令牌
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
5、ProviderManager
(1)内部维护一个列表,存放多种认证方式(委托者模式),每种认证方式对应一个 AuthenticationProvider
private List<AuthenticationProvider> providers = Collections.emptyList();
(2)authenticate 认证方法
@Override
//传入未认证的Authentication对象
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//通过反射,获取传入Authentication类型,即UsernamePasswordAuthenticationToken
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//遍历List<AuthenticationProvider> providers
for (AuthenticationProvider provider : getProviders()) {
//判断当前AuthenticationProvider是否支持UsernamePasswordAuthenticationToken类型
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
//成功找到适配当前认证方式的AuthenticationProvider,此处为DaoAuthenticationProvider
try {
//调用AuthenticationProvider的authenticate进行认证
result = provider.authenticate(authentication);
//认证成功,返回的result为已标记认证Authentication对象
if (result != null) {
//认证成功后,将传入的Authentication对象中的details信息,拷贝到已认证的Authentication对象
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
//SEC-546:如果认证失败是由于无效的账户状态,应避免轮询其他provider
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
//认证失败,使用父类AuthenticationManager进行验证
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
//认证完成,从reslut中删除凭证和其他秘密数据,要求相关类实现CredentialsContainer接口
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
//CredentialsContainer接口的移除方法eraseCredentials
((CredentialsContainer) result).eraseCredentials();
}
//发布认证成功的事件
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
//认证失败后,抛出的失败异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
6、认证成功:调用父类 successfulAuthentication 方法
/*
成功认证的默认行为
1、在SecurityContextHolder上设置成功的Authentication对象
2、向配置的RememberMeServices通报成功登录的情况
3、通过配置的ApplicationEventPublisher发射一个InteractiveAuthenticationSuccessEvent,将其他行为委托给AuthenticationSuccessHandler
子类可以重写此方法,以便在成功认证后继续使用FilterChain
形参:
request
response
chain
authResult - 从 attemptAuthentication 方法返回的对象
抛出:
IOException - ServletException
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
//对认证成功的用户对象,封装到SecurityContext
context.setAuthentication(authResult);
//SecurityContext封装到SecurityContextHolder
SecurityContextHolder.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);
}
7、认证失败:调用父类 unsuccessfulAuthentication 方法
/*
对认证失败的默认行为
1、清除SecurityContextHolder
2、将异常存储在会话中(如果它存在或allowSesssionCreation被设置为true)
3、通知配置的RememberMeServices登录失败的情况
4、将其他行为委托给AuthenticationFailureHandler
*/
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.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);
}
权限访问流程
1、ExceptionTranslationFilter
(1)处理异常的过滤器
(2)doFilter 方法
@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 {
try {
//对前端异常,直接放行
chain.doFilter(request, response);
}
//对后端异常,进行相应的处理
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
2、FilterSecurityInterceptor
(1)过滤链中的最后一个过滤器,根据资源权限,判断当前请求是否有权限访问资源
(2)doFilter 方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
(3)invoke 方法
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
//过滤器已经应用于这个请求,并且用户希望观察每一次请求的处理,所以不需要重新进行安全检查
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
//第一次调用这个请求,所以要进行安全检查
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//根据资源权限,判断当前请求是否有权限访问资源
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
请求间认证信息共享
1、成功认证方法
/*
成功认证的默认行为
1、在SecurityContextHolder上设置成功的Authentication对象
2、向配置的RememberMeServices通报成功登录的情况
3、通过配置的ApplicationEventPublisher发射一个InteractiveAuthenticationSuccessEvent,将其他行为委托给AuthenticationSuccessHandler
子类可以重写此方法,以便在成功认证后继续使用FilterChain
形参:
request
response
chain
authResult - 从 attemptAuthentication 方法返回的对象
抛出:
IOException - ServletException
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
//SecurityContext封装认证成功的Authentication对象
context.setAuthentication(authResult);
//SecurityContext存入SecurityContextHolder
SecurityContextHolder.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);
}
2、SecurityContextHolder
(1)使用 ThreadLocal 进行操作,与当前线程做绑定
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
private static void initializeStrategy() {
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
return;
}
if (!StringUtils.hasText(strategyName)) {
//设置默认策略
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
return;
}
if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
return;
}
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
(2)getContext 方法:若当前线程存在 Context,则返回;若不存在,则创建
3、SecurityContextPermissionFilter
(1)最开始的过滤器,已弃用,代替:SecurityContextHolderFilter
(2)doFilter 方法:将 Authentication 和 Session 进行绑定
@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 (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//判断Session是否存在认证信息,如果有认证信息,则取出SecurityContext对象,如果没有,则创建SecurityContext对象
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
//放行执行其他过滤器
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//获取SecurityContextHolder的SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//在执行其他操作之前,必须先删除SecurityContextHolder中的SecurityContext
SecurityContextHolder.clearContext();
//先前获取的SecurityContext再放入Session
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战