一、Spring Security 简介
Spring Security 是一个强大、容易定制的、基于 Spring 开发的实现认证登录与资源授权的应用安全框架;
核心功能主要是:认证(你是谁)、授权(你能访问什么网页或接口)、安全防护(防止跨站攻击等)
Spring Security 与 Spring Boot 的集成做得很不错,不需要xml配置;
以下的解析将以 Spring Boot 为基础;以 UsernamePasswordAuthenticationFilter 过滤器为例;
官网:https://projects.spring.io/spring-security/
二、认证流程解析
1. 基本流程
如果要我们基于原生的 JavaEE 开发一个认证授权模块,肯定会想到可以用 filter 过滤器来实现。Spring Security 就是通过一个过滤器链来实现授权认证功能的;
1)上下文对象 SecurityContext 和认证主体对象 Authentication 贯穿了整个 Spring Security 认证流程;每个用户都会有他的上下文对象,这个上下文对象保存在 SecurityContextHolder 中;
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
可以看到 SecurityContext 接口只有两个方法,就是用来保存认证主体对象的,所以接下来我们看看 Authentication 接口:
public interface Authentication extends Principal, Serializable {
// 获取权限集合,用户都有哪些权限
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
// 是否已经通过认证
boolean isAuthenticated();
// 设置认证状态
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
关于 SecurityContextHolder,在多用户系统中,它将 SecurityContext 保存在 ThreadLocal 中,使得每个线程都有一个SecurityContext;
2)如果某一个主体通过了(上图第二个方块中的,如UsernamePasswordAuthenticationFilter等的)任意一个过滤器的认证,则其 Authentication 对象的 isAuthenticated 被设置为 true,表示认证成功;
3)如果一个过滤器也没认证成功,则 FilterSecurityInterceptor(虽然叫Interceptor,但其实也是Filter)会拦住它并抛出异常;如果认证成功则放行;
4)在响应阶段,ExceptionTransactionFilter 会根据配置处理 FilterSecurityInterceptor 抛出的异常,比如跳转到指定的登录页面,或返回失败响应;
5)如果登录成功,没有异常,则 SecurityContextPersistenceFilter 会将 SecurityContext 放入 session,下次直接取出即可;
不同的认证方式由不同的过滤器实现,如:BasicAuthenticationFilter 实现 http basic 认证,UsernamePasswordAuthenticationFilter 实现用户名密码表单认证;
2. 源码解析
现在我们以 UsernamePasswordAuthenticationFilter 提供的认证方式为例,跟随源码了解具体的认证流程(先跳过SecurityContextPersistentFilter):
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter
由于 UsernamePasswordAuthenticationFilter 是一个过滤器,所以在其父类 AbstractAuthenticationProcessingFilter 中找到 doFilter 方法如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断 request 访问的是否为登录接口,如果是则直接放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
// 认证主体
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);
}
观察 doFilter 方法:首先,requiresAuthentication() 方法判断访问的是否为登陆接口,若是则放行,追踪该方法可以发现,在 UsernamePasswordAuthenticationFilter 的构造函数指定了默认登录接口为 "/login",方式为"post",如下:
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
(除了登陆接口,其它的不需要认证的接口是怎么被放行的呢?这是后面的 AnonymousAuthenticationFilter 的工作,暂且不谈)
然后,通过 authResult = attemptAuthentication(request, response); 尝试认证, 其代码如下(是在子类中实现的):
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);
}
attemptAuthentication 方法首先从 request 中获取用户名和密码,然后实例化了一个认证凭证 UsernamePasswordAuthenticationToken 对象:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
......
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal; // 用户名
this.credentials = credentials; // 密码
setAuthenticated(false);
}
......
}
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {...}
可以看到 UsernamePasswordAuthenticationToken 实现了 Authentication 接口;
setDetails 方法将 UsernamePasswordAuthenticationToken 的 "private Object details" 字段设置为一个 WebAuthenticationDetails对象,这个对象包含一个 remoteAddress 和一个 sessionID;
最后通过 return this.getAuthenticationManager().authenticate(authRequest); 对Token进行验证,返回认证后的 Authentication 对象;
现在,看 AuthenticationManager 是如何对 UsernamePasswordAuthenticationToken 进行认证的:
首先,AuthenticationManager 是一个接口,只有一个 authenticate 方法用来作认证,如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
ProviderManager 实现了 AuthenticaionManager:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
......
private List<AuthenticationProvider> providers = Collections.emptyList();
......
}
ProviderManager 保管了一个 AuthenticationProvider 列表,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体;
ProviderManager 对 authenticate 方法的实现的主要代码如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = 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;
}
}
......
}
用一个for循环,让 AuthenticationProvider 们分别去看自己支不支持认证这个 Authentication 对象,如果支持就尝试认证;
AuthenticationProvider 的接口如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
那么是哪个 AuthenticationProvider 实现类负责认证 UsernamePasswordAuthenticationToken 呢?在 idea 编辑器中,按住 ctrl + alt 并点击 AuthenticationProvider 可以看到它的各种实现类,其中的 DaoAuthenticationProvider 专门负责认证此类 token;
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private PasswordEncoder passwordEncoder; // 密码一般是加密存在数据库中的
......
}
在其父类中可以找到其对 supports 的实现:
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken
authentication) throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser =
this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract
violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
可以看到在其 retrieveUser 方法中,通过 DetailService 调用 loadUserByUsername 方法,从数据库获取用户信息,所以我们需要实现这个 DetailService 接口,重写 loadUserByUsername 方法,其返回的 UserDetails 如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
其中保存了用户的用户名、密码、权限、是否被锁定等信息;