04 动力云客之登录后获取用户信息+JWT存进Redis+Filter验证Token + token续期

在这里插入图片描述

1. 登录后获取用户信息

非常好实现. 只要新建一个controller, 并调用SS提供的Authentication对象即可

package com.sunsplanter.controller;

@RestController
public class UserController {

    @GetMapping(value = "api/login/info")
    public R loginInfo(Authentication authentication) {
        TUser tUser = (TUser)authentication.getPrincipal();

        return R.OK(tUser);
    }

}

未登录状态下可以直接访问 api/login/info吗?

不可以. 因为在安全配置类已经写明了, 仅登陆界面允许任何人访问, 其他所有界面都需要认证

<由于未写JWT, 默认使用Session 保存会话,> ???好像不对
因此只要我们先通过登录接口登录, 然后再直接访问获取用户信息接口即可
在这里插入图片描述

2. 使用JWT打通登录后各个页面的认证

前后端分离的项目一般会使用token(jwt)实现登录状态的保持;(java web : session)

token其实就是一个随机字符串(字符串要求是唯一的,不同人的token都不能相同),当用户在登录页面输入账号和密码后,前端将账号密码发送给后端,后端检验完账号和密码后,会生成一个随机不重复的字符串即(token),并将其响应给前端,前端拿到token后,需要在客户端进行持久化存储(一般会写在localStorage或者sessionStorage中),那么下次在向后端数据接口发送请求的时候,一般需要将token一并发送给后端数据接口,后端数据接口会对token进行校验,如果合法则正常响应请求,如果不合法,则提示未登录。

2.1 sessionStorage与localStorage

它们是javascript对象,浏览器支持这两个对象,可以直接使用. 属于前端范畴
localStorage和sessionStorage都是用来在浏览器客户端存储临时信息的对象;

sessionStorage、localStorage区别?

sessionStorage只在一个浏览器页面有效,比如你打开一个新的tab浏览器页会失效,你关闭浏览器后,再打开浏览器也会失效;
localStorage在整个浏览器中都有效,重启浏览器也有效;除非你手动删除了localStorage,才会失效;

2.2 修改登录成功拦截器

根据流程图, 在查询到对象并返回给SS后, SS会调用成功拦截器,
就在这个拦截器中, 根据查询到的对象生成JWT并同时存进Redis和返回前端.

//登录成功会自动执行这个类中的onAuthenticationSuccess方法, 该方法返回自定义的Json给前端
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private RedisService redisService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //登录成功,执行该方法,在该方法中返回json给前端,就行了
        TUser tUser = (TUser) authentication.getPrincipal();

        //1.生成JWT
        //由于createJWT方法定义的参数是序列化后的对象, 因此先调用JSONUtils序列化对象
        String userJSON = JSONUtils.toJSON(tUser);
        String jwt = JWTUtils.createJWT(userJSON);

        //2.写入Redis
        redisService.setValue(Constants.REDIS_JWT_KEY + tUser.getId(), jwt);

        //3. 设置JWT的过期时间(如果选择记住我, 过期时间是7天, 否则30分钟)
        String rememberMe = request.getParameter("rememberMe");
        //勾选了记住我就设置为7天, 其中EXPIRE_TIME就是7天,DEFAULT_EXPIRE_TIME是半小时
        if (Boolean.parseBoolean(rememberMe)){
            redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId() , Constants.EXPIRE_TIME, TimeUnit.SECONDS);
        } else {
            redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId() , Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
        }

        //登录成功的统一结果
        R result = R.OK(jwt);

        //把R对象转成json
        String resultJSON = JSONUtils.toJSON(result);

        //把R以json返回给前端
        ResponseUtils.write(response, resultJSON);
    }
}

常量类为

package com.sunsplanter.constant;

/**
 * 常量类
 */
public class Constants {

    public static final String LOGIN_URI = "/api/login";

    //redis的key的命名规范: 项目名:模块名:功能名:唯一业务参数(比如用户id)
    public static final String REDIS_JWT_KEY = "dlyk:user:login:";

    //redis中负责人的key
    public static final String REDIS_OWNER_KEY = "dlyk:user:owner";

    //jwt过期时间7天
    public static final Long EXPIRE_TIME = 7 * 24 * 60 * 60L;

    //jwt过期时间30分钟
    public static final Long DEFAULT_EXPIRE_TIME = 30 * 60L;
}

redis类 为

package com.sunsplanter.service

public interface RedisService {

    void setValue(String key, Object value);

    Object getValue(String key);

    Boolean removeValue(String key);

    //给jwt设置过期时间
    Boolean expire(String key, Long timeOut, TimeUnit timeUnit);
}
package com.sunsplanter.service.impl;

@Service
public class RedisServiceImpl implements RedisService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void setValue(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public Object getValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public Boolean removeValue(String key) {
        return redisTemplate.delete(key);
    }

    //给JWT设置过期时间
    @Override
    public Boolean expire(String key, Long timeOut, TimeUnit timeUnit) {
        return redisTemplate.expire(key, timeOut, timeUnit);
    }
}

在网页登录后可以看到已经存到了localstorage
在这里插入图片描述

3 Filter验证Token

如流程图所示, 第一次登录成功过后, 往后每次登录会携带token登录,

因此, 在config.filter文件夹下新建一个token过滤器类, 该类实现OncePerRequestFilter
并在SS的安全配置类中中添加进去

package com.sunsplanter.config.filter;

@Component
public class TokenVerifyFilter extends OncePerRequestFilter{

    @Resource
    private RedisService redisService;

    //spring boot框架的ioc容器中已经创建好了该线程池,可以注入直接使用
    @Resource
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getRequestURI().equals(Constants.LOGIN_URI)) { //如果是登录请求,此时还没有生成jwt,那不需要对登录请求进行jwt验证
            //验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
            filterChain.doFilter(request, response);

        } else {
            String token = null;
            if (request.getRequestURI().equals(Constants.EXPORT_EXCEL_URI)) {
                //从请求路径的参数中获取token
                token = request.getParameter("Authorization");
            } else {
                //其他请求都是从请求头中获取token
                token = request.getHeader("Authorization");
            }

            if (!StringUtils.hasText(token)) {
                //token验证未通过的统一结果类
                R result = R.FAIL(CodeEnum.TOKEN_IS_EMPTY);
                //把R对象转成json
                String resultJSON = JSONUtils.toJSON(result);
                //把R以json返回给前端
                ResponseUtils.write(response, resultJSON);
                return;
            }

            //验证token有没有被篡改过
            if (!JWTUtils.verifyJWT(token)) {
                //token验证未通过统一结果类
                R result = R.FAIL(CodeEnum.TOKEN_IS_ERROR);

                //把R对象转成json
                String resultJSON = JSONUtils.toJSON(result);

                //把R以json返回给前端
                ResponseUtils.write(response, resultJSON);

                return;
            }

            TUser tUser = JWTUtils.parseUserFromJWT(token);
            String redisToken = (String) redisService.getValue(Constants.REDIS_JWT_KEY + tUser.getId());

            //验证token非空
            if (!StringUtils.hasText(redisToken)) {
                //token验证未通过统一结果类
                R result = R.FAIL(CodeEnum.TOKEN_IS_EXPIRED);

                //把R对象转成json
                String resultJSON = JSONUtils.toJSON(result);

                //把R以json返回给前端
                ResponseUtils.write(response, resultJSON);

                return;
            }

            //验证token是否与redis中的一致
            if (!token.equals(redisToken)) {
                //token验证未通过的统一结果类
                R result = R.FAIL(CodeEnum.TOKEN_IS_NONE_MATCH);

                //把R对象转成json
                String resultJSON = JSONUtils.toJSON(result);

                //把R以json返回给前端
                ResponseUtils.write(response, resultJSON);
                return;
            }

            //jwt验证通过了,那么在spring security的上下文环境中要设置一下,设置当前这个人是登录过的,你后续不要再拦截他了
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(tUser, tUser.getLoginPwd(), tUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

            //刷新一下token(异步处理,new一个线程去执行)
            /*new Thread(() -> {
                //刷新token
                String rememberMe = request.getHeader("rememberMe");
                if (Boolean.parseBoolean(rememberMe)) {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                } else {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                }
            }).start();*/

            //异步处理(更好的方式,使用线程池去执行)
            threadPoolTaskExecutor.execute(() -> {
                //刷新token
                String rememberMe = request.getHeader("rememberMe");
                if (Boolean.parseBoolean(rememberMe)) {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                } else {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                }
            });

            //验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
            filterChain.doFilter(request, response);
        }
    }
}

4. token续期

现在一个问题是

token一旦发布, 是7天/30分钟 .

原来没有续期时 , token只能获得与提前删除.

然而一个现实需求是

假如用户获得是30分钟的token. 在这三十分钟内, 只要用户有任何操作, 我们应当自动刷新token到30分钟 , 否则假如不管用户怎么操作, token都固定30分钟刷新 , 这显然不符合逻辑

目标为, 任何除登出的用户登录都会重置token过期时间

然后是代码放在哪里的问题

我们知道, 即使登录过后 , 我们在每一次操作时仍会用TokenVerifyFilter检查token的有效期.

因此直接把续期代码放到该类中即可, 每次有操作->执行TokenVerifyFilter检查token有效期->有效则续期token

在TokenVerifyFilter中新增刷新代码

//刷新一下token(异步处理,new一个线程去执行)
//没必要因为续期token而阻塞整个进程, 毕竟此时已经校验过了不影响本次执行
            new Thread(() -> {
                //刷新token
                String rememberMe = request.getHeader("rememberMe");
                if (Boolean.parseBoolean(rememberMe)) {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                } else {
                    redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                }
            }).start();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/400583.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IO进程线程day5作业

1、使用多线程完成两个文件的拷贝&#xff0c;第一个线程拷贝前一半&#xff0c;第二个线程拷贝后一半&#xff0c;主线程回收两个线程的资源 代码&#xff1a; #include<myhead.h>//定义文件拷贝函数 int copy_file(int start,int len) {int srcfd,destfd;//以只读的形…

MySQL 安装步骤

下载地址&#xff1a;https://downloads.mysql.com/archives/community/&#xff0c; 选择第二个 将下载的压缩包解压到自己想要放到的目录下&#xff08;路径中最好不要有中文&#xff09; 一、添加环境变量 环境变量里面有很多选项&#xff0c;这里我们只用到Path这个参数…

微信小程序错误----config is not defined

微信小程序出错 请求头发生错误 修改 options.header {// 为请求头对象添加 token 验证的 Authorization 字段Access-Token: token,platform: MP-WEIXIN,// 保留原有的 header...options.header,}

【Java程序员面试专栏 数据结构】四 高频面试算法题:哈希表

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,一个O(1)查找的利器哈希表,所以放到一篇Blog中集中练习 题目关键字解题思路时间空间两数之和辅助哈希使用map存储出现过的值,key为值大小,value为下标位置,…

Vue+SpringBoot打造超市商品管理系统

目录 一、摘要1.1 简介1.2 项目录屏 二、研究内容2.1 数据中心模块2.2 超市区域模块2.3 超市货架模块2.4 商品类型模块2.5 商品档案模块 三、系统设计3.1 用例图3.2 时序图3.3 类图3.4 E-R图 四、系统实现4.1 登录4.2 注册4.3 主页4.4 超市区域管理4.5 超市货架管理4.6 商品类型…

伦敦金行情分析需要学习吗?

对于伦敦金交易来说&#xff0c;目前大致分成两派&#xff0c;一派是实干派&#xff0c;认为做伦敦金交易重要的是实战&#xff0c;不需要学习太多东西&#xff0c;否则容易被理论知识所局限。另一派则是强调学习&#xff0c;没有理论知识&#xff0c;投资者很难做好伦敦金交易…

什么是接口测试?为什么要做接口测试?

1. 什么是接口测试&#xff1f;为什么要做接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互…

Web自动化测试 Selenium 1/3

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

Spring Framework

Spring Framework Spring 是一款开源的轻量级 Java 开发框架&#xff0c;旨在提高开发人员的开发效率以及系统的可维护性。 Spring 框架指的都是 Spring Framework&#xff0c;它是很多模块的集合&#xff0c;如下图所示&#xff1a; 一、Core Container Spring 框架的核心模…

Atcoder ABC341 B - Foreign Exchange

Foreign Exchange&#xff08;外汇&#xff09; 时间限制&#xff1a;2s 内存限制&#xff1a;1024MB 【原题地址】 所有图片源自Atcoder&#xff0c;题目译文源自脚本Atcoder Better! 点击此处跳转至原题 【问题描述】 【输入格式】 【输出格式】 【样例1】 【样例输入1…

【JavaEE】_form表单构造HTTP请求

目录 1. form表单的格式 1.1 form表单的常用属性 1.2 form表单的常用搭配标签&#xff1a;input 2. form表单构造GET请求实例 3. form表单构造POST请求实例 4. form表单构造法的缺陷 对于客户端浏览器&#xff0c;以下操作即构造了HTTP请求&#xff1a; 1. 直接在浏览器…

AcWing 2868. 子串分值(贡献法)

AcWing 2868. 子串分值 原题链接&#xff1a;https://www.acwing.com/problem/content/2871/ 具体分析过程如下图&#xff1a; 直接遍历的话太麻烦&#xff0c;且时间复杂度太高&#xff0c;所以另寻他路 字符串中只有小写字母26个&#xff0c;所以可以从此着手&#xff0c; …

Netty中的PooledByteBuf池化原理剖析

PooledByteBuf PooledByteBuf是池化的ByteBuf&#xff0c;提高了内存分配与释放的速度&#xff0c;它本身是一个抽象泛型类&#xff0c; 有三个子类:PooledDirectByteBuf、PooledHeapByteBuf、PooledUnsafeDirectByteBuf. Jemalloc算法 Netty的PooledByteBuf采用与jemalloc一…

java在当前项目创建文件

public static void main(String[] arge) throws IOException {File file new File("dade02\\dade");//不存在创建if(!file.exists()){file.mkdirs();}File file1 new File("dade02\\dade\\daade.txt");file1.createNewFile();}或者 public static void …

Rust ?运算符 Rust读写txt文件

一、Rust &#xff1f;运算符 &#xff1f;运算符&#xff1a;传播错误的一种快捷方式。 如果Result是Ok&#xff1a;Ok中的值就是表达式的结果&#xff0c;然后继续执行程序。 如果Result是Err&#xff1a;Err就是整个函数的返回值&#xff0c;就像使用了return &#xff…

租用海外服务器,自己部署ChatGPT-Next-Web,实现ChatGPT聊天自由,还可以分享给朋友用

前言 如果有好几个人需要使用ChatGPT&#xff0c;又没有魔法上网环境&#xff0c;最好就是自己搭建一个海外的服务器环境&#xff0c;然后很多人就可以同时直接用了。 大概是情况是要花80元租一个一年的海外服务器&#xff0c;花15元租一个一年的域名&#xff0c;然后openai 的…

搜索专项---DFS之连通性模型

文章目录 迷宫红与黑 一、迷宫OJ链接 本题思路:DFS直接搜即可。 #include <iostream> #include <cstring> #include <algorithm>constexpr int N110;int n; char g[N][N]; bool st[N][N]; int x1, y1, x2, y2;int dx[4] {-1, 0, 1, 0}, dy[4] {0, 1, 0, …

多线程完成文件拷贝:2024/2/21

作业1&#xff1a;使用多线程完成两个文件的拷贝 要求&#xff1a;第一个线程拷贝前一半&#xff0c;第二个线程拷贝后一半&#xff0c;主线程回收两个线程的资源 代码&#xff1a; #include <myhead.h>//定义结构体 typedef struct Info {char *src;char *dest;int s…

springboot访问webapp下的jsp页面

一&#xff0c;项目结构。 这是我的项目结构&#xff0c;jsp页面放在WEB-INF下的page目录下面。 二&#xff0c;file--->Project Structure,确保这两个地方都是正确的&#xff0c;确保Source Roots下面有webapp这个目录&#xff08;正常来说&#xff0c;应该本来就有&#…

c语言经典测试题1

1.题1 int x5,y7; void swap() { int z; zx; xy; yz; } int main() { int x3,y8; swap(); printf("%d,%d\n"&#xff0c;x, y); return 0; } A: 5,7 B: 7,5 C: 3,8 D: 8,3 大家思考一下选哪一个呢&#xff1f; 我们来分析一下&#xff1a;上述代码中我们创建了4…