前言

用户登录前,默认生成的Authentication对象处于未认证状态,登录时会交由Authentication Manager负责进行认证。 AuthenticationManager会将Authentication中的用户名/密码与UserDetails中的用户名/密码对比,完成认证工作,认证成功后会生成一个已认证状态的Authentication对象; 最后把认证通过的Authentication对象写入到SecurityContext中,在用户有后续请求时,可从Authentication中检查权限。
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
//模拟输入用户名密码
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
//根据用户名/密码,生成未认证Authentication
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
//交给AuthenticationManager 认证
Authentication result = am.authenticate(request);
//将已认证的Authentication放入SecurityContext
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch (AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: "
+ SecurityContextHolder.getContext().getAuthentication());
}
}
//认证类
class SampleAuthenticationManager implements AuthenticationManager {
//配置一个简单的用户权限集合
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
//如果用户名和密码一致,则登录成功,这里只做了简单认证
if (auth.getName().equals(auth.getCredentials())) {
//认证成功,生成已认证Authentication,比未认证多了权限
return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}



WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter:如果之前的认证机制都没有更新 Security Context Holder 拥有的 Authentication,那么一个 Anonymous Authen tication Token 将会设给 Security Context Holder。
SessionManagementFilter
ExceptionTranslationFilter:用于处理在 Filter Chain 范围内抛出的 Access Denied Exception 和 Authentication Exception,并把它们转换为对应的 Http 错误码返回或者跳转到对应的页面。
FilterSecurityInterceptor:负责保护 Web URI,并且在访问被拒绝时抛出异常。
2. SecurityContextPersistenceFilter
在上面的过滤器链中,我们可以看到 Security Context PersistenceFilter这个过滤器。Security Context Persistence Filter是Security中的一个拦截器,它的执行时机非常早,当请求来临时它会从Security Context Repository中把SecurityContext对象取出来,然后放入Security Context Holder的Thread Local中。在所有拦截器都处理完成后,再把Security Context存入Security Context Repository,并清除Security Context Holder 内的 Security Context 引用。



requiresAuthentication(request, response);
authResult = attemptAuthentication(request, response);
sessionStrategy.onAuthentication(authResult, request, response);
unsuccessfulAuthentication(request, response, failed);
successfulAuthentication(request, response, chain, authResult);
首先执行 requiresAuthentication(HttpServletRequest, Http Servlet Response) 方法,来决定是否需要进行验证操作; 如果需要验证,接着就会调用 attempt Authentication(Http Servlet Request, Http Servlet Response) 方法来封装用户信息再进行验证,可能会有三种结果产生:
如果返回的Authentication对象为Null,则表示身份验证不完整,该方法将立即结束返回。 如果返回的Authentication对象不为空,则调用配置的 SessionAuthenticationStrategy 对象,执行onAuthentication()方法,然后调用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。 验证时如果发生 AuthenticationException,则执行unsuccessful Authentication (HttpServletRequest, HttpServletResponse, Authentication Exception) 方法。



4. UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
...
...
// ~ Constructors
// ===================================================================================================
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
......其他略......
}
我来解释一下上面的源码:
首先我们从构造方法中可以得知,该过滤器 只对post请求方式的"/login"接口有效; 然后在该过滤器中,再利用 obtainUsername 和 obtainPassword 方法,提取出请求里边的用户名/密码,提取方式就是 request.getParameter,这也是为什么 Spring Security 中默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数。如果像传递 JSON 参数,我们可以通过修改这里的代码来进行实现。
获取到请求里传递来的用户名/密码之后,接下来会 构造一个 Username Password AuthenticationToken 对象,传入 username 和 password。其中 username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。 接下来 再利用 setDetails 方法给 details 属性赋值,UsernamePasswordAuthenticationToken 本身是没有 details 属性的,这个属性是在它的父类 AbstractAuthenticationToken 中定义的。details 是一个对象,这个对象里边存放的是 Web Authentication Details 实例,该实例主要描述了 请求的 remote Address 以及请求的 sessionId 这两个信息。 最后一步,就是利用AuthenticationManager对象来调用 authenticate() 方法去做认证校验。
5. AuthenticationManager与ProviderManager
咱们在上面 Username Password Authentication Token 类的 attempt Authentication() 方法中得知,该方法的最后一步会进行关于认证的校验,而要进行认证操作首先要获取到一个 Authentication Manager 对象,这里默认拿到的是Authentication Manager的子类Provider Manager ,如下图所示:







首先利用反射,获取到要认证的 authentication 对象的 Class字节码,如下图: 判断当前 provider 是否支持该 authentication 对象,如下图: 如果当前provider不支持该 authentication 对象,则退出当前判断,进行下一次判断。
如果支持,则调用 provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication,如下图: 这里的 provider 会有多个,我们在上一章节给大家介绍过,如下图:


而如果通过了校验,返回了一个Authentication 认证对象,则调用 copyDetails()方法把旧 Token 的 details 属性拷贝到新的 Token 中,如下图。

接下来会调用 eraseCredentials()方法来擦除凭证信息,也就是我们的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空。


最后通过 publishAuthenticationSuccess() 方法将认证成功的事件广播出去。
6. DaoAuthenticationProvider

public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
//获取authentication中存储的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//判断是否使用了缓存
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//retrieveUser()是一个抽象方法,由子类DaoAuthenticationProvider来实现,用于根据用户名查询用户。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
......
}
try {
//进行必要的认证前和额外认证的检查
preAuthenticationChecks.check(user);
//这是抽象方法,由子类DaoAuthenticationProvider来实现,用于进行密码对比
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
//在发生异常时,尝试着从缓存中进行对象的加载
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
//认证后的检查操作
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//认证成功后,封装认证对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
结合上面的源码,我们再做进一步的分析梳理:
我在上面说过,DaoAuthenticationProvider这个子类并没有重写authenticate()方法,而是在父类AbstractUserDetailsAuthenticationProvider中实现的。Abstract User Details Authentication Provider 类中的 authenticate()方法执行时,首先会从 Authentication 提取出登录用户名,如下图所示:

然后利用得到的 username,先去缓存中查询是否有该用户,如下所示:

如果缓存中没有该用户,则去执行 retrieveUser() 方法获取当前用户对象。而这个retrieveUser()方法是个抽象方法,在Abstract User Details Authentication Provider类中并没有实现,是由子类DaoAuthenticationProvider来实现的。

在DaoAuthenticationProvider类的retrieveUser() 方法中,会调用get User Details Service()方法,得到UserDetailsService对象,执行我们自己在登录时候编写的 loadUserByUsername()方法,然后返回一个UserDetails对象,也就是我们的登录对象。如下图所示。

接下来会继续往下执行preAuthenticationChecks.check()方法,检验 user 中各账户属性是否正常,例如账户是否被禁用、是否被锁定、是否过期等,如下所示。


接着会继续往下执行additionalAuthenticationChecks()方法,进行密码比对。而该方法也是抽象方法,也是由子类DaoAuthenticationProvider进行实现。我们在注册用户时对密码加密之后,Spring Security就是在这里进行密码比对的。如下所示。


然后在 postAuthenticationChecks.check()方法中检查密码是否过期,如下所示。


然后判断是否进行了缓存,如果未进行缓存,则执行缓存操作,这个缓存是由 SpringCacheBasedUserCache类来实现的。 我们这里如果没有对缓存做配置,则会执行默认的缓存配置操作。如果我们对缓存进行了自定义的配置,比如配置了RedisCache,就可以把对象缓存到redis中。
接下来有一个 forcePrincipalAsString 属性,该属性表示 是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性其实我们一开始就在 UsernamePasswordAuthenticationFilter 类中定义为了字符串(即username)。但是默认情况下,当用户登录成功之后,这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。 最后通过createSuccessAuthentication()方法构建出一个新的 Username Password Authentication Token对象。


这样我们最终得到了认证通过的Authentication对象,并把该对象利用publish Authentication Success()方法,将该事件发布出去。

Spring Security会监听这个事件,接收到这个Authentication对象,进而调用 SecurityContextHolder.getContext().setAuthentication(...)方法,将 Authentication Manager返回的 Authentication对象,存储在当前的 Security Context 对象中。
7. 保存Authentication认证信息
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
......
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//处理认证后的操作
successfulAuthentication(request, response, chain, authResult);
}


8. ExceptionTranslationFilter
9. FilterSecurityInterceptor
注:
AuthenticationEntryPoint 是在用户未登录时,用于引导用户进行登录认证的; AccessDeniedHandler 是在用户已经登录后,但是访问了自身没有权限的资源时做出的对应处理。
10. 认证流程总结
首先用户在登录表单中,填写用户名和密码,进行登录操作; AbstractAuthenticationProcessingFilter结合UsernamePasswordAuthenticationToken过滤器,将获取到的用户名和密码封装成一个实现了 Authentication 接口的实现子类 UsernamePasswordAuthenticationToken对象。 将上述产生的 token 对象传递给 AuthenticationManager的具体子类ProviderManager 进行登录认证。 ProviderManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 Security Context。
我们可以结合下面两图,和上面的源码,深入理解掌握Spring Security的认证授权流程。


四. 相关面试题
在 Web 应用中这是通过 SecurityContextPersistentFilter 实现的,默认情况下其在每次请求开始的时候,都会从 session 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。 在请求结束后又会将 SecurityContextHolder 所持有的 SecurityContext 保存在 session 中,并且清除 SecurityContextHolder 所持有的 SecurityContext。 这样当我们第一次访问系统的时候,SecurityContextHolder 所持有的 Security Context 肯定是空的。待我们登录成功后,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有认证成功的 Authentication 对象。 待请求结束后我们就会将 SecurityContext 存在 session 中,等到下次请求的时候就可以从 session 中获取到该 SecurityContext 并把它赋予给 Security Context Holder 了。 由于 SecurityContextHolder 已经持有认证过的 Authentication 对象了,所以下次访问的时候也就不再需要进行登录认证了。

文章转载自Java架构栈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。