JWT单点登录

单点登录



概念:登录某集团的某一产品之后,访问其他产品的网站时就会是登录状态,比如登录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;
    }
}
posted @ 2024-09-03 14:26  鱼摆摆不摆  阅读(53)  评论(0编辑  收藏  举报