一、security原理
security是通过一系列filter完成认证和授权的

security默认提供了30多个过滤器,spring boot在对security进行自动化配置时,会创建一个名为springsecuritychian过滤链并注入到spring容器中,filterchianproxy做为顶层管理者统一管理springsecuritychian,filterchianproxy本身通过DelegatinFilterProxy整合到原生web过滤链中
过滤器列表
过滤器 | 作用 | 默认开启 |
ChannelProcessingFilter 过滤请求协议 https http 默认no | 过滤请求协议 https http | no |
WebAsyncManagerIntegrationFilter | 将WebAsyncManager和security上下文结合 | yes |
SecurityContextPersistenceFilter | 处理请求前将安全新加载到securitycontextholder | yes |
HeaderWriterFilter | 处理头信息加入到响应中 | yes |
CorsFilter | 处理跨域问题 | no |
CsrfFilter | 处理csrf攻击 | yes |
LogoutFilter | 处理注销登录 | yes |
OAuth2AuthorizationRequestRedirectFilter | 处理outh2认证重定向 | no |
Saml2WebSsoAuthenticationRequestFilter | 处理saml认证 | no |
X509AuthenticationFilter | 处理x509认证 | no |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | no |
CasAuthenticationFilter | 处理cas认证 | no |
OAuth2LoginAuthenticationFilter | 处理oauth2认证 | no |
Saml2WebSsoAuthenticationFilter | 处理saml认证 | no |
UsernamePasswordAuthenticationFilter | 处理表单认证 | yes |
OpenIDAuthenticationFilter | 处理openid认证 | no |
DefaultLoginPageGeneratingFilter | 配置默认登录界面 | yes |
DefaultLogoutPageGeneratingFilter | 默认注销页面 | yes |
ConcurrentSessionFilter | 处理session有效期 | no |
DigestAuthenticationFilter | 处理http摘要认证 | no |
BearerTokenAuthenticationFilter | 处理oauth2的accesstoken | no |
BasicAuthenticationFilter | 处理httpbasic认证 | yes |
RequestCacheAwareFilter | 处理请求缓存 | yes |
SecurityContextHolderAwareRequestFilter | 包装原始请求 | yes |
JaasApiIntegrationFilter | 处理jaas认证 | no |
RememberMeAuthenticationFilter | 处理rememberme登录 | no |
AnonymousAuthenticationFilter | 配置匿名认证 | yes |
OAuth2AuthorizationCodeGrantFilter | 处理oauth2认证授权码 | no |
SessionManagementFilter | 处理session并发问题 | yes |
ExceptionTranslationFilter | 处理认证授权中的异常 | yes |
FilterSecurityInterceptor | 处理授权相关 | yes |
SwitchUserFilter | 处理账户切换 | no |
二、认证流程分析
1、前端输入完用户名密码之后,会进入UsernamePasswordAuthenticationFilter类中去获取用户名和密码,然后去构建一个UsernamePasswordAuthenticationToken对象。
UsernamePasswordAuthenticationToken这个类是实现了Authentication接口,在调用UsernamePasswordAuthenticationToken的构造函数的时候先调用父类AbstractAuthenticationToken的构造方法,传递一个null,因为在认证的时候并不知道这个用户有什么权限,之后去给用户名密码赋值,最后有一个setAuthenticated(false)方法,代表未认证,源码如下:
实例化UsernamePasswordAuthenticationToken之后调用了setDetails(request,authRequest)将请求的信息设到UsernamePasswordAuthenticationToken中去,包括ip、session等内容
2、然后执行
this.getAuthenticationManager().authenticate(authRequest)
AuthenticationManager本身不包含验证的逻辑,它的作用是管理AuthenticationProvider。
3、authenticate这个方法是在ProviderManager类上的,这个类实现了AuthenticationManager接口,在authenticate方法中有一个for循环,拿到所有的AuthenticationProvider,真正校验的逻辑是写在AuthenticationProvider中的,为什么是一个集合去进行循环?是因为不同的登陆方式认证逻辑是不一样的,可能是微信等社交平台登陆,也可能是用户名密码登陆。AuthenticationManager其实是将AuthenticationProvider收集起来,然后登陆的时候挨个去AuthenticationProvider中问你这种验证逻辑支不支持此次登陆的方式,根据传进来的Authentication类型会挑出一个适合的provider来进行校验处理。
然后去调用provider的验证方法authenticate方法,authenticate是DaoAuthenticationProvider类中的一个方法,DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider。实际上authenticate的校验逻辑写在了AbstractUserDetailsAuthenticationProvider抽象类中,首先实例化UserDetails对象,调用了retrieveUser方法获取到了一个user对象,retrieveUser是一个抽象方法。
AbstractUserDetailsAuthenticationProvider的 authenticate 方法的一部分:
注意:如果自己自定义了一个MyAuthenticationProvider继承了AuthenticationProvider,这里就会走自己的认证类。
4、DaoAuthenticationProvider实现了 retrieveUser 方法,在实现的方法中实例化了UserDetails对象
注意:如果自己自定义了UserDetailsServiceImpl 实现了 UserDetailsService就会走自己的方法。
5、也就是相当于自定义验证逻辑的那个类,去实现UserDetailService类,这个返回结果就是我们自己在数据库中根据username查询出来的用户信息。在AbstractUserDetailsAuthenticationProvider中如果没拿到信息就会抛出异常,如果查到了就会去调用preAuthenticationChecks的check方法去进行预检查。
AbstractUserDetailsAuthenticationProvider的 authenticate 方法的一部分:
在预检查中进行了三个检查,因为UserDetail类中有四个布尔类型,去检查其中的三个,用户是否锁定、用户是否过期,用户是否可用。
预检查之后紧接着去调用了additionalAuthenticationChecks方法去进行附加检查,这个方法也是一个抽象方法,在DaoAuthenticationProvider中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念。
如果通过了预检查和附加检查,还会进行厚检查,检查4个布尔中的最后一个。
所有的检查都通过,则认为用户认证是成功的。用户认证成功之后,会将这些认证信息和user传递进去,调用createSuccessAuthentication方法。
createSuccessAuthentication方法中同样会实例化一个user,但是这个方法不会调用之前传两个参数的函数,而是会调用三个参数的构造函数。这个时候,在调super的构造函数中不会再传null,会将authorities权限设进去,之后将用户密码设进去,最后setAuthenticated(true),代表验证已经通过。
最后创建一个authentication会沿着验证的这条线返回回去。如果验证成功,则在这条路中调用我们系统的业务逻辑。如果在任何一处发生问题,就会抛出异常,调用我们自己定义的认证失败的处理器。
三、总结
1、UserDetails与UserDetailsService区别
UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。!UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
2、authenticationmanager ,providermanager, authenticationprovider关系
security中支持多种不同的认证方式,比如用户名和密码认证、手机号验证码认证等,不同的认证方式对应了不同authenticationprovider所以一个完整的流程可能由多个authenticationprovider提供,多个authenticationprovider组成一个列表,这个列表将由providermanager代理,换句话说providermanager中存在一个列表,在providermanager中遍历列表中每一个authenticationprovider去执行身份认证,最终得到认证结果。
providermanager本身也可以再配置一个authenticationmanager作为parent,这样当providermanager认证失败就可以进入到parent中再次认证,理论上providermanager的parent可以是任意类型的authenticationmanager,但是通常由providermanager来扮演parent角色,也就是providermanager是providermanager的parent。
providermanager本身也可以有多个,多个providermanager共用一个parent,有时一个应用程序有受保护资源的逻辑组,比如所有符合路径模式的资源/api/**.每个组可以有自己专用的authenticationmanager,通常每个组都是一个providermanager,他们共享一个父级,然后父级是一种全局资源,作为所有提供者的后备资源,由此三者关系如图
3、用户信息获取
3.1、业务代码中获取
security会将登录的用户信息数据保存在session中,security在此基础上做了改进,通过一个线程将登陆成的信息保存到SecurityContextHolder中,
SecurityContextHolder中数据保存默认是通过Theadlocal实现的,使用Theadlocal创建的变量只能通过当前线程访问,不能被其他线程或者 当前线程的子线程访问,就是用户数据和请求线程是绑定在一起的,请求完成后,security会将在session中登录信息清空,以后有请求时security会从session信息保存到SecurityContextHolder方便该请求后续处理
实际上SecurityContextHolder存储的是SecurityContext ,在SecurityContext存储的是Authentication
SecurityContextHolder源码如下
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";
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
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;
}
SecurityContentHolderStrategy源码如下
public interface SecurityContextHolderStrategy {
/**
* 清除存储的securityContext
*/
void clearContext();
/**
* 获取存储的securityContext
*/
SecurityContext getContext();
/**
*设置存储的securityContext
*/
void setContext(SecurityContext context);
/**
* 创建一个空的存储的securityContext
*/
SecurityContext createEmptyContext();
}
代码中获取
@GetMapping("/index")
public String index(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
//身份信息
user.getUsername();
//权限信息
user.getAuthorities();
return "index";
}
其他方式
方法二:
@GetMapping("/me")
public Object getCurrentUser(Authentication authentication){
return authentication;
}
方法三:
@GetMapping("/me")
public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
3.2、页面中获取
<!--thymeleaf与security整合包-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!--thymeleaf启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name">123</span><br/>
登录账号:<span sec:authentication="principal.username">456</span><br/>
凭证:<span sec:authentication="credentials">456</span><br/>
权限和角色:<span sec:authentication="authorities">456</span><br/>
客户端地址:<span sec:authentication="details.remoteAddress">456</span><br/>
sessionId:<span sec:authentication="details.sessionId">456</span><br/>
</body>
</html>