单点登录



概念:登录某集团的某一产品之后,访问其他产品的网站时就会是登录状态,比如登录QQ之后,进入QQ游戏的时候就是登录过的状态,具体实现方法有以下:

Redis+token实现单点登录:

生成一个随机字符串token,以token为key,用户信息为value存储在redis缓存中,用户访问时带着这个token就可以实现单点登录;

这里的token是无实际意义的,和用户信息无关的,否则用户每次登录的时候token都是同一个,容易被黑;

JWT实现单点登录:

这里jwt维护的也是一个token,但是这个token是有意义的,可以理解成用户信息的加密数据。通过这串加密数据就可以反向解出当前登录的是哪一个用户。

Redis+Token登录和校验流程:

登录->校验用户名密码->生成随机token->将token放入redis中并返回给前端->结束

校验->后端拦截请求,从header中获取token,如果没有就返回错误->根据token在redis中获取数据->如果有数据校验成功登录,否则登录校验失败->结束

JWT单点登录流程

登录->校验用户名密码->JWT工具包随机生成token->将token放入redis(也可以不放),并返回给前端->结束

校验->后端拦截请求,获取header中的token,如果没有就返回错误->使用工具包解密校验token->如果校验成功登录,否则登录校验失败->结束

JWT原理

Hutool参考文档

结构

  • Header 头部信息,主要声明了JWT的签名算法等信息
  • Payload 载荷信息,主要承载了各种声明并传递明文数据
  • Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWS,用于校验数据

整体结构是:

header.payload.signature

使用

JWT模块的核心主要是两个类:

  1. JWT类用于链式生成、解析或验证JWT信息。
  2. JWTUtil类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作

JWT生成

  1. HS265(HmacSHA256)算法

    // 密钥

点击查看代码
      // 密钥
byte[] key = "1234567890".getBytes(); String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();
  byte[] key = "1234567890".getBytes();

  String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();

生成的内容为:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40
  1. 其他算法
点击查看代码

// 密钥
byte[] key = "1234567890".getBytes(); // SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 随机生成密钥对,此处用户可自行读取`KeyPair`、公钥或私钥生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id))); String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
  // 密钥
byte[] key = "1234567890".getBytes(); // SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 随机生成密钥对,此处用户可自行读取`KeyPair`、公钥或私钥生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id))); String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
  1. 不签名JWT
点击查看代码

//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()
  //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()

JWT解析

JWT验证

  1. 验证签名
点击查看代码

String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40"; // 密钥
byte[] key = "1234567890".getBytes(); // 默认验证HS265的算法
JWT.of(rightToken).setKey(key).verify()
  String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40"; // 密钥
byte[] key = "1234567890".getBytes(); // 默认验证HS265的算法
JWT.of(rightToken).setKey(key).verify()
  1. 详细验证
点击查看代码

String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40"; JWT jwt = JWT.of(rightToken); // JWT
jwt.getHeader(JWTHeader.TYPE);
// HS256
jwt.getHeader(JWTHeader.ALGORITHM); // 1234567890
jwt.getPayload("sub");
// looly
jwt.getPayload("name");
// true
jwt.getPayload("admin");

除了验证签名,Hutool提供了更加详细的验证:validate,主要包括:

  • Token是否正确
  • 生效时间不能晚于当前时间
  • 失效时间不能早于当前时间
  • 签发时间不能晚于当前时间

使用方式如下:

String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNb0xpIiwiZXhwIjoxNjI0OTU4MDk0NTI4LCJpYXQiOjE2MjQ5NTgwMzQ1MjAsInVzZXIiOiJ1c2VyIn0.L0uB38p9sZrivbmP0VlDe--j_11YUXTu3TfHhfQhRKc";

byte[] key = "1234567890".getBytes();
boolean validate = JWT.of(token).setKey(key).validate(0);

其他自定义详细验证见JWT验证-JWTValidator章节。

JWT存在的问题及解决方案讲解

1、token被解密

加盐值(密钥),每个项目的盐值不能一样

2、token被拿到第三方使用(别人包装你的页面使多个用户的操作走一个用户)

没啥好方法,使用限流

包装成工具类

点击查看代码
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.HashMap;
import java.util.Map; public class JwtUtil {
private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class); /**
* 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中
*/
private static final String key = "Yubaibai12306"; /**
* 生成token
* @param id
* @param mobile
* @return token
*/
public static String createToken(Long id, String mobile) {
DateTime now = DateTime.now();
DateTime expTime = now.offsetNew(DateField.HOUR, 24);
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;
} /**
* 验证token
* @param token
* @return 校验结果
*/
public static boolean validate(String token) {
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
// validate包含了verify
boolean validate = jwt.validate(0);
LOG.info("JWT token校验结果:{}", validate);
return validate;
} /**
* 解密token对应的内容
* @param token
* @return token对应内容对象
*/
public static JSONObject getJSONObject(String token) {
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.eyJuYmYiOjE3MjUyNjQ1MzMsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MjUzNTA5MzMsImlhdCI6MTcyNTI2NDUzM30.lSeDNb5QAp_CH1A-nF5Xw5Qk2zyvd4L3KrDmw97gQvM";
validate(token); getJSONObject(token);
}
}

前端使用vuex保存登录信息

vuex或称store,用于存储全局变量,可用于各页面传递参数,或放置项目全局信息

state:定义一个全局变量

getters:获取变量时,做些额外的转换,如日期格式化

mutations:相当于java的setter,用于修改变量

actions:发起异步任务

modules:项目较大,变量较多时,可以模块化

缺点:页面刷新后,数据会丢失

vuex配合h5的session解决浏览器刷新问题

前端小技巧,使用:|| {} 为变量赋值,可防止空指针异常

演示gateway拦截器的使用

登录校验两个步骤:

前端请求带上token,放在header里

后端校验token有效性,在gateway里统一校验

gateway有多个拦截器时,使用order来确定拦截器的顺序

点击查看代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; @Component
public class Test1Filter implements GlobalFilter, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(Test1Filter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
LOG.info("Test1Filter");
return chain.filter(exchange);
// return exchange.getResponse().setComplete();
} @Override
public int getOrder() {
return 1;
}
}
点击查看代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; @Component
public class Test2Filter implements GlobalFilter, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(Test2Filter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
LOG.info("Test2Filter");
return chain.filter(exchange);
// return exchange.getResponse().setComplete();
} @Override
public int getOrder() {
return 0;
}
}

测试结果:

结果
33:11.638 INFO  c.g.t.g.config.Test2Filter    :17   reactor-http-nio-2                    Test2Filter
33:11.638 INFO c.g.t.g.config.Test1Filter :17 reactor-http-nio-2 Test1Filter

为gateway增加登录校验拦截器

登录拦截器
import com.guaigen.train.gateway.util.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; @Component
public class LoginMemberFilter implements GlobalFilter, Ordered {
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("/hello")
|| path.contains("/member/member/login")
|| path.contains("/member/member/sendCode")) {
LOG.info("不需要登录验证:{}", path);
return chain.filter(exchange);
} 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是否有效
boolean validate = JwtUtil.validate(token);
if (validate) {
LOG.info("token有效,请求放行");
return chain.filter(exchange);
} else {
LOG.info("token无效,请求拦截");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
} @Override
public int getOrder() {
return 0;
}
}

JWT单点登录的更多相关文章

  1. 看图理解JWT如何用于单点登录

    单点登录是我比较喜欢的一个技术解决方案,一方面他能够提高产品使用的便利性,另一方面他分离了各个应用都需要的登录服务,对性能以及工作量都有好处.自从上次研究过JWT如何应用于会话管理,加之以前的项目中也 ...

  2. JWT(JSON Web Token) 多网站的单点登录,放弃session

    多个网站之间的登录信息共享, 一种解决方案是基于cookie - session的登录认证方式,这种方式跨域比较复杂. 另一种替代方案是采用基于算法的认证方式, JWT(json web token) ...

  3. Spring Security构建Rest服务-1300-Spring Security OAuth开发APP认证框架之JWT实现单点登录

    基于JWT实现SSO 在淘宝( https://www.taobao.com )上点击登录,已经跳到了 https://login.taobao.com,这是又一个服务器.只要在淘宝登录了,就能直接访 ...

  4. 170810、spring+springmvc+Interceptor+jwt+redis实现sso单点登录

    在分布式环境中,如何支持PC.APP(ios.android)等多端的会话共享,这也是所有公司都需要的解决方案,用传统的session方式来解决,我想已经out了,我们是否可以找一个通用的方案,比如用 ...

  5. springboot+security+JWT实现单点登录

    本次整合实现的目标:1.SSO单点登录2.基于角色和spring security注解的权限控制. 整合过程如下: 1.使用maven构建项目,加入先关依赖,pom.xml如下: <?xml v ...

  6. 基于JWT机制的单点登录

    使用JWT实现单点登录时,需要注意token时效性.token是保存在客户端的令牌数据,如果永久有效,则有被劫持的可能.token在设计的时候,可以考虑一次性有效或一段时间内有效.如果设置有效时长,则 ...

  7. 初识单点登录及JWT实现

    单点登录 多系统,单一位置登录,实现多系统同时登录的一种技术 (三方登录:某系统使用其他系统的用户,实现本系统登录的方式.如微信登录.支付宝登录) 单点登录一般是用于互相授信的系统,实现单一位置登录, ...

  8. 手把手教你学会 基于JWT的单点登录

      最近我们组要给负责的一个管理系统 A 集成另外一个系统 B,为了让用户使用更加便捷,避免多个系统重复登录,希望能够达到这样的效果--用户只需登录一次就能够在这两个系统中进行操作.很明显这就是单点登 ...

  9. Spring Security整合JWT,实现单点登录,So Easy~!

    前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况 ...

  10. JWT(JSON Web Token) 多网站的单点登录,放弃session 转载https://www.cnblogs.com/lexiaofei/p/7409846.html

    多个网站之间的登录信息共享, 一种解决方案是基于cookie - session的登录认证方式,这种方式跨域比较复杂. 另一种替代方案是采用基于算法的认证方式, JWT(json web token) ...

随机推荐

  1. Spring Cloud提供者actuator依赖

    <!-- actuator依赖 --> <dependency> <groupId>org.springframework.boot</groupId> ...

  2. SpringSecurity安全管理

    SpringSecurity安全管理 一.安全简介 在 Web 开发中,安全一直是非常重要的一个方面,因此从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中. 主要安全框架:S ...

  3. Claude是否超过Chatgpt,成为生成式AI的一哥?

    Anthropic 周一推出了 Claude 3 ,据这家初创公司称,该系列中最有能力的 Claude 3 Opus 在各种基准测试中都优于 Openai 的竞争对手 GPT-4 和谷歌的 Gemin ...

  4. 妙趣横生:利用Echarts实现SpreadJS引用从属关系的可视化魅力

    最新技术资源(建议收藏) https://www.grapecity.com.cn/resources/ 在金融行业,我们经常会有审计审查的需求,对某个计算结果进行审查,但是这个计算结果可能依赖多个单 ...

  5. thinkphp模型hasOne、hasMany、belongsTo详解

    在ThinkPHP框架中,hasOne.hasMany和belongsTo是用于定义模型间一对多(1:n).一对一(1:1)和多对一(n:1)关联关系的方法.以下是一些简单的示例来解释这些关系: 1. ...

  6. django 计算两个TimeField的时差

    在 Django 中,你可以使用 datetime 模块来计算两个 TimeField 字段的时间差.以下是一个示例: from datetime import datetime, timedelta ...

  7. oeasy教您玩转vim - 73 - # 映射map

    ​ 映射map 回忆上次缩写的细节 这次了解到了:abbrivate缩写 可以定义缩写 :ab o1z oeasy 这里面还可以包括方向键.回车键之类的东西 可以定义到指定的模式 iab cab 查看 ...

  8. 基于 Three.js 的 3D 模型加载优化

    作者:来自 vivo 互联网前端团队- Su Ning 作为一个3D的项目,从用户打开页面到最终模型的渲染需要经过多个流程,加载的时间也会比普通的H5项目要更长一些,从而造成大量的用户流失.为了提升首 ...

  9. 靶机: EvilBox---One

    靶机: EvilBox---One 准备工作 靶机地址: https://download.vulnhub.com/evilbox/EvilBox---One.ova MD5 校验:c3a65197b ...

  10. Jenkins+docker 部署SpringCloud微服务

    部署需要提前准备的环境:安装好Jenkins.docker.Maven.Jdk1.8.Git 说明:由于本例只说明如何部署,所以有关项目其他服务如nacos.mysql.redis.seata等默认已 ...