简介:
Spring Secuirity 是Spring家族中的一个安全管理框架。相比于另一个框架shiro,他提供了更加丰富的功能,社区资源也比市容丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity, Shiro的上手更加的简单。
—般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
整体架构
在的架构设计中,认证和授权是分开的,无论使用什么样的认证方式。都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是可以非常方便地整合一些外部的解决方案。
认证
AuthenticationManager
在Spring Security中认证是由AuthenticationManager接口来负责的,接口定义为:
- 返回 Authentication 表示认证成功
- 返回 AuthenticationException 异常,表示认证失败。
AuthenticationManager 主要实现类为 ProviderManager,在 ProviderManager 中管理了众多 AuthenticationProvider 实例。在一次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider ,用来实现多种认证方式,这些 AuthenticationProvider 都是由 ProviderManager 进行统一管理的。
Authentication
认证以及认证成功的信息主要是由 Authentication 的实现类进行保存的,其接口定义为:
- getAuthorities 获取用户权限信息
- getCredentials 获取用户凭证信息,一般指密码
- getDetails 获取用户详细信息
- getPrincipal 获取用户身份信息,用户名、用户对象等
- isAuthenticated 用户是否认证成功
SecurityContextHolder
SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。
总结:
AuthenticationManager被ProviderManager实现,用来管理AuthenticationProvider 实例,这个实例包含很多认证方式,如短信,表单等等。入参是只含有用户名和密码的Authentication,返回数据库中包含的该用户的所有信息。
Authentication保存认证者的信息。
SecurityContextHolder用来获取用户登录后的信息。注意点是讲用户信息进行线程绑定。在使用时将用户信息绑定到当前线程的SecurityContextHolder中,使用完毕清除当前线程的SecurityContextHolder同时将用户信息放到session中,方便下次使用。
授权
当完成认证后,接下米就是授权了。在 Spring Security 的授权体系中,有两个关键接口,
AccessDecisionManager
AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter
AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
ConfigAttribute
ConfigAttribute,用来保存授权时的角色信息
在 Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具各的角色和请求某个 资源所需的 ConfigAtuibute 之间的关系。
总结:
AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter (访问决定投票器),比较用户所具各的角色和请求某个资源所需的 ConfigAtuibute 之间的关系,具备访问权限则投出赞成票。
ConfigAttribute,用来保存授权时的角色信息。
环境搭建
创建SpringBoot工程
略
引入SpringSecurity
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。
必须登陆之后才能对接口进行访问。
实现原理
https://docs.spring.io/spring-security/site/docs/5.5.4/reference/html5/#servlet-architecture
SpringBootWebSecurityConfiguration
对anyRequest()都进行拦截
默认生效条件
- 条件一 classpath中存在 SecurityFilterChain.class, HttpSecurity.class
- 条件二 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class
总结:
只要我们不自定义配置类(比如WebSecurityConfigurerAdapter) ,条件都是满足的,也就加载默认的配置。否则如果要进行自定义配置,就要继承这个WebSecurityConfigurerAdapter类,通过覆盖类中方法达到修改默认配置的目的。
执行流程(即SecurityFilter的使用情况和调用顺序)
在 SpringSecurity 中 认证、授权 等功能都是基于过滤器完成的。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个 FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中。FilterChainProxy 作为一个顶层的管理者,将统一管理 Security Filter。FilterChainProxy 本身是通过 Spring 框架提供的 DelegatingFilterProxy 整合到原生的过滤器链中。
SecurityFilter的使用情况和调用顺序如下:
过滤器 |
过滤器作用 |
默认是否加载 |
ChannelProcessingFilter |
过滤请求协议 HTTP 、HTTPS |
NO |
WebAsyncManagerIntegrationFilter |
将 WebAsyncManger 与 SpringSecurity 上下文进行集成 |
YES |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到 SecurityContextHolder 中 |
YES |
HeaderWriterFilter |
处理头信息加入响应中 |
YES |
CorsFilter |
处理跨域问题 |
NO |
CsrfFilter |
处理 CSRF |
YES |
LogoutFilter |
处理注销登录 |
YES |
OAuth2AuthorizationRequestRedirectFilter |
处理 OAuth2 认证重定向 |
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 认证的 Access Token |
NO |
BasicAuthenticationFilter |
处理 HttpBasic 登录 |
YES |
RequestCacheAwareFilter |
处理请求缓存 |
YES |
SecurityContextHolder<br />AwareRequestFilter |
包装原始请求 |
YES |
JaasApiIntegrationFilter |
处理 JAAS 认证 |
NO |
RememberMeAuthenticationFilter |
处理 RememberMe 登录 |
NO |
AnonymousAuthenticationFilter |
配置匿名认证 |
YES |
OAuth2AuthorizationCodeGrantFilter |
处理OAuth2认证中授权码 |
NO |
SessionManagementFilter |
处理 session 并发问题 |
YES |
ExceptionTranslationFilter |
处理认证/授权中的异常 |
YES |
FilterSecurityInterceptor |
处理授权相关 |
YES |
SwitchUserFilter |
处理账户切换 |
NO |
可以看出,Spring Security 提供了 30 多个过滤器。默认情况下Spring Boot 在对 Spring Security 进入自动化配置时,会创建一个名为 SpringSecurityFilerChain 的过滤器,并注入到 Spring 容器中,这个过滤器将负责所有的安全管理,包括用户认证、授权、重定向到登录页面等。具体可以参考WebSecurityConfiguration的源码:
自定义
自定义资源认证规则
前面提到,默认认证规则生效需要满足两个条件
- 条件一 classpath中存在 SecurityFilterChain.class, HttpSecurity.class
- 条件二 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class
条件一当我们启动springboot项目时就已经生成,无法修改。所以我们可以通过自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class两个类来实现资源认证规则的自定义。
对于index放行,对于其他的页面采取认证表单认证。
总结:
要实现自定义的认证规则,建议实现WebSecurityConfigurerAdapter类,重写configure方法。
自定义登陆界面
在WebSecurityConfigurerAdapter的实现类中指定跳转的路径、页面,以及前端会被拦截器捕捉到的请求。
定义跳转的controller以及页面
总结:
通过自定义configure方法中的对应的方法来指定我们需要修改的认证页面即可达到自定义认证界面的目的。
前后端分离自定义认证成功的默认行为
对于前后端分离的情况,前面提到的.successForwardUrl("/index") 和 .defaultSuccessUrl("/index")就不再试用了,我们更期待的是后端在认证成功之后像前端传递一些数据,前端再根据这些数据或者信息进行渲染。这个时候就有另外一个方法.successHandler(new MyAuthenticationSuccessHandler())。
需要传递AuthenticationSuccessHandler对象,所以我们可以通过实现这个类来传递需要传递的信息。
此时再进行测试我们发现他就不是跳转页面了而是展示了我们在重写的方法中打印的数据。
前后端分离自定义认证失败的默认行为
和成功差不多,就是要实现AuthenticationFailureHandler接口 。调用failureHandler(传AuthenticationFailureHandler接口实现类)。
总结:
成功:successHandler(new MyAuthenticationSuccessHandler())。
失败:failureHandler(new MyAuthenticationFailureHandler())。
注销登录(前后端不分离)
注销登录默认开启。需要在地址栏以get的方式访问/logout。
对于传统前后端不分离的方式,可以直接返回登录页面。
注销登录(前后端分离)
调用.logoutSuccessHandler(new MyLogoutSuccessHandler())方法,传入自定义的AuthenticationSuccessHandler的实现类。
获取用户认证信息
SpringContextHolder
Spring Security 会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到SecurityContextHolder中。
SecurityContextHolder中的数据保存默认是通过ThreadLocal 来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContexHolder中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session中取出用户登录数据,保存SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。
实际上securityContextHolder中存储是SecurityContext,在SecurityContext中存储是Authentication。
存放策略
- MODE THREADLOCAL︰
- 这种存放策略是将SecurityContext存放在ThreadLocal中,大家
知道Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达 Servlet,都是由一个线程来处理的。这也是SecurityContextHolder的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
- MODE INHERITABLETHREADLOCAL∶
- 这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
- MODE GLOBAL︰
- 这种存储模式实际上是将数据保存在一个静态变量中,在JavaWeb开发中,这种模式很少使用到。
SecurityContextHolderStrategy通过SecurityContextHolder可以得知,SecurityContextHolderStrategy接口用来定义存储策略方法。所以我们想要使用不同的存储策略只需要在java虚拟机运行环境上配置属性-Dspring.security.strategy=策略即可。
自定义认证数据源
认证流程
三者关系
AuthenticationManager与ProviderManager
弄清楚认证原理之后我们来看下具体认证时数据源的获取。默认情况下
AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过 UserDetailsService 完成数据源的校验。他们之间调用关系如下:
总结:
AuthenticationManager是认证管理器,在Spring Security中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManger进行实现。每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用UserDetailService来实现。
定义内存数据源
总结:
默认
1.默认自动配置创建全局AuthenticationManager默认找当前项目中是否存在自定义UserDetailService实例自动将当前项目UserDetailService实例设置为数据源
2.默认自动配置创建全局AuthenticationManager在工厂中使用时直接在代码中注入即可
自定义
1.一旦通过configure方法自定义AuthenticationManager实现就回将工厂中自动配AuthenticationManager进行覆盖
2.一旦通过configure方法自定义AuthenticationManager实现需要在实现中指定认证数据源对象UserDetaiService实例
3.一旦通过configure方法自定义AuthenticationManager实现,这种方式创建的AuthenticationManager对象只存在于工厂内部,即不可以通过@Autowired注解在别的地方注入。想要在工厂中暴露这个实例就需要实现以下方法:
定义数据库数据源
依赖
略
数据库及类
略
配置jdbc、mybatis等等
UserDao
UserDao.xml
WebSecurityConfig
总结:
基于数据库的数据源主要还是实现UserDetailsService接口,重写loadUserByUsername方法。一旦我们实现了这个接口并且重写了对应的方法,通过configure(AuthenticationManagerBuilder auth)方法指定了userDetailsService后,security在认证的时候就会调用我们重写的loadUserByUsername方法去数据库中拿数据。
验证码
使用google的kapacha。
依赖
配置类
处理验证码生成请求的controller
注意:要在自定义的SecurityConfig里面放行生成验证码的请求路径。
待更新。。。