一、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 |
处理表单认证 |
yes |
|
OpenIDAuthenticationFilter |
处理openid认证 |
no |
DefaultLoginPageGeneratingFilter |
配置默认登录界面 |
yes |
DefaultLogoutPageGeneratingFilter |
默认注销页面 |
yes |
ConcurrentSessionFilter |
处理session有效期 |
no |
处理http摘要认证 |
no |
|
BearerTokenAuthenticationFilter |
处理oauth2的accesstoken |
no |
处理httpbasic认证 |
yes |
|
RequestCacheAwareFilter |
处理请求缓存 |
yes |
SecurityContextHolderAwareRequestFilter |
包装原始请求 |
yes |
JaasApiIntegrationFilter |
处理jaas认证 |
no |
RememberMeAuthenticationFilter |
处理rememberme登录 |
no |
AnonymousAuthenticationFilter |
配置匿名认证 |
yes |
OAuth2AuthorizationCodeGrantFilter |
处理oauth2认证授权码 |
no |
SessionManagementFilter |
处理session并发问题 |
yes |
处理认证授权中的异常 |
yes |
|
处理授权相关 |
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对Spring Security的支持都放在thymeleaf-extras-springsecurityX中,目前最新版本为5。所以需要在项目中添加此jar包的依赖和thymeleaf的依赖。后面的X要根据所有的springboot版本来决定
引入依赖
<!--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>
html页面
<!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>