Spring Security 快速上手
Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。
创建工程
创建maven工程
1)创建maven工程 security-spring-security,工程结构如下:
2)引入以下依赖:
在security-springmvc的基础上增加spring-security的依赖:
1 | <dependency> |
Spring容器配置
在config
包下定义ApplicationConfig.java
,它对应web.xml
中ContextLoaderListener
的配置
1 |
|
Servlet Context配置
本案例采用Servlet3.0无web.xml方式,的config包下定义WebConfig.java,它对应于DispatcherServlet配置。
1 |
|
加载 Spring容器
在init包下定义Spring容器初始化类SpringApplicationInitializer
,此类实现WebApplicationInitializer
接口,Spring容器启动时加载WebApplicationInitializer
接口的所有实现类。
1 | public class SpringApplicationInitializer extends |
认证
认证页面
springSecurity默认提供认证页面,不需要额外开发。
安全配置
spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
1) 在config包下定义WebSecurityConfig,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
1 |
|
在 userDetailsService()
方法中,我们返回了一个UserDetailsService
给spring容器,Spring Security会使用它来获取用户信息。我们暂时使用InMemoryUserDetailsManager
实现类,并在其中分别创建了zhangsan、lisi
两个用户,并设置密码和权限。
而在configure()
中,我们通过HttpSecurity
设置了安全拦截规则,其中包含了以下内容:
(1)url匹配/r/**的资源,经过认证后才能访问。
(2)其他url完全开放。
(3)支持form表单认证,认证成功后转向/login-success。
关于HttpSecurity的配置清单请参考附录 HttpSecurity。
2) 加载 WebSecurityConfig
修改SpringApplicationInitializer的getRootConfigClasses()方法,添加WebSecurityConfig.class:
1 |
|
Spring Security初始化
Spring Security 初始化,这里有两种情况
- 若当前环境没有使用 Spring或Spring MVC,则需要将 WebSecurityConfig(Spring Security配置类) 传入超类,以确保获取配置,并创建spring context。
- 相反,若当前环境已经使用 spring,我们应该在现有的springContext中注册Spring Security(上一步已经做将WebSecurityConfig加载至rootcontext),此方法可以什么都不做。
在init包下定义SpringSecurityApplicationInitializer:
1 | public class SpringSecurityApplicationInitializer |
默认根路径请求
在WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:
1 | // 默认Url根路径跳转到/login,此url为spring security提供 |
spring security默认提供的登录页面。
认证成功页面
在安全配置中,认证成功将跳转到/login-success,代码如下:
1 | // 配置安全拦截机制 |
spring security
支持form表单认证,认证成功后转向/login-success
。
在LoginController
中定义/login-success:
1 | "/login‐success",produces = {"text/plain;charset=UTF‐8"}) (value = |
测试
(1)启动项目,访问http://localhost:8080/security-spring-security/
路径地址
页面会根据WebConfig
中addViewControllers
配置规则,跳转至/login
,/login
是Spring Security提供的登录页面。
(2)登录
1、输入错误的用户名、密码
2、输入正确的用户名、密码,登录成功
(3)退出
1、请求/logout退出
2、退出 后再访问资源自动跳转到登录页面
授权
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。
在LoginController
添加/r/r1
或/r/r2
1 | /** |
在安全配置类 WebSecurityConfig.java中配置授权规则:
1 | .antMatchers("/r/r1").hasAuthority("p1") |
.antMatchers("/r/r1").hasAuthority("p1")
表示:访问/r/r1
资源的 url 需要拥有p1
权限。.antMatchers("/r/r2").hasAuthority("p2")
表示:访问/r/r2
资源的 url 需要拥有p2
权限。
完整的WebSecurityConfig方法如下:
1 |
|
测试:
1、登录成功
2、访问/r/r1和/r/r2,有权限时则正常访问,否则返回403(拒绝访问)
小结
通过快速上手,咱们使用Spring Security实现了认证和授权,Spring Security提供了基于账号和密码的认证方式,通过安全配置即可实现请求拦截,授权功能,Spring Security能完成的不仅仅是这些。
Spring Security 应用详解
集成SpringBoot
Spring Boot 介绍
Spring Boot
是一套Spring的快速开发框架,基于Spring 4.0
设计,使用Spring Boot
开发可以避免一些繁琐的工程搭建和配置,同时它集成了大量的常用框架,快速导入依赖包,避免依赖包的冲突。基本上常用的开发框架都支持Spring Boot
开发,例如:MyBatis、Dubbo
等,Spring
家族更是如此,例如:Spring cloud、Spring mvc、Spring security
等,使用Spring Boot
开发可以大大得高生产率,所以Spring Boot
的使用率非常高。
本章节讲解如何通过 Spring Boot
开发Spring Security
应用,Spring Boot
提供spring-boot-starter-security
用于开发Spring Security
应用。
创建maven工程
1)创建maven工程 security-spring-boot,工程结构如下:
2)引入以下依赖:
1 |
|
spring 容器配置
SpringBoot工程启动会自动扫描启动类所在包下的所有Bean,加载到spring容器。
1)Spring Boot配置文件
在resources下添加application.properties,内容如下:
1 | server.port=8080 |
2 )Spring Boot 启动类
1 |
|
Servlet Context配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebMvc与@ComponentScan,WebConfig如下:
1 |
|
视图解析器配置在application.properties
中
1 | spring.mvc.view.prefix=/WEB‐INF/views/ |
安全配置
由于Spring boot starter自动装配机制,这里无需使用@EnableWebSecurity,WebSecurityConfig内容如下:
1 |
|
测试
LoginController的内容同同Spring security入门程序。
1 |
|
测试过程:
1、测试认证
2、测试退出
3、测试授权
工作原理
结构总览
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security
时,会创建一个名为 SpringSecurityFilterChain
的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy
,它实现了javax.servlet.Filter
,因此外部的请求会经过此类,下图是Spring Security
过滤器链结构图:
FilterChainProxy
是一个代理,真正起作用的是FilterChainProxy
中SecurityFilterChain
所包含的各个Filter,同时
这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及其作用:
SecurityContextPersistenceFilter 这个Filter
是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository
中获取 SecurityContext
,然后把它设置给SecurityContextHolder
。在请求完成后将SecurityContextHolder
持有的 SecurityContext
再保存到配置好的 SecurityContextRepository
,同时清除 securityContextHolder
所持有的 SecurityContext
;
UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler
和AuthenticationFailureHandler
,这些都可以根据需求做相关改变;
FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager
对当前用户进行授权访问,前面已经详细介绍过了;
ExceptionTranslationFilter 能够捕获来自 FilterChain
所有的异常,并进行处理。但是它只会处理两类异常:
AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
认证流程
认证流程
让我们仔细分析认证过程:
- 用户提交用户名、密码被
SecurityFilterChain
中的UsernamePasswordAuthenticationFilter
过滤器获取到,封装为请求Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。 - 然后过滤器将
Authentication
提交至认证管理器(AuthenticationManager
)进行认证 - 认证成功后,
AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。 SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(…)
方法,设置到其中。
可以看出AuthenticationManager
接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager
。而Spring Security
支持多种认证方式,因此ProviderManager
维护着一个List<AuthenticationProvider>
列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider
完成的。咱们知道web表单的对应的AuthenticationProvider
实现类为DaoAuthenticationProvider
,它的内部又维护着一个UserDetailsService
负责UserDetails
的获取。最终AuthenticationProvider
将UserDetails
填充至Authentication
。
认证核心组件的大体关系如下:
AuthenticationProvider
通过前面的Spring Security认证流程我们得知,认证管理器(AuthenticationManager)委托AuthenticationProvider完成认证工作。
AuthenticationProvider是一个接口,定义如下:
1 | public interface AuthenticationProvider { |
authenticate()方法定义了认证的实现过程,它的参数是一个Authentication
,里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication
,这个Authentication
则是在认证成功后,将用户的权限及其他信息重新组装后生成。
Spring Security中维护着一个 List<AuthenticationProvider>
列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider
。如使用用户名密码登录时,使用AuthenticationProvider1
,短信登录时使用AuthenticationProvider2
等等这样的例子很多。
每个AuthenticationProvider
需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security
会生成UsernamePasswordAuthenticationToken
,它是一个Authentication
,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider
来处理它?
我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider
发现以下代码:
1 | public boolean supports(Class<?> authentication) { |
也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。
最后,我们来看一下 Authentication(认证信息)的结构,它是一个接口,我们之前提到的UsernamePasswordAuthenticationToken
就是它的实现之一:
1 | public interface Authentication extends Principal, Serializable { (1) |
(1)Authentication
是spring security
包中的接口,直接继承自Principal
类,而Principal
是位于 java.security
包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
(2)getAuthorities()
,权限信息列表,默认是GrantedAuthority
接口的一些实现类,通常是代表权限信息的一系列字符串。
(3)getCredentials()
,凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
(4)getDetails()
,细节信息,web应用中的实现接口通常为 WebAuthenticationDetails
,它记录了访问者的ip
地址和sessionId
的值。
(5)getPrincipal()
,身份信息,大部分情况下返回的是UserDetails
接口的实现类,UserDetails
代表用户的详细信息,那从Authentication
中取出来的UserDetails
就是当前登录用户信息,它也是框架中的常用接口之一。
UserDetailsService
1)认识UserDetailsService
现在咱们现在知道DaoAuthenticationProvider
处理了 web 表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现)
,里面包含了身份信息(Principal
)。这个身份信息就是一个 Object
,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider
中包含了一个UserDetailsService
实例,它负责根据用户名提取用户信息UserDetails
(包含密码),而后DaoAuthenticationProvider
会去对比UserDetailsService
提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService
公开为spring bean
来定义自定义身份验证。
1 | public interface UserDetailsService { |
很多人把 DaoAuthenticationProvider
和UserDetailsService
的职责搞混淆,其实UserDetailsService
只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider
的职责更大,它完成完整的认证流程,同时会把UserDetails
填充至Authentication
。
上面一直提到UserDetails
是用户信息,咱们看一下它的真面目:
1 | public interface UserDetails extends Serializable { |
它和Authentication
接口很类似,比如它们都拥有username,authorities
。Authentication
的getCredentials()
与UserDetails
中的getPassword()
需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication
中的getAuthorities()
实际是由UserDetails
的getAuthorities()
传递而形成的。还记得Authentication接口中的getDetails()
方法吗?其中的UserDetails
用户详细信息便是经过了AuthenticationProvider
认证之后被填充的。
通过实现UserDetailsService
和UserDetails
,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security
提供的InMemoryUserDetailsManager
(内存认证),JdbcUserDetailsManager(jdbc认证)
就是UserDetailsService
的实现类,主要区别无非就是从内存还是从数据库加载用户。
2)测试自定义UserDetailsService
1 |
|
屏蔽安全配置类中 UserDetailsService
的定义
1 | /* @Bean |
重启工程,请求认证,SpringDataUserDetailsService
的loadUserByUsername
方法被调用 ,查询用户信息。
PasswordEncoder
1)认识PasswordEncoder
DaoAuthenticationProvider
认证处理器通过UserDetailsService
获取到UserDetails
后,它是如何与请求Authentication
中的密码做对比呢?
在这里Spring Security
为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider
通过PasswordEncoder
接口的matches
方法进行密码的对比,而具体的密码对比细节取决于实现:
1 | public interface PasswordEncoder { |
而Spring Security
提供很多内置的PasswordEncoder
,能够开箱即用,使用某种PasswordEncoder
只需要进行如下声明即可,如下:
1 |
|
NoOpPasswordEncoder
采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
- 用户输入密码(明文 )
DaoAuthenticationProvider
获取UserDetails
(其中存储了用户的正确密码)DaoAuthenticationProvider
使用PasswordEncoder
对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncoder
的校验规则拿 输入的密码和UserDetails
中的正确密码进行字符串比较,字符串内容一致则校验通过,否则 校验失败。
实际项目中推荐使用BCryptPasswordEncoder
, Pbkdf2PasswordEncoder
, SCryptPasswordEncoder
等,感兴趣的大家可以看看这些PasswordEncoder的具体实现。
2)使用CryptPasswordEncoder
1、配置BCryptPasswordEncoder
在安全配置类中定义:
1 |
|
测试发现认证失败,提示:Encoded password does not look like BCrypt。
原因:
由于UserDetails
中存储的是原始密码(比如:123),它不是BCrypt
格式。
跟踪 DaoAuthenticationProvider
第 33 行代码查看 userDetails
中的内容 ,跟踪第 38 行代码查看PasswordEncoder
的类型。
2、测试BCrypt
通过下边的代码测试BCrypt加密及校验的方法
添加依赖:
1 | <dependency> |
编写测试方法:
1 | (SpringRunner.class) |
3、修改安全配置类
将UserDetails中的原始密码修改为BCrypt格式
1 | manager.createUser(User.withUsername("zhangsan").password("$2a$10$1b5mIkehqv5c4KRrX9bUj.A4Y2hug3I |
实际项目中存储在数据库中的密码并不是原始密码,都是经过加密处理的密码。
授权流程
授权流程
通过快速上手我们知道,Spring Security
可以通过 http.authorizeRequests()
对web请求进行授权保护。Spring Security
使用标准Filter
建立了对web请求的拦截,最终实现对资源的授权访问。
Spring Security
的授权流程如下:
分析授权流程:
- 拦截请求,已认证用户访问受保护的web资源将被
SecurityFilterChain
中的FilterSecurityInterceptor
的子类拦截。 获取资源访问策略,
FilterSecurityInterceptor
会从SecurityMetadataSource
的子类DefaultFilterInvocationSecurityMetadataSource
获取要访问当前资源所需要的权限Collection<ConfigAttribute>
。SecurityMetadataSource
其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:1
2
3
4
5http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...最后,
FilterSecurityInterceptor
会调用AccessDecisionManager
进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。AccessDecisionManager
(访问决策管理器)的核心接口如下:1
2
3
4
5
6
7
8public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication , Object object, Collection<ConfigAttribute>
configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
//略..
}
这里着重说明一下decide
的参数:
authentication:要访问资源的访问者的身份
object:要访问的受保护资源,web请求对应FilterInvocation
configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource
获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
AccessDecisionManager
采用投票的方式来确定是否能够访问受保护资源。
通过上图可以看出,AccessDecisionManager
中包含的一系列AccessDecisionVoter
将会被用来对Authentication
是否有权访问受保护对象进行投票,AccessDecisionManage
r根据投票结果,做出最终决策。
AccessDecisionVoter
是一个接口,其中定义有三个方法,具体结构如下所示。
1 | public interface AccessDecisionVoter<S> { |
vote()
方法的返回结果会是AccessDecisionVoter
中定义的三个常量之一。ACCESS_GRANTED
表示同意,ACCESS_DENIED
表示拒绝,ACCESS_ABSTAIN
表示弃权。如果一个AccessDecisionVoter
不能判定当前Authentication
是否拥有访问对应受保护对象的权限,则其vote()
方法的返回值应当为弃权ACCESS_ABSTAIN
。
Spring Security
内置了三个基于投票的AccessDecisionManager
实现类如下,它们分别是AffirmativeBased、ConsensusBased
和UnanimousBased
。AffirmativeBased
的逻辑是:
- 只要有
AccessDecisionVoter
的投票为ACCESS_GRANTED
则同意用户进行访问; - 如果全部弃权也表示通过;
- 如果没有一个人投赞成票,但是有人投反对票,则将抛出
AccessDeniedException
。
Spring security默认使用的是AffirmativeBased。
ConsensusBased的逻辑是:
- 如果赞成票多于反对票则表示通过。
- 反过来,如果反对票多于赞成票则将抛出
AccessDeniedException
。 - 如果赞成票与反对票相同且不等于0,并且属性
allowIfEqualGrantedDeniedDecisions
的值为true,则表示通过,否则将抛出异常AccessDeniedException
。参数allowIfEqualGrantedDeniedDecisions
的值默认为true。 - 如果所有的AccessDecisionVoter都弃权了,则将视参数
allowIfAllAbstainDecisions
的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException
。参数allowIfAllAbstainDecisions
的值默认为false。
UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter
进行投票,而UnanimousBased
会一次只传递一个ConfigAttribute
给AccessDecisionVoter
进行投票。这也就意味着如果我们的AccessDecisionVoter
的逻辑是只要传递进来的ConfigAttribute
中有一个能够匹配则投赞成票,但是放到UnanimousBased
中其投票结果就不一定是赞成了。
UnanimousBased的逻辑具体来说是这样的:
- 如果受保护对象配置的某一个
ConfigAttribute
被任意的AccessDecisionVoter
反对了,则将抛出AccessDeniedException
。 - 如果没有反对票,但是有赞成票,则表示通过。
- 如果全部弃权了,则将视参数
allowIfAllAbstainDecisions
的值而定,true则通过,false则抛出AccessDeniedException
。
Spring Security也内置一些投票者实现类如RoleVoter、AuthenticatedVoter和WebExpressionVoter
等,可以自行查阅资料进行学习。
自定义认证
Spring Security提供了非常好的认证扩展方法,比如:快速上手中将用户信息存储到内存中,实际开发中用户信息通常在数据库,Spring security可以实现从数据库读取用户信息,Spring security还支持多种授权方法。
自定义登录页面
在快速上手中,你可能会想知道登录页面从哪里来的?因为我们并没有提供任何的HTML或JSP文件。Spring Security的默认配置没有明确设定一个登录页面的URL,因此Spring Security会根据启用的功能自动生成一个登录页面URL,并使用默认URL处理登录的提交内容,登录后跳转的到默认URL等等。尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望定义自己的登录页面。
认证页面
将security-springmvc工程的login.jsp拷贝到security-springboot下,目录保持一致。
配置认证页面
在WebConfig.java中配置认证页面地址:
1 | // 默认Url根路径跳转到/login,此url为spring security提供 |
安全配置
在WebSecurityConfig中配置表章登录信息:
1 | // 配置安全拦截机制 |
(1)允许表单登录
(2)指定我们自己的登录页,spring security以重定向方式跳转到/login-view
(3)指定登录处理的URL,也就是用户名、密码表单提交的目的路径
(4)指定登录成功后的跳转URL
(5)我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个 formLogin().permitAll()
方法允许任意用户访问基于表单登录的所有的URL。
测试
当用户没有认证时访问系统的资源会重定向到login-view页面
输入账号和密码,点击登录,报错:
Whitelabel Error Page
Type=Forbidden,Status=403
问题解决:
spring security为防止CSRF(Cross-site request forgery跨站请求伪造)的发生,限制了除了get以外的大多数方法。
解决方法1:
屏蔽CSRF控制,即spring security不再限制CSRF。
配置WebSecurityConfig
1 |
|
本案例采用方法1
解决方法2:
在login.jsp页面添加一个token
,spring security会验证token
,如果token
合法则可以继续请求。
修改login.jsp
1 | <form action="login" method="post"> |
连接数据库认证
前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,本节实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。
创建数据库
创建user_db数据库
1 | CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'; |
创建t_user表
1 | CREATE TABLE `t_user` ( |
代码实现
1)定义dataSource
在application.properties配置
1 | spring.datasource.url=jdbc:mysql://localhost:3306/user_db |
2)添加依赖
1 | <dependency> |
3)定义Dao
定义模型类型,在 model包定义UserDto:
1 |
|
在Dao包定义UserDao:
1 |
|
定义UserDetailService
在service包下定义SpringDataUserDetailsService
:
1 |
|
测试
输入账号和密码请求认证,跟踪代码。
使用BCryptPasswordEncoder
按照我们前边讲的PasswordEncoder的使用方法,使用BCryptPasswordEncoder需要完成如下工作:
1、在安全配置类中定义BCryptPasswordEncoder
1 |
|
2、UserDetails中的密码存储BCrypt格式
前边实现了从数据库查询用户信息,所以数据库中的密码应该存储BCrypt格式
会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security
提供会话管理,认证通过后将身份信息放入SecurityContextHolder
上下文,SecurityContext
与当前线程进行绑定,方便获取用户身份。
获取用户身份
编写LoginController,实现/r/r1、/r/r2
的测试资源,并修改loginSuccess
方法,注意getUsername
方法,Spring
Security获取当前登录用户信息的方法为SecurityContextHolder.getContext().getAuthentication()
1 |
|
测试
登录前访问资源
被重定向至登录页面。
登录后访问资源
成功访问资源,如下:
zhangsan 访问资源1
会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 | 描述 |
---|---|
always | 总是创建一个session |
ifRequired | 如果需要就创建一个Session(默认)登录时 |
never | SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么SpringSecurity将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session,也不使用Session |
通过以下配置方式对该选项进行配置:
1 |
|
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。
若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了session,那么Spring Security会用它的。
若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建session。并且它会暗示不使用cookie
,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制。
会话超时
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s;
spring boot 配置文件:
1 | server.servlet.session.timeout=3600s |
session超时之后,可以通过Spring Security 设置跳转的路径。
1 | http.sessionManagement() |
expired
指session过期,invalidSession
指传入的sessionid无效。
安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie:
- httpOnly :如果为true,那么浏览器脚本将无法访问cookie
- secure :如果为true,则cookie将仅通过HTTPS连接发送
spring boot 配置文件:1
2server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
4.6 退出
Spring security默认实现了logout退出,访问/logout,果然不出所料,退出功能Spring也替我们做好了。
点击“Log Out”退出 成功。
退出 后访问其它url判断是否成功退出。
这里也可以自定义退出成功的页面:
在WebSecurityConfig的protected void configure(HttpSecurity http)
中配置:
1 | .and() |
当退出操作出发时,将发生:
- 使
HTTP Session
无效 - 清除
SecurityContextHolder
- 跳转到
/login -view?logout
但是,类似于配置登录功能,咱们可以进一步自定义退出功能:1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//...
.and()
.logout() (1)
.logoutUrl("/logout") (2)
.logoutSuccessUrl("/login‐view?logout") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.addLogoutHandler(logoutHandler) (5)
.invalidateHttpSession(true); (6)
}
(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
(2)设置触发退出操作的URL (默认是 /logout ).
(3)退出之后跳转的URL。默认是 /login?logout 。
(4)定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么logoutSuccessUrl() 的设置会被忽略。
(5)添加一个 LogoutHandler ,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 。
(6)指定是否在退出时让 HttpSession 无效。 默认设置为 true。
注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用post方式请求/logout
logoutHandler:
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现:
PersistentTokenBasedRememberMeServices
基于持久化token
的RememberMe功能的相关清理TokenBasedRememberMeService
基于token
的RememberMe功能的相关清理CookieClearingLogoutHandler
退出时Cookie
的相关清理CsrfLogoutHandler
负责在退出时移除csrfToken
SecurityContextLogoutHandler
退出时SecurityContext
的相关清理
链式API提供了调用相应的 LogoutHandler
实现的快捷方式,比如deleteCookies()
。
授权
概述
授权的方式包括 web授权和方法授权,web授权是通过 url拦截进行授权,方法授权是通过 方法拦截进行授权。他们都会调用accessDecisionManager
进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor
;若为方法授权则拦截器为MethodSecurityInterceptor
。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。
类关系如下:
准备环境
数据库环境
在t_user数据库创建如下表:
角色表:
1 | CREATE TABLE `t_role` ( |
用户角色关系表:
1 | CREATE TABLE `t_user_role` ( |
权限表:
1 | CREATE TABLE `t_permission` ( |
角色权限关系表:
1 | CREATE TABLE `t_role_permission` ( |
修改UserDetailService
1、修改dao接口
在UserDao中添加:
1 | // 根据用户id查询用户权限 |
2、修改UserDetailService
实现从数据库读取权限
1 |
|
web授权
在上面例子中我们完成了认证拦截,并对/r/**
下的某些资源进行简单的授权保护,但是我们想进行灵活的授权控制该怎么做呢?通过给 http.authorizeRequests()
添加多个子节点来定制需求到我们的URL,如下代码:
1 |
|
(1) http.authorizeRequests() 方法有多个子节点,每个macher按照他们的声明顺序执行。
(2)指定”/r/r1”URL,拥有p1权限能够访问
(3)指定”/r/r2”URL,拥有p2权限能够访问
(4)指定了”/r/r3”URL,同时拥有p1和p2权限才能够访问
(5)指定了除了r1、r2、r3之外”/r/**”资源,同时通过身份认证就能够访问,这里使用SpEL(Spring Expression Language)表达式。。
(6)剩余的尚未匹配的资源,不做保护。
注意:
规则的顺序是重要的,更具体的规则应该先写.现在以/admin
开始的所有内容都需要具有ADMIN角色的身份验证用户,即使是/admin/login
路径(因为/admin/login
已经被/admin/**
规则匹配,因此第二个规则被忽略).
1 | .antMatchers("/admin/**").hasRole("ADMIN") |
因此,登录页面的规则应该在/admin/**
规则之前.例如.
1 | .antMatchers("/admin/login").permitAll() |
保护URL常用的方法有:authenticated()
保护URL,需要用户登录permitAll()
指定URL无需保护,一般应用与静态资源文件hasRole(String role)
限制单个角色访问,角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较.hasAuthority(String authority)
限制单个权限访问hasAnyRole(String… roles)
允许多个角色访问.hasAnyAuthority(String… authorities)
允许多个权限访问.access(String attribute)
该方法使用 SpEL表达式, 所以可以创建复杂的限制.hasIpAddress(String ipaddressExpression)
限制IP地址或子网
方法授权
现在我们已经掌握了使用如何使用 http.authorizeRequests()
对web资源进行授权保护,从Spring Security2.0版本开始,它支持服务层方法的安全性的支持。本节学习@PreAuthorize
,@PostAuthorize
, @Secured
三类注解。我们可以在任何 @Configuration
实例上使用 @EnableGlobalMethodSecurity
注释来启用基于注解的安全性。
以下内容将启用Spring Security的 @Secured
注释。
1 | true) (securedEnabled = |
然后向方法(在类或接口上)添加注解就会限制对该方法的访问。 Spring Security的原生注释支持为该方法定义了
一组属性。 这些将被传递给AccessDecisionManager
以供它作出实际的决定:
1 | public interface BankService { |
以上配置标明readAccount
、findAccounts
方法可匿名访问,底层使用WebExpressionVoter
投票器,可从AffirmativeBased
第23行代码跟踪。。post
方法需要有TELLER
角色才能访问,底层使用RoleVoter
投票器。
使用如下代码可启用prePost
注解的支持
1 | true) (prePostEnabled = |
相应Java代码如下:
1 | public interface BankService { |
以上配置标明readAccount
、findAccounts
方法可匿名访问,post
方法需要同时拥有p_transfer
和p_read_account
权限才能访问,底层使用WebExpressionVoter
投票器,可从AffirmativeBased
第23行代码跟踪。