登录校验过程

image-20211215094003288

原理初探

想要知道如何实现自己的登陆流程就必须要先知道入门案例中SpringSecurity的流程。

SpringSecurity完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

image-20211214144425527

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。【判断你的用户名和密码是否正确】
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException 。【处理认证授权过程中的所有异常,方便统一处理】
  • FilterSecurityInterceptor:负责权限校验的过滤器。【它会判断你登录成功的用户是“谁”,“你”具有什么权限,当前访问的资源需要什么权限】

可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

image-20211214145824903

我们可以看到一共有15个过滤器。大概了解几个过滤器:

DefaultLoginPageGeneratingFilter:默认登录页就是这个过滤器显示出来的,如果不想要默认登录页,就去掉这个过滤器就可以了。

DefaultLogoutPageGeneratingFilter:用来显示默认注销的页面

认证流程详解

UsernamePasswordAuthenticationFilter这个过滤器来实现认证过程逻辑的。实际上不是它这一个类就实现了,它还通过其他类来帮助他实现的,下图就是该过滤器内部实现大致流程。

image-20211214151515385

过程详解:

当前端提交用户名和密码过来时,进入了UsernamePasswordAuthenticationFilter过滤器。

  • UsernamePasswordAuthenticationFilter过滤器里,将传进来的用户名和密码被封装成了Authentication对象【这时候最多只有用户名和密码,权限还没有】,Authentication对象通过ProviderManagerauthenticate方法进行认证。

    • ProviderManager里面,通过调用DaoAuthenticationProviderauthenticate方法进行认证。

      • DaoAuthenticationProvider里,调用InMemoryUserDetailsManagerloadUserByUsername方法查询用户。【传入的参数只有用户名字符串

        • InMemoryUserDetailsManagerloadUserByUsername方法里执行了以下操作
          1. 根据用户名查询对于用户以及这个用户的权限信息【在内存里查
          2. 把对应的用户信息包括权限信息封装成UserDetails对象
          3. 返回UserDetails对象
      • 返回给了DaoAuthenticationProvider,在这个对象里执行了以下操作

        1. 通过PasswordEncoder对比UserDetails中的密码和Authentication密码是否正确。【校验密码(经过加密的)
        2. 如果正确就把UserDetails权限信息设置到Authentication对象中。
        3. 返回Authentication对象。
  • 又回到了过滤器里面UsernamePasswordAuthenticationFilter

    1. 如果上一步返回了Authentication对象

      就使用**SecurityContextHolder.getContext().setAuthentication()**方法存储对象。

      其他过滤器会通过SecurityContextHolder来获取当前用户信息。【当前过滤器认证完了,后面的过滤器还需要获取用户信息,比如授权过滤器】

彩色字体的类均是比较重要的接口,在实现认证的过程中均需要自定义一个类来重新实现或者变更为Spring中其他实现类。

概念速查:

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户的权限等相关信息。

  • AuthenticationManager接口:定义了认证Authentication的方法 ,实现类是ProviderManager

    • 它的实现类是ProviderManager,它的功能主要是实现认证用户,因为在写登录接口时,可以通过配置类的方式,注入Spring容器中来使用它的**authenticate方法**。
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法

    • 原本的实现类是InMemoryUserDetailsManager,它是在内存中查询,因为我们需要自定义改接口。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

    • 当我们自定义UserDetailsService接口时,需要我们定义一个实体类来实现这个接口来供UserDetailsService接口返回。【注意是实体类】

实现登录认证

思路分析

登录

​ ①自定义登录接口 IMG_0414(20220818-140941)

​ 调用ProviderManager的方法进行认证 如果认证通过生成jwt

​ 把用户信息存入redis中【userId作为Key,用户信息作为Value】

​ ②自定义UserDetailsService

​ 【因为原本这个接口的实现类在内存中查询用户信息,不符合我们的要求,所以需要我们自己去实现它来自定义】

​ 在这个实现类中去查询数据库

校验:【校验的话,需要我们自己去自定义一个过滤器

IMG_0415(20220818-141117)

​ ①定义Jwt认证过滤器

​ 获取token

​ 解析token获取其中的userid

​ 从redis中获取用户信息【如果每次请求都查询数据库就很浪费时间】

​ 存入SecurityContextHolder

准备工作

添加依赖

  • spring-boot-starter-security

  • JWT

  • spring-boot-starter-data-redis

  • fastjson

  • mybatis-plus

  • mysql-connector-java

  • lombok

    对于JWT,由于版本原因还需要引入以下maven坐标。

    1
    2
    3
    4
    <dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    </dependency>

配置文件【数据库redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
datasource:
url: jdbc:mysql://192.168.37.131:3306/Security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: spring-redis
redis:
host: 192.168.37.131
port: 6379 #默认端口号
password: 123456
database: 0 #默认提供了16个数据库(可以在配置文件中改) 默认操作0号数据库,可以在命令行 select 1 选择1号数据库,
jedis:
#Redis连接池配置
pool:
max-active: 8 #最大链接数
max-wait: 1ms #连接池最大阻塞等待时间
max-idle: 4 #连接池的最大空闲连接
min-idle: 0 #连接池的最小空闲连接

添加配置类

Redis的配置类

Key序列化为String,Value序列化为json

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Redis使用FastJson序列化
*
* @author sg
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}

public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}

@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);

return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class RedisConfig {

@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);

// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);

template.afterPropertiesSet();
return template;
}
}

使用Mybatis-plus,要在启动类上加上@MapperScan注解,来配置Mapper扫描

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan("com.example.Mapper")
public class Demo1Application {

public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}

}

响应类

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
31
32
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
/**
* 状态码
*/
private Integer code;
/**
* 提示信息,如果有错误时,前端可以获取该字段进行提示
*/
private String msg;
/**
* 查询到的结果数据,
*/
private T data;

public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}

public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}

public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}

工具类

JWt的工具类还有Redis的工具类。【针对于工具类,我觉得有必要总结一个博客,以后开发肯定是常用的。代码太长就不贴了】

Redis工具类使用@Component注解来注入到Spring容器中。

实体类

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
31
32
33
34
35
36
37
38
//用户表(User)实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
@TableId
private Long id;//主键

private String userName;//用户名

private String nickName;//昵称

private String password;//密码

private String status;//账号状态(0正常 1停用)

private String email;// 邮箱

private String phonenumber;//手机号

private String sex;//用户性别(0男,1女,2未知)

private String avatar;//头像

private String userType;//用户类型(0管理员,1普通用户)

private Long createBy;//创建人的用户id

private Date createTime;//创建时间

private Long updateBy;//更新人

private Date updateTime;//更新时间

private Integer delFlag;//删除标志(0代表未删除,1代表已删除)
}

创建一个用户表

建表语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

OK准备工作就完成了….

核心代码实现

数据库校验用户

首先你要写好UserMapper接口,来实现用户查询。

我们要创建一个类UserDetailsServiceImpl来实现UserDetailsService接口,来让它实现在数据库里面查询,因为它原本的实现类是查询内存的。【在Service包中】

这个接口要使用@Service注解,注入到Spring容器中。

重写**loadUserByUsername**方法,传入了Username参数

  1. 首先要根据传入的Username参数,查询数据库

  2. 如果没有这个用户Objects.isNull(user),就抛出异常

  3. 根据用户查询权限信息

  4. 添加到**UserDetails接口的实现类**中

    1. 在domain包中创建类LoginUser,实现UserDetails接口。

    2. 完整代码如下

      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
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class LoginUser implements UserDetails {

      private User user;

      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
      return null;
      }

      @Override
      public String getPassword() {return user.getPassword();}

      @Override
      public String getUsername() {return user.getUserName();}

      @Override
      public boolean isAccountNonExpired() {return true;}

      @Override
      public boolean isAccountNonLocked() {return true;}

      @Override
      public boolean isCredentialsNonExpired() {return true;}

      @Override
      public boolean isEnabled() {return true;}
      }

UserDetailsServiceImpl实现类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService service;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据传入了Passward查询用户
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = service.getOne(queryWrapper);
//如果没有查询到用户就抛出异常
if(Objects.isNull(user)){
throw new UsernameNotFoundException("用户名不存在");
}
//TODO 查询对应的权限


return new LoginUser(user);
}
}

注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如

image-20211216123945882

这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。

密码加密存储

image-20220819003356284

实际项目中我们不会把密码明文存储在数据库中。

​ 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder

​ 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder

​ 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

​ 我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter

1
2
3
4
5
6
7
8
9
10
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

}

配置之后,我们要将数据库sys_user表的用户密码,从 1234,改为加密之后的。

image-20220819003102757

当我们进行注册时,要将密码进行加密,我们可以将PasswordEncoder注入进Controller里。下面我们进行测试演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootTest
class Demo1ApplicationTests {

@Autowired
private PasswordEncoder passwordEncoder;

@Test
void contextLoads() {
String encode = passwordEncoder.encode("1234");
System.out.println(encode);//$2a$10$OHOzZsC9RMCqJdWWpBzgfOfQZlEVedDXrUqHhp3HSINu4HghI59kq
/*
加密后的信息是动态变化的,因为我们要使用,matches()来进行比较。
*/
boolean matches = passwordEncoder.matches("1234", encode);
System.out.println(matches);//true

boolean matches2 = passwordEncoder.matches("12345", encode);
System.out.println(matches2);//false
}

}

登录接口

image-20220819004454194

接下我们需要自定义登陆接口。

  1. 放行

    登录接口需要SpringSecurity对这个接口放行【不通过过滤器链】,让用户访问这个接口的时候不用登录也能访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter{

    //...注入BCryptPasswordEncoder....

    @Override
    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();
    }
    }
  2. 接口中的认证

    • 在接口中我们通过AuthenticationManagerauthenticate方法来进行用户认证。

    • 所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter{
      //...注入BCryptPasswordEncoder....

      @Bean
      @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
      }

      //配置放行....
      }

流程:

定义一个Controller,不多说了~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private LoginService loginService;

@PostMapping("/login")
public ResponseResult<Map<String,String>> login(@RequestBody User user){
String userName = user.getUserName();
String password = user.getPassword();
return loginService.login(userName,password);
}
}

loginService.login();的核心流程:

注入**AuthenticationManagerRedisCache**

  • 调用AuthenticationManagerauthenticate方法来进行用户认证。返回Authentication

    • 使用authenticate方法,需要传入Authentication,但Authentication是接口,因此需要去找它的实现类。这里我们使用它的实现类是UsernamePasswordAuthenticationToken

      传入的Authentication只有用户名和密码

      • principal 属性为用户名
      • credentials 属性为密码
    • 使用authenticate方法,返回的Authentication

      如果不为空的话,传出的Authentication

      Principal属性是Userdetails

      credentials 属性为null

      image-20220819010124847

  • 如果Authentication为NULL,说明认证没通过,要么没查询到这个用户,要么密码比对不通过。然后就抛出异常

  • 如果认证通过,获取UserIdJwtUtil要将UserId加密成一个toekn

  • 将用户信息Authentication,存入redis

完整代码:

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
@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private RedisCache redisCache;

@Override
public ResponseResult<Map<String,String>> login(String userName, String password) {
//调用`AuthenticationManager`的方法进行认证
Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
if (Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
// 如果认证通过生成token
//获取userid
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
//生成token
String token = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:"+userId,loginUser);
//把token响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",token);
return new ResponseResult<Map<String,String>>(200,"登陆成功",map);
}
}

认证过滤器

为什么要写这么一个过滤器?

image-20211214144425527

SpringSecurity自带的过滤器中是用来认证用户名和密码的但我们并没有使用它,在配置的时候就去掉了。之前的登录接口我们生成了一个token,当前端访问后端的时候需要携带这个token。而这个过滤器就是认证token的。

自定义一个过滤器

  1. 获取请求头中的token

    1. 如果获取的token字符串为空,说明前端访问后端就没有携带token。然后放行,return

      为什么是放行而不是抛异常呢?

      因为没有携带token,有可能前端是想要登录,因此不能抛异常。

      就算是要访问其他资源,我们直接放行,Authentication对象没有用户任何信息,后面的过滤器也会抛出异常。后面也不会进行认证。

  2. 使用JwtUtiltoken进行解析取出其中的userid

    如果token解析失败,说明前端携带的token不合法,就会抛出异常。

  3. 使用useridredis中获取对应的LoginUser对象。

  4. 然后封装Authentication对象存入SecurityContextHolder

    在封装Authentication时,使用的实现类是UsernamePasswordAuthenticationToken,使用的构造方法是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public 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
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
31
32
33
34
35
36
37
38
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
private RedisCache redisCache;

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//首先需要获取token
String token = httpServletRequest.getHeader("token");
//判断token是否为Null
if(!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//如何不为空,解析token,获得了UserId
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("toen非法");
}
//根据UserId查redis获取用户数据
String key = "login:"+userId;
LoginUser LoginUser = redisCache.getCacheObject(key);
if(Objects.isNull(LoginUser)){
throw new RuntimeException("用户未登录");
}
//然后封装Authentication对象存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(LoginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

注意:

该过滤器实现的接口并不是之前的Filter,而是去继承OncePerRequestFilter

OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤他能够确保在一次请求只通过一次filter,而不需要重复执行

ServletFilter可能会执行多次。

然后我们将过滤器加到UsernamePasswordAuthenticationFilter的前面,在配置类中进行配置。

1
2
3
4
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

退出登录

定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private LoginService loginService;

@GetMapping("/logout")
public ResponseResult logout(){
return loginService.logout();
}

}

Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private RedisCache redisCache;


@Override
public ResponseResult logout() {
//获取Authentication
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser Loginuser = (LoginUser) authentication.getPrincipal();
User user = Loginuser.getUser();
String userId = user.getId().toString();
//清除cache
redisCache.deleteObject("login:"+userId);
return new ResponseResult(200,"注销成功");
}
}

总结

是不是感觉有点乱,让我们缕一缕。先说一下我们对哪些接口进行了实现,或者是更改

  • 我们自定义了UserDetailsService接口,来实现数据库查询。当中用到了**UserDetails接口的实现类——LoginUser**。

  • UserDetailsService接口是上一层面,我们需要对密码进行解密解析并对比。因为我们使用了**PasswordEncoder接口的其他实现类BCryptPasswordEncoder**。

  • 在实现登录接口的时候

    • 需要AuthenticationManagerauthenticate方法进行认证。

    • 传入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过滤器来认证。

图例演示

  1. 当我们登录时

    IMG_0418(20220819-175440)

  2. 登录之后,前端访问其他资源

    • 携带token

      IMG_0421(20220819-181345)

      通过DEBUG发现,如果携带token。

      • context域中存入已认证的Authentication,就不会访问UserDetailsServiceImpl

      • context域中存入未认证的Authentication,就会访问UserDetailsServiceImpl,来进行认证。

      至于为什么,我也不清楚….( ̄ ‘i  ̄;)

    • 未携带token

      image-20220819181740017

补充说明

在我学习这个登录认证的过程中,我一直有一个疑惑,就是感觉这个SpringSecurity自带的认证处理器好像没有”干活”的样子。

它的作用仅仅是“借用了”它内部的东西(方法)来进行认证。

到了三更老师讲到其他认证方案时,我才明白,当我们使用配置类时,就**去掉了UsernamePasswordAuthenticationFilter**。

因为

1
2
3
4
5
6
7
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}

​ 看一下这个super.configure(http);这个父类的方法,它进行了默认的配置

1
2
3
4
5
6
7
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest())
.authenticated().and())
.formLogin().and()) //formLogin()......
.httpBasic();
}

查看这个formLogin()方法:

1
2
3
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());
}

再查看FormLoginConfigurer()这个方法。

1
2
3
4
5
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), (String)null);
this.usernameParameter("username");
this.passwordParameter("password");
}

我们可以看到,它添加了UsernamePasswordAuthenticationFilter()这个过滤器。

但是我们配置的时候,去掉了super.configure(http);,也就是说不使用默认配置

那也就是说,不添加了UsernamePasswordAuthenticationFilter()这个过滤器。所以我们根本没使用这个过滤器。

如果有什么问题,欢迎评论区留言,思路整理了好久…..( ̄ ‘i  ̄;)

__END__