springboot结合JWT实现单点登录的示例

 更新时间:2025年01月31日 09:35:16   作者:Sao_E  
本文主要介绍了springboot结合JWT实现单点登录的示例,包括生成Token、验证Token及使用Redis存储Token,具有一定的参考价值,感兴趣的可以了解一下

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/

存在问题及解决方案

  • token被解密:如工具包被获取。可通过增加“盐值”来解决。

  • token被拿到第三方使用:如被包装到第三方使用(ChatGPT工具),可以通过限流来解决。

登录流程

后端程序实现

封装hutool工具类:

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);
    }
}

后端定义登录业务:

    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中

import { createStore } from 'vuex'

const MEMBER = "MEMBER";

export default createStore({
  state: {
    member: {}
  },
  getters: {
  },
  mutations: {
    setMember (state, _member) {
      state.member = _member;
    }
  },
  actions: {
  },
  modules: {
  }
})

    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
// 所有的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
<!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" rel="external nofollow" >
        <!-- 引入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
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增加登录校验拦截器

  • 添加依赖
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.10</version>
            </dependency>
  • 拦截器类
@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;
    }
}
  • 测试结果:
  • 直接调用不需要验证登录的接口
@RestController
public class TestController {

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

}

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

调用请求打印日志:

在这里插入图片描述

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

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

  • 登录流程:
    校验用户名密码->生成随机Token->将Token存放到Redis,并返回给前端。
    之后前端发请求携带该Token就能验证是哪个用户了。
  • 校验流程:
    从前端请求的header获取Token->根据Token到Redis获取用户数据->若有数据则登录校验通过,否则失败

到此这篇关于springboot结合JWT实现单点登录的示例的文章就介绍到这了,更多相关springboot JWT单点登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • springboot实现打印彩色日志

    springboot实现打印彩色日志

    这篇文章主要介绍了springboot实现打印彩色日志的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • SpringBoot整合JDBC的实现

    SpringBoot整合JDBC的实现

    这篇文章主要介绍了SpringBoot整合JDBC的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • MyBatis Generator介绍及使用方法

    MyBatis Generator介绍及使用方法

    MyBatis Generator 是一款针对 MyBatis 或 iBATIS 设计的代码生成器,由 MyBatis 官方提供,这篇文章主要介绍了MyBatis Generator介绍及使用方法,需要的朋友可以参考下
    2023-06-06
  • SpringBoot中实现订单30分钟自动取消的三种方案分享

    SpringBoot中实现订单30分钟自动取消的三种方案分享

    在电商和其他涉及到在线支付的应用中,通常需要实现一个功能:如果用户在生成订单后的一定时间内未完成支付,系统将自动取消该订单,本文将详细介绍基于Spring Boot框架实现订单30分钟内未支付自动取消的几种方案,并提供实例代码,需要的朋友可以参考下
    2023-10-10
  • springboot 在linux后台运行的方法

    springboot 在linux后台运行的方法

    这篇文章主要介绍了springboot 在linux后台运行的方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-06-06
  • Java for循环倒序输出的操作代码

    Java for循环倒序输出的操作代码

    在Java中,要实现一个for循环的倒序输出,通常我们会使用数组或集合(如ArrayList)作为数据源,然后通过倒序遍历这个数组或集合来实现,这篇文章主要介绍了Java for循环倒序输出,需要的朋友可以参考下
    2024-07-07
  • Java try catch语句异常处理详解

    Java try catch语句异常处理详解

    这篇文章主要给大家介绍了关于Java try catch语句异常处理的相关资料,Java中的try-catch用于捕获和处理异常,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • Mybatis原始执行方式Executor代码实例

    Mybatis原始执行方式Executor代码实例

    这篇文章主要介绍了Mybatis原始执行方式Executor代码实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • Java 读取PDF中的文本和图片的方法

    Java 读取PDF中的文本和图片的方法

    本文将介绍通过Java程序来读取PDF文档中的文本和图片的方法。分别调用方法extractText()和extractImages()来读取,需要的朋友可以参考下
    2019-07-07
  • Java基础之FileInputStream和FileOutputStream流详解

    Java基础之FileInputStream和FileOutputStream流详解

    这篇文章主要介绍了Java基础之FileInputStream和FileOutputStream流详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04

最新评论