一、背景

传统的单体应用基于cookie-session的身份验证流程一般是这样的:

  1. 用户向服务器发送账户和密码。
  2. 服务器验证账号密码成功后,相关数据(用户角色、登录时间等)都保存到当前会话中。
  3. 服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中。
  4. 以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid作为索引获取到具体session。

但是这种模式存在如下几个问题:

  1. 没有分布式架构无法支持横向扩展,例如站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站,满足不了这种需求。
  2. 如果用户很多,这些信息存储在服务器内存中会给服务器增加负担。
  3. 还有就是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数据结构就像这样。

enter description here

它是一个很长的字符串,中间用点分隔成三个部分,红色是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

  1. public class JwtUtils { 


  2. /** 

  3. * 加密算法 

  4. */ 

  5. public static final String HEADER_ALG = "HS256"; 


  6. public static final String HEADER_TYP = "JWT"; 

  7. /** 

  8. * 加密串 

  9. */ 

  10. public static final String SECRET = "d5ec0a02"; 


  11. public static String generateToken(PayLoad payLoad) throws Exception { 

  12. Gson gson = new Gson(); 

  13. Header header = new Header(HEADER_ALG, HEADER_TYP); 

  14. String encodedHeader = Base64Utils.encodeToUrlSafeString(gson.toJson(header).getBytes( 

  15. Charsets.UTF_8)); 

  16. String encodePayLoad = Base64Utils.encodeToUrlSafeString(gson.toJson(payLoad).getBytes( 

  17. Charsets.UTF_8)); 

  18. String signature = HMACSHA256(encodedHeader + "." + encodePayLoad, SECRET); 

  19. return encodedHeader + "." + encodePayLoad + "." + signature; 




  20. public static boolean checkSignature(String token) { 

  21. String[] array = token.split("\\."); 

  22. if (array.length != 3) { 

  23. throw new IllegalArgumentException("token error"); 



  24. try { 

  25. String signature = HMACSHA256(array[0] + "." + array[1], SECRET); 

  26. return signature.equals(array[2]); 

  27. } catch (Exception e) { 



  28. return false; 




  29. public static PayLoad getPayLoad(String token) { 

  30. String[] array = token.split("\\."); 

  31. if (array.length != 3) { 

  32. throw new IllegalArgumentException("token error"); 



  33. String payLoad = new String(Base64Utils.decodeFromUrlSafeString(array[1]), Charsets.UTF_8); 

  34. Gson gson = new Gson(); 

  35. return gson.fromJson(payLoad, PayLoad.class); 





  36. public static String HMACSHA256(String data, String key) throws Exception { 


  37. Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 


  38. SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); 


  39. sha256_HMAC.init(secret_key); 


  40. byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); 


  41. StringBuilder sb = new StringBuilder(); 


  42. for (byte item : array) { 


  43. sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 





  44. return sb.toString().toUpperCase(); 





  45. @Data 

  46. @AllArgsConstructor 

  47. static class Header { 


  48. private String alg; 

  49. private String typ; 







JWT的工具类,提供了一些生成token,验证签名等方法。代码地址

漫谈JSON Web Token(JWT)的更多相关文章

  1. JSON WEB Token(JWT)

    最近面试被问及单点登陆怎么解决?自己的项目前后端分离,自己实现token认证,token有失效时间,token中包含用户基本的信息.且一个当用户重新登陆后,原来的token就会失效,这么安全的一个to ...

  2. JSON Web Token (JWT) 简介

    JSON Web Token (JWT) 是一种基于 token 的认证方案. JSON Web Token 的结构 一个 JWT token 看起来是这样的: eyJhbGciOiJIUzI1NiI ...

  3. JSON Web Token (JWT) 实现与使用方法

    1. JSON Web Token是什么 JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的.自包含的方式,用于作为JSON对象在各方之间安全地传输信息.该 ...

  4. Json Web Token(JWT)详解

    什么是Json Web Token Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的 ...

  5. JSON Web Token (JWT),服务端信息传输安全解决方案。

    JWT介绍 JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑独立的基于JSON对象在各方之间安全地传输信息的方式.这些信息可以被验证和信任,因为它是数字签名的 ...

  6. 基于 Token 的身份验证:JSON Web Token(JWT)

    1.传统身份验证和JWT的身份验证 传统身份验证:       HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用.这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过 ...

  7. JSON Web Token (JWT) - Introduction

    To validate the challenge, connect as admin.------------以admin登陆 https://jwt.io/introduction/        ...

  8. json web token JWT实现TP5创建和验证

    根据博客进行jwt初始化配置 https://blog.csdn.net/weixin_43389208/article/details/117442266?spm=1001.2014.3001.55 ...

  9. JSON Web Token (JWT) - Weak secret

    This API with its /hello endpoint (accessible with GET) seems rather welcoming at first glance but i ...

随机推荐

  1. PyQt(Python+Qt)学习随笔:QTextEdit的setText、setHtml、setPlainText之间的区别

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QTextEdit中提供了三个设置编辑器文本的方法,分别是setTex ...

  2. 第8.21节 Python中__lt__、__gt__等 “富比较”(“rich comparison”)方法用途探究

    一. 富比较方法 Python的基类object提供一系列可以用于实现同类对象进行"比较"的方法,可以用于同类对象的不同实例进行比较.他们也是实例方法,定义如下: object.l ...

  3. PyQt(Python+Qt)学习随笔:窗口部件大小策略sizePolicy与SizeConstraint布局大小约束的关系

    在<PyQt(Python+Qt)学习随笔:Qt Designer中部件的三个属性sizeHint缺省尺寸.minimumSizeHint建议最小尺寸和minimumSize最小尺寸>. ...

  4. HTTP助记

    1** 信息,服务器收到请求,需要请求者继续执行操作 100 continue 继续,客户端应继续请求 101 swithching protocls 切换协议,服务器根据客户端的请求切换协议.只能切 ...

  5. 自动化测试架构设计 &&自动化持续集成测试任务实战[线性测试、模块驱动测试、数据驱动测试、关键字驱动测试]

    1 为什么设计自动化测试架构 1.1 企业现状分析 压力大:产品需求不明确,上线时间确定,压力山大. 混乱:未立项,开发时间已过半,前期无控制,后期无保障. 疲于应付:开发人员交付的文件质量差,测试跟 ...

  6. iOS崩溃日志 如何看

    日志主要分为六个部分:进程信息.基本信息.异常信息.线程回溯.线程状态和二进制映像. 我们在进行iPhone应用测试时必然会在"隐私"中找到不少应用的崩溃日志,但是不会阅读对于很多 ...

  7. jquery.sticky 粘性滚动插件使用

    一个jQuery插件,使你能够做任何元素在您的网页上总是可见的,可以作为顶部固定导航显示插件. 官网地址:http://stickyjs.com/ github:https://github.com/ ...

  8. kubernetes 中的证书工作机制

    一文带你彻底厘清 Kubernetes 中的证书工作机制 搬砖者: 张首富 时 间: 2020-05-26 w x: y18163201 原文地址:https://zhaohuabing.com/po ...

  9. 题解 CF830D Singer House

    \(\texttt{Solution}\) 首先考虑 \(\texttt{dp}\) 维护题目要求的深度为 \(i\), 每个节点最多经过一次的不同有向路径数量 \(f_i\). 明显的,只维护这个东 ...

  10. OI知识点/得分技巧的归纳总结

    网络流 拆点/拆边技巧 题目来源 bzoj1070 题目描述 同一时刻有\(N\)位车主带着他们的爱车来到了汽车维修中心.维修中心共有\(M\)位技术人员,不同的技术人员对不同 的车进行维修所用的时间 ...