课程链接:SpringSecurity框架教程
开始时间:2022-07-17
快速入门
搭建一个Spring Boot项目
添加基础依赖和创建启动类和controller
controller
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
启动类
package com.bupt.security_test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecurityTestApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityTestApplication.class, args);
}
}
此时访问页面localhost:8080/hello
可以看到显示hello这一个单词
引入Security的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
此时我们再去访问
localhost:8080/hello
会自动跳转到security带的登录界面
登录名默认是user
密码会在控制台打印给你
如果输入错误的账户密码
输入正确
默认有一个退出的接口logout
认证
登录校验流程图
Spring Security流程
本质是一个过滤器链
-
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
-
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。 发现异常并重定向
-
FilterSecurityInterceptor:负责权限校验的过滤器。(他实现了过滤器接口)
没想到还有重温之前学拦截器的部分,参考博客
这里只是选了三个典型的,还有其他过滤器,暂不研究
我们可以查看有哪些过滤器
认证流程详解
区分一下概念
-
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
-
AuthenticationManager接口:定义了认证Authentication的方法
-
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
-
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
-
第一步:提交用户名和密码 传入到UsernamePasswordAuthenticationFilter,这个过滤器把用户名和密码封装为一个Authentication对象,此时还没有权限信息
-
第二步 调用authenticate方法进行认证,链条走到AuthenticationManager,但他也没完,继续调用DaoAuthenticationProvider的authenticate方法进行认证
-
第三步 即使已经经过了三个过滤器,还是没有认证,需要继续调用loadUserByUsername方法查询用户,走到第四个链条处了
-
第四步 根据用户名去查询用户及该用户对应的权限信息,InMemoryUserDetailsManager是在内存中查找的,并把对应用户信息添加上权限信息封装成UserDetails对象进行返回
-
第五步 UserDetails返回到Provider处,判断PasswordEncoder对比UserDetails中的密码和Authentication的密码是否正确,如果正确就把UserDetails中的权限信息设置到Authentication对象中
-
第六步 返回Authentication对象到第一个过滤器处,如果成功返回,就使用SecurityContextHolder.getContext().setAuthentication方法村吃该对象,其他过滤器可以通过SecurityContextHolder来获取当前用户信息
我们想想,在第四步中,查询的信息是在内存中查的,而我们需要从数据库查,那就得自己来实现这个接口
之后再请求
那么我们经过JWT拿到UserID后,怎么获取完整信息呢?再去数据库一条条查?会增大IO开销
因此,Redis闪亮登场。
那JWT要去Redis里面查,那总得Redis里面有东西吧,什么时候放呢?就在我们登录接口那里,如果认证通过,生成一个JWT的同时,再把UserID:用户信息存入Redis里面即可
解决问题
思路分析
-
登录
①自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库 -
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
准备工作,添加所需要的Maven依赖以及相应的工具类
功能实现
数据库校验用户
我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
建表并引入MybatisPuls和mysql驱动的依赖
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='用户表'
配置数据库信息
配置mapper
package com.bupt.security_test.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bupt.security_test.domain.User;
public interface UserMapper extends BaseMapper<User> {
}
在主启动类上扫描mapper
@MapperScan("com.bupt.security_test.mapper")
public class SecurityTestApplication
测试一下
@SpringBootTest
public class SecurityTestApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testUserMapper() {
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
}
读取到了数据库数据,说明MyBatisPlus没问题
那我们就要尝试链接自己的数据库的账号密码了
创建一个类实现UserDetailsService接口,重写其中的方法。
这里的==@service注解不能忘记==
package com.bupt.security_test.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.bupt.security_test.domain.LoginUser;
import com.bupt.security_test.domain.User;
import com.bupt.security_test.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
//没查到用户就抛异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
//TODO 查询对应权限
return new LoginUser(user);
}
}
返回的实体类LoginUser我们也定义一下,因为我们的User本身和UserService没关系,需要借助LoginUser包装一下
注意后面几个方法虽然没实现,但返回改为了true而不是默认的false
package com.bupt.security_test.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@NoArgsConstructor
@AllArgsConstructor
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;
}
}
测试要求输入账号密码
现阶段要把password密码明文前面加上{noop}
不然识别不出来
这个括号里面是说明该密码采取什么方式进行的加密,{noop}表示没有加密
为什么呢,我们再看看上面的图
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
配置一下SecurityConfig
package com.bupt.security_test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
写一个测试类看看
@Test
public void TestBCryptPasswordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode1 = bCryptPasswordEncoder.encode("1234");
String encode2 = bCryptPasswordEncoder.encode("1234");
System.out.println(encode1);
System.out.println(encode2);
}
我们打印输出,会发现每次encode的结果都不一致
$2a$10$NMAeABItflg2UyrWNuOwvu0hFhEqGIk.zyx0RUJbeAm6jrjQq54oi
$2a$10$p5J1O.5THmLqv9ATSv/juep3QxhsUOyGTh/5bL51.8kcwKMNNYlle
我们存数据库的密码不是明文,而是encode后的结果
但不管怎样,用谁加密,对应匹配他还是认识
再看一看
@Test
public void TestBCryptPasswordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode1 = bCryptPasswordEncoder.encode("1234");
String encode2 = bCryptPasswordEncoder.encode("1234");
System.out.println("encode1输出" + encode1);
System.out.println("encode1判断是否能匹配rawPassword" + bCryptPasswordEncoder.matches("1234", encode1));
System.out.println("encode2输出" + encode2);
System.out.println("encode2判断是否能匹配rawPassword" + bCryptPasswordEncoder.matches("1234", encode2));
}
输出
encode1输出$2a$10$PJNq2HpUoHdj52zp3dPdNO9wKTpuGJHYeF.nX.NyDjt8jWUBqRM46
encode1判断是否能匹配rawPasswordtrue
encode2输出$2a$10$WAwFkHZ8xWoR3e5D9zkWgefxIrrRsZv8B093x9FYA7I/jCqMw8mvi
encode2判断是否能匹配rawPasswordtrue
这里为什么不同的编码都能匹配上呢,知识超纲了,这好像是什么 盐 值
配置好之后,我们直接输入用户密码就不行了,要更新数据库的密码为加密后的密码才行
JWT工具类
package com.bupt.security_test.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "bupt";
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jwt
*
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("jdh") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
*
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String jwt = createJWT("2123");
System.out.println(jwt);
//base64解码
Claims claims = parseJWT(jwt);
System.out.println(claims.getSubject());
//String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
//Claims claims = parseJWT(token);
//System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
执行主方法得到
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJiMGQxYmNjODI0NzI0ODljYjZlMDdiMGIxNDkyMzhkZiIsInN1YiI6IjIxMjMiLCJpc3MiOiJqZGgiLCJpYXQiOjE2NTgwNDM0ODQsImV4cCI6MTY1ODA0NzA4NH0.R4WV-QID5r2gACp7qXYV_fXy13VFAV0cjBzUfgFO-v8
2123
登录接口
自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
首先说如何放行
SecurityConfig重写方法
//用来放行,总不能登录界面都要让你认证吧
@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();
}
然后正常的controller service serviceimpl走一遍
package com.bupt.security_test.controller;
import com.bupt.security_test.domain.ResponseResult;
import com.bupt.security_test.domain.User;
import com.bupt.security_test.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
//RequestBody用来拿JSON传过来的用户名和密码
public ResponseResult login(@RequestBody User user) {
//登录
return loginService.login(user);
}
}
public interface LoginService {
ResponseResult login(User user);
}
@Service
public class LoginServiceImpl implements LoginService {
//这个类在SecurityConfig里面实现的
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//AuthenticationManager authenticate进行用户认证
//用户名和密码先封装,而Authentication是接口,我们需要找一个他的实现类
//在接口名上 ctrl+alt+鼠标左键,可以看其常用实现类
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//通过authenticationManager来实现认证,会调用UserDetailsServiceImpl
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
//把完整的用户信息存入redis userid作为key
redisCache.setCacheObject("login:" + userid, loginUser);
return new ResponseResult(200, "登录成功", map);
}
}
而我们需要先暴露AuthenticationManager
因此要在SecurityConfig添加
//重写方法,用来暴露AuthenticationManager
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
使用postman进行debug
debug看,
//通过authenticationManager来实现认证,会调用UserDetailsServiceImpl
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
拿到的 authenticate包含的principal下有user信息
集成redis后,我们再来看看测试结果
我们把这个token拿回工具类解析
Claims claim = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwNWNkN2E1ZDhlYzg0Y2NiYWU5NmM3ZDliMjUyOWViOSIsInN1YiI6IjEiLCJpc3MiOiJqZGgiLCJpYXQiOjE2NTgwNDgyMjcsImV4cCI6MTY1ODA1MTgyN30.8yp6VBEHL2aAB5dTlsG13lsRg_XNlNmVNZ88zumhT4c");
System.out.println(claim.getSubject());
得到的输出为1,即该用户的id为1
认证过滤器
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
过滤器代码
package com.bupt.security_test.filter;
import com.bupt.security_test.domain.LoginUser;
import com.bupt.security_test.utils.JwtUtil;
import com.bupt.security_test.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
//认证过滤器,那这个过滤器需要在SecurityConfig里面配置他出现的位置
//他要出现在UsernamePasswordAuthenticationFilter之前
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token,从请求头里面拿
String token = request.getHeader("token");
//没有token,直接放行
//放行后会去执行后面的过滤器
//走完后面的过滤器,响应回来的时候还会走一遍过滤器链
//如果没有return,回来的时候还会执行一次,就会报错,因为根本没有token
//登录接口也会从这里过,但因为没有token就被放行了
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
String userid;
//解析token
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis里面获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
//存入SecurityContextHolder
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
配置过滤器位置
SecurityConfig
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
测试一下,发送post请求
debug发现在
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
直接就放行了
然后一直走,完成登录后就返回token
此时我们再来测试一下hello接口
被拦截下来了,需要补充token信息
这样就能访问了
退出登录
清空redis SecurityContextHolder
//退出登录
@RequestMapping("/user/logout")
public ResponseResult logout() {
return loginService.logout();
}
实现类
//退出登录
@Override
public ResponseResult logout() {
//获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
//用户如果是未登录状态发起退出请求会被拦截下来,根本到不了这个方法
//删除redis中的值
redisCache.deleteObject("login:" + userid);
return new ResponseResult(200, "注销成功");
}
此时我们再使用原来的token访问就不行
结束时间:2022-07-17