《 Spring Security 源码解析 (一)》
1 简介
最近一个项目,使用了试下流行的Spring Cloud 微服务架构,使用Spring Cloud oauth2.0 协议搭建访问授权器。由于项目体量较小,不对外提供授权服务,故采用oauth协议的密码模式来做令牌授权。项目中深深体会到了spring Security的强大,写下此篇博客来记录所学到的知识,希望对大家建立spring Security的体系能产生帮助。鉴于本人水平所限,有错误不足之处,望各位指正。
spring security 核心组件
2.1 SecurityContextPersistenceFilter
spring 使用过滤链对url进行拦截,在过滤链的顶端是SecurityContextPersistenceFilter,这个过滤器的作用:用户在登录过后,后续的访问通过sessionid来识别,当如今微服务流行时,前后端传送是不传输seesion的,但过滤器还是可以帮我们清除上下文的一些信息。具体的代码:
//执行过滤链
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
//获取请求和响应
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (request.getAttribute("__spring_security_scpf_applied") != null) {
chain.doFilter(request, response);
} else {
boolean debug = this.logger.isDebugEnabled();
request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
this.logger.debug("Eagerly created session: " + session.getId());
}
}
//包装request 和response
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//从安全上下文长裤中获取SecurityContext容器
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var13 = false;
try {
var13 = true;
// 获取安全响应上下文
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
var13 = false;
} finally {
if (var13) {
//最终清除安全上下文信息
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
从源码中可以看出安全上下文的信息是存在SecurityContextHolder这个容器当中的,当服务请求结束的时候,SecurityContextHolder清除了session信息。过滤器一般负责核心处理,具体业务,交给其他类实现,在spring的设计中我们可以看到很多诸如此类的设计除了SecurityContextPersistenceFilter,在spring Security的一堆过滤器链中,在学习中我们应该举一反三,这样有利于我们对源码的学习,也能提高自己的代码能力。还有一些很重要的filter,比如AbstractAuthenticationProcessingFilter,以及它的子类SecurityContextPersistenceFilter等等都是很总要的过滤器,我们将重点对AbstractAuthenticationProcessingFilter进行讲解
2.2 SecurityContextHolder
SecurityContextHolder用于存储安全上下文(SecurityContext)信息。当前用户信息都被保存到SecurityContextHolder中,其实是保存到TContextHolderStrategy的实现类中,顾名思义,SecurityContextHolder绑定了本地线程,在用户退出后, SecurityContextPersistenceFilter会qing清除Context信息。我们来看源码:
public class SecurityContextHolder {
//存储策略
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty("spring.security.strategy");
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
public SecurityContextHolder() {
}
//清除上下文
public static void clearContext() {
strategy.clearContext();
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
//创建真正的安全上下文容器
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
++initializeCount;
}
//....代码省略
// 初始化
static {
initialize();
}
}
从代码中我们也可以看出,SecurityContextHolder就是这么和线程关联的,并提供了clear的方法,方便进程结束后对安全上线文的清除。写在这里我们也可以仿照SecurityContextHolder的策略来实现我们的业务逻辑:
@Slf4j
@Service
@Lazy(false)
public class TenantContextHolder implements ApplicationContextAware, DisposableBean {
private static ThreadLocal<String> tenantThreadLocal= new ThreadLocal<>();
private static ApplicationContext applicationContext =null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
TenantContextHolder.applicationContext =applicationContext;
}
public static final void setTenant(String schema){
tenantThreadLocal.set(schema);
}
public static final String getTenant(){
String schema = tenantThreadLocal.get();
if(schema == null){
schema = "";
}
return schema;
}
@Override
public void destroy() throws Exception {
TenantContextHolder.clearHolder();
}
public static void clearHolder() {
if (log.isDebugEnabled()) {
log.debug("清除TenantContextHolder中的ApplicationContext:" + applicationContext);
}
applicationContext = null;
}
}
2.3 AbstractAuthenticationProcessingFilter
从名字也可以看出这是一个抽象类,这是一个很重要的类,用户权限校验都与此类有关,请求进入filter会执行doFilter()方法,首先判断是否能够处理当前请求,如果是则调用子类的attemptAuthentication()方法进行验证,在这个抽象类中,引入了统一认证管理AuthenticationManager ,登录成功AuthenticationSuccessHandler等等,登录成功后,又定义了登录成功后的方法,将封装的Authentication信息封装到SecurityContextHolder中,这个类中业务逻辑很多,我这里代码没有贴全,希望朋友们自己能够阅读源码,了解这个类的核心。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
//事件发布器
protected ApplicationEventPublisher eventPublisher;
//核心几口 所有的权限校验都由它进行统一管理
private AuthenticationManager authenticationManager;
//记住我
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
//登录成功处理
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
//失败处理
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
// 代码省略.........
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
//判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理
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);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
//权限校验的方法
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
AbstractAuthenticaitonProcessingFilter有很4个子类,比如:UsernamepasswordProcessingFiter,OAuth2ClientAuthenticationFilter,他们都实现了**attemptAuthentication**方法,我们来看:
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";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
//在webconfig里,配置http.login 并且路径为/login、方法为post 就会被这个拦截器拦截
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
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 = "";
}
// 注意 划重点
//获取用户名和密码 并将用户名和密码封装成一个UsernamePasswordAuthenticationToken这么个token
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//将校验交由AuthenticationManger的authenticate方法去执行
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2.4 AuthenticationManager
上文我们提到,所有的权限校验都是由AuthenticationManager去完成的 那么我们去看看它的源码
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
我们看到AuthenticationManager是一个接口,所有的校验都是由他的实现类去完成的,AuthenticationManager制作统一的管理,这里我们就要开动脑筋想想,这么设计的用意是什么,这里我留着空间让大家思考,废话不多说我们来看看他的两个核心实现类OAuth2AuthenticationManager、ProviderManager ,每个都为我们提供了校验接入点,接下来我们就说说ProviderManager,OAuth2AuthenticationManager则放到Oauth相关章节去讲
2.5 ProviderManager
我们知道,用户校验的话有很多种,比如说用户名+密码、手机号+验证码、邮箱+密码、social登录等等,那么ProviderManager是怎么知道我们是哪一种请求呢?我们继续来看看源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 代码省略 ....
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager)null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
this.eventPublisher = new ProviderManager.NullEventPublisher();
this.providers = Collections.emptyList();
this.messages = SpringSecurityMessageSource.getAccessor();
this.eraseCredentialsAfterAuthentication = true;
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
this.checkState();
}
//权限校验
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取当前认证的类型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//获取迭代器
Iterator var6 = this.getProviders().iterator();
//循环获取 这里和4.x版本有所不同个 有兴趣的可以看看4.x的版本
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
//通过当前类型获取到认证信息且不为null 则停止循环
if (result != null) {
将result装换成Authentication信息
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var11) {
this.prepareException(var11, authentication);
throw var11;
} catch (InternalAuthenticationServiceException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (AuthenticationException var13) {
lastException = var13;
}
}
}
//如果结果为空 则调用父类的authenticate方法 这里可以理解成递归
if (result == null && this.parent != null) {
try {
result = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var9) {
;
} catch (AuthenticationException var10) {
lastException = var10;
}
}
//获取到结果
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
//移除密码
((CredentialsContainer)result).eraseCredentials();
}
//发布验证成功事件 并返回结果
this.eventPublisher.publishAuthenticationSuccess(result);
return result;
} else {
//执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
this.prepareException((AuthenticationException)lastException, authentication);
throw lastException;
}
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
// .................
}
看到这里的话,我想刚才留的思考就可以解答了,当获取的认证信息和Providers不相符的时候,就调用父类AuthenticationManger的authenticate方法 ,与下个provider进行匹配,通俗来说,当前台是已邮箱加密码登录的,首先 Iterator 获取的可能是usernamepassword模式的,那么查出结果是空,就调用父类的方法,又走了一遍子类实现,又继续认证,直到认证成功,将返回的 result 既 Authentication 对象进一步封装为 Authentication Token,比如usernamepasswordtoken 、remembermetoken等等。
2.6 Authentication
上文我们一直有提到过Authentication ,那么我们来看一下Authentication具体是什么
public interface Authentication extends Principal, Serializable {
//权限信息集合
Collection<? extends GrantedAuthority> getAuthorities();
//获取凭证
Object getCredentials();
//获取详情
Object getDetails();
//获取当前用户
Object getPrincipal();
//是否认证
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
认证授权信息都会封装到这里,比如我们用户登录后 将用户名和密码封装成UsernamePasswordToken 就是实现了这个接口,它封装了用户权限信息,以及对象信息,是整个框架的核心类
2.7 UserDetailsService和 UserDetails
这也是Spring Security 最重要的接口,通过该接口的loadUserByUsername()方法返回一个UserDetails对象,那么这个对象是什么呢?
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
我们看到UserDetailsService就定义了一个接口,那么想必它也有很多适用不同业务功能的实现类,我们稍后再看,先瞧瞧UserDetails这个类
public interface UserDetails extends Serializable {
//权限集合
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
//是否过期
boolean isAccountNonExpired();
//是被锁
boolean isAccountNonLocked();
//凭证(密码)是否失效
boolean isCredentialsNonExpired();
//用户是否可用(是否删除)
boolean isEnabled();
}
是不是和Authentication很像呢!那为什么要定义两个呢?这里依旧留一个疑问。我们再来看UserDetails,还是个接口,那么我们来看他的子类实现
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 500L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
// 代码省略.............
}
当我们使用用户名密码登录的时候,从数据库中查到的用户名和密码都会封装到这里
2.8DaoAuthenticationProvider
用户登录后将用户登录提交信息封装成了Authentication对象,那么我们如何从数据库中获取用户名和密码来进行校验,如何进行密码加解密。这里我们就要说说DaoAuthenticationProvider了:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
//问价加解密处理
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
//注入UserDetailsService 调用子类的loadUserByUsername方法 获得UserDetails对象
private UserDetailsService userDetailsService;
public DaoAuthenticationProvider() {
//密码处理
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
//仔细看好这里 从数据库中获取到的UserDetails对象和前台表单提交封装的UsernamePasswordAuthenticationToken对象 进行比较
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
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.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//在这里通过用户名从数据库中拿到UserDetails 然后交给additionalAuthenticationChecks去验证
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
// ..........................
}
我们以表单登录的来看一下用户登录的执行流程
图片来源于网络,侵权请留言!!
让我们再梳理一下Spring security的核心类