当前位置: 代码网 > it编程>数据库>Redis > 双 Token 三验证解决方案

双 Token 三验证解决方案

2024年08月02日 Redis 我要评论
基于redis的双token三验证方案最佳实践

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验


问题分析


以往的项目大部分解决方案为单 token:

  • 用户登录后,服务端颁发 jwt 令牌作为 token 返回
  • 每次请求,前端携带 token 访问,服务端解析 token 进行校验和鉴权

存在的问题:

  • 有效期设置问题:有效期设置需要对时间做平衡,不能太短也不能太长
  • 续期问题:一旦过期,用户必须重新登录,很难做无感刷新
  • 无状态问题:token 是无状态的,单 token 颁发后服务端无法主动使其失效

原理解析


这里引入双 token 机制:

  • accesstoken:时间较短,一般为 5 分钟或者更短
  • refreshtoken:时间较长,一般为 1 到 3 天

登录过程:

  • 用户携带用户名和密码登录
  • 服务端为其颁发 accesstoken 和 refreshtoken

三验证环节:

  • 一验证:前端请求携带 accesstoken,验证是否过期,不过期放行,过期则进入第二个验证环节
  • 二验证:前端请求携带 refreshtoken,验证是否过期,不过期进入第三个验证环节,过期则要求用户重新登录
  • 三验证:在 redis 种验证 refreshtoken 是否存在,存在则颁发新的 accesstoken 和 refreshtoken 返回前端更新,将原来的 refreshtoken 删除,再把新的 refreshtoken 存入 redis

该机制的 uml 图如下:


最佳实践


生成 token


基于 springcache 来操作 redis,利用 md5 算法对 token 进行加密,防止其作为键的后缀存入时过长,导致”大key“的问题出现

public class commonredisconstants {
    public static class rediskey {
        /**
         * refreshtoken 前缀
         */
        public static final string refresh_token_prefix = "refresh_token_prefix_%s";
    }
}
@resource
private stringredistemplate stringredistemplate;


// 生成 accesstoken
private string createaccesstoken(map<string, object> claims) {
    // 这里是利用 jjwt 编写的工具类方法,读者可以自行实现相关工具类
	return jwtutils.generateaccesstoken(claims);
}

// 生成 refreshtoken 并存入 redis
private string createrefreshtoken(map<string, object> claims) {
    string refreshtoken = jwtutils.generaterefreshtoken(claims);
    // rediskey 的形式为固定前缀+md5转换的token
    string rediskey = string.format(commonredisconstants.rediskey.refresh_token_prefix, md5util.generatemd5str(refreshtoken));
    // 设置有效期为 3 days
    this.stringredistemplate.opsforvalue().set(rediskey, refreshtoken, duration.ofdays(3l));
    return refreshtoken;
}

校验 token


基于自定义注解和 spring aop 实现校验 token,并将解析后的信息存储到上下文

自定义的注解:

@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface currentuser {
}

aop 切面:

@aspect
@component
@slf4j
public class currentuseraspect {

    private final httpservletrequest request;

    public currentuseraspect(httpservletrequest request) {
        this.request = request;
    }

    @before("@annotation(currentuser)")
    public void setusercontext(currentuser currentuser) {
        string token = request.getheader("authorization");
        if (token != null) {
            try {
                // 这里是利用 jjwt 编写的工具类方法,读者可以自行实现相关工具类
                claims claims = jwtutils.parsetoken(token);
                // 这里是利用 threadlocal 存储用户信息到上下文,读者可以自行实现相关工具类
                usercontextutil.set(claims);
            } catch (exception e) {
                // token 解析失败后的逻辑
            }
        } else {
            // 请求头未携带 token 的逻辑
        }
    }

    // 方法执行完后释放资源,防止内存泄漏
    @after("@annotation(currentuser)")
    public void clearusercontext(currentuser currentuser) {
        usercontextutil.clear();
    }

}

刷新 token


前端调用刷新 token 后,服务端返回新的 accesstoken 和 refreshtoken:

@data
@allargsconstructor
public class adminloginvo {
    private string accesstoken;
    private string refreshtoken;
}
public adminloginvo refreshlogin(string refreshtoken) {
        // 校验 token 是否有效
        boolean isvalidated = jwtutils.validatetoken(refreshtoken);
        if (!isvalidated) {
            // token 
        }

        /**
         * 校验 redis 里的 refreshtoken 是否失效
         * 未失效:将 redis 里的 refreshtoken 删除,重新颁发新的 accesstoken 和 refreshtoken
         * 已失效:重新登录
         */
        string rediskey = string.format(commonredisconstants.rediskey.refresh_token_prefix, md5util.generatemd5str(jwtutils.predecodetoken(refreshtoken)));
        boolean haskey = this.stringredistemplate.haskey(rediskey);
        if (objectutil.notequal(haskey, boolean.true)) {
            // 原 token 过期或已经使用过的逻辑
        }
        // 删除原 token
        this.stringredistemplate.delete(rediskey);
        // 颁发新的 accesstoken 和 refreshtoken
        claims claims = jwtutils.parsetoken(refreshtoken);
        string accesstoken = createaccesstoken(claims);
        refreshtoken = createrefreshtoken(claims);
        return new adminloginvo(accesstoken, refreshtoken);
    }
(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com