漫谈JSON Web Token(JWT)
一、背景
传统的单体应用基于cookie-session的身份验证流程一般是这样的:
- 用户向服务器发送账户和密码。
- 服务器验证账号密码成功后,相关数据(用户角色、登录时间等)都保存到当前会话中。
- 服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中。
- 以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid作为索引获取到具体session。
但是这种模式存在如下几个问题:
- 没有分布式架构无法支持横向扩展,例如站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站,满足不了这种需求。
- 如果用户很多,这些信息存储在服务器内存中会给服务器增加负担。
- 还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
针对问题1,有两种解决方案,第一种就是使用session共享,比如使用redis实现,第二种解决方案就是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
二、什么是JWT
JSON WEB TOKEN(以下称JWT),JWT也是一种token,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
2.1JWT的特点
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
自包含(Self-contained): 负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。
2.2JWT的消息结构
实际的JWT数据结构就像这样。
它是一个很长的字符串,中间用点分隔成三个部分,红色是header,紫色部分是Payload,蓝色部分是Signature。即格式都是Header.Payload.Signature。
Header(头部)
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。
Payload(负载)
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段我们还可以自定义字段,比如:
{
"userId": 1234567890,
"name": "John Doe"
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。然后这个JSON对象也要使用Base64URL算法转成字符串。
Signature(签名)
Signature是对前两个部分的签名,作用是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
Base64URL算法
Base64URL算法与Base64算法有一些区别,作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_" 替换,这就是Base64URL算法。
三、JWT的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信都要带上JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
还可以将JWT放在POST请求的数据体中,或者跟在URL后面。
所以使用JWT实现单点登录,也可以放在Cookie中或者Header中,基于Cookie的实现可以参考这篇文章《八幅漫画理解使用 JWT设计的单点登录系统》,之前公司的项目是基于Header的Authorization字段字段实现的。
四、JWT的缺点
当然,JWT并不是完美的,它也有一些缺点。
1.JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
2.JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。所以为了避免盗用,可以将有效期设置的短一些,使用HTTPS协议加密传输。
《理解JWT的使用场景和优劣》这篇文章介绍的很详细,但是我不太认同他说的使用jwt做单点登录+会话管理没有传统的cookie-session 机制工作得更好。
五、代码示例
JWT一般是在网关服务配合Filter来实现认证的,考虑再弄个Zuul网关比较麻烦这里就简化了下。
JwtTestController
/**
* Created by 2YSP on 2019/8/26.
*/
@Slf4j
@RestController
@RequestMapping("/jwt")
public class JwtTestController {
@GetMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password, HttpServletResponse response) throws Exception {
if (!checkUserNameAndPwd(username, password)) {
return "login error:invalid username or password";
}
// 过期时间
Long exp = System.currentTimeMillis() + 20 * 1000;
PayLoad payLoad = new PayLoad(1L, username, exp);
String token = JwtUtils.generateToken(payLoad);
Cookie cookie = new Cookie("token", token);
// HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击
cookie.setHttpOnly(true);
// 30秒
cookie.setMaxAge(30);
response.addCookie(cookie);
return token;
}
@GetMapping("/verify")
public Boolean verifyToken(HttpServletRequest request) {
String token = getTokenFromCookie(request);
if (StringUtils.isBlank(token)){
return false;
}
// 验证签名
if (!JwtUtils.checkSignature(token)){
return false;
}
PayLoad payLoad = JwtUtils.getPayLoad(token);
if (payLoad.getExp() < System.currentTimeMillis()){
// 已过期
return false;
}
Gson gson = new Gson();
log.info("verify successfully ,payLoad:{} ", gson.toJson(payLoad));
return true;
}
private String getTokenFromCookie(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
if (cookie.getName().equals("token")){
return cookie.getValue();
}
}
return null;
}
private boolean checkUserNameAndPwd(String username, String pwd) {
if (StringUtils.isBlank(username)) {
return false;
}
if (StringUtils.isBlank(pwd)) {
return false;
}
if (username.equals("admin") && pwd.equals("1234")) {
return true;
}
return false;
}
}
这里提供了两个接口,一个是模拟登录(登录一般是POST的这里方便测试就改为GET方式了),登录成功后返回一个token同时将token保存在cookie中。另一个是校验token的接口,依次进行验签、过期时间等校验。
PayLoad
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PayLoad {
private Long userId;
private String name;
private Long exp;
}
保存一些用户信息和过期时间的实体类。
JwtUtils
- public class JwtUtils {
- /**
- * 加密算法
- */
- public static final String HEADER_ALG = "HS256";
- public static final String HEADER_TYP = "JWT";
- /**
- * 加密串
- */
- public static final String SECRET = "d5ec0a02";
- public static String generateToken(PayLoad payLoad) throws Exception {
- Gson gson = new Gson();
- Header header = new Header(HEADER_ALG, HEADER_TYP);
- String encodedHeader = Base64Utils.encodeToUrlSafeString(gson.toJson(header).getBytes(
- Charsets.UTF_8));
- String encodePayLoad = Base64Utils.encodeToUrlSafeString(gson.toJson(payLoad).getBytes(
- Charsets.UTF_8));
- String signature = HMACSHA256(encodedHeader + "." + encodePayLoad, SECRET);
- return encodedHeader + "." + encodePayLoad + "." + signature;
- }
- public static boolean checkSignature(String token) {
- String[] array = token.split("\\.");
- if (array.length != 3) {
- throw new IllegalArgumentException("token error");
- }
- try {
- String signature = HMACSHA256(array[0] + "." + array[1], SECRET);
- return signature.equals(array[2]);
- } catch (Exception e) {
- }
- return false;
- }
- public static PayLoad getPayLoad(String token) {
- String[] array = token.split("\\.");
- if (array.length != 3) {
- throw new IllegalArgumentException("token error");
- }
- String payLoad = new String(Base64Utils.decodeFromUrlSafeString(array[1]), Charsets.UTF_8);
- Gson gson = new Gson();
- return gson.fromJson(payLoad, PayLoad.class);
- }
- public static String HMACSHA256(String data, String key) throws Exception {
- Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
- SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
- sha256_HMAC.init(secret_key);
- byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
- StringBuilder sb = new StringBuilder();
- for (byte item : array) {
- sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
- }
- return sb.toString().toUpperCase();
- }
- @Data
- @AllArgsConstructor
- static class Header {
- private String alg;
- private String typ;
- }
- }
JWT的工具类,提供了一些生成token,验证签名等方法。代码地址。
漫谈JSON Web Token(JWT)的更多相关文章
- JSON WEB Token(JWT)
最近面试被问及单点登陆怎么解决?自己的项目前后端分离,自己实现token认证,token有失效时间,token中包含用户基本的信息.且一个当用户重新登陆后,原来的token就会失效,这么安全的一个to ...
- JSON Web Token (JWT) 简介
JSON Web Token (JWT) 是一种基于 token 的认证方案. JSON Web Token 的结构 一个 JWT token 看起来是这样的: eyJhbGciOiJIUzI1NiI ...
- JSON Web Token (JWT) 实现与使用方法
1. JSON Web Token是什么 JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的.自包含的方式,用于作为JSON对象在各方之间安全地传输信息.该 ...
- Json Web Token(JWT)详解
什么是Json Web Token Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的 ...
- JSON Web Token (JWT),服务端信息传输安全解决方案。
JWT介绍 JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑独立的基于JSON对象在各方之间安全地传输信息的方式.这些信息可以被验证和信任,因为它是数字签名的 ...
- 基于 Token 的身份验证:JSON Web Token(JWT)
1.传统身份验证和JWT的身份验证 传统身份验证: HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用.这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过 ...
- JSON Web Token (JWT) - Introduction
To validate the challenge, connect as admin.------------以admin登陆 https://jwt.io/introduction/ ...
- json web token JWT实现TP5创建和验证
根据博客进行jwt初始化配置 https://blog.csdn.net/weixin_43389208/article/details/117442266?spm=1001.2014.3001.55 ...
- JSON Web Token (JWT) - Weak secret
This API with its /hello endpoint (accessible with GET) seems rather welcoming at first glance but i ...
随机推荐
- 庐山真面目之六微服务架构Consul集群、Ocelot网关集群和Nginx版本实现
庐山真面目之六微服务架构Consul集群.Ocelot网关集群和Nginx版本实现 一.简介 在上一篇文章<庐山真面目之五微服务架构Consul集群.Ocelot网关和Nginx版本实 ...
- python核心高级学习总结6------面向对象进阶之元类
元类引入 在多数语言中,类就是一组用来描述如何生成对象的代码段,在python中同样如此,但是在python中把类也称为类对象,是的,你没听错,在这里你只要使用class关键字定义了类,其解释器在执行 ...
- PyQt(Python+Qt)学习随笔:部件的inputMethodHints属性
inputMethodHints属性只对输入部件有效,输入法使用它来检索有关输入法应如何操作的提示,例如,如果设置了只允许输入数字的标志,则输入法可能会更改其可视组件,以反映只能输入数字.相关取值及含 ...
- MapReduce简单执行过程及Wordcount案例
MapReducer运行过程 以单词统计为案例. 假如现在文件中存在如下内容: aa bb aa cc dd aa 当然,这是小文件,如果文件大小较大时会将文件进行 "切片" ,此 ...
- crawlergo动态爬虫去除Spidername使用
本来是想用AWVS的爬虫来联动Xray的,但是需要主机安装AWVS,再进行规则联动,只是使用其中的目标爬虫功能感觉就太重了,在github上面找到了由360 0Kee-Team团队从360天相中分离出 ...
- Scrum 冲刺第一天
一.团队信息 1.团队名称 挑战极限队 2.团队成员 张博愉(3118005074) 张润柏(3118005075) 郑堉涵(3118005077) 周伟建(3118005079) 林梓琦(31180 ...
- C#软件性能优化
C#软件性能优化 1. 性能 衡量一个软件系统性能的常见指标有:响应时间.负载.资源使用率.并发数.在软件中有具体的提高性能需求时,我们需分析该系统性能的影响由哪些因素组成,再针对各部分进行性能 ...
- REHの收藏列表
搬运自本人的AcWing,所以那里的文章会挺多. 友链(同类文章) :bztMinamoto 世外明月 mlystdcall 新人手册:AcWing入门使用指南 前言 有看到好文欢迎推荐(毛遂自荐也可 ...
- ACM训练赛:第20次
这次的题思维都很强,等之后的考试结束会集中精力重新训练一些思维题. A - A simple question CodeForces - 520B 思路: 直接看的话,很容易发现如果 \(n > ...
- ftp之filezilla使用记录
1.550 can't access file错误 我们在客户端查看文件时,有些文件被另外的程序占用了,就会出现这个错误.比如,log日志文件.解决办法:在server端,找到settings设置,找 ...