Redis实战之共享session + jwt 实现登录拦截、刷新token

news2024/11/26 4:34:56

共享session问题

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?

  • 早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了。
  • 问题1:每台服务器中都有完整的一份session数据,服务器压力过大。
  • 问题2:session拷贝数据时,可能会出现延迟
  • 解决:redis天然满足共享session的条件

在这里插入图片描述

设计key的结构

使用哪种结构呢?

  • 由于存入的数据比较简单,我们可以考虑使用String,或者是使用Hash
  • 如果使用String,value 多占用一点空间
  • 如果使用Hash,value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以。

设计key的具体细节

共享session是每个用户都有自己的session,所以要满足:

  • key要具有唯一性
  • key要方便携带

我们在后台使用 jwt 生成一个字符串 token,然后让前端在 Header 带来这个token就能完成我们的整体逻辑了。

整体访问流程

解决状态登录刷新问题

第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

在这里插入图片描述

实例代码

pom文件

 <dependency>
    <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
  </dependency>
<!--hutool-->
 <dependency>
    <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.7.17</version>
 </dependency>
 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
 </dependency>

jwt 工具类

/**
 * jwt工具类
 */
public class JwtUtils {

    //加密 解密时的密钥(盐) 用来生成key
    public static final String JWT_KEY = "campus2022";

    /**
     * 生成加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtils.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 创建jwt密钥
     *
     * @param subject   加密主体
     * @param ttlMillis 过期时间
     * @return String
     */
    public static String createJWT(String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        long nowMillis = System.currentTimeMillis();//生成JWT的时间
        Date now = new Date(nowMillis);
        SecretKey key = generalKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
//                .setClaims(claims)            //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(UUID.randomUUID().toString())                    //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)            //iat: jwt的签发时间
                .setSubject(subject)        //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key);//设置签名使用的签名算法和签名使用的秘钥
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);        //设置过期时间
        }
        return builder.compact();            //就开始压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
    }

    /**
     * 解密
     *
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)         //设置签名的秘钥
                .parseClaimsJws(jwt).getBody();//设置需要解析的jwt
        return claims;
    }

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {

        String userId = "1234";
        //加密
        String jwt = createJWT(userId, 3600 * 24);
        System.out.println("加密后:" + jwt);

        //解密
        Claims claims = parseJWT(jwt);
        String subject = claims.getSubject();
        System.out.println("解密后:" + subject);
    }

}

controller

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserServiceImpl userService;


    //刷新token普通请求
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    //登录
    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        return userService.login(user);
    }
}

service

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    public Result login(User user) {
        if (user==null || user.getUsername()==null){
            return Result.fail("账号为空");
        }
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(true, User::getUsername, user.getUsername());
        User one = userMapper.selectOne(wrapper);
        if (one==null){
            return Result.fail("账号未注册");
        }
        if (!one.getPassword().equals(user.getPassword())){
            return Result.fail("密码错误");
        }
        //根据用户账号生成token
        String token = JwtUtils.createJWT(one.getUsername(), 24 * 3600);
        //将用户信息转为Map
        Map<String, Object> userMap = BeanUtil.beanToMap(one,
                new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //将(token,用户信息)存入redis
        redisTemplate.opsForHash().putAll("user:token:"+token,userMap);
        //设置过期时间
        redisTemplate.expire("user:token:"+token, Duration.ofMinutes(30));
        //返回token,登录成功
        return Result.ok(token);
    }
}

ThreadLocal

public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();

    public static void saveUser(User user){
        tl.set(user);
    }

    public static User getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

拦截器

登录拦截
/**
 * 登录拦截
 */
public class LoginInterceptor implements HandlerInterceptor {

    //目标资源执行前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }

    //请求完成后执行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
请求拦截,刷新 token 有效期
/**
 * 请求拦截,刷新 token 有效期
 */
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = stringRedisTemplate;
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于token获取redis中的用户
        String key  = "user:token:" + token;
        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 4.将查询到的hash数据转为User
        User user = BeanUtil.fillBeanWithMap(userMap, new User(), false);
        // 5.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(user);
        // 6.刷新token有效期
        redisTemplate.expire(key, Duration.ofMinutes(30));
        // 7.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
拦截器配置
/**
 * 拦截器配置
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns("/user/login","/user/hello") //排除拦截路径
                .order(1); //拦截器优先级,值越大优先级越低

        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate))
                .addPathPatterns("/**") //拦截所有路径,用于token刷新
                .order(0); //拦截器优先级,值越小优先级越高
    }
}

结果图

登录,并返回token

在这里插入图片描述

刷新token普通请求

在这里插入图片描述

redis,token及用户信息,只要有请求就会刷新 TTL存活时间

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

百度边止血边扩张

在经过一系列的“内部调整”之后&#xff0c;百度交出了一份超预期的财报。北京时间11月22日&#xff0c;百度发布了截至2022年9月30日的第三季度财务报告。第三季度&#xff0c;百度实现营收325.4亿元&#xff0c;同比增长2%&#xff1b;归属百度的净利润&#xff08;non-GAAP…

在线杂志小程序开发,开启在线阅读时代

互联网技术的快速发展&#xff0c;让人们越来越依赖从网络上查看各种新闻资讯。传统的纸质杂志已经很难顺应时代的发展脚步&#xff0c;但是人们对于杂志的需求并没有减少。因此为了更好的满足众多读者对于杂志的需求&#xff0c;开发在线杂志小程序是十分必要的。在线杂志阅读…

时光倒流-第12届蓝桥杯Scratch选拔赛真题精选

[导读]&#xff1a;超平老师计划推出Scratch蓝桥杯真题解析100讲&#xff0c;这是超平老师解读Scratch蓝桥真题系列的第88讲。 蓝桥杯选拔赛每一届都要举行4~5次&#xff0c;和省赛、国赛相比&#xff0c;题目要简单不少&#xff0c;再加上篇幅有限&#xff0c;因此我精挑细选…

医院预约小程序源码,挂号陪护就医功能,提供全方位服务

随着人口老龄化形式加剧&#xff0c;年轻人工作压力大&#xff0c;没有闲余时间陪伴父母老人&#xff0c;因此针对解决独自去医院排队以及现代化设备需要等要求&#xff0c;而衍生出来的一个新型行业-挂号陪护。医院预约小程序源码开发的出现无非就是解决了这一难题&#xff0c…

教培行业迎来重大变局,三大方向或成新机遇

“双减”政策落地&#xff0c;教培行业迎来重大变局。校内教育深化改革正在路上&#xff0c;而学科类机构或将踏上转型之路&#xff0c;结合政策和市场来看&#xff0c;素质教育类、职业教育类、教育数字化3大方向或成新机遇。 “双减”的总体思路是什么呢&#xff1f; 教育部有…

[附源码]计算机毕业设计SpringbootON-FIT

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

QGIS下载在线地图(Google 卫星、esri 卫星)

前言 国内外有很多在线地图下载软件&#xff0c;但功能单一&#xff0c;基本上只能下载数据&#xff0c;无法做GIS分析&#xff0c;且多为收费&#xff0c;即便是免费&#xff0c;也多少有所限制。 在QGIS中&#xff0c;可以完全免费且迅速下载在线地图&#xff0c;首先请看阅…

Kafka集群环境的部署

Kafka集群环境的搭建一、Kafka集群二、搭建两台服务器2.1、 zookeeper部署2.2、 启动1号机器的broker2.3、 启动2号机器的broker2.4、查看kafka集群2.5、测试集群总结后言一、Kafka集群 二、搭建两台服务器 笔者的两台服务器IP&#xff1a; 服务器IP1号192.168.11.592号192.1…

matplotlib你真的明白plt,fig和ax吗

你真的明白plt&#xff0c;fig和ax吗&#xff1f; plt系列接口是用来模仿MATLAB的风格的。如果用户熟悉MATLAB&#xff0c;还是建议以plt为主的。对于基本的画图&#xff0c;plt系列足够了。 个人觉得&#xff0c;真正不合适的用法&#xff0c;反而是各种混淆plt接口和其它更细…

单目相机模型

针孔相机模型 针孔相机模型是实际研究中最常用的模型。针孔是一个中间有一个小孔的假想墙壁&#xff0c;光只能从小孔通过。 fff是摄像机焦距&#xff0c;ZZZ是摄像机到物体的距离&#xff0c;XXX是物体长度&#xff0c;是图像平面上的物体长度。由相似三角形可得&#xff1a…

护眼灯真的可以保护眼睛吗?市面上的护眼台灯到底是不是智商税

护眼灯能不能保护眼睛&#xff0c;护眼灯是不是智商税&#xff0c;这是很多人一直以来都有的疑惑&#xff0c;其实啊&#xff0c;这并不难判断&#xff0c;直接入手体验一下就知道了&#xff0c;有没有护眼效果&#xff0c;照明体验是否舒适&#xff0c;一试便知。 护眼灯怎样…

Java解决鸡兔同笼问题

1 问题 利用java程序&#xff1a;输入鸡和兔的总数和总腿数&#xff0c;来计算兔子和鸡的各个数量。 2 方法 import java.util.Scanner; /** * 已知鸡和兔的总数量为n,总腿数为m。 * 输入n和m,依次输出鸡和兔的数目&#xff0c; * 如果无解&#xff0c;则输出“No answer” */ …

[漏洞复现]Text4shell(CVE-2022-42889)

文章目录简介影响版本环境搭建漏洞复现漏洞修复参考简介 Apache Commons Text 项目实现了一系列关于文本字符串的算法&#xff0c;专注于处理字符串和文本块。10月13日&#xff0c;Apache发布安全公告&#xff0c;修复了Apache Commons Text中的一个远程代码执行漏洞&#xff…

GJB 5000B简介

“软件定义装备”是武器装备信息化职能化发展的比如趋势,J用软件在信息化战争和J事智能化进程中起着基础性决定性作用。 1、GJB 5000由来 1991年 推出CMM 目的:用来衡量组织软件综合能力 2000年,SEI发布CMMIV1.0(Capability Maturity Model Integration)能力成熟度模型集…

「低碳」不是特步的解药

&#xff08;图片来源于网络&#xff0c;侵删&#xff09; 文|螳螂观察 作者|叶小安 一双跑鞋就能减少碳排放424克&#xff0c;相当于回收16个饮料瓶。 这样一双低碳跑鞋&#xff0c;你会购买吗&#xff1f; 今年进博会上&#xff0c;特步带来了首款Mass Balance低碳环保概…

资本赋能,跨境电商Starday逐鹿年终场

国内巨头很早就看到跨境电商行业的发展前景了&#xff0c;最有代表性的就是阿里巴巴旗下的全球速卖&#xff0c;其在2012年就开始布局并开展跨境电商业务了&#xff0c;这些年来&#xff0c;诸如字节跳动、京东、拼多多等国内电商巨头纷纷跟上阿里巴巴步伐&#xff0c;进军跨境…

Scrapy基本概念——Scrapy shell

Scrapy shell是一个交互式shell&#xff0c;可以在不运行Spider的情况下&#xff0c;测试和调试自己的数据提取代码。事实上&#xff0c;Scrapy shell可以测试任何类型的代码&#xff0c;因为它本就是一个常规的Python shell。 一、Scrapy shell的使用 1、启动Scrapy shell …

【iMessage苹果家庭】共享推软件设备安装创作AppleScript增加了iMessage群应用会得到一个硬件token

推荐内容IMESSGAE相关 作者推荐内容iMessage苹果推软件 *** 点击即可查看作者要求内容信息作者推荐内容1.家庭推内容 *** 点击即可查看作者要求内容信息作者推荐内容2.相册推 *** 点击即可查看作者要求内容信息作者推荐内容3.日历推 *** 点击即可查看作者要求内容信息作者推荐…

计算机图形学-算法总结

文章目录计算机图形学-算法总结一、直线转换1、DDA算法2、中点法3、Bresenhan算法二、圆1、中点Bresenham画圆算法2、椭圆的中点Bresenham算法计算机图形学-算法总结 一、直线转换 1、DDA算法 Δyyn−y0Δxxn−x0ε1max(∣Δx∣,∣Δy∣)\Delta yy_n-y_0 \\ \Delta xx_n-x_0\…

小杨哥陷入打假风波,会变成下一个辛巴吗?

最近&#xff0c;网红疯狂小杨哥频繁登上热搜。最初的起因是他花了1亿元在合肥一家高科技公司购买了5万多平方米的房产&#xff0c;作为他名下公司的全球总部&#xff0c;由此带来了争议。 据了解&#xff0c;该物业总建筑面积为53874.33平方米&#xff0c;包括1个生产综合体、…