JWT实现单点登录

文章目录

JWT实现单点登录

  • 登录流程:
    校验用户名密码->生成随机JWT Token->返回给前端。之后前端发请求携带该Token就能验证是哪个用户了。
  • 校验流程:
    从前端请求的header获取JWT Token->根据工具包校验JWT Token->校验成功或失败

JWT 简介

结构

Header 头部信息,主要声明了JWT的签名算法等信息

Payload 载荷信息,主要承载了各种声明并传递明文数据

Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWT,用于校验数据

整体结构是:

header.payload.signature

参考文档:https://doc.hutool.cn/pages/jwt/

存在问题及解决方案

    1. token被解密:如工具包被获取。可通过增加"盐值"来解决。
    1. token被拿到第三方使用:如被包装到第三方使用(ChatGPT工具),可以通过限流来解决。

登录流程

后端程序实现

封装hutool工具类:

java 复制代码
public class JwtUtil {
    private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);

    /**
     * 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中
     */
    private static final String key = "xxx";

    public static String createToken(Long id, String mobile) {
        LOG.info("开始生成JWT token,id:{},mobile:{}", id, mobile);
        GlobalBouncyCastleProvider.setUseBouncyCastle(false);
        DateTime now = DateTime.now();
        DateTime expTime = now.offsetNew(DateField.HOUR, 24);
//        DateTime expTime = now.offsetNew(DateField.SECOND, 10);

        Map<String, Object> payload = new HashMap<>();
        // 签发时间
        payload.put(JWTPayload.ISSUED_AT, now);
        // 过期时间
        payload.put(JWTPayload.EXPIRES_AT, expTime);
        // 生效时间
        payload.put(JWTPayload.NOT_BEFORE, now);
        // 内容
        payload.put("id", id);
        payload.put("mobile", mobile);
        String token = JWTUtil.createToken(payload, key.getBytes());
        LOG.info("生成JWT token:{}", token);
        return token;
    }

    public static boolean validate(String token) {
        LOG.info("开始JWT token校验,token:{}", token);
        GlobalBouncyCastleProvider.setUseBouncyCastle(false);
        JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
        // validate包含了verify
        boolean validate = jwt.validate(0);
        LOG.info("JWT token校验结果:{}", validate);
        return validate;
    }

    public static JSONObject getJSONObject(String token) {
        GlobalBouncyCastleProvider.setUseBouncyCastle(false);
        JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
        JSONObject payloads = jwt.getPayloads();
        payloads.remove(JWTPayload.ISSUED_AT);
        payloads.remove(JWTPayload.EXPIRES_AT);
        payloads.remove(JWTPayload.NOT_BEFORE);
        LOG.info("根据token获取原始内容:{}", payloads);
        return payloads;
    }

    public static void main(String[] args) {
        createToken(1L, "123");

        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MzY0ODczMDQsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MzY1NzM3MDQsImlhdCI6MTczNjQ4NzMwNH0.Bui7guCvPEF557eqxRLwmt5tO-W-3oVLnn37H4qOVfA";
        validate(token);

        getJSONObject(token);
    }
}

后端定义登录业务:

java 复制代码
    public MemberLoginResp login(MemberLoginReq memberLoginReq){
        String mobile = memberLoginReq.getMobile();
        String code = memberLoginReq.getCode();
        Member memberDB = selectByMobile(mobile);

        if (ObjectUtil.isEmpty(memberDB)){
            throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);
        }

        if(!code.equals("8888")){
            throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);
        }

        MemberLoginResp memberLoginResp = new MemberLoginResp();
        memberLoginResp.setId(memberDB.getId());
        memberLoginResp.setMobile(mobile);

        String token = JwtUtil.createToken(memberDB.getId(), memberDB.getMobile());
        memberLoginResp.setToken(token);

        return memberLoginResp;
    }

通过调用封装的JwtUtil生成token并返回前端

成功返回Token结果

前端保存Token

Vuex全局保存Token到store中

js 复制代码
import { createStore } from 'vuex'

const MEMBER = "MEMBER";

export default createStore({
  state: {
    member: {}
  },
  getters: {
  },
  mutations: {
    setMember (state, _member) {
      state.member = _member;
    }
  },
  actions: {
  },
  modules: {
  }
})
js 复制代码
    const login = () => {
      axios.post("/member/member/login", loginForm).then((response) => {
        let data = response.data;
        if (data.success) {
          notification.success({ description: '登录成功!' });
          // 登录成功,跳到控台主页
          router.push("/welcome");
          store.commit("setMember", data.content);
        } else {
          notification.error({ description: data.message });
        }
      })
    };

store存放信息的缺点及解决

store存放用户信息后,如果刷新页面,那么信息也会消失!

store可以理解为缓存,一旦重新加载,则缓存全都没了。

解决方法:

  • step1. 新增session-storage.js,封装会话缓存sessionStorage
js 复制代码
// 所有的session key都在这里统一定义,可以避免多个功能使用同一个key
SESSION_ORDER = "SESSION_ORDER";
SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";

SessionStorage = {
    get: function (key) {
        var v = sessionStorage.getItem(key);
        if (v && typeof(v) !== "undefined" && v !== "undefined") {
            return JSON.parse(v);
        }
    },
    set: function (key, data) {
        sessionStorage.setItem(key, JSON.stringify(data));
    },
    remove: function (key) {
        sessionStorage.removeItem(key);
    },
    clearAll: function () {
        sessionStorage.clear();
    }
};
  • step2. 在index.html中引入该js
html 复制代码
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <!-- 引入js -->
      <script src="<%= BASE_URL %>js/session-storage.js"></script>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
  • step3. 修改store的index.js
js 复制代码
const MEMBER = "MEMBER";

export default createStore({
  state: {
    member: window.SessionStorage.get(MEMBER) || {} # 读取
  },
  getters: {
  },
  mutations: {
    setMember (state, _member) {
      state.member = _member;
      window.SessionStorage.set(MEMBER, _member); # 设置
    }
  },

不再是把member定义为{},而是首先在缓存中获取,如果没有则设置为{}。同时避免空指针

同时在用户登录后设置MEMBER缓存

校验流程:为gateway增加登录校验拦截器

  • 添加依赖
xml 复制代码
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.10</version>
            </dependency>
  • 拦截器类
java 复制代码
@Component
public class LoginMemberFilter implements Ordered, GlobalFilter {

    private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 排除不需要拦截的请求
        if (path.contains("/admin")
                || path.contains("/redis")
                || path.contains("/test")
                || path.contains("/member/member/login")
                || path.contains("/member/member/send-code")) {
            LOG.info("不需要登录验证:{}", path);
            return chain.filter(exchange);
        } else {
            LOG.info("需要登录验证:{}", path);
        }
        // 获取header的token参数
        String token = exchange.getRequest().getHeaders().getFirst("token");
        LOG.info("会员登录验证开始,token:{}", token);
        if (token == null || token.isEmpty()) {
            LOG.info( "token为空,请求被拦截" );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // 校验token是否有效,包括token是否被改过,是否过期
        boolean validate = JwtUtil.validate(token);
        if (validate) {
            LOG.info("token有效,放行该请求");
            return chain.filter(exchange);
        } else {
            LOG.warn( "token无效,请求被拦截" );
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

    }

    /**
     * 优先级设置  值越小  优先级越高
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
  • 测试结果:
  1. 直接调用不需要验证登录的接口
java 复制代码
@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "test";
    }

}
  1. 调用需要登录的接口方法(未登录)

同时服务器端没有打印,表示请求已被拦截

  1. 调用login登陆后再次执行上述请求
    login打印日志:

    调用请求打印日志:

可见成功校验token,并读取登录用户信息,通过校验

另一种单点登录方法:Token+Redis实现单点登录

  • 登录流程:
    校验用户名密码->生成随机Token->将Token存放到Redis,并返回给前端。
    之后前端发请求携带该Token就能验证是哪个用户了。
  • 校验流程:
    从前端请求的header获取Token->根据Token到Redis获取用户数据->若有数据则登录校验通过,否则失败
相关推荐
C澒3 小时前
Remesh 框架详解:基于 CQRS 的前端领域驱动设计方案
前端·架构·前端框架·状态模式
前端不太难4 小时前
HarmonyOS 游戏里,Ability 是如何被重建的
游戏·状态模式·harmonyos
Dragon Wu9 小时前
Spring Security Oauth2.1 授权码模式实现前后端分离的方案
java·spring boot·后端·spring cloud·springboot·springcloud
程序员agions10 小时前
2026年,微前端终于“死“了
前端·状态模式
Cult Of11 小时前
Alicea Wind的个人网站开发日志(2)
开发语言·python·vue
源力祁老师1 天前
深入解析 Odoo 中的 return 特殊用法-Odoo Action 的本质
状态模式
Byron07071 天前
从多端割裂到体验统一:基于 Vue 生态的跨端架构落地实战
vue·多端
闻哥1 天前
从测试坏味道到优雅实践:打造高质量单元测试
java·面试·单元测试·log4j·springboot
计算机程序设计小李同学1 天前
基于 Spring Boot + Vue 的龙虾专营店管理系统的设计与实现
java·spring boot·后端·spring·vue
沐墨染1 天前
Vue实战:自动化研判报告组件的设计与实现
前端·javascript·信息可视化·数据分析·自动化·vue