参考文件:Spring Security 参考手册|Spring Security中文版
学习一个技术想要深入研究肯定需要先看官网介绍,否则你可能不太好入手分析代码,可以根据官网的思路一点点的拆解。这是Spring-Security的中文介绍,有能力的可以看英文原版,对我这样只会hello world的只能看中文文档了。
git源码下载地址:https://github.com/spring-projects/spring-security
Spring Security是一个权限框架权限框架,常见的还有Shiro。权限框架的核心功能就是 认证和授权
下面来简单分析一下Spring-Security是怎么实现认证授权的。首先在Security的过滤器中找一下认证过滤器。认证带有Authentication单词,这就是规范代码的好处。我们可以找到UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、CasAuthenticationFilter、OAuth2LoginAuthenticationFilter等。CAS和OAuth2暂时不分析了,这些扩展过滤器可以自己看一下源码,BasicAuthenticationFilter与UsernamePasswordAuthenticationFilter是Spring-Security的基本过滤器,我们通常搭建个简单demo就会用到的,其doFilter()方法的逻辑其实都差不多的,就以UsernamePasswordAuthenticationFilter用户名密码认证过滤器为例来分析一下
UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
/**
* 获取用户名密码信息,而且只能用POST方式发送。很简单的操作获取Request中的用户名和密码防撞到Token中。然后调用认证管理器获取认证类来认证Token
*/
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);
}
}
...
}
认证器管理器包括认证我们都在配置文件中配置过,就是下面这段代码
/**
* 认证管理器,这里也可以设置多个认证策略,Security支持多认证体系
* @return
* @throws Exception
*/
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Arrays.asList(authenticationProvider));
}
看一下有关用户的AuthenticationProvider
AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
//核心认证方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
//获取用户名,getPrincipal()就是获取用户信息,Shiro和Security权限框架都叫这个名
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");
}
...//省略用户校验和缓存代码
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//返回用户信息封装的Token
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
...
}
看一下刚才有个retrieveUser获取用户的代码
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
//使用用户服务通过用户名获取用户信息,是不是看着眼熟,只要是用过这个框架的都知道需要重写个用户服务获取用户信息的,就在这调用的
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...//省略其他代码try/catch
}
解释下刚才那个获取用户名的代码,在权限框架中我们一般这样获取用户信息:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//principal就是用户信息,刚才获取用户名就是这里来的
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
再往里深入一下看一下用户上下文维持类SecurityContextHolder,这是使用的ThreadLocal维护的用户信息,ThreadLocal就不用说了吧,多线程基础知识,可以创建每个线程独享的资源,也就是Map线程ID做Key,把变量放进去了。这样就不会导致线程安全问题了。
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";
/*使用的策略模式,如果没有设置默认为ThreadLocal模式*/
private static String strategyName = System.getProperty("spring.security.strategy");
...
/*初始化,这里采用的策略模式*/
private static void initialize() {
/*如果没有设置模式,默认为ThreadLocal模式*/
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
/*可继承的ThreadLocal策略(百度翻译出来的)*/
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
/*不适用ThreadLocal模式,全局使用一个常见于SWing组件中*/
strategy = new GlobalSecurityContextHolderStrategy();
} else {
/*自定义策略*/
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception var2) {
ReflectionUtils.handleReflectionException(var2);
}
}
}
...
}
看一下默认的用户信息存放的ThreadLocal类ThreadLocalSecurityContextHolderStrategy
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
/*通过ThreadLocal保存认证信息(每个线程一份互不影响)*/
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
...
public SecurityContext getContext() {
SecurityContext ctx = (SecurityContext)contextHolder.get();
if (ctx == null) {
ctx = this.createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
...
}
打个断点看一下:
下面再看一下用户信息是怎么维护进去的,这就需要看另一个过滤器SecurityContextPersistenceFilter 了,它的主要工作就是维护SecurityContext的,下面看一下它的源码:
/**
* 用来维护SecurityContextHolder上下文
*/
public class SecurityContextPersistenceFilter extends GenericFilterBean {
...
//进入请求将请求信息维护到SecurityContextHolder
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());
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var13 = false;
//将请求信息维护到SecurityContextHolder
try {
var13 = true;
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
var13 = false;
} finally {
if (var13) {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//清除SecurityContextHolder维护的信息。
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维护的信息。这个阿里代码规范中可能会遇到,ThreadLocal用完后要清除,否则内存泄漏。
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");
}
}
}
...
}
授权部分很简单可以自己尝试着分析下我这就不介绍了。
ThreadLocal以前也没怎么用过,但是上次写的代码里面每个Controller用到了一个状态标记,为了减少代码抽出到BaseController了,但是这时候就出问题了,Spring 的Controller好像不是线程安全的,状态不对报错了,用了ThreadLocal,用阿里代码规范扫描又扫出了高级漏洞,所以记住了每次用完都得clear一下防止内存泄漏。关于阿里代码规范插件安装的方式就不说了,网上有的是,个人建议:对于一些中小公司没有代码管理规范的可以用一下,一方面减少代码隐藏漏洞,就像我的漏洞可能用户量大了一直维持的用户信息没用释放掉,最终内存不足宕机了;第二方面代码交接的时候别人来读你的代码。