登录校验过程

原理初探
想要知道如何实现自己的登陆流程就必须要先知道入门案例中SpringSecurity的流程。
SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。【判断你的用户名和密码是否正确】ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。【处理认证授权过程中的所有异常,方便统一处理】FilterSecurityInterceptor:负责权限校验的过滤器。【它会判断你登录成功的用户是“谁”,“你”具有什么权限,当前访问的资源需要什么权限】
可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

我们可以看到一共有15个过滤器。大概了解几个过滤器:
DefaultLoginPageGeneratingFilter:默认登录页就是这个过滤器显示出来的,如果不想要默认登录页,就去掉这个过滤器就可以了。
DefaultLogoutPageGeneratingFilter:用来显示默认注销的页面
认证流程详解
UsernamePasswordAuthenticationFilter这个过滤器来实现认证过程逻辑的。实际上不是它这一个类就实现了,它还通过其他类来帮助他实现的,下图就是该过滤器内部实现大致流程。

过程详解:
当前端提交用户名和密码过来时,进入了UsernamePasswordAuthenticationFilter过滤器。
在
UsernamePasswordAuthenticationFilter过滤器里,将传进来的用户名和密码被封装成了Authentication对象【这时候最多只有用户名和密码,权限还没有】,Authentication对象通过ProviderManager的authenticate方法进行认证。在
ProviderManager里面,通过调用DaoAuthenticationProvider的authenticate方法进行认证。在
DaoAuthenticationProvider里,调用InMemoryUserDetailsManager的loadUserByUsername方法查询用户。【传入的参数只有用户名字符串】- 在
InMemoryUserDetailsManager的loadUserByUsername方法里执行了以下操作- 根据用户名查询对于用户以及这个用户的权限信息【在内存里查】
- 把对应的用户信息包括权限信息封装成
UserDetails对象。 - 返回
UserDetails对象。
- 在
返回给了
DaoAuthenticationProvider,在这个对象里执行了以下操作- 通过
PasswordEncoder对比UserDetails中的密码和Authentication密码是否正确。【校验密码(经过加密的)】 - 如果正确就把
UserDetails的权限信息设置到Authentication对象中。 - 返回
Authentication对象。
- 通过
又回到了过滤器里面
UsernamePasswordAuthenticationFilter。如果上一步返回了
Authentication对象就使用**
SecurityContextHolder.getContext().setAuthentication()**方法存储对象。其他过滤器会通过
SecurityContextHolder来获取当前用户信息。【当前过滤器认证完了,后面的过滤器还需要获取用户信息,比如授权过滤器】
彩色字体的类均是比较重要的接口,在实现认证的过程中均需要自定义一个类来重新实现或者变更为Spring中其他实现类。
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户的权限等相关信息。AuthenticationManager接口:定义了认证Authentication的方法 ,实现类是ProviderManager- 它的实现类是
ProviderManager,它的功能主要是实现认证用户,因为在写登录接口时,可以通过配置类的方式,注入Spring容器中来使用它的**authenticate方法**。
- 它的实现类是
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。- 原本的实现类是
InMemoryUserDetailsManager,它是在内存中查询,因为我们需要自定义改接口。
- 原本的实现类是
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。- 当我们自定义
UserDetailsService接口时,需要我们定义一个实体类来实现这个接口来供UserDetailsService接口返回。【注意是实体类】
- 当我们自定义
实现登录认证
思路分析
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中【userId作为Key,用户信息作为Value】
②自定义UserDetailsService
【因为原本这个接口的实现类是在内存中查询用户信息,不符合我们的要求,所以需要我们自己去实现它来自定义】
在这个实现类中去查询数据库
校验:【校验的话,需要我们自己去自定义一个过滤器】
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息【如果每次请求都查询数据库就很浪费时间】
存入SecurityContextHolder
准备工作
添加依赖
spring-boot-starter-securityJWTspring-boot-starter-data-redisfastjsonmybatis-plusmysql-connector-javalombok对于
JWT,由于版本原因还需要引入以下maven坐标。1
2
3
4<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
配置文件【数据库和redis】
1 | spring: |
添加配置类
Redis的配置类
Key序列化为String,Value序列化为json
1 | /** |
1 |
|
使用Mybatis-plus,要在启动类上加上@MapperScan注解,来配置Mapper扫描
1 |
|
响应类
1 |
|
工具类
JWt的工具类还有Redis的工具类。【针对于工具类,我觉得有必要总结一个博客,以后开发肯定是常用的。代码太长就不贴了】
Redis工具类使用@Component注解来注入到Spring容器中。
实体类
1 | //用户表(User)实体类 |
创建一个用户表
建表语句如下:
1 | CREATE TABLE `sys_user` ( |
OK准备工作就完成了….
核心代码实现
数据库校验用户
首先你要写好UserMapper接口,来实现用户查询。
我们要创建一个类UserDetailsServiceImpl来实现UserDetailsService接口,来让它实现在数据库里面查询,因为它原本的实现类是查询内存的。【在Service包中】
这个接口要使用@Service注解,注入到Spring容器中。
重写**loadUserByUsername**方法,传入了Username参数
首先要根据传入的
Username参数,查询数据库如果没有这个用户
Objects.isNull(user),就抛出异常根据用户查询权限信息
添加到**
UserDetails接口的实现类**中在domain包中创建类
LoginUser,实现UserDetails接口。完整代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LoginUser implements UserDetails {
private User user;
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public String getPassword() {return user.getPassword();}
public String getUsername() {return user.getUserName();}
public boolean isAccountNonExpired() {return true;}
public boolean isAccountNonLocked() {return true;}
public boolean isCredentialsNonExpired() {return true;}
public boolean isEnabled() {return true;}
}
UserDetailsServiceImpl实现类如下
1 |
|
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如

这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。
密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
1 |
|
配置之后,我们要将数据库sys_user表的用户密码,从 1234,改为加密之后的。

当我们进行注册时,要将密码进行加密,我们可以将PasswordEncoder注入进Controller里。下面我们进行测试演示。
1 |
|
登录接口
接下我们需要自定义登陆接口。
放行
登录接口需要让
SpringSecurity对这个接口放行【不通过过滤器链】,让用户访问这个接口的时候不用登录也能访问。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//...注入BCryptPasswordEncoder....
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()//允许匿名用户访问,不允许已登入用户访问
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}接口中的认证
在接口中我们通过
AuthenticationManager的authenticate方法来进行用户认证。所以需要在
SecurityConfig中配置把AuthenticationManager注入容器。1
2
3
4
5
6
7
8
9
10
11
12
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//...注入BCryptPasswordEncoder....
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置放行....
}
流程:
定义一个Controller,不多说了~
1 |
|
loginService.login();的核心流程:
注入**AuthenticationManager和RedisCache**
调用
AuthenticationManager的authenticate方法来进行用户认证。返回Authentication使用
authenticate方法,需要传入Authentication,但Authentication是接口,因此需要去找它的实现类。这里我们使用它的实现类是UsernamePasswordAuthenticationToken。传入的
Authentication只有用户名和密码:principal属性为用户名credentials属性为密码
使用
authenticate方法,返回的Authentication。如果不为空的话,传出的
Authentication:Principal属性是Userdetailscredentials属性为null
如果
Authentication为NULL,说明认证没通过,要么没查询到这个用户,要么密码比对不通过。然后就抛出异常。如果认证通过,获取
UserId,JwtUtil要将UserId加密成一个toekn。将用户信息
Authentication,存入redis。
完整代码:
1 |
|
认证过滤器
为什么要写这么一个过滤器?
SpringSecurity自带的过滤器中是用来认证用户名和密码的但我们并没有使用它,在配置的时候就去掉了。之前的登录接口我们生成了一个token,当前端访问后端的时候需要携带这个token。而这个过滤器就是认证token的。
自定义一个过滤器
获取请求头中的
token如果获取的
token字符串为空,说明前端访问后端就没有携带token。然后放行,return为什么是放行而不是抛异常呢?
因为没有携带token,有可能前端是想要登录,因此不能抛异常。
就算是要访问其他资源,我们直接放行,
Authentication对象没有用户任何信息,后面的过滤器也会抛出异常。后面也不会进行认证。
使用
JwtUtil对token进行解析取出其中的userid。如果
token解析失败,说明前端携带的token不合法,就会抛出异常。使用
userid去redis中获取对应的LoginUser对象。然后封装
Authentication对象存入SecurityContextHolder。在封装
Authentication时,使用的实现类是UsernamePasswordAuthenticationToken,使用的构造方法是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
private final Object principal;
private Object credentials;
//.....
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);//标志为已认证状态,这样就不用再让`UsernamePasswordAuthenticationFilter`过滤器再进行认证了。
}
//.....
}
代码:
1 |
|
注意:
该过滤器实现的接口并不是之前的
Filter,而是去继承OncePerRequestFilter。
OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。他能够确保在一次请求只通过一次filter,而不需要重复执行而
Servlet的Filter可能会执行多次。
然后我们将过滤器加到UsernamePasswordAuthenticationFilter的前面,在配置类中进行配置。
1 |
|
退出登录
定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
Controller层
1 |
|
Service层
1 |
|
总结
是不是感觉有点乱,让我们缕一缕。先说一下我们对哪些接口进行了实现,或者是更改
我们自定义了
UserDetailsService接口,来实现数据库查询。当中用到了**UserDetails接口的实现类——LoginUser**。在
UserDetailsService接口是上一层面,我们需要对密码进行解密解析并对比。因为我们使用了**PasswordEncoder接口的其他实现类BCryptPasswordEncoder**。在实现登录接口的时候
需要
AuthenticationManager的authenticate方法进行认证。传入
Authentication接口的实现类是UsernamePasswordAuthenticationToken。构造方法如下1
public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
这表示该
Authentication是未认证的。之后会通过UsernamePasswordAuthenticationFilter过滤器来认证。
在实现认证过滤器时,
需要使用
SecurityContextHolder.getContext().setAuthentication()方法,将用户信息Authentication存进去。方便其他Filter使用。传入
Authentication接口的实现类是UsernamePasswordAuthenticationToken。构造方法是1
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
这表示该
Authentication是认证的。之后就不会通过UsernamePasswordAuthenticationFilter过滤器来认证。
图例演示
当我们登录时
登录之后,前端访问其他资源
携带token
通过DEBUG发现,如果携带token。
context域中存入已认证的Authentication,就不会访问
UserDetailsServiceImpl。context域中存入未认证的Authentication,就会访问
UserDetailsServiceImpl,来进行认证。
至于为什么,我也不清楚….( ̄ ‘i  ̄;)
未携带token

补充说明
在我学习这个登录认证的过程中,我一直有一个疑惑,就是感觉这个SpringSecurity自带的认证处理器好像没有”干活”的样子。
它的作用仅仅是“借用了”它内部的东西(方法)来进行认证。
到了三更老师讲到其他认证方案时,我才明白,当我们使用配置类时,就**去掉了UsernamePasswordAuthenticationFilter**。
因为
1 |
|
看一下这个super.configure(http);这个父类的方法,它进行了默认的配置
1 | protected void configure(HttpSecurity http) throws Exception { |
查看这个formLogin()方法:
1 | public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { |
再查看FormLoginConfigurer()这个方法。
1 | public FormLoginConfigurer() { |
我们可以看到,它添加了UsernamePasswordAuthenticationFilter()这个过滤器。
但是我们配置的时候,去掉了super.configure(http);,也就是说不使用默认配置了
那也就是说,不添加了UsernamePasswordAuthenticationFilter()这个过滤器。所以我们根本没使用这个过滤器。
如果有什么问题,欢迎评论区留言,思路整理了好久…..( ̄ ‘i  ̄;)
__END__