SpringSecurity
权限管理
权限管理实现了对用户访问系统的控制 ,按照 安全规则 或 安全策略 控制用户跨域访问,而且只能访问自己被授权的资源
包括两部分(先进行身份认证,认证通过后用户具有该资源的访问权限)
- 身份认证:就是判断⼀个⽤户是否为合法⽤户的处理过程
- 授权:即访问控制,控制谁能访问哪些资源
整体架构
认证和授权是分开的,⽆论使⽤什么样的认证⽅式,都不会影响授权。这是两个独⽴的存在,这种独⽴带来的好处之⼀,就是可以⾮常⽅便地整合⼀些外部的解决⽅案
认证
AuthenticationManager
在SpringSecurity中认证是由AuthenticationManager
接口进行负责的,接口定义为:
- 返回Authentication表示认证成功
- 返回AuthenticationException异常,表示认证失败
AuthenticationManager
主要实现类为 ProviderManager
,在 ProviderManager
中管理了众多 AuthenticationProvider
实例。
在⼀次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider
,⽤来实现多种认证⽅式,这些 AuthenticationProvider
都是由 ProviderManager
进⾏统⼀管理的
Authentication
认证成功后的信息主要是由该类的实现类进行保存
- getAuthorities 获取⽤户权限信息
- getCredentials 获取⽤户凭证信息,⼀般指密码
- getDetails 获取⽤户详细信息
- getPrincipal 获取⽤户身份信息,⽤户名、⽤户对象等
- isAuthenticated ⽤户是否认证成功
SecurityContextHolder
SecurityContextHolder ⽤来获取登录之后⽤户信息。SpringSecurity 会将登录⽤户数据保存在 Session 中
- 当⽤户登录成功后,SpringSecurity 会将登录成功的⽤户信息保存到 SecurityContextHolder 中
- SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使⽤ ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改
- 当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空
- 每当有请求到来时,SpringSecurity 就会先从 Session 中取出⽤户登录数据,保存到 SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤
- 在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这⼀策略非常方便⽤户在 Controller、Service 层以及任何代码中获取当前登录⽤户数据
授权
AccessDecisionManager和AccessDecisionVoter都有众多实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问
AccessDecisionManager
访问决策管理器,用来决定此次访问是否被允许
AccessDecisionVoter
访问决定投票器,投票器会检查用户是否具备应有的角色,进而投出赞成,反对或弃权票
ConfigAttribute
用来保存授权时的角色信息
在SpringSecurity
中,用户请求资源时,需要的角色会被封装成一个ConfigAttribute
对象
在ConfigAttribute中只有一个getAttribute()
方法,该方法会返回一个String字符串(即为角色的名称)
角色名称都带有一个 ROLE 前缀,投票器AccessDecisionVoter
所做的事情其实就是:比较用户具有的各个角色和请求资源所需的ConfigAttribute
之间的关系
整合springboot
依赖
1 | <dependency> |
定义两个Controller,在访问
localhost:8080/hello
和localhost:8080/index
的时候分别打印出hello security与hello index
1 |
|
当引入Spring Security后没有进行任何配置,所有请求都会跳转到localhost:8080/login
这其中关系到SpringBootWebSecurityConfiguration
这个类是SpringBoot的自动配置类,通过源码得知,默认情况下对所有请求进行权限控制
这也就是在引入SpringSecurity后,没有进行任何配置,请求都会被拦截的原因
1 |
|
默认的生效条件
默认情况下,条件都满足。,SpringSecurity核心配置都在WebSecurityConfigurerAdapter类中,我们一般通过重写该类,来对SpringSecurity实现功能的增强
1 | class DefaultWebSecurityCondition extends AllNestedConditions { |
原理
在 Spring Security 中认证、授权 等功能都是基于过滤器完成的 官方文档
SpringSecurity的Servlet包含在FilterChainProxy
中。FilterChainProxy
是SpringSecurity提供的一个特殊的Filter,SpringSecurity通过SecurityFilterChain
委托许多Filter实例
FilterChainProxy是一个Bean,它通常被包裹在DelegatingFilterProxy中
SecurityFilterChain
由FilterChainProxy
调用,然后FilterChainProxy
再调用Spring的一些安全过滤器
FilterChainProxy可以用来确定应该使用哪个SecurityFilterChain。这允许为不同的环境提供一个完全独立的配置
在出现多个SecurityFilterChain
中,FilterChainProxy
决定应该使用哪个SecurityFilterChain
,只有第一个匹配的SecurityFilterChain才会被调用
请求的URL为
/api/messages/
,它将首先与SecurityFilterChain0的/api/**
相匹配,所以只有SecurityFilterChain0会被调用,尽管它也与另外一个匹配。请求的URL
/messages/
的URL,它将不会与SecurityFilterChain0的/api/**
模式相匹配,所以FilterChainProxy将继续尝试每个SecurityFilterChain。假设没有其他的SecurityFilterChain实例与之匹配,就会调用SecurityFilterChainn
每个SecurityFilterChain都可以是唯一的,并且是单独配置的
Security Filters一览
过滤器 | 过滤器作用 | 默认是否加载 |
---|---|---|
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 |
自定义认证
自定义资源权限规则
当存在公共资源和受限资源时,为了方便资源进行访问与管理,我们需要自定义资源权限规则
注意:
permitAll() 代表放⾏该资源,⽆需认证和授权可以直接访问
anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
formLogin() 代表开启表单认证
可以通过继承WebSecurityConfigurerAdapter,重写configure方法来进行自定义资源权限认证
现在该方式已被弃用,可以使用自定义一个filterChain的Bean来实现自定义资源权限认证
1 |
|
自定义登录界面
1、引入thymeleaf
1
2
3
4 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<dependency>2、定义controller
1
2
3
4
5
6
7
public class LoginController {
public String login() {
return "login";
}
}3、定义登录界面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">
用户名:<input name="username" type="text"><br>
密码:<input name="password" type="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
登录表单 method 必须为 post
action 的请求路径为 /doLogin
⽤户名的 name 属性为 uname
密码的 name 属性为 passwd
注意:
successForwardUrl
是forward转发(始终在认证成功之后跳转到指定请求,不会跳转到之前请求路径)defaultSuccessUrl
是redirect重定向(根据上一条保存请求进行成功跳转,可以传入第二个参数进行修改)- 二者只能选择一个进行配置
1 |
|
自定义登录成功处理
在认证成功后,不进行页面跳转,而是给前端返回一个JSON通知登录成功与否
根据接⼝的描述信息,得知登录成功会自动回调这个方法,successForwardUrl、defaultSuccessUrl也是由它的⼦类实现的
自定义AuthenticationSuccessHandler实现
- 在自定义资源权限规则中,设置successHandler()
- 实现AuthenticationSuccessHandler
在重写方法中配置返回前端的数据和ContentType
1 | public class SuccessHandler implements AuthenticationSuccessHandler { |
显示登录失败信息
Spring Security 在登录失败之后会将异常信息存储到 request 、session作⽤域中 key 为SPRING_SECURITY_LAST_EXCEPTION 命名属性中
- failureUrl 失败以后的重定向跳转
- failureForwardUrl 失败以后的 forward 跳转
- 注意:如果要获取 request 中异常信息,这⾥只能使⽤failureForwardUrl
1 |
|
自定义登录失败处理
1、和自定义登录成功处理类似,在自定义资源权限规则中,设置failureHandler()
2、实现AuthenticationFailureHandler,在重写方法中配置返回前端的数据和ContentType
1 | public class FailureHandler implements AuthenticationFailureHandler { |
注销登录
SpringSecurity中提供了默认的注销登录的配置,LogoutFilter过滤器专门用来处理注销登录的相关请求
1、注销成功后跳转页面
invalidateHttpSession:退出时是否是 session 失效,默认值为 true
clearAuthentication:退出时是否清除认证信息,默认值为 true
logoutSuccessUrl:退出登录时跳转地址
logoutRequestMatcher:配置多个注销登录请求
1 | public class WebSecurityConfigurer { |
按照上面配置之后,当访问localhost:8080/logout-1
后,界面将会跳转到登录界面
2、注销成功后返回信息
- 与上面类似登录成功或失败后返回前端信息类似,都是通过实现LogoutSuccessHandler类来进行
1 | public class LogoutHandler implements LogoutSuccessHandler { |
- 在自定义资源权限管理器做以下修改
获取登录用户数据
SecurityContextHolder ⽤来获取登录之后用户信息。SpringSecurity 会将登录用户数据保存在 Session 中
在SpringSecurity中使用策略设计模式来进行数据的获取与存储
MODE_THREADLOCAL
:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其 实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储 策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中 去获取登录⽤户数据,就会获取不到MODE_INHERITABLETHREADLOCAL
:这种存储模式适⽤于多线程环境,如果希望在子线程中也能够获取到登录⽤户数据,那么可以使用这种存储模式MODE_GLOBAL
:这种存储模式实际上是将数据保存在⼀个静态变量中,这种模式很少使⽤到
可以通过增加 VM Options 参数进行修改
1 | 如:-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL |
SecurityContextHolderStrategy 接⼝⽤来定义存储策略方法
1 | public interface SecurityContextHolderStrategy { |
clearContext
:该⽅法⽤来清除存储的 SecurityContext对象getContext
:该⽅法⽤来获取存储的 SecurityContext 对象setContext
:该⽅法⽤来设置存储的 SecurityContext 对象createEmptyContext
:该⽅法则⽤来创建⼀个空的 SecurityContext 对象
页面上获取用户信息
- 引入依赖
1 | <dependency> |
- 页面加入命名空间
1 | <html lang="en" xmlns:th="https:!"www.thymeleaf.org" |
- 在页面中使用
1 | <!--获取认证⽤户名--> |
⾃定义认证数据源
认证流程分析
1、当发起一个认证用户的请求时,会被UsernamePasswordAuthenticationFilter
给拦截到,在这个Filter中,会从HttpServletRequest
中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken
2、认证被传递到AuthenticationManager
中进行认证
3、如果认证失败
- 清除
SecurityContextHodler
- 清除记住我中的信息
- 回调
AuthenticationFailureHandler
处理
如果认证成功
- 将认证信息存储到
SecurityContextHodler
- 调⽤记住我中的功能
- 回调
AuthenticationSuccessHandler
处理
注意:
AuthenticationManager
是认证的核⼼类,但实际上在底层真正认证时还离不开 ProviderManager
以及 AuthenticationProvider
AuthenticationManager
是一个认证管理器,它实现了SpringSecurity 过滤器要执行的认证操作ProviderManager
是AuthenticationManager
接口的实现类,SpringSecurity 认证时默认使用AuthenticationProvider
就是针对不同的身份类型执行的具体的身份认证
ProviderManager
是AuthenticationManager
的唯⼀实现,默认情况下AuthenticationManager
就是⼀ 个ProviderManager
ProviderManager与AuthenticationProvider
SpringSecurity 允许系统同时支持多种不同的认证方式(如:同时支持用户名密码验证,Remember认证,手机号码动态认证)
不同的认证方式对应了不同的
AuthenticationProvider
,所以一个完整的认证流程可能由多个AuthenticationProvider
来提供多个
AuthenticationProvider
组成一个列表,然后由ProviderManager
代理ProviderManager
本身还可以配置一个AuthenticationManager
作为parent,在ProviderManager
认证失败后,可以进入到parent再次认证(ProviderManager
也可作为parent)ProviderManager本身也可以有多个,多个ProviderManager共用一个parent,这样就可以实现采用不同的请求路径实现不同的认证规则
配置全局 AuthenticationManager
默认的AuthenticationManager
找当前项⽬中是否存在自定义 UserDetailService 实例,自动将当前项目 UserDetailService 实例设置为数据源
1 |
|
进行自定义
1 | //自定义AuthenticationManager |