Java设置token有效期的5个应用场景(双token实现)

 更新时间:2024年04月14日 10:52:49   作者:夏诗曼CharmaineXia  
Token最常见的应用场景之一就是身份验证,本文主要介绍了Java设置token有效期的5个应用场景(双token实现),具有一定的参考价值,感兴趣的可以来了解一下

token的简介和生成校验已经在前面分享过,有需要的小伙伴可以先进行回顾。

传送门:token介绍,以及如何生成以及校验token

前言:

Token最常见的应用场景之一就是身份验证。在传统的身份验证方式中,用户需要输入用户名和密码才能登录系统,这种方式容易被破解和盗用。而使用token方式进行身份验证,可以有效防止用户身份信息被盗用。

当用户进行身份验证后,服务器会生成一个token并将其返回给客户端,客户端可以使用token来访问受保护的资源,而不需要重新输入用户名和密码。这种方式不仅可以提高安全性,还可以提高用户体验。

在这里插入图片描述

场景一:网吧计时

场景分析:

严格规定登陆时长,超时则跳转登陆页面,必须重新输入密码才能继续使用

对token的要求:

登陆成功后,服务器生成的token需要携带时间戳(token时间戳=当前时间+有效时长),后台定制一个有效期时长,在网吧计时收费场景中有效范围就是可以上机的时长。

每次请求都要校验token是否过期( 时间戳是否小于现在时间)

token也只用存在浏览器缓存即可,减少服务器端的存储压力。

实操:

javaWebToken为例:

    <!-- 引入jwt -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.8.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.8.0</version>
    </dependency>
     /**
     * @description: 生成token
     * @param:  userInfo 用户手机号和用户Id
     * @return: java.lang.String 返回token
     **/
    public static String getToken(String userPhone) {
        try{
            //从当前时间算起,再加上有效时长30分钟
            Date date = new Date(System.currentTimeMillis() + 30*60*1000);
            //用秘钥生成签名
            Algorithm algorithm = Algorithm.HMAC256('P1ooisyGFJhgzrctMOofvaHLuiNFOmktedw');
            //默认头部+载荷(手机号/id)+过期日期+签名=jwt
            String jwtToken= JWT.create()
                    .withClaim("userPhone", userPhone)
                    .withClaim("userId", "xxxxxxx")
                    .withExpiresAt(date)
                    .sign(algorithm);
            return jwtToken;
        }catch (Exception e){
            log.error("用户{}的token生成异常:{}",userPhone,e);
            return null;
        }
    }

验证token有效期:

    // 判断 token 是否过期
    public static String isExpire(String token) {
        DecodedJWT jwt = JWT.decode(token);//解码token
        // 如果token的有效期小于当前时间,则表示已过期,为true
        boolean isExpire = jwt.getExpiresAt().getTime() < System.currentTimeMillis();
        if(isExpire){
           return jwt.getClaim("userPhone").asString();//获取token携带的数据
        }else{
            return null;
        }
    }

拦截请求,开始验证token

public class JwtToken implements AuthenticationToken {
    /**
     * JWT的字符token
     */
    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}


@Component
public class ShiroRealm extends AuthorizingRealm {

    /**
     * @Title: doGetAuthenticationInfo
     * @description: 校验token是否正确
     * @param:  auth
     * @return: org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        try{
            String token = (String) auth.getCredentials();
            String userPhone=JwtUtil.isExpire(token);
            return new SimpleAuthenticationInfo(userPhone, token, getName());
        }catch(Exception e){
            throw new AuthenticationException("验证token失败");
        }
    }
}

总结:

优点:

  • 严格规定登陆时长,简单来说就是安全性高。
  • 代码逻辑简单,token过期了就重定向到登陆页面,不需要做延时等处理。

缺点:

  • 时间一到就跳转到登陆页面,对用户来说非常突然,某种程度上说用户体验非常不好。
  • 用户有可能停留在页面不做任何操作,因此客户端必须定期主动的给服务器发请求(或使用消息队列),以便及时发现校验token过期。

场景二: Esxi系统页面、jumpServer

场景分析:

Esxi系统页面、jumpServer的web终端页面等,超过一段时间内不操作(例如0.5小时)自动退出登录,再想继续操作需要重新登录。

对token的要求:

和场景一类似,区别是增加了一个判断:每次请求都会判断token是否快要过期(例如设置token还有10分钟过期)。

如果将要过期,则重置token有效期(服务器发个新token给客户端);如果已经过期,需要重新登录。

实操:

重新生成一个新的token,前端收到新的token后把旧token丢弃(前端代码略)

总结:

优点:

  • 用户一直在使用页面,token就会被一直重置,对活跃用户友好。
  • token有效期短,人离开一段时间就无法继续使用软件,这个设计安全性较高。
  • 用户在token过期后再次操作才会要求再次登陆,也就是说,不需要客户端时时给服务器发消息验证token是否有效,减少网络开销。

缺点:

  • 如果有效期设计的短,用户操作也不频繁的情况下,会导致用户频繁登陆,体验较差。合理设置有效期非常重要。

场景三:微信、支付宝等app

场景分析:

微信、支付宝等手机app,我们一旦安装并登陆以后,除了涉及资金或信息安全的场景需要输入一些密码,基本上我们打开app就能用,不会让我们重复登陆。

对token的要求:

token有效期设置的很长(3个月、6个月、一年等)

每次请求还是会验证token有效期,但如果token过期,则发消息给客户端。

客户端收到消息以后,给服务器发送重置token的请求。

总结:

适用于安全性有保证的场景(例如手机App:手机有锁屏码等安全机制,涉及到金钱等重要信息还有其他验证方式)

优点:

  • 用户只需要登陆一次,用户体验很好

缺点:

  • 客户端可以发送重置token的请求,故token一直有效,手机锁屏被破解,任何人都能使用,也是个安全隐患。
  • 只用登陆一次,用户很有可能忘记密码,想要避免用户体验差,必须绑定手机号,支持验证码登陆。

场景四:语雀等pc应用程序(双token)

场景分析:

场景四和场景二类似,区别是用户长时间不使用的情况下才会被强制用户登录。

例如“语雀”等应用程序,长时间不使用是会被要求重新登录的。

我们每个人都安装过一些使用频率不高的软件,这些软件在产品设计时就决定了用户的使用频率和周期,那他们是如何界定“一直在使用的活跃用户”和“长时间不使用的非活跃用户”呢?

还是靠设置token有效期,有效期设置的长一些,例如3个月或6个月不使用才算非活跃用户。

但是如何沿用场景二的token要求,设置有效期长的token,会留下很大的安全隐患:token一旦被黑客截获后长时间可以被使用,还不会被服务器察觉。

解决方案:双token

什么是双token?以现有的短token的基础上再增加一个长token,形成两个token校验有效期的模式。

短token可以防止被截获后无休止使用,所以还要使用有效期短的token用来验证有效期。

而新增加的长token,它的有效期用来区分“活跃用户”和“非活跃用户”,用来实现短token过期后,活跃用户系统自动给重置token,非活跃用户需要重新登录。

对token的要求:

用户登录成功后,服务器生成一短一长两个token返回给客户端,客户端每次请求服务器携带的是短token。

如果服务器发现短token过期,则通知给客户端,此时客户端携带长token给服务器校验。

如果长token未过期,表示用户为“活跃用户”,服务器重置短token和长token发给客户端。
如果长token过期,表示用户为“非活跃用户”,用户需要重新登录。

在这里插入图片描述

总结:

优点:

  • 帮助使用频率不高的软件区别“活跃用户”和“非活跃用户”,提升“活跃用户”的体验,保证“非活跃用户”的信息安全

缺点:

  • 刷新token期间,原有的token不能用,在并发情况下会导致其他问题。

场景五:提升响应速度(redis)

场景分析:

场景一到四的共同点:①token内置了时间戳,②服务器端不存储token
场景五是对以上以上四个场景在验证速度上的优化,亲测使用redis可以提高2-4倍的验证速度。

对token的要求:

token内不设置时间戳,而是将token存在redis中(例如:键为token,值为用户信息),并设置token存在redis中的保存时间。

客户端仍然保存着token,每次请求都携带token。服务器接到请求,把token当做键去redis中拿数据:

如果能拿出数据,则说名token没过期,如果拿不到,则说明token过期了。

想要重置token有效期,直接根据键重置数据在redis中的有效期。

在这里插入图片描述

实操:

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>3.0.0</version>
        </dependency>

redis工具类

/**
 * Redis工具类
 */
@Component
public final class RedisUtil<V> {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public String get(String key) {
        return key == null ? null : (String) redisTemplate.opsForValue().get(key);
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
        /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

拦截请求,开始验证token

public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private RedisUtil redisUtil;
 
   /**
     * @Title: doGetAuthenticationInfo
     * @description: 校验token是否正确
     * @param:  auth
     * @return: org.apache.shiro.authc.AuthenticationInfo
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {

        //不使用token验证来校验token是否可用,改成把token存redis中,在redis中设置数据有效期
        String token = (String) auth.getCredentials();
        if (StringUtils.isEmpty(token)) {
            throw new AuthenticationException(Constant.TOKEN_EXPIRED);
        }
        //判断是否能从redis中用token拿到过期时间
        try{
            Long times=redisUtil.getExpire(token);
            //如果还有0.5小时过期,就刷新token在redis中的有效时间(有效期设置为2小时)
            if(1800>times){
                redisUtil.expire(token,7200);
            }
            String userId =redisUtil.get(token);  //获取key中的用户id
            return new SimpleAuthenticationInfo(userId, token, getName());
        }catch(Exception e){
            throw new AuthenticationException("验证token失败");
        }
    }
}

验证redis校验速度

       //token时间戳校验
       long startTime=System.currentTimeMillis();   //获取开始时间

       for(int i=0;i<99;i++){
           String userPhone = JwtUtil.isExpire(token);
       }

       long endTime=System.currentTimeMillis(); //获取结束时间

       System.out.println("用时间戳方式校验100次token是否有效: "+(endTime-startTime)+"毫秒");
       //redis校验
       long startTime=System.currentTimeMillis();   //获取开始时间

       for(int i=0;i<99;i++){
           if(StringUtils.isEmpty(redisUtil.get(token))){
               return null;
           }
       }
       long endTime=System.currentTimeMillis(); //获取结束时间

       System.out.println("用redis设置过期时间方式校验100次token是否有效: "+(endTime-startTime)+"毫秒");

在这里插入图片描述

总结:

优点:

  • 服务器生成了token就直接存到redis中,token是不会被拦截篡改的,因此默认token是正确的,也就减少了验证token是否正确这一步骤
  • 重置了token,token本身也不会变,减少客户端处理废弃token、再存储新token的逻辑
  • redis的数据存在内存中,IO速度快

缺点:

  • 依赖第三方软件,需要搭建redis服务器,考虑到redis挂了软件也不能使用,还要搭建redis集群

思想升华:

每种设置token有效期的方案都有对应的场景,抛开场景谈方案是狭隘的。再学习计算机的过程中,我发现无论是磁盘调度方式、内存存储方式、raid0-6,所有方式方法的诞生都和当时的场景相关,并且往往在时间和空间上面进行取舍。所以没有最优的方案,只有当下相对合适的方案。

到此这篇关于Java设置token有效期的5个应用场景(双token实现)的文章就介绍到这了,更多相关Java设置token有效期内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解RabbitMQ中延迟队列结合业务场景的使用

    详解RabbitMQ中延迟队列结合业务场景的使用

    这篇文章主要介绍了详解RabbitMQ中延迟队列结合业务场景的使用,延迟队列中的元素都是带有时间属性的,延迟队列就是用来存放需要在指定时间被处理的元素的队列,需要的朋友可以参考下
    2023-05-05
  • 详解Spring基于xml的两种依赖注入方式

    详解Spring基于xml的两种依赖注入方式

    这篇文章主要介绍了详解Spring基于xml的两种依赖注入方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • SpringBoot实现elasticsearch 查询操作(RestHighLevelClient 的案例实战)

    SpringBoot实现elasticsearch 查询操作(RestHighLevelClient 

    这篇文章主要给大家介绍了SpringBoot如何实现elasticsearch 查询操作,文中有详细的代码示例和操作流程,具有一定的参考价值,需要的朋友可以参考下
    2023-07-07
  • Java基础详解之内存泄漏

    Java基础详解之内存泄漏

    这篇文章主要介绍了Java基础详解之内存泄漏,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-04-04
  • springboot mybatis调用多个数据源引发的错误问题

    springboot mybatis调用多个数据源引发的错误问题

    这篇文章主要介绍了springboot mybatis调用多个数据源引发的错误问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • java中读写Properties属性文件公用方法详解

    java中读写Properties属性文件公用方法详解

    在项目开发中我们会将很多环境特定的变量定义到一个配置文件中,比如properties文件,把数据库的用户名和密码存放到此属性文件中。下面这篇文章就主要介绍了java中读写Properties属性文件公用方法,需要的朋友可以参考借鉴。
    2017-01-01
  • java基础学习笔记之泛型

    java基础学习笔记之泛型

    所谓泛型,就是变量类型的参数化。泛型是JDK1.5中一个最重要的特征。通过引入泛型,我们将获得编译时类型的安全和运行时更小的抛出ClassCastException的可能。在JDK1.5中,你可以声明一个集合将接收/返回的对象的类型。
    2016-02-02
  • Java 解析XML数据的4种方式

    Java 解析XML数据的4种方式

    这篇文章主要介绍了Java 解析XML数据的4种方式,帮助大家更好的用Java处理数据,感兴趣的朋友可以了解下
    2020-09-09
  • 关于springboot的接口返回值统一标准格式

    关于springboot的接口返回值统一标准格式

    这篇文章主要介绍了关于springboot的接口返回值统一标准格式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • Java实现萝卜勇者游戏的示例代码

    Java实现萝卜勇者游戏的示例代码

    《萝卜勇者》是由国内玩家自制的一款独立游戏,玩家扮演萝卜勇士闯关,打败各种邪恶的敌人,获得最后的胜利。本文将利用Java实现这一游戏,感兴趣的可以了解一下
    2022-02-02

最新评论