spring security 主要有两大功能,即认证和授权
一、Spring Security 认证介绍
总体认证过程:
1、用户使用用户名和密码进行登录。
2、Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
3、将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
4、AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象,如果没有认证成功抛出AuthenticationException异常,由ExceptionTranslationFilter获取,将调用 AuthenticationEntryPoint 引导用户重新登录,重新执行1的步骤,如果认证成功继续往下。
5、通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。
在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。权限鉴定通过AuthorizationFilter,有权限则能够访问资源。如果用户没有权限访问当前资源会抛出 AccessDeniedException,异常会被ExceptionTranslationFilter处理,返回一个表示不存在对应权限的 403 错误码。
Web 应用的认证过程:
如果用户直接访问登录页面,那么认证过程跟上述描述的基本一致,只是在认证完成后将跳转到指定的成功页面,默认是应用的根路径。如果用户直接访问一个受保护的资源,那么认证过程将如下:
1、AuthorizationFilter进行判断是否具有权限,有权限直接访问。如果没有权限抛出AccessDeniedException异常,由ExceptionTranslationFilter进行处理,判断用户有没有通过认证,如果已经通过认证,继续执行4的步骤。没有则调用 AuthenticationEntryPoint 引导用户进行登录认证,引导用户进行登录,通常是重定向到一个基于 form 表单进行登录的页面,具体视配置而定。
2、用户输入用户名和密码后请求认证,后台还是会像上节描述的那样获取用户名和密码封装成一个 UsernamePasswordAuthenticationToken 对象,然后把它传递给 AuthenticationManager 进行认证。
3、如果认证成功则会保存返回的 Authentication 到 SecurityContext,然后默认会将用户重定向到之前访问的页面,继续下面的步骤。如果认证失败将抛出AuthenticationException 异常由ExceptionTranslationFilter获取进行处理,将调用 AuthenticationEntryPoint 引导用户重新登录,继续2的步骤。
4、用户登录认证成功后再次访问之前受保护的资源时就会对用户进行权限鉴定,主要通过AuthorizationFilter,如不存在对应的访问权限,则会抛出AccessDeniedException异常,异常会被ExceptionTranslationFilter处理,返回一个表示不存在对应权限的 403 错误码。
在上述步骤中主要的参与的类是 ExceptionTranslationFilter,这里着重介绍下。
AuthorizationFilter
他的主要作用是判断当前的用户用没有权限可以访问目标资源。
如果用户没有权限访问当前资源 会抛出 AccessDeniedException。异常会被ExceptionTranslationFilter处理。
ExceptionTranslationFilter:
ExceptionTranslationFilter 是用来处理来自 AbstractSecurityInterceptor 抛出的 AuthenticationException 和 AccessDeniedException 的。
AbstractSecurityInterceptor 是 Spring Security 用于拦截请求进行权限鉴定的,其拥有两个具体的子类,拦截方法调用的 MethodSecurityInterceptor 和拦截 URL 请求的 FilterSecurityInterceptor。
当 ExceptionTranslationFilter 捕获到的是 AuthenticationException 时,将调用 AuthenticationEntryPoint 引导用户进行登录;如果捕获的是 AccessDeniedException,但是用户还没有通过认证,则调用 AuthenticationEntryPoint 引导用户进行登录认证,否则将返回一个表示不存在对应权限的 403 错误码。
在 request 之间共享 SecurityContext是如何实现的:
既然 SecurityContext 是存放在 ThreadLocal 中的,而且在每次权限鉴定的时候都是从 ThreadLocal 中获取 SecurityContext 中对应的 Authentication 所拥有的权限,并且不同的 request 是不同的线程,为什么每次都可以从 ThreadLocal 中获取到当前用户对应的 SecurityContext 呢?
在 Web 应用中这是通过 SecurityContextPersistentFilter 实现的,默认情况下其会在每次请求开始的时候从 session 中获取 SecurityContext,然后把它设置给 SecurityContextHolder,在请求结束后又会将 SecurityContextHolder 所持有的 SecurityContext 保存在 session 中,并且清除 SecurityContextHolder 所持有的 SecurityContext。
这样当我们第一次访问系统的时候,SecurityContextHolder 所持有的 SecurityContext 肯定是空的,待我们登录成功后,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有认证成功的 Authentication 对象,待请求结束后我们就会将 SecurityContext 存在 session 中,等到下次请求的时候就可以从 session 中获取到该 SecurityContext 并把它赋予给 SecurityContextHolder 了,由于 SecurityContextHolder 已经持有认证过的 Authentication 对象了,所以下次访问的时候也就不再需要进行登录认证了。
二、从源码角度分析认证过程:
认证过程的整体流程图如下:
下面依次介绍:
AbstractAuthenticationProcessingFilter 抽象类
/**
* 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。
* 如果需要验证,则会调用 #attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法。
* 有三种结果:
* 1、返回一个 Authentication 对象。
* 配置的 SessionAuthenticationStrategy` 将被调用,
* 然后 然后调用 #successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
* 2、验证时发生 AuthenticationException。
* #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法将被调用。
* 3、返回Null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。
* 假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
*
*/
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
attemptAuthentication () 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证(即 this.getAuthenticationManager().authenticate(authRequest) )。
默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。
ProviderManager(AuthenticationManager的实现类)
/**
* 尝试验证 Authentication 对象
* AuthenticationProvider 列表将被连续尝试,直到 AuthenticationProvider 表示它能够认证传递的过来的Authentication 对象。然后将使用该 AuthenticationProvider 尝试身份验证。
* 如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。
* 如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
从代码中不难看出,由 provider 来验证 authentication, 核心点方法是:
Authentication result = provider.authenticate(authentication);
此处的 provider 是 AbstractUserDetailsAuthenticationProvider, AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的实现,看看它的 #authenticate(authentication) 方法:
//验证 authentication
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//根据用户输入的用户名,从数据库中查询出用户实体对象
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
//校验用户输入的用户名和密码,与查询出的用户实体对象用户名密码是否一致
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
上述代码中:
1、首先看下this.retrieveUser方法,该方法是根据用户输入的用户名,从数据库中查询出用户实体对象。
AbstractUserDetailsAuthenticationProvider 内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用如下方法获取用户信息:
// 获取用户信息
UserDetails user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
retrieveUser() 方法在 DaoAuthenticationProvider 中实现,DaoAuthenticationProvider 是AbstractUserDetailsAuthenticationProvider 的子类。具体实现如下:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
//根据用户名,获取用户信息
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
} catch (UsernameNotFoundException var6) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
}
throw var6;
} catch (Exception var7) {
throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
}
2、再看下this.additionalAuthenticationChecks方法,该方法是把1中查询出的用户信息与用户输入的信息(用户名和密码)进行比对判断是否验证成功。retrieveUser() 方法也是在 DaoAuthenticationProvider 中实现。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}