什么是JWT呢?
1、JWT(JSON Web Token)
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
简单的讲就是,JWT是一个带签名及用户相关的信息加密成串,页面请求校验登录接口时,请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据。
使用JWT的优势 分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。 传统的登录认证都是通过session方式来实现校验,而且是存在内存当中,JWT是携带token到服务端解析。
2、JWT的组成
JWT由三部分组成,分别是头信息、有效载荷、签名,中间以(.)分隔,如下格式:
xxx.yyy.zzz
2.1、头信息
Header通常由两部分组成:令牌的类型,即JWT。和常用的散列算法,如HMAC SHA256或RSA。
{
"alg": "HS256",
"typ": "JWT"
}
Header部分的JSON被Base64Url编码,形成JWT的第一部分。
2.2、Payload(有效载荷)
JWT的第二部分是payload,其中包含claims。claims是关于实体(常用的是用户信息)和其他数据的声明,claims有三种类型: registered, public, and private claims。 Registered claims: 这些是一组预定义的claims,非强制性的,但是推荐使用, iss(发行人), exp(到期时间), sub(主题), aud(观众)等; Public claims: 自定义claims,注意不要和JWT注册表中属性冲突,这里可以查看JWT注册表 Private claims: 这些是自定义的claims,用于在同意使用这些claims的各方之间共享信息,它们既不是Registered claims,也不是Public claims。 以下是payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后,再经过Base64Url编码,形成JWT的第二部分;
注意:对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非加密,否则不要将敏感信息放入到Payload或Header元素中。
Signature 要创建签名部分,必须采用编码的Header,编码的Payload,秘钥,Header中指定的算法,并对其进行签名。先获取到base64UrlEncode(header)和base64UrlEncode(payload)部分, secret是在服务器的jwt签发和验证,只能存在服务器。否则被客户拿到可以实现自我签发JWT。我们所拿到的token都是服务器根据secret签发的。 例如,如果要使用HMAC SHA256算法,将按以下方式创建签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
我这里展示一个生成的token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
3、JWT工作机制?
在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token(即:JWT)。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,不应将令牌保留的时间超过要求。理论上超时时间越短越好。
每当用户想要访问受保护的路由或资源时,用户代理应该使用Bearer模式发送JWT,通常在Authorization header中。标题内容应如下所示:
Authorization: Bearer <token>
服务器的受保护路由将检查Authorization header中的有效JWT ,如果有效,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库或缓存信息。
4、JWT优缺点
4.1、JWT优点
相比Simple Web Tokens (SWT)(简单Web令牌) and Security Assertion Markup Language Tokens (SAML)(安全断言标记语言令牌):
- JWT比SAML更简洁,在HTML和HTTP环境中传递更方便;
- 在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名可能会存在安全漏洞;
- JSON解析成对象相比XML更流行、方便。
4.2、JWT缺点
① 安全性:由于JWT的payload是使用Base64编码的,并没有加密,因此JWT中不能存储敏感数据。而Session的信息是存在服务端的,相对来说更安全
② 性能:JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致JWT非常长,Cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在LocalStorage里面。并且用户在系统中的每一次Http请求都会把JWT携带在Header里面,Http请求的Header可能比Body还要大。而SessionId只是很短的一个字符串,因此使用JWT的Http请求比使用Session的开销大得多。
③ 一次性:无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。即缺陷是一旦下发,服务后台无法拒绝携带该jwt的请求(如踢除用户)
下面我们可以看看JWT的流程图
实现流程:
- 项目一开始我先封装了一个JWTHelper工具包(GitHub下载),主要提供了生成JWT、解析JWT以及校验JWT的方法,其他还有一些加密相关操作,稍后我会以代码的形式介绍下代码。工具包写好后我将打包上传到私服,能够随时依赖下载使用;
- 接下来,我在客户端项目中依赖JWTHelper工具包,并添加Interceptor拦截器,拦截需要校验登录的接口。拦截器中校验JWT有效性,并在response中重新设置JWT的新值;
- 最后在JWT服务端,依赖JWT工具包,在登录方法中,需要在登录校验成功后调用生成JWT方法,生成一个JWT令牌并且设置到response的header中。
5、Springboot集成JWT
5.1、maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
application.xml配置文件
server:
port: 8080
spring:
application:
name: springboot-jwt
config:
jwt:
# 加密密钥
secret: abcdefg1234567
# token有效时长
expire: 3600
# header 名称
header: token
5.2、JWT配置文件
package com.example.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@ConfigurationProperties(prefix = "config.jwt")
@Component
public class JwtConfig {
private String secret;
private long expire;
private String header;
public String createToken (String subject){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getTokenClaim (String token) {
try {
return Jwts
.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
return null;
}
}
public boolean isTokenExpired (Date expirationTime) {
return expirationTime.before(new Date());
}
public Date getExpirationDateFromToken(String token) {
return getTokenClaim(token).getExpiration();
}
public String getUsernameFromToken(String token) {
return getTokenClaim(token).getSubject();
}
public Date getIssuedAtDateFromToken(String token) {
return getTokenClaim(token).getIssuedAt();
}
}
5.3、JWT拦截器
package com.example.interceptor;
import com.example.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Resource
private JwtConfig jwtConfig ;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws SignatureException {
String uri = request.getRequestURI() ;
if (uri.contains("/login")){
return true ;
}
String token = request.getHeader(jwtConfig.getHeader());
if(StringUtils.isEmpty(token)){
token = request.getParameter(jwtConfig.getHeader());
}
if(StringUtils.isEmpty(token)){
throw new SignatureException(jwtConfig.getHeader()+ "不能为空");
}
Claims claims = null;
try{
claims = jwtConfig.getTokenClaim(token);
if(claims == null || jwtConfig.isTokenExpired(claims.getExpiration())){
throw new SignatureException(jwtConfig.getHeader() + "失效,请重新登录。");
}
}catch (Exception e){
throw new SignatureException(jwtConfig.getHeader() + "失效,请重新登录。");
}
request.setAttribute("identityId", claims.getSubject());
return true;
}
}
5.4、注册拦截器到MVC
package com.example.config;
import com.example.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private TokenInterceptor tokenInterceptor ;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
6、JWT总结
-
基于JSON,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,Node.JS,PHP等很多语言都可以使用。 -
payload部分,需要时JWT可以存储一些其他业务逻辑所必要的非敏感信息。 -
体积小巧,便于传输;JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。
|