赞
踩
JSON Web Token (JWT)是一个开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,
用于作为 JSON 对象在各方之间安全地传输信息
。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
虽然可以对 JWT 进行加密,以便在各方之间提供保密性,但是我们将关注已签名的Token。签名Token可以验证其中包含的声明的完整性,而加密Token可以向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,该签名还证明只有持有私钥的一方才是对其进行签名的一方(签名技术是保证传输的信息不可抵赖,并不能保证信息传输的安全
)。
官网地址:https://jwt.io/introduction
在其紧凑的形式中,JWT由以点(.)分隔的三个部分组成,它们是:
类似于xxxx.xxxx.xxxx格式,真实情况如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
并且你可以通过官网https://jwt.io/#debugger-io解析出三部分表示的信息( 可使用 JWT.io Debugger 来解码、验证和生成 JWT
):
报头通常由两部分组成: Token的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256或 RSA)。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
最终这个 JSON 将由base64进行加密(该加密是可以对称解密的),用于构成 JWT 的第一部分,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9就是base64加密后的结果。
Token的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明: registered claims, public claims, and private claims。
例如:
{
"sub": "1234567890",// 注册声明
"name": "John Doe",// 公共声明
"admin": true // 私有声明
}
这部分的声明也会通过base64进行加密,最终形成JWT的第二部分eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
registered claims(注册声明)
这些是一组预定义的声明,它们
不是强制性的,而是推荐的
,以提供一组有用的、可互操作的声明
。
例如:
注意:声明名称只有三个字符,因为 JWT 意味着是紧凑的。
Public claims(公共的声明)
使用 JWT 的人可以随意定义这些声明(
可以自己声明一些有效信息如用户的id,name等,但是不要设置一些敏感信息,如密码
)。但是为了避免冲突,应该在 JWT注册表中定义它们,或者将它们定义为包含抗冲突名称空间的 URI。
Private claims(私人声明)
这些是创建用于在同意使用它们的各方之间共享信息的习惯声明,既不是注册声明,也不是公开声明(
私人声明是提供者和消费者所共同定义的声明
)。
注意:对于已签名的Token,这些信息虽然受到保护,不会被篡改,但任何人都可以阅读。除非加密,否则不要将机密信息放在 JWT 的有效负载或头元素中。
要创建Signature,您必须获取编码的标头(header)、编码的有效载荷(payload)、secret、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256算法,签名将按以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +base64UrlEncode(payload),
secret
)
上面的JSON将会通过HMACSHA256算法结合secret进行加盐签名(私钥加密),其中header和payload将通过base64UrlEncode()方法进行base64加密然后通过字符串拼接 "."
生成新字符串,最终生成JWT的第三部分SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
JWT输出是三个由点分隔的 Base64-URL 字符串,这些字符串可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更加紧凑。
下面显示了一个 JWT,该 JWT 对前一个头和有效负载进行了编码,并使用一个 secret 进行签名。
真实情况,一般是在请求头里加入Authorization,并加上Bearer标注最后是JWT(格式:Authorization: Bearer <token>
):
使用的是JJWT框架
)先导入JJWT的依赖(JJWT是JWT的框架)
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
测试代码如下:
public class JjwtTest { @Test public void generateToken() { // JWT头部分信息【Header】 Map<String, Object> header = new HashMap<>(); header.put("alg", "HS256"); header.put("typ", "JWT"); // 载核【Payload】 Map<String, Object> payload = new HashMap<>(); payload.put("sub", "1234567890"); payload.put("name","John Doe"); payload.put("admin",true); // 声明Token失效时间 Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND,300);// 300s // 生成Token String token = Jwts.builder() .setHeader(header)// 设置Header .setClaims(payload) // 设置载核 .setExpiration(instance.getTime())// 设置生效时间 .signWith(SignatureAlgorithm.HS256,"secret") // 签名,这里采用私钥进行签名,不要泄露了自己的私钥信息 .compact(); // 压缩生成xxx.xxx.xxx System.out.println(token); } }
运行结果:
通过官网进行解码:
使用的是JJWT框架
)测试代码如下:
@Test public void getInfoByJwt() { // 生成的token String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTY2MzI5NzQzMX0.Ju5EzKBpUnuIRhDG1SU0NwMGsd9Jl_8YBcMM6PB2C20"; // 解析head信息 JwsHeader jwsHeader = Jwts .parser() .setSigningKey("secret") .parseClaimsJws(token) .getHeader(); System.out.println(jwsHeader); // {typ=JWT, alg=HS256} System.out.println("typ:"+jwsHeader.get("typ")); // 解析Payload Claims claims = Jwts .parser() .setSigningKey("secret") .parseClaimsJws(token) .getBody(); System.out.println(claims);// {sub=1234567890, name=John Doe, admin=true, exp=1663297431} System.out.println("admin:"+claims.get("admin")); // 解析Signature String signature = Jwts .parser() .setSigningKey("secret") .parseClaimsJws(token) .getSignature(); System.out.println(signature); // Ju5EzKBpUnuIRhDG1SU0NwMGsd9Jl_8YBcMM6PB2C20 }
运行结果:
使用的是JJWT框架
)当然在实际项目中一般会将上面的操作封装成工具类来使用如下( 该项目是一个Spring Security+JWT的项目
):
应用配置文件application.yaml中加入如下配置:
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mall-admin-secret #JWT加解密使用的密钥【私钥】
expiration: 604800 #JWT的超期限时间(60*60*24*7)
tokenHead: 'Bearer ' #JWT负载中拿到开头
工具类代码如下:
package com.dudu.mall.utils; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JwtToken生成的工具类 * JWT token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"wang","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) */ public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Value("${jwt.tokenHead}") private String tokenHead; /** * 根据负责生成JWT的token */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 从token中获取JWT中的负载 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式验证失败:{}", token); } return claims; } /** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证token是否还有效 * * @param token 客户端传入的token * @param userDetails 从数据库中查询出来的用户信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 从token中获取过期时间 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 当原来的token没过期时是可以刷新的 * * @param oldToken 带tokenHead的token */ public String refreshHeadToken(String oldToken) { if(StrUtil.isEmpty(oldToken)){ return null; } String token = oldToken.substring(tokenHead.length()); if(StrUtil.isEmpty(token)){ return null; } //token校验不通过 Claims claims = getClaimsFromToken(token); if(claims==null){ return null; } //如果token已经过期,不支持刷新 if(isTokenExpired(token)){ return null; } //如果token在30分钟之内刚刷新过,返回原token if(tokenRefreshJustBefore(token,30*60)){ return token; }else{ claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } } /** * 判断token在指定时间内是否刚刚刷新过 * @param token 原token * @param time 指定时间(秒) */ private boolean tokenRefreshJustBefore(String token, int time) { Claims claims = getClaimsFromToken(token); Date created = claims.get(CLAIM_KEY_CREATED, Date.class); Date refreshDate = new Date(); //刷新时间在创建时间的指定时间内 if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){ return true; } return false; } }
下图显示了 JWT 是如何获得并用于访问 API 或资源的:
本项目JWT使用的是
JJWT框架
来实现,下图流程图来源于【编程不良人】JWT认证原理、流程整合springboot实战应用,前后端分离认证的解决方案!,项目采用SpringBoot+mybatis实现,业务逻辑非常简单,下面截取关键代码来了解整个流程。
业务流程图如下:
登入功能通过业务层提供的login(user)方法进行登入,登入成功返回token信息,如果token信息不为null就将token信息返回给前端系统
业务层login(User user)方法实现逻辑非常简单,就是通过查询数据库中是否存在该用户,存在表示登入成功,并通过Token工具类生成token
JWT工具类中generateToken(User user)虽然接受的是user对象,但是实际只使用了user的name作为载荷( 不要将敏感信息写入到Token中
)
Postman中模拟前端访问http://localhost:8086/user/login?name=test&password=123456:
执行成功后前端会接受到服务器端传递过来的Token信息
访问Api信息/资源,前端只需要将token信息保存到请求头中,发送请求到对应Api/资源即可:
Postman模拟前端访问:http://localhost:8086/user/info?id=1
至于Token的验证交给拦截器进行处理:
Postman访问/info:
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/932642
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。