JWT
JWT,即JSON Web Token,定义了一种紧凑型的、自包含的方式,用于在网络应用环境间以JSON对象安全地传输信息
JWT 是一个开发的行业标准 RFC 7519。JWT传输的信息可以被验证和信任,因为它经过了数字签名
JWT 一般被用来在身份提供者和服务提供者间传递被认证用户的身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所需的声明信息。
JWT 常用于代替Session,用于识别用户身份,传统上使用Session机制区别用户身份有两个缺点:一是占用服务器的存储资源,二是在集群部署下设计会非常复杂。JWT完全可以解决Session方案存在的问题
Internet服务无法与用户身份验证分开,一般过程如下,
- 用户向服务器发送用户名和密码
- 验证服务器后,相关数据(如用户角色,登录时间等)将会保存在当前会话中
- 服务器向用户返回session_id,session信息都会写入到用户的Coookie
- 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器
- 服务器收到session_id并对比之前保存的数据,确认用户的身份
这种模式最大的问题是,没有分布式架构,无法支持横向扩展。如果使用一个服务器,该模式完全没有问题。但是,如果它是服务器群集或面向服务的跨域体系结构的话,则需要一个统一的session数据库库来保存会话数据实现共享,这样负载均衡下的每个服务器才可以正确的验证用户身份
JWT的使用场景
- 认证授权 (Authorization) :
这是使用 JWT 的最常见场景。一旦用户登录,后续每个请求都将包含 JWT,允许用户访问该 Token 允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,而且可以轻松地跨域使用
- 信息交换 (Information Exchange) :
对于安全的在各方之间传输信息而言,JSON Web Token 无疑是一种很好的方式。因为 JWT 可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改
优缺点:
优点:
支持主流语言(跨语言);包含必要的所有信息,如用户信息和签名(自包含);很方便通过HTTP头部传递(易传递)
JWT默认是不加密的,但也是可以加密的。生成原始Token后,可以用密钥再加密一次,JWT不加密的情况下,不能将秘密数据写入JWT
JWT不仅可以用于认证,也可以用于交换信息。有效使用JWT,可以降低服务器查询数据库的次数
缺点:
- 由于服务器不保存session状态,因此JWT无法在使用过程中废止某个token,或更改token的权限,一旦JWT签发,在到期之间就会始终有效,除非服务器部署额外的逻辑
- JWT本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限,为了减少盗用,JWT的有效期应该设置的比较短,对于一些重要的权限,使用时应该再次对用户进行认证
- 为减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输
JWT和Session对比
由于 HTTP 协议是无状态协议,所以 HTTP 协议本身是无法区别身份的。
传统上,一般通过 Session 配合 Cookie 机制解决身份识别问题。
但是在高并发、高流量的服务集群化时,Session 机制遇到极大挑战。而 JWT 认证机制就可以完美的解决这些问题
传统的Session认证
HTTP 协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据 HTTP 协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于 session 认证。
但是这种基于 session 的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于 session 认证应用的问题就会暴露出来:
- 存储开销:每个用户经过应用认证后,应用都要在服务端做一次记录,一方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着用户的增多,服务端的开销会明显增大
- 扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味这用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力
- CSRF:因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易收到跨站请求伪造的攻击
基于token的鉴权机制
基于 token 的鉴权机制类似于 HTTP 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
基于 token 的鉴权机制流程如下:
- 用户使用用户名密码来请求服务器;
- 服务器进行验证用户的信息;
- 服务器通过验证发送给用户一个token;
- 客户端存储token,并在每次请求时附送上这个token值;
- 服务端验证token值,并返回数据;
这个 token 必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
JWT方式
JWT,即 JSON Web Token,它是一种 token 的鉴权机制。
JWT 是一个开放的行业标准(RFC 7519),它定义了一种紧凑的、自包含的方式,通过 JSON 对象在网络应用环境间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所须的声明信息。
JWT 最常用于代替 Session,用于识别用户身份。使用 Session 区别用户身份有两个缺点:一是占用服务器存储资源,二是在集群部署下设计非常复杂。JWT 完全可以解决 Session 方案存在的问题。
JWT和Session对比总结
JWT 和 Session 相同点是,它们都是存储用户信息;然而,Session 是在服务器端的,而 JWT 是在客户端的
Session 方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销
而 JWT 方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session 的状态是存储在服务器端,客户端只有 session id;而 Token 的状态是存储在客户端
JWT原则
- 在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,例如:
{
"UserName":"dsk",
"Role":"Admin",
"Expire":"202.-07-07 09:12:36"
}
- 之后,当用户与服务器通信时,客户在请求中发回JSON对象,服务器仅依赖于这个JSON对象来标识用户,为了防止用户篡改数据,服务器将在生成对象时添加签名
- 服务器不会保存任何会话数据,即服务器变为无状态,使其更容易扩展
JWT的数据结构
JWT的数据结构就是,改对象为一个很长的字符串,使 用" . " 分隔符将这个字符串分为三个子串,各子串之间也没有换行符,每一个子串表示了一个功能块,总共有以下三个部分:
- JWT头:
JWT头部分是一个描述JWT元数据的JSON对象,如下:
{ "alg":"HS256", "typ":"JWT" }
alg
属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)
typ
属性表示令牌的类型,JWT令牌统一写为JWT最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
- 有效载荷:
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除了以上默认字段外,我们还可以自定义私有字段,例如:
{ "sub":"1234567890", "name":"dsk", "admin":true }
注意,默认情况下,JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露
JSON对象也使用Base64 URL算法转换为字符串保存
- 签名哈希
前面哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况为HMAC SHA256)根据以下公式生成签名:
H M A C S H A 256 ( b a s e 64 U r l E n c o d e ( h e a d e r ) + " . " + b a s e 64 U r l E n c o d e ( p a y l o a d ) , s e c r e t ) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
在计算出签名哈希后,JWT头,有效载荷和哈希签名的三个部分组合成有一个字符串,每个部分之间使用 “.” 分隔,就构成了整个JWT对象
JWT基本使用
- 引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
- 示例
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.alibaba.druid.util.StringUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
public class Test {
/**
* APP登录Token的生成和解析
*
*/
/** token秘钥,请勿泄露,请勿随便修改 backups:JKKLJOoasdlfj */
public static final String SECRET = "JKKLJOoasdlfj";
/** token 过期时间: 10天 */
public static final int calendarField = Calendar.DATE;
public static final int calendarInterval = 10;
/**
* JWT生成Token.<br/>
*
* JWT构成: header, payload, signature
*
* @param user_id
* 登录成功后用户user_id, 参数user_id不可传空
*/
public static String createToken(Long user_id) throws Exception {
Date iatDate = new Date();
// expire time
Calendar nowTime = Calendar.getInstance();
nowTime.add(calendarField, calendarInterval);
Date expiresDate = nowTime.getTime();
// header Map
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
// build token
// param backups {iss:Service, aud:APP}
String token = JWT.create().withHeader(map) // header
.withClaim("iss", "Service") // payload
.withClaim("aud", "APP").withClaim("user_id", null == user_id ? null : user_id.toString())
.withIssuedAt(iatDate) // sign time
.withExpiresAt(expiresDate) // expire time
.sign(Algorithm.HMAC256(SECRET)); // signature
return token;
}
/**
* 解密Token
*
* @param token
* @return
* @throws Exception
*/
public static Map<String, Claim> verifyToken(String token) {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
jwt = verifier.verify(token);
} catch (Exception e) {
// e.printStackTrace();
// token 校验失败, 抛出Token验证非法异常
}
return jwt.getClaims();
}
/**
* 根据Token获取user_id
*
* @param token
* @return user_id
*/
public static Long getAppUID(String token) {
Map<String, Claim> claims = verifyToken(token);
Claim user_id_claim = claims.get("user_id");
if (null == user_id_claim || StringUtils.isEmpty(user_id_claim.asString())) {
// token 校验失败, 抛出Token验证非法异常
}
return Long.valueOf(user_id_claim.asString());
}
}
示例:
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; public class JWTUtil { /** *过期时间 1天 * @date 2022/3/23 9:37 */ private static final long EXPIRE_TIME = 1 * 24 * 60 * 60 * 1000; /** * 校验token是否正确 * @param token 密钥 * @param secret 用户的密码 * @return 是否正确 */ public static boolean verify(String token, Long pkId, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("pkId", pkId) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * 获得token中的信息无需secret解密也能获得 * @return token中包含的用户名 */ public static Long getPkId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("pkId").asLong(); } catch (JWTDecodeException e) { return null; } } /** * 生成签名 通过用户的密码来进行加密 * @param pkId 用户的id * @param secret 用户的密码 * @return 加密的token */ public static String sign(Long pkId, String secret) { Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); // 附带username信息 return JWT.create() .withClaim("pkId", pkId) .withExpiresAt(date) .sign(algorithm); } }
- 最终存放的数据在JWT内部的实体claims里。它是存放数据的地方
JWT消息构成
一个token分3部分,按顺序为:头部(header),载荷(payload),签证(signature)
头部:
JWT的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法,通常直接使用HMAC SHA256
// header Map Map<String, Object> map = new HashMap<>(); map.put("alg", "HS256"); map.put("typ", "JWT");
载荷:
载荷就是存放有效信息的地方,基本上填两种数据类型:
- 标准中注册的声明的数据,如下:
标准中注册的声明,建议但不强制使用 iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 自定义数据:存放我们想在token中存放的key-value值
JWT.create().withHeader(map) // header .withClaim("name", "cy") // payload .withClaim("user_id", "11222");
由这两部分内部做base64加密,最终数据进入JWT的chaims里存放
JWT.create().withHeader(map) // header .withClaim("iss", "Service") // payload .withClaim("aud", "APP") .withIssuedAt(iatDate) // sign time .withExpiresAt(expiresDate) // expire time
签名:
jwt的第三部分,一个签证信息,签证信息算法如下:
H M A C S H A 256 ( b a s e 64 U r l E n c o d e ( h e a d e r ) + " . " + b a s e 64 U r l E n c o d e ( p a y l o a d ) , s e c r e t ) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
这部分需要base64加密后的header和base64加密后的payload使用,连接组成字符串,然后通过header中声明的加密方式进行加盐secret组合加密(上述公式是默认的HMAC SHA256加密),然后就构成了jwt的第三部分
基本上至此,JWT的API相关知识已经学完了,但是API不够友好,不停的用withClaim放数据。不够友好。下面推荐一款框架JJWT,相当于对JWT的实现框架
JJWT
JJWT是为了更友好在JVM上使用JWT,是基于JWT, JWS, JWE, JWK框架的java实现
- 引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
- 新建JwtConstants类,用于token的chaims保存有效信息字段名
public class JwtConstants {
public static final String JWT_KEY_USER_ID = "uid";
}
- 新建JwtInfo类,用于token的chaims保存有效信息
public class JwtInfo {
private String uid;
public JwtInfo(String uid) {
this.uid = uid;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
}
- 新建JwtTokenUtils工具类,用于token的生成和解析
package com.yibo.user.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
/**
* @Description: 生成token的工具类
*/
public class JwtTokenUtils {
private static Key getKeyInstance(){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = DatatypeConverter.parseBase64Binary("mall-user");
return new SecretKeySpec(bytes,signatureAlgorithm.getJcaName());
}
/**
* 生成token的方法
* @param jwtInfo
* @param expire
* @return
*/
public static String generatorToken(JwtInfo jwtInfo,int expire){
return Jwts.builder().claim(JwtConstants.JWT_KEY_USER_ID,jwtInfo.getUid())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(SignatureAlgorithm.HS256,getKeyInstance()).compact();
}
/**
* 根据token获取token中的信息
* @param token
* @return
*/
public static JwtInfo getTokenInfo(String token){
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return new JwtInfo(claims.get(JwtConstants.JWT_KEY_USER_ID).toString());
}
}
- 新建JwtTokenService服务类,调用其方法即可进行认证和授权使用
@Component
public class JwtTokenService {
/**
* token过期时间
*/
private int expire = 6000;
public String generatorToken(JwtInfo jwtInfo){
return JwtTokenUtils.generatorToken(jwtInfo,expire);
}
public JwtInfo stringInfoFromToken(String token){
return JwtTokenUtils.getTokenInfo(token);
}
}