一、背景

传统的单体应用基于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. DRF的限流配置

    在settings.py中添加配置 REST_FRAMEWORK = { #3.限流(防爬虫) 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.thrott ...

  2. 【手把手学习flutter】Flutter打Android包的基本配置和包体积优化策略

    [手把手学习flutter]Flutter打Android包的基本配置和包体积优化策略 关注「松宝写代码」,回复"加群" 加入我们一起学习,天天向上 前言 因为最近参加2020FE ...

  3. python之Bug之字符串拼接bug

    \r\n拼接Bug 环境: python3.4.pycharm2017 偶然的学习中遇到了一个问题,百思不得姐,什么问题呢,大家输入太快了,难免有失误就如下面的代码 #构造响应数据 response_ ...

  4. 老猿学5G专栏文章目录

    ☞ ░ 前往老猿Python博文目录 ░ 一.3GPP规范文档及其他推荐阅读博文 老猿学5G扫盲贴:3GPP规范文档命名规则及同系列文档阅读指南 老猿学5G扫盲贴:3GPP规范中部分与计费相关的规范序 ...

  5. 初识Flask——基于python的web框架

    参考教程链接: https://dormousehole.readthedocs.io/en/latest/ (主要)https://www.w3cschool.cn/flask/ 目录: 1.写了一 ...

  6. 【题解】「CF363A」Soroban

    哎呀呀,咕值要掉光了,赶快水篇题解( solution 这题就是个纯模拟,首先我们根据输出样例看一下输出算盘的规则. 看数最大的 720 ,我们发现,输出的算盘张这样(之所以我不用代码框而用 \(\K ...

  7. I/O方式(本章最重要)

    目录 程序查询方式 程序查询方式接口结构 例题 本节回顾 程序中断方式 中断的基本概念 工作流程 中断请求 分类 中断请求标记 中断响应 判优实现 优先级设置 中断处理过程 中断隐指令 硬件向量法 中 ...

  8. 小米k30 pro刷国际版rom

    时间:2020.8.20 最新的是miui12但是普遍反映耗电量巨大,所以还是刷miui11了. 知乎上有个教程:https://zhuanlan.zhihu.com/p/86160027 但是是针对 ...

  9. 超详细分析Bootloader到内核的启动流程(万字长文)

    @ 目录 Bootloader启动流程分析 Bootloader第一阶段的功能 硬件设备初始化 为加载 Bootloader的第二阶段代码准备RAM空间(初始化内存空间) 复制 Bootloader的 ...

  10. Spark性能调优篇三之广播方式传输数据

    广播大变量,重复用到的变量 原因见 https://www.jianshu.com/p/2c297b23ebda