Spring-Security 入门
SpringSecurity的原理
本质上是一个过滤器链,内部包含了提供各种功能的过滤器。
常见的三种过滤器:
UsernamePasswordAuthenticationFilter
:负责处理我们在登录页面填写了用户名密码后的登录请求ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
FilterSecurityInterceptor
:负责权限校验的过滤器
通过 debug 可以查看当前系统过滤器链中有哪些过滤器以及它们的顺序
各个 Filter 的作用浏览
WebAsyncManagerIntegrationFilter
- 提供了对
SecurityContext
和WebAsyncManager
的集成,其会把SecurityContext
设置到异步线程中,使其也能获取到用户上下文认证信息。
- 提供了对
SecurityContextPersistenceFilter
- 该类在所有的 Filter 之前,从
SecurityContextRepository
中取出用户认证信息,默认实现类为HttpSessionSecurityContextRepository
,其会从 Session 中取出已认证用户的信息,提高效率,避免每一次请求都要查询用户认证信息。 - 取出之后会放入
SecurityContextHolder
中,以便其他 Filter 使用,SecurityContextHolder
使用ThreadLocal
存储用户认证信息,保证了线程之间的信息隔离,最后在finally
中清除该信息。 - 可以配置 http 的
security-context-repository-ref
属性来自行控制获取到已认证用户信息的方式,比如使用 Redis 存储 session 等。当不使用 session 的话,最好配置为NullSecurityContextRepository
,避免占用服务器内存。
- 该类在所有的 Filter 之前,从
HeaderWriterFilter
- 其会往该请求的 Header 中添加相应的信息,在 http 标签内部使用
security:headers
来控制。
- 其会往该请求的 Header 中添加相应的信息,在 http 标签内部使用
CsrfFilter
- 验证方式是通过客户端传来的 token 与服务端存储的 token 进行对比,来判断是否为伪造请求。
LogoutFilter
- 匹配 URL,默认为
/logout
,匹配成功后则用户退出,清除认证信息。如果有自己的退出逻辑,那么这个过滤器可以 disable。
- 匹配 URL,默认为
UsernamePasswordAuthenticationFilter
- 登录认证过滤器,默认是对
/login
的 POST 请求进行认证,首先该方法会先调用attemptAuthentication
尝试认证获取一个Authentication
的认证对象,然后通过sessionStrategy.onAuthentication
执行持久化,也就是保存认证信息,转向下一个 Filter,最后调用successfulAuthentication
执行认证后事件。 attemptAuthentication
- 该方法是认证的主要方法,认证是委托配置的
authentication-manager -> authentication-provider
进行。 - 比如对于该 Demo 配置的为如下,则默认使用的 manager 为
ProviderManager
,使用的 provider 为DaoAuthenticationProvider
,userDetailService
为InMemoryUserDetailsManager
,也就是从内存中获取用户认证信息,也就是下面 xml 配置的 user 与 admin 信息。 - 认证基本流程为
UserDeatilService
根据用户名获取到认证用户的信息,然后通过UserDetailsChecker.check
对用户进行状态校验,最后通过additionalAuthenticationChecks
方法对用户进行密码校验成功后完成认证,返回一个认证对象。
- 该方法是认证的主要方法,认证是委托配置的
- 登录认证过滤器,默认是对
DefaultLoginPageGeneratingFilter
- 当请求为登录请求时,生成简单的登录页面返回,有自己的登录逻辑的话同样可以 disable。
BasicAuthenticationFilter
- Http Basic 认证的支持,该认证会把用户名密码使用 base64 编码后放入 header 中传输,如下所示,认证成功后会把用户信息放入
SecurityContextHolder
中。
- Http Basic 认证的支持,该认证会把用户名密码使用 base64 编码后放入 header 中传输,如下所示,认证成功后会把用户信息放入
RequestCacheAwareFilter
- 恢复被打断的请求,具体未研究。
SecurityContextHolderAwareRequestFilter
- 针对 Servlet api 不同版本做的一些包装。
AnonymousAuthenticationFilter
- 当
SecurityContextHolder
中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder
中。
- 当
SessionManagementFilter
- 与登录认证拦截时作用一样,持久化用户登录信息,可以保存到 session 中,也可以保存到 cookie 或者 Redis 中。
ExceptionTranslationFilter
- 异常拦截,其处在 Filter 链后部分,只能拦截其后面的节点并且着重处理
AuthenticationException
与AccessDeniedException
两个异常。可以在此处定义一个 entryPoint,对错误请求返回 403。
- 异常拦截,其处在 Filter 链后部分,只能拦截其后面的节点并且着重处理
FilterSecurityInterceptor
- 主要是授权验证,方法为
beforeInvocation
,在其中调用 - 获取到所配置资源访问的授权信息,对于上述配置,获取到的则为
hasRole('ROLE_USER')
,然后根据SecurityContextHolder
中存储的用户信息来决定其是否有权限,没权限则返回 403。具体想了解可以关注HttpConfigurationBuilder.createFilterSecurityInterceptor()
方法,分析其创建流程加载了哪些数据,或者分析SecurityExpressionOperations
的子类,其是权限鉴定的实现方法。
- 主要是授权验证,方法为
重点关注
- 登录验证
UsernamePasswordAuthenticationFilter
- 访问验证
BasicAuthenticationFilter
- 权限验证
FilterSecurityInterceptor
再导入 SpringSecurity 以来之后,访问接口会默认跳转到一个 SpringSecurity 自带的登录页面。
可以通过配置 SecurityConfig
来自定义的设置密码校验的过程。
通过配置 Bean UserDetailsService
来用户数据进行认证。
通过配置 Bean PasswordEncoder
来对密码进行加密。
在实际开发中,可以自定义认证逻辑,自定义的逻辑需要实现 UserDetailsService
接口,在 SpringSecurity 中传入。
具体而言,自定义的 UserDetailsService
实现的是 loadUserByUsername(String username)
方法,我们需要根据传递的用户名查询到该用户(一般是从数据库查询,缓存也可以)并将查询到的用户封装为一个 UserDetails
对象,该对象由 SpringSecurity 提供,包含用户名、密码、权限。SpringSecurity 会根据 UserDetails
对象中的密码和客户端提供的密码进行比较。相同则认证通过。
准备测试的数据库
CREATE TABLE `users` ( |
创建 UserDetailsService 的实现类,编写自定义认证逻辑
在实际开发中,为了数据的安全性,一般不会在数据库中存放密码原文,而是会存放加密后的密码。用户传入的参数是明文密码。此时必须使用密码解析器才能将加密密码与明文密码做对比。Spring Security 中的密码解析器是 PasswordEncoder
。
SpringSecurity 要求 容器中必须有 PasswordEncoder
实例,之前使用的 NoOpPasswordEncoder
是 PasswordEncoder
的实现类,意思是不解析密码,使用明文密码。
SpringSecurity 官方推荐的密码解析器是 BCryptPasswordEncoder
。
在实际开发中,将 BCryptPasswordEncoder
的实例放入 Spring 容器即可,并且在用户注册完成后,将密码加密再保存到数据库。
如何自定义加密算法?
- 继承
PasswordEncoder
接口 - 加解密本身在 SpringSecurity 中是高度独立的。
- 可以在其他地方使用。
SpringSecurity认证,自定义登陆界面
虽然 Spring Security 给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security 也支持用户自定义登录页面。用法如下:
- 编写登录页面
- 在 Spring Security 配置类自定义登录页面
|
CSRF 防护
CSRF:跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。Spring Security 为了防止 CSRF 攻击,默认开启了 CSRF 防护,CSRF 防护限制了除了 GET 请求以外的大多数方法。我们要想正常使用 Spring Security 需要突破 CSRF 防护。
突破方法
- 关闭 CSRF 防护
http.csrf().disable();
- 突破 CSRF 防护
- 请求在访问的时候需要携带参数名为
_csrf
值为令牌的请求头,令牌在服务器产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问。
- 请求在访问的时候需要携带参数名为
Spring Security 认证_会话管理
- 用户认证通过后,我们可能需要获取用户信息,SpringSecurity 将用户信息保存再会话中,并提供会话管理,我们可以从
SecurityContext
对象中获取用户信息,SecurityContext 对象与当前线程绑定
登录成功之后的处理方式
登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。
- 自定义登录成功处理器
- 配置登录成功处理器
认证失败后的处理方式
登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数,记录日志等,可以自定义登录失败处理器。
- 自定义登录失败处理器
- 配置登录失败处理器
系统中一般都有退出登录的操作,退出登陆后,SpringSecurity 进行了以下操作:
- 清除认证状态
- 销毁
HttpSession
对象 - 跳转到登录页面
在 Spring Security 中,退出登录的写法如下:
配置退出登录的路径和退出后跳转的路径
在网页中添加退出登录超链接
SpringSecurity 中也可以配置退出成功处理器
处理器需要实现 LogoutSuccessHandler
接口,之后在 SecurityConfig
中添加。
SpringSecurity 认证 Remember me
具体实现方式
- 编写“记住我”配置类
- 修改
Security
配置类 - 在登录页面添加“记住我”复选框
资源的访问控制
在给用户授权后,我们就可以给系统中的资源设置访问控制,即拥有什么权限才能访问什么资源。
一般控制访问权限的方式
- 在配置类中设置
- 自定义访问控制逻辑
- 在配置文件中使用自定义访问控制逻辑
注解设置访问控制
除了配置类,在 SpringSecurity 中提供了一些访问控制的注解,这些注解默认都是不可用的,需要开启后使用。
@Secure
:基于角色的权限控制,要求
UserDetails
中的权限名必须以
ROLE_
开头。
@PreAuthorize
:可以在方法执行前判断用户是否具有权限。
需要开启
自定义403处理逻辑
- 制作权限不足处理类
- 在 SpringSecurity 中添加
在前端实现访问控制
SpringSecurity 可以在一些视图技术中进行控制显示效果。例如在 Thymeleaf 中,只有登录用户拥有某些权限才会展示一些菜单。
在 pom.xml
中引入 Spring Security 和 Thymeleaf 的整合依赖
<!-- Spring Security 整合 Thymeleaf --> |
在 Thymeleaf 中使用 Security 标签,控制前端的显示内容
|
pringSecurityFilter 运行总结
FilterComparator
比较器中初始化了 Spring Security 自带的 Filter 的顺序,即在创建时已经确定了默认 Filter 的顺序。并将所有过滤器保存在一个filterToOrder
Map 中。key 值是 Filter 的类名,value 是过滤器的顺序号。- 当我们调用
HttpSecurity#addFilterAt(A, B.class)
方法时(其中 B 一定是先于 A 添加,或者 B 本身就是默认的过滤器),它会将我们添加的过滤器 A 在FilterComparator
中,并给给我们一个和 B 相同的序号(addFilterBefore(A, B.class)
给 A 的序号比 B 小1,addFilterAfter(A, B.class)
给 A 的序号比 B 大1)。同时,HttpSecurity#addFilter(Filter filter)
会将我们添加的过滤器添加在filters
List 集合中,而在 List 集合中我们手动添加的拦截器在除了WebAsyncManagerIntegrationFilter
之外的所有系统默认的拦截器之前。 - 最后 Spring Security 会调用
HttpSecurity#performBuild
方法,在这里会使用FilterComparator
比较器对filters
进行比较排序,序号小的在前,序号大的在后,序号相等则按照原先的filters
中的顺序。 - 由于在
filters
List 集合中,我们自己添加的过滤器要在除了WebAsyncManagerIntegrationFilter
之外的所有系统默认的拦截器之前。导致了当我们调用了HttpSecurity#addFilterAt(A, B.class)
方法时,A 拦截器要先于 B 拦截器执行。