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原理
结构
- Header 头部信息,主要声明了JWT的签名算法等信息
- Payload 载荷信息,主要承载了各种声明并传递明文数据
- Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWS,用于校验数据
整体结构是:
header.payload.signature
JWT模块的核心主要是两个类:
JWT
类用于链式生成、解析或验证JWT信息。JWTUtil
类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作
JWT生成
-
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
- 其他算法
点击查看代码
// 密钥
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();
- 不签名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验证
- 验证签名
点击查看代码
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()
- 详细验证
点击查看代码
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;
}
}