官方文档网址:JWT introduction
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. —[官方文档]
# 翻译
- JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/专用密钥对对JWT进行签名。
# 通俗的理解
- JWT简称JSONWeb Token ,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
JWT实际上是一个JSON WEB 令牌
# 1.授权
- 这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
# 2.信息交换
- JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
基于传统的session认证
# 1.认证方式
- http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的服务器,这样服务器就能识别请求来自哪个用户了,这就是传统的基于session认证。
# 2.认证流程
# 3.暴露问题
- 1.每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
- 2.用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
- 3.因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
- 4.在前后端分离系统中就更加痛苦:如下图所示;也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。
# 1.认证流程
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同xxxxx.yyyyy.zzzzz的字符串(token head.payload.singurater)
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)HEADER
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
# 2.jwt优势
- 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
JSON Web令牌以紧凑的形式由三部分组成,这些部分由点(.
)分隔,分别是:
因此,JWT通常如下所示。
xxxxx.yyyyy.zzzzz
让我们分解不同的部分。
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,此JSON被Base64Url编码以形成JWT的第一部分。
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。索赔有以下三种类型:注册索赔,公共索赔和私人索赔。
已注册的权利要求:这些是一组非强制性的但建议使用的预定义权利要求,以提供一组有用的可互操作的权利要求。其中一些是: iss(发出者), exp(到期时间), sub(主题), aud(受众)等。
请注意,声明名称仅是三个字符,因为JWT是紧凑的。
公共声明:使用JWT的人可以随意定义这些声明。但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含抗冲突名称空间的URI。
私人权利:这些都是使用它们同意并既不是当事人之间建立共享信息的自定义声明注册或公众的权利要求。
有效负载示例可能是:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对有效负载进行Base64Url编码,以形成JSON Web令牌的第二部分。
请注意,对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的有效负载或报头元素中。
要创建签名部分,您必须获取编码的标头,编码的有效负载,机密,标头中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
# Signature
- 前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的 header和 payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过
- 如:HMACSHA256(base64UrlEncode( header) + "." + base64UrlEncode(payload) , secret);
# 签名目的
- 璟后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
# 信息安全问题
- 在这里大家一定会问一个问题: Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
- 是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第﹐三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
- 输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XNL的标准(例如SAML)相比,它更紧凑。
- 简洁(Compact):可以通过URL,POST参数或者在 HTTP header发送,因为数据量小,传输速度快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
下面显示了一个JWT,它已对先前的标头和有效负载进行了编码,并用一个秘密进行签名。
如果您想使用JWT并将这些概念付诸实践,则可以使用jwt.io Debugger解码,验证和生成JWT。
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.12.1version>
dependency>
package com.shidiankeji.springcloud.controller;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Calendar;
import java.util.HashMap;
/**
* @author 浮沉逆旅
* @create 2021/1/30 0030 11:49
* JWT生成
*/
@RestController
public class JETTestController {
@GetMapping("/getJWT/{name}")
public String getJWT(@PathVariable("name") String name){
HashMap<String,Object> map = new HashMap<>();
// 使用日期类获取一个时间对象
Calendar instance = Calendar.getInstance();
// 设置时间单位为秒,指定时间,放入到Calendar对象中
instance.add(Calendar.SECOND,60);
String token = JWT.create().withHeader(map) //设置头信息,传入一个map集合,默认可以不设置,选择的算法为HS256和JWT
.withClaim("userName",name) //Payload部分,里面需要传入一个键值对
.withClaim("userId",10) //只要是不敏感的信息都可以放置在Payload中
.withExpiresAt(instance.getTime()) //指定令牌过期的时间
.sign(Algorithm.HMAC256("!@#$SLF12GD")); //算法,调用算法类(常用)Algorithm.HMAC256()并且传入密钥
return token;
}
@GetMapping("/require/token/{token}")
public String requireToken(@PathVariable("token") String token){
// 创建验证对象
JWTVerifier build = JWT.require(Algorithm.HMAC256("!@#$SLF12GD")).build();//放入生成验证密钥的算法
DecodedJWT verify = build.verify(token);
// 返回值
// 去出多个使用getClaims获取map类型的值,取出单个getClaim("key").asString
return "{\"userName\": \""+ verify.getClaims().get("userName").asString()+"\",\"userId\": \""+verify.getClaim("userId").asInt()+"\",\"outTime\": \""+verify.getExpiresAt()+"\"}";
}
}
# 常见异常信息
- SignatureVerificationException:签名不一致异常
- TokenExpiredException:令牌过期异常
- AlgorithmMismatchException:算法不匹配异常
- InvalidClaimException:失效的payload异常
package com.shidiankeji.springcloud.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
/**
* @author 浮沉逆旅
* @create 2021/1/30 0030 13:17
* JWT工具封装
*/
public class JWTUtils {
private static final String SIGNATURE = "!IT_XIAOXIONG@TENPOINT$RJGC171ban#";
/**
* 生成Token -> header.payload.signature
*/
public static String getToken(Map<String, String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); //设置默认七天过期
// 创建jwt builder
JWTCreator.Builder jwtBuilder = JWT.create();
// 设置payload
map.forEach((k,v)->{
jwtBuilder.withClaim(k,v);
});
// 设置指令过期时间
jwtBuilder.withExpiresAt(instance.getTime());
// 设置signature
String token = jwtBuilder.sign(Algorithm.HMAC256(SIGNATURE));
return token;
}
/**
* 验证token
*/
public static int verifyToken(String token){
// try最好做一个全局处理,或者在调用方法的地方进行处理,定制化返回提示消息
try {
JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
} catch (SignatureVerificationException e) {
// 签名异常
return 1;
}catch (TokenExpiredException e){
// 令牌过期异常
return 2;
}catch (AlgorithmMismatchException e){
// 算法不一致异常
return 3;
}catch (InvalidClaimException e){
// 失效的payload异常
return 4;
}catch (Exception e){
e.printStackTrace();
return 5;
}
return 0;
}
/**
* 获取token信息
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
}
在单体应用里面可以采用拦截器对代码进行优化连接,如果是分布式的项目可以在网关中对token进行拦截验证。
package com.shidiankeji.springcloud.intercepotors;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.shidiankeji.springcloud.utils.JWTUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @author 浮沉逆旅
* @create 2021/1/30 0030 14:02
* JWT拦截器
*/
@Component
//实现HandlerInterceptor,重写preHandle方法(在所有请求前执行)
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
// 获取请求头中的token
String token = request.getHeader("token");
try {
JWTUtils.verifyToken(token);
// 放行请求
return true;
} catch (SignatureVerificationException e) {
// 签名异常
map.put("message","签名无效!");
}catch (TokenExpiredException e){
// 令牌过期异常
map.put("message","token过期!");
}catch (AlgorithmMismatchException e){
// 算法不一致异常
map.put("message","token算法不一致!");
}catch (InvalidClaimException e){
// 失效的payload异常
map.put("message","token无效!");
}catch (Exception e){
map.put("message","操作失败,请联系管理员!");
e.printStackTrace();
}
map.put("status",false);
// 将map转化为json
String json = new ObjectMapper().writeValueAsString(map);
// 设置相应类型以及编码
response.setContentType("application/json;charset=utf-8");
// 将信息响应给用户
response.getWriter().println(json);
// 结束本次拦截,不继续进行业务逻辑处理
return false;
}
}
package com.shidiankeji.springcloud.config;
import com.shidiankeji.springcloud.intercepotors.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 浮沉逆旅
* @create 2021/1/30 0030 14:16
* 拦截器配置类
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") //拦截所有请求路径
.excludePathPatterns("/user/login"); //排除/user/login登录请求
}
}