1.前言
这是本系列的第二篇文章,上一篇文章主要讲了一个入门例子,全系列传送门如下:
第一篇:Spring Security零基础入门之一。
第二篇:Spring Security零基础入门之二~验证
第三篇:Spring Security零基础入门之三~使用数据库进行验证
第四篇:Spring Security零基础入门之四~鉴权
Spring Security主要包括两大功能:验证和鉴权。验证就是确认用户的身份,一般采用用户名和密码的形式;鉴权就是确认用户拥有的身份(角色、权限)能否访问受保护的资源。本文主要讲述如何在Spring Security中进行验证功能的开发,阅读本文需要的基础知识:
- 熟练掌握Java
- 掌握Spring Boot基础知识
- 一点点前端知识包括html/css/javascript
- 了解一点后端框架thymeleaf
2.需求以及示例代码
本文是根据第一篇文章进行的改进,在第一篇文章中,实现了以下需求:
- 网站分为首页、登录页、用户页面、管理员页面和报错页面;
- 使用用户名加密码登录,登录错误要报错;
- 不同的用户拥有不同的权限,不同的权限可以访问不同的网页;
- 首页和登录页不需要任何权限;
- 用户页面需要USER权限;
- 管理员页面需要ADMIN权限;
- 如果用户没有登录,则访问需要权限的页面时自动跳转登录页面。
为了让读者掌握具体的验证原理、流程和代码,本文加入了以下三个功能:
- 修改用户名密码的参数名称
- 通过自定义一个AuthenticationProvider在系统中加入一个后门
- 将验证身份信息展示到前端
代码在我的github中:
apkkids/spring_security_examgithub.com
该项目一共包含四个可独立运行的子项目:本文对应的是normal_security子项目。从Github下载代码后,进入normal_security项目直接运行即可观察效果。
能力强的同学可以直接去看代码了,但还是建议先看下面的原理解释,我将在文章的最后给出代码解释和执行界面展示。
3.原理图
由于Spring Security本身关于验证的架构较为复杂,设计的原理、概念、流程和实现代码很多,因此先画了一个思维导图帮助理解,如下图所示:
图1 Spring Security原理图
各部分内容将在后面一一解释。
4.参与验证的要素
Spring Security不仅仅支持网页登录这一种验证模式,它还支持多种其他验证模式。但是其参与验证的要素基本上还是用户、密码、角色、受保护的资源这四种。
用户名称一般在前端由访问者填入,而系统用户在后端一般存储在内存中或数据库中。
密码一般在前端由访问者填入,用于验证用户身份,在Security 5.0后密码必须使用PasswordEncoder加密,一般来说使用BCryptPasswordEncoder即可。
角色,又可以被看做权限,在Spring Security中,有时用Role表示,有时用Authority表示。前端一般看不到系统的角色。后端角色由管理者赋予给用户(可以事先赋予,也可以动态赋予),角色信息一般存储在内存或数据库中。
受保护的资源,一般来说就是指网址,有时也可以将某些函数方法定义为资源,但本文不涉及这类情况。
参与验证的要素(用户名、密码)在前端由表单提交,由网络传入后端后,会形成一个Authentication类的实例。(尽管我很讨厌直接介绍各种Class和Interface,但是本文不可避免的要涉及到比较多的类,本文会尽量只介绍最重要的)该实例在进行验证前,携带了用户名、密码等信息;在验证成功后,则携带了身份信息、角色等信息。Authentication接口代码节选如下:
public
其中getCredentials()返回一个Object credentials,它代表验证凭据,即密码;getPrincipal()返回一个Object principal,它代表身份信息,即用户名等;getAuthorities()返回一个Collection<? extends GrantedAuthority>,它代表一组已经分发的权限,即本次验证的角色(本文中权限和角色可以通用)集合。
有了Authentication实例,则验证流程主要围绕这个实例来完成。它会依次穿过整个验证链,并存储在SecurityContextHolder中。也可以像本文中的代码一样,在验证途中伪造一个Authentication实例,骗过验证流程,获得所有权限。
5.验证的规则
验证规则定义了以下几个东西:
- 受保护的资源即网址,它们一般按访问所需权限分为几类
- 哪一类资源可以由哪些角色访问
- 规则定义在WebSecurityConfigurerAdapter的子类中
具体规则的定义方法可以在代码中观察。
6.验证流程
前文已经介绍了Authentication类,它代表了验证信息。
再介绍一个类AuthenticationManager,它是验证管理类的总接口;而具体的验证管理需要ProviderManager类,它具有一个List<AuthenticationProvider> providers属性,这实际上是一个AuthenticationProvider实例构成的验证链。链上都是各种AuthenticationProvider实例,这些实例进行具体的验证工作,它们之间的关系如下图(图来自互联网)所示:
图2 验证管理类关系图
验证成功后,验证实例Authentication会被存入SecurityContextHolder中,而它则利用线程本地存储TLS功能。在验证成功且验证未过期的时间段内,验证会一直有效。而且,可以在需要的地方,从SecurityContextHolder中取出验证信息,并进行操作。例如将验证信息展示在前端。
具体的验证流程如下:
- 后端从前端的表单得到用户密码,包装成一个Authentication类的对象;
- 将Authentication对象传给“验证管理器”ProviderManager进行验证;
- ProviderManager在一条链上依次调用AuthenticationProvider进行验证;
- 验证成功则返回一个封装了权限信息的Authentication对象(即对象的Collection<? extends GrantedAuthority>属性被赋值);
- 将此对象放入安全上下文SecurityContext中;
- 需要时,可以将Authentication对象从SecurityContextHolder上下文中取出。
注意,在ProviderManager管理的验证链上,任何一个AuthenticationProvider通过了验证,则验证成功。所以,要在系统中留一个后门,只需要在代码中添加一个AuthenticationProvider的子类BackdoorAuthenticationProvider,并在输入特定的用户名(alex)时,直接伪造一个验证成功的Authentication,即可通过验证,代码如下:
@Component
然后在SecurityConfiguration类中,将BackdoorAuthenticationProvider的实例加入到验证链中即可,代码如下:
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
BackdoorAuthenticationProvider backdoorAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
...省略
//将自定义验证类注册进去
auth.authenticationProvider(backdoorAuthenticationProvider);
}
...省略
}
7.代码解释
1)修改用户名密码的参数名称
在前端login.html:
<!-- 1.自定义参数名:myusername和mypassword-->
在后端SecurityConfiguration.java:
@Override
2)通过自定义一个AuthenticationProvider加入一个后门
上一节中已经详细介绍了这个功能的实现。留下后门后,使用用户名alex,配合任何密码,都可以成功登陆并获得管理员权限,而且登陆后的页面上显示的也是admin用户。
3)将验证身份信息展示到前端
前面已经提到,验证成功后,验证信息存入SecurityContextHolder中。因此可以在需要的地方,将其提取出来,然后在前端展示出来。
后端代码:
@Controller
前端代码:
<
8.小结
关于验证的内容实在是比较多,因此文本中依然没有介绍如何利用数据库中存储的信息进行验证,那将会在下一篇文章中介绍。