1、JWT
1.1、基本的认证机制
1) HTTP Basic Auth
HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放RESTful API时,尽量避免采用HTTP Basic Auth。
2) Cookie Auth
Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie的expire time使cookie在一定时间内有效。
3)OAuth
- OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。
- OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
- 下面是OAuth2.0的流程:
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。
缺点:过重。
4)Token Auth
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
- 客户端使用 用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
Token Auth比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。
具体来说,Token Auth的优点(Token机制相对于Cookie机制又有什么好处呢?):
A. 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
B. 无状态:Token机制在服务端不需要存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
C. 更适用CDN:可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
D. 解耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
E. 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 10等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
其他:
- CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
- 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多。
- 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。
- 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google,Microsoft)
1.2、JWT简介
- JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
官网: https://jwt.io/ 标准: https://tools.ietf.org/html/rfc7519 - JWT令牌的优点:
- jwt基于json,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
- 资源服务使用JWT可不依赖认证服务即可完成授权。
- 缺点:
- JWT令牌较长,占存储空间比较大。
1.3、JWT组成
- 一个JWT实际上就是一个字符串,由头部、载荷(负载)、签名三部分组成。
1)头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象。
typ :是类型。
alg :签名所采用的算法,这里使用的算法是HS256算法
我们对头部的json字符串进行BASE64编码(网上有很多在线编码的网站),编码后的字符串如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们可以非常方便的完成基于 BASE64 的编码和解码。
2)负载(Payload)
负载是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
-
A.标准中注册的声明(建议但不强制使用)
{ “alg”: “HS256”, “typ”: “JWT” }eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不
建议添加敏感信息,因为该部分在客户端可解密.
私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对
称解密的,意味着该部分信息可以归类为明文信息。
这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟
JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对
这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉
接收方要对这些claim进行验证以及规则才行。
其中 sub 是标准的声明, name 是自定义的声明(公共的或私有的)
然后将其进行base64编码,得到Jwt的第二部分:
提示:声明中不要放一些敏感信息。 -
负载部分是JWT主要的信息存储部分,其中包含了许多类型的声明(claims)
-
Claims的实体一般包含用户信息和一些元数据,这些claims分成三种类型:
- reserved claims:预定义的一些声明(推荐不强制包含)
- public claims:公有声明,该部分可以随便定义
- private claims:私有声明,该部分是共享被认定信息中的自定义部分
一个简单的Payload实例如下:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3)签名(Signature)
- Signature是用来验证发送者的JWT时,能确保在此期间不被篡改
- 在创建签名时应该拥有了编码后的Header和Payload,再使用保存在服务端的密钥对其签名
- 使用JWT具有如下好处:
1)通用:因为JSON的通用性,JWT支持跨语言
2)紧凑:JWT的构成非常简单,字节占用很小,可以通过GET、POST等放在HTTP的Header中,非常便于传输
3)扩展:JWT是自我包含的,包含了必要的所有信息,不需要在服务端保存会话信息,非常易于应用的扩展
2、JJWT
2.1、简介
- JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
- 规范官网:https://jwt.io/
2.2、快速入门
1)创建token
- 引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
- 创建测试类,生成token:
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.Base64Codec;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
public class Jwt01ApplicationTests {
@Test
public void testJwtBuilder() {
// 创建jwt的构建对象
JwtBuilder jwtBuilder = Jwts.builder()
// 设置Jwt的唯一身份标识(jti) {"jti":"8686"}
.setId("8686")
// 设置签发用户, {"sub":"Jack"}
.setSubject("Jack")
// 设置签发日期, {"iat":new Data()}
.setIssuedAt(new Date())
// 设置签名方法,参数1:签名算法,参数2: 盐
.signWith(SignatureAlgorithm.HS256, "adminXyl");
// 获取jwt的token
String token = jwtBuilder.compact();
System.out.println(token);
String[] split = token.split("\\.");
// token中,头部 split[0]、荷载 split[1]、签名 split[2]用 '.' 分隔
// 头部、负载、签名 三部分的 base64解密
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
// 签名无法解密,会存在乱码
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
}
- 运行testJwtBuilder方法:
再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了签发时间。
2)解析token
// 根据密钥(运行testJwtBuilder方法,在控制台获得)解析token
@Test
public void parseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4Njg2Iiwic3ViIjoiSmFjayIsImlhdCI6MTY0MDg0NzYyNn0.8f1dIwRSgpXgpK_OCzNxg3fHp0N5eSPMAnwtxugUSJk";
// claims --> 负载
Claims claims = Jwts.parser()
.setSigningKey("adminXyl")
.parseClaimsJws(token)
.getBody();
System.out.println("jwt id: " + claims.getId());
System.out.println("jwt 签发用户: " + claims.getSubject());
System.out.println("jwt 签发时间:" + claims.getIssuedAt());
}
3)token过期校验
// 创建存在过期时间的token
@Test
public void testCreatTokenHasExp(){
long now = System.currentTimeMillis();
long expire = now + 80 * 1000;
JwtBuilder jwtBuilder = Jwts.builder()
.setId("1010")
.setSubject("Mike")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "zhang")
.setExpiration(new Date(expire));
String token = jwtBuilder.compact();
System.out.println(token);
}
- 运行testCreatTokenHasExp方法,在控制台获取生成的密钥(token,testParseTokenHasExp方法中的token为该密钥),
// 解析存在过期时间的token
@Test
public void testParseTokenHasExp(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDEwIiwic3ViIjoiTWlrZSIsImlhdCI6MTY0MDg1MTk0MSwiZXhwIjoxNjQwODUyMDIxfQ.B9sXHRG9IHMIvd46OSSe25L9G0p8ckWkW38RdFTdQro";
Claims claims = Jwts.parser()
.setSigningKey("zhang")
.parseClaimsJws(token)
.getBody();
System.out.println(claims.getId() + "," + claims.getSubject() + "," + claims.getIssuedAt());
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间:" + df.format(claims.getIssuedAt()));
System.out.println("当前时间:" + df.format(new Date()));
System.out.println("jwt过期时间:" + df.format(claims.getExpiration()));
}
-
运行testParseTokenHasExp方法,控制台结果如下:
-
过一段时间后(token失效时间已过),再次运行testParseTokenHasExp方法,会有Jwt失效异常
ExpiredJwtException
-
注意:设置签名方法signWith(SignatureAlgorithm.HS256, “xyl”)中的第二个参数长度过短,长度过短会被默认为密钥为空。
4)自定义claims
@Test
public void myClaims() {
HashMap<String,Object> claimsMap = new HashMap<>();
claimsMap.put("brand1","honor");
claimsMap.put("brand2","oppo");
claimsMap.put("brand3","QICQ");
// 创建jwt的构建对象
JwtBuilder jwtBuilder = Jwts.builder()
// 设置Jwt的唯一身份标识(jti) {"jti":"8686"}
.setId("8080")
// 设置签发用户, {"sub":"Jack"}
.setSubject("maliu")
// 设置签发日期, {"iat":new Data()}
.setIssuedAt(new Date())
// 设置签名方法,参数1:签名算法,参数2: 盐
.signWith(SignatureAlgorithm.HS256, "zhangsan")
.claim("logo","realme.jpg")
.claim("map",claimsMap);
// claim方法中的第二个参数是Object类型,JwtBuilder claim(String name, Object value);
// 获取jwt的token
String token = jwtBuilder.compact();
System.out.println(token);
}
@Test
public void testMyClaims(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4MDgwIiwic3ViIjoibWFsaXUiLCJpYXQiOjE2NDA4NTM5NjIsImxvZ28iOiJyZWFsbWUuanBnIiwibWFwIjp7ImJyYW5kMyI6IlFJQ1EiLCJicmFuZDIiOiJvcHBvIiwiYnJhbmQxIjoiaG9ub3IifX0.FVYhxFcxedV20ZRpLhEsIBFKv1puwI54zlhbbNOQ3pw";
Claims claims = Jwts.parser()
.setSigningKey("zhangsan")
.parseClaimsJws(token)
.getBody();
// Claims使用key-value键值对的方式存储负荷
//public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> { // ...}
System.out.println("logo:"+claims.get("logo"));
System.out.println("claimsMap:"+claims.get("map"));
}
- 先后运行myClaims(生成的密钥更新到testMyClaims方法的token参数)和testMyClaims方法,最后的执行结果如下: