JWT & 用户身份认证演变过程
HTTP是无状态的,服务端和客户端如何保持登录状态?
工程师在服务端搞了亿点事情, 就有了下面的解决方案。

服务器为了保存用户状态而创建的一个特殊的对象。
当浏览器第一次登录时,服务器创建一个session对象(该对象有一个唯一的id,一般称之为sessionId),服务器会将sessionId以cookie的方式发送给浏览器。当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据sessionId就可以找到对应的session对象。
主要针对Java Web(JSP)+ Tomcat
为了避免Session中存储的数据过大,Session需要销毁:
- 超时自动销毁。
从用户最后一次访问网站开始,超过一定时间后,服务器自动销毁Session,以及保存在Session中的数据。
Tomcat 服务器默认的Session超时时间是30分钟,可以利用web.xml设置超时时间单位是分钟,设置为0表示不销毁。
<session-config>
<session-timeout>30</session-timeout>
</session-config>
- 调用API方法,主动销毁Session
session.invalidate();
- session保存在Tomcat中,一定程度上会增大服务器压力
- 无法解决分布式共享用户登录状态的问题

使用内存缓存系统(内存数据库),将Session存储到同一内存数据库(内存数据库集群)中,所有Tomcat从内存数据库中获取Session。
- MemCache
- Redis(NoSQL)

NoSQL仅仅是一个概念,最常见的解释是“non-relational”, “Not Only SQL”也被很多人接受。下图是常见NoSQL数据库的分类及对比

- MemCache无法持久化,停机后失去所有用户的登录Session,无安全机制等,可以使用Redis代替
- 前后端分离怎么解,部署到不同服务器,移动端、小程序
(1)基本原理
主要使用Redis(单节点/集群)存储用户的登录信息,基本原理类似于session,将token(sessionId)作为key,用于登录鉴权的用户信息作为value保存到Redis中,用户登录后所有请求携带token,与后端约定好,可以放到header中,也可以是cookies(允许请求携带cookies)。
后端在处理请求前(拦截器、过滤器)获取token,然后进行登录鉴权,鉴权通过后继续api请求,失败返回token失效信息提示用户登录。
- 实现前后端分离部署,移动端、小程序,登录认证
- 借助Redis的expire,可以设置登录有效时长、对用户登录状态延期,跟web端cookies类似
- 中心化,最大优点是主动让Token失效(删除,不能设置expire为0,最小为1)
// 保存
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
// 设置过期时间 > 0
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
// 删除
redisTemplate.delete(key);
// 是否存在key
redisTemplate.hasKey(key);
// 根据key获取
redisTemplate.opsForValue().get(key);
- 每个需要鉴权的请求都要读取redis,会增大redis的压力
- 如果是分布式Redis,会增大系统复杂性
请继续往下看
JSON Web Token(JWT)是一个轻量级的认证规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其本质是一个token,是一种紧凑的URL安全方法,用于在网络通信的双方之间传递。
{header_urlbase64}.{payload_urlbase64}.{signature}
header,payload,signature三个部分的字符串通过 . 连接起来。
- header
描述JWT的元数据
{
"alg": "HS256",
"typ": "JWT"
}
alg:表示前面算法,默认是 HMAC SHA256(写成 HS256)
typ:表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT
- payload
存放实际的数据,JWT规定了7个官方字段
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):唯一id
除了上述官方字段,这里还可以存放自定义的数据,如:
{
"exp": 1664365790511,
"tenantId": "1",
"appId": "",
"userId": "131SG161E610001",
"serverToken": "7360dbb8-067d-4339-90a4-8955921c9e65",
"refreshToken": "d2b0083f-442d-42ea-a765-3c98d95119cb",
"expiredTime": 0,
"reloginVersion": 0,
"ip": "127.0.0.1"
}
- signature
对前两个字符串的签名,防止数据被篡改。签名方法如下:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
secret:需要传入salt(盐值),存放在服务端

Tips
什么是base64UrlEncode?
Base64有三个字符+、/和=,在URL里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。
- 去中心化,便于分布式系统使用
- 基本信息可以直接放在token中。username,nickname,role
- 功能权限较少的话,可以直接放在token中。用bit位表示用户所具有的功能权限(类似于价税那种)
- 服务端不能主动让token失效,这里是一个很大的安全问题,失效时间越长,越不安全
如果将过期时间设置太短,会影响用户体验
- jwt token无法续期
<!-- JWT maven 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

package com.admin.api.utils; import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.admin.api.constant.Constants;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* Jwt工具类
* @author wang_dgang
* @since 2018-10-23 10:28:40
*/
public class JwtUtil { // 日志
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class); // JWT 加解密类型
private static final SignatureAlgorithm JWT_ALG = SignatureAlgorithm.HS512;
/**
* 生成JWT
* @param subjectJson JSON
* @param expire 超时时间,单位秒
* @return
*/
public static String buildJWT(String subjectJson, int expire) {
// 生成JWT的时间
Date startDate = DateUtil.date();
// 过期时间
Date endDate = DateUtil.offset(startDate, DateField.SECOND, expire);
// 为payload添加各种标准声明和私有声明
JwtBuilder builder = Jwts.builder() // new一个JwtBuilder,设置jwt的body
.setId(IdUtil.simpleUUID()) // JWT的唯一标识,回避重放攻击
.setExpiration(endDate) // 过期时间
.setIssuedAt(startDate) // 签发时间
.setSubject(subjectJson) // json格式的字符串
// .compressWith(CompressionCodecs.DEFLATE) // 压缩
.signWith(JWT_ALG, generalKey()); // 设置签名算法和密钥
return builder.compact();
}
/**
* 生成JWT
* @param claims Map
* @param expire 超时时间,单位秒
* @return
*/
public static String buildJWT(Map<String, Object> claims, int expire) {
// 生成JWT的时间
Date startDate = DateUtil.date();
// 过期时间
Date endDate = DateUtil.offset(startDate, DateField.SECOND, expire);
// 为payload添加各种标准声明和私有声明
JwtBuilder builder = Jwts.builder() // new一个JwtBuilder,设置jwt的body
.setClaims(claims) // 设置payload数据,需先进行设置,会覆盖其他属性
// .setId(IdUtil.simpleUUID()) // JWT的唯一标识,回避重放攻击
.setExpiration(endDate) // 过期时间,需设置在claims之后,否则会被覆盖
// .compressWith(CompressionCodecs.DEFLATE) // 压缩
.signWith(JWT_ALG, generalKey()); // 设置签名算法和密钥
return builder.compact();
}
/**
* Jwt验证(true:验证通过,false:验证失败)
* @param jwt 内容文本
* @return
*/
public static boolean checkJWT(String jwt) {
return ObjectUtil.isNotNull(getClaimsJws(jwt));
}
/**
* parseClaimsJws
* @param jwt 内容文本
* @return
*/
private static Jws<Claims> getClaimsJws(String jwt) {
Jws<Claims> parseClaimsJws = null;
try {
SecretKey generalKey = generalKey();
JwtParser parser = Jwts.parser().setSigningKey(generalKey);
parseClaimsJws = parser.parseClaimsJws(jwt);
} catch (Exception e) {
log.warn("JWT验证失败,原因:{}", e.getMessage());
}
return parseClaimsJws;
}
/**
* 生成加密key
* @return
*/
private static SecretKey generalKey() {
return JwtSecretKeyHolder.instance;
}
private static class JwtSecretKeyHolder {
// 服务端保存的jwt秘钥转为字节数组
static final byte[] encodedKey = Constants.JWT_SECRET.getBytes();
private static final SecretKey instance = new SecretKeySpec(encodedKey, JWT_ALG.getJcaName());
}
/**
* Jwt解析
* @param jwt 内容文本
* @return Subject中json串
*/
public static String parseJWT(String jwt) {
Jws<Claims> claimsJws = getClaimsJws(jwt);
Claims body = claimsJws.getBody();
// 获取加密的内容JSON并返回
String subjectJson = body.getSubject();
return subjectJson;
}
/**
* Jwt解析
* @param jwt
* @return Claims对象,直接get获取对应值
*/
public static Claims parseJWTMap(String jwt) {
Jws<Claims> claimsJws = getClaimsJws(jwt);
Claims body = claimsJws.getBody();
return body;
} // demo
public static void main(String[] args) throws InterruptedException, RuntimeException {
// 包装payload数据
JSONObject json = new JSONObject();
json.put("name", "77hub");
json.put("userId", "131SG161E610001");
json.put("serverToken", "7360dbb8-067d-4339-90a4-8955921c9e65");
// 生成jwt
String jwtWithExpire = buildJWT(json.toJSONString(), 2);
System.out.println(jwtWithExpire);
// Thread.sleep(3 * 1000);
// 解析,获取Subject中的json串
System.out.println(parseJWT(jwtWithExpire));
System.out.println();
System.out.println("---------------------------------");
System.out.println();
// 包装payload数据
Map<String, Object> claimsMap = new HashMap<>();
claimsMap.put("name", "77hub");
claimsMap.put("userId", "131SG161E610001");
claimsMap.put("serverToken", "7360dbb8-067d-4339-90a4-8955921c9e65");
// 生成jwt
String buildJWT = buildJWT(claimsMap, 2);
System.out.println(buildJWT);
// Thread.sleep(3 * 1000);
// 解析jwt
Claims claims = parseJWTMap(buildJWT);
System.out.println(claims.get("name"));
System.out.println(claims.get("serverToken"));
}
}
OAuth(Open Authorization)协议为用户资源的授权提供了一个安全的、开放而又简易的标准。此处省略好几个字,请移步链接查看。
有两个令牌 token , 分别是 access_token 和 refresh_token
(1)access_token
访问令牌, 它是一个用来访问受保护资源的凭证。
(2)refresh_token
刷新令牌, 它是一个用来获取 access_token 的凭证,OAuth 2.0 安全最佳实践中, 推荐 refresh_token 是一次性的,使用 refresh_token 获取 access_token 时,同时会返回一个 新的 refresh_token,之前的 refresh_token 就会失效,但是两个 refresh_token 的绝对过期时间是一样的,所以不会存在 refresh_token 快过期就获取一个新的,然后重复,永不过期的情况。
注意:确保 refresh_token 安全性,OAuth2.0 引入了 client_id、client_secret 机制。即每一个应用都会被分配到一个 client_id 和一个对应的 client_secret。应用必须把 client_secret 妥善保管在服务器上,决不能泄露。刷新 access_token 时,需要验证这个 client_secret。
sha256(client_id + refresh_token + client_secret)

假设有一个用户需要在后台管理界面上操作6个小时。
(1)颁发一个有效性很长的 access_token,比如 6 个小时,或者可以更长,这样用户只需要刚开始登录一次,access_token 可以一直使用,直到 access_token 过期,然后重复,这种是不安全的,access_token 的时效太长,也就失去了本身的意义。

(2)颁发一个1小时有效期的 access_token,过期后重新登录授权,这样用户需要登录 6 次,安全倒是有了,但是用户体验极差。

(3)颁发1小时有效期的 access_token 和6小时有效期的 refresh_token,当 access_token 过期后(或者快要过期的时候),使用 refresh_token 获取一个新的 access_token,直到 refresh_token 过期,用户重新登录,这样整个过程中,用户只需要登录一次,用户体验好。
access_token 泄露了怎么办? 没关系,它很快就会过期。
refresh_token 泄露了怎么办? 没关系,使用 refresh_token 是需要客户端秘钥 client_secret 的。

(4)用户登录后,在后台管理页面上操作1个小时后,离开了一段时间,然后 5个小时后,回到管理页面继续操作,此时 refresh_token 有效期6个小时,一直没有过期,也就可以换取新的 access_token,用户在这个过程中,可以不用重复登录。但是在一些安全要求较高的系统中,第二次操作是需要重新登录的,即使 refresh_token 没有过期,因为中间有几个小时,用户是没有操作的,系统猜测用户已离开,并关闭会话。

优点:
- access_token 有效期短,被盗损失更小,安全性更高
- 如果 refresh_token 被盗了,想刷新 access_token的话,也需要提供过期的access_token,盗取难度增加
- 同时 refresh_token 只有在第一次获取和刷新,access_token 时才会在网络中传输,因此被盗的风险远小于 access_token 从而在一定程度上更安全了一点
- 所谓的更安全就是让盗取信息者更不容易获得而已
缺点:
- 开发复杂度增加,这也是一个系统到一定规模必然的情况,可以借助一些认证框架(Spring Security等)
- 相对的增加了安全性
{
"exp": 1664365790511,
"tenantId": "1",
"appId": "",
"userId": "131SG161E610001",
"serverToken": "7360dbb8-067d-4339-90a4-8955921c9e65",
"refreshToken": "d2b0083f-442d-42ea-a765-3c98d95119cb",
"expiredTime": 0,
"reloginVersion": 0,
"ip": "127.0.0.1"
}
讲到这,对公司系统当前使用的JWT里面 serverToken、refreshToken 就有了一些了解了,serverToken --> access_token,refreshToken --> refresh_token。
具体后端实现还是要使用 Redis,存储一些认证相关的信息。
关于 OAuth2.0 在实际系统中使用的介绍可参照《OAuth2.0 详解》。
接着看这个博客补充
https://sunshinehu.blog.csdn.net/article/details/127526921?spm=1001.2014.3001.5502
JWT & 用户身份认证演变过程的更多相关文章
- 构建具有用户身份认证的 React + Flux 应用程序
原文:Build a React + Flux App with User Authentication 译者:nzbin 译者的话:这是一篇内容详实的 React + Flux 教程,文章主要介绍了 ...
- 构建具有用户身份认证的 Ionic 应用
序言:本文主要介绍了使用 Ionic 和 Cordova 开发混合应用时如何添加用户身份认证.教程简易,对于 Ionic 入门学习有一定帮助.因为文章是去年发表,所以教程内关于 Okta 的一些使用步 ...
- webapp用户身份认证方案 JSON WEB TOKEN 实现
webapp用户身份认证方案 JSON WEB TOKEN 实现Deme示例,Java版 本项目依赖于下面jar包: nimbus-jose-jwt-4.13.1.jar (一款开源的成熟的JSON ...
- Django:学习笔记(9)——用户身份认证
Django:学习笔记(9)——用户身份认证 User
- 移动 APP 端与服务器端用户身份认证的安全方案
最近要做一个项目是java开发后端服务,然后移动APP调用.由于之前没有接触过这块,所以在网上搜索相关的方案.然后搜到下面的一些方案做一些参考. 原文:移动 APP 端与服务器端用户身份认证的安全方案 ...
- Keycloak 13 自定义用户身份认证流程(User Storage SPI)
Keycloak 版本:13.0.0 介绍 Keycloak 是为现代应用程序和服务提供的一个开源的身份和访问管理的解决方案. Keycloak 在测试环境可以使用内嵌数据库,生产环境需要重新配置数据 ...
- 在Asp.net MVC中使用Authorization Manager (AzMan)进行Windows用户身份认证
背景 创建需要通过Windows用户进行身份认证的Asp.net MVC应用 要点 在Asp.net MVC应用基于Windows用户进行身份认证的方法有很多,如MVC自带的Windows认证就经常被 ...
- PHP中对用户身份认证实现两种方法
用户在设计和维护站点的时候,经常需要限制对某些重要文件或信息的访问.通常,我们可以采用内置于WEB服务器的基于HTTP协议的用户身份验证机制. 当访问者浏览受保护页面时,客户端浏览器会弹出对话 ...
- PHP 使用 jwt 方式用户身份认证
封装类 // +---------------------------------------------------------------------- // | Created by PhpSt ...
- 《图解Http》8: 用户身份认证Cookie管理session; 9:HTTP的追加协议(websoket, webDAV)
基本认证,(安全等级低,多数网站不使用) Digest认证:(也不怎么用) SSL客户端认证:(凭借客户端证书认证,如网银登陆) 表单认证:用户名/密码.(常用) SSL客户端认证采用two-fact ...
随机推荐
- NGINX websocket 配制
http { map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstre ...
- map转换 bean和list
转实体类 JSONObject JSONObj = JSONObject.parseObject(JSON.toJSONString(resultMap));String result = JSONO ...
- AndroidQ 打通应用层到HAL层(转)
1. 参考https://blog.csdn.net/qq_34211365/category_9903135.html 直通式,绑定式,从应用端调到hal接口,亲自实现能够更加理解
- Android学习——控件ImageView
1.主要属性 2.缩放类型
- 直播带货源码,flutter 顶部滚动栏+页面
直播带货源码,flutter 顶部滚动栏+页面 tabPage.dart import 'package:flutter/cupertino.dart';import 'package:flutter ...
- java时间日期API
package java1; import org.junit.Test; import java.util.Date; /** * @author 高槐玉 * #Description JDK 8之 ...
- centos7.8 安装 redis5.0.2
1.安装gcc依赖 redis是由C语言开发,因此安装之前必须要确保服务器已经安装了gcc,可以通过如下命令查看机器是否安装: gcc -v 如果没有安装则通过以下命令安装: yum install ...
- 1067- invalid default value for ""
创建表时报的,一个时间字段类型是datetime,一保存就报错,网上说是datetime类型只支持mysql 5.6.5+,我看了下我的版本,5.8的,没什么问题,然后找了许久找到了错误的地方. 很蠢 ...
- df -T 和 du 统计的内存不一致
1.问题原因 客户反馈,df -T 查看到挂载到flash文件的emmc使用了6G左右,但是在flash下看到du -h 只用了2G左右,客户疑问,还有4G去哪儿了? 2.问题怀疑方向 1.怀疑启动阶 ...
- LeetCode习题集
作为一名本科计算机科学与技术的学生,<数据结构与算法><算法分析与设计>这两门课没有学到特别好 上了研究生之后感觉,这些东西需要重新拾起 于是,我找了一个平台,LeetCode ...