概述
概念
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
构成
- 第一部分我们称它为头部(header),
- 第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
- 第三部分是签证(signature).
header
jwt的头部承载两部分信息
声明加密的算法 通常直接使用 HMAC SHA256
声明加密的算法 通常直接使用 HMAC SHA256
playload
载荷就是存放有效信息的地方。这些有效信息包含三个部分
标准中注册的声明
公共的声明
私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
-
header (base64后的)
-
payload (base64后的)
-
secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
注意事项
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
使用
应用场景
JWT的一个最常用的场景就是用于登录认证
流程如下:
当前端浏览器发送请求登录请求给后端服务器后,如果后端验证登录成功后就会生成一个jet令牌一起发送给前端。前端接收到令牌后就会储存起来,并且在之后的每一次请求中都会携带一个jwt令牌发送给后端,后端拦截到请求后会判断是否携带令牌,如果没有携带令牌则会拒绝访问,如果携带了令牌,则会进一步验证其是否有效,有效就会放行本次请求。
想要做到这样的登录拦截功能,使用session也可以做到,那么为什么要使用jwt而不使用session呢?
首先就是使用session存在很多弊端
- 每个用户在登录之后就会记录下一个session并把会话的所有信息储存在内存之中,这样就会时服务器的压力较大。并且session在分布式中是非常不方便的,因为分布式通常是几个服务之间的调用,而session把认证信息储存在一个服务器中,所以就限制了分布式负载均衡的能力。
- session的信息是不安全的,如果被截获,就可能会受到跨站脚本攻击(XSS)和跨站请求伪造攻击(CSRF)。
- 在跨域使用不方便,因为session默认只会在一个域名下,因此在后端多节点场景中会很受限制。
而使用JET就不是有这些问题
- JWT是无状态的,且数据量小,传输的就会很快,不会给服务器很大的压力。
- JWT令牌可以使用 HTTPS 加密传输,可以防止在传输过程中的窃听和篡改,提高了安全性
- JWT令牌在不同的域名中使用,不会受到限制。
因此在很多的登录认证场景在会使用JWT令牌。
SpringBoot中使用JWT
导入依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成token
token是通过荷载Claims或playholder两者之一构造的,两者不能同时提供,否则compact()将报异常。token构建是荷载一般必须两个属性:sub和exp.
sub:即token中保存的必要信息。
exp:即token的过期时间,当token过了过期时间时,解析token时会抛出ExpiredJwtException
异常。
1.通过Jwts.builder()方法直接构建。
/*1.通过信息构建token*/
public String buildLoginUserToken(UserDetails userDetails)
{
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + expired))
.setIssuedAt(new Date(System.currentTimeMillis() + (expired / 2)))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
2.也可以先构建Claims对象,通过Claims对象构建token
/*工具方法:构建荷载*/
public Claims buildClaims(UserDetails userDetails)
{
Claims claims = new DefaultClaims();
claims.setSubject(userDetails.getUsername());
claims.setExpiration(new Date(System.currentTimeMillis() + expired));
return claims;
}
/*1.通过信息构建token*/
public String buildLoginUserToken(UserDetails userDetails)
{
return Jwts.builder()
.setClaims(buildClaims(userDetails))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
解析token
将token转换成Claims的方法是:Jwts类中的如下方法完成。
解析过程:
第一步:将token转换成Claims对象
第二步:通过Claims对象的getSubject()方法获取token中保存的信息。
将token转换成Claims的方法是:Jwts类中的如下方法完成
public static JwtParser parser() {
return new DefaultJwtParser();
}
具体实现如下:
public Claims getClaimsByToken(String token) throws ExpiredJwtException
{
if(null == token)
return null;
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
将token转换成Claims过程中将抛出如下异常
UnsupportedJwtException
:当token不是通过Claims对象构建的token时。
ExpiredJwtException
:当token已过期时。
MalformedJwtException
:当token不是有效的Claims对象构建的token时。
SignatureException
:当token的Signature验证失败时。
IllegalArgumentException
:当token为null或token是空字符串或token中只有空字符时。
还可以通过Claims的方法获取token中保存的信息
具体实现如下
/*通过token获取构建时的信息*/
public String getUserNameFromToken(String token) throws Exception
{
Claims claims = getClaimsByToken(token);
return claims.getSubject();
}
可以配置JWT的工具类
使用下面的配置类可以用来配置JWT
@Component
public class JwtTokenUtils
{
private static final String CLAIM_KEY_USERNAME = "user";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 工具方法:该方法是私有方法,用于生成过期时间
* */
private Date generateExpirationDate()
{
return new Date(System.currentTimeMillis() + expiration*1000);
}
/**
* 工具方法:该方法是私有方法,用于通过token获取荷载
*/
private Claims getClaimsFormToken(String token)
{
Claims claims = null;
try
{
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
catch (Exception e)
{
e.printStackTrace();
}
return claims;
}
/**
* 工具方法:用于通过token获取失效时间
* */
private Date getExpiredTimeFromToken(String token)
{
Claims claims = getClaimsFormToken(token);
return claims.getExpiration();
}
/**
* 工具方法:用于判断token是否达到失效时间
* 依据:
* 1)如果过期时间早于当前时间,说明token已失效
* 2)如果过期时间晚于当前时间,说明token未失效
* */
private Boolean isTokenExpired(String token)
{
Date expirted = getExpiredTimeFromToken(token);
return expirted.before(new Date());
}
/**
* 1.根据信息生成token
* */
public String generateToken(UserDetails details)
{
/*第一步:构建荷载:用于存放token的容器*/
Map<String, Object> claim = new HashMap<>();
claim.put(CLAIM_KEY_USERNAME, details.getUsername());
claim.put(CLAIM_KEY_CREATED, new Date());
/**
* 第二步:通过荷载构建token,并返回
* 需要:
* 1.Map类型的荷载
* 2.过期时间,在配置中设置了token有效时长,因此,过期时间是从创建时间+有效时长,生成的一个Date对象。
* 3.指定签名算法:通过signWith方法设置
* */
return Jwts.builder()
.setClaims(claim)//设置荷载
.setExpiration(generateExpirationDate())//设置过期时间
.signWith(SignatureAlgorithm.HS512, secret)//设置签名算法
.compact();//该方法用于生成token
}
/**
* 2.外界调用方法:根据token获取用户名
*/
public String getUserNameFromToken(String token)
{
/*
* 第一步:从token中获取荷载
* */
String username = null;
try
{
Claims claims = getClaimsFormToken(token);
username = claims.getSubject();
}catch (Exception e)
{
username = null;
}
return username;
}
/**
* 3.外界调用方法:判断token是否有效。
* */
public Boolean validateToken(String token, UserDetails userDetails)
{
return getUserNameFromToken(token).equals(userDetails.getUsername()) && !isTokenExpired(token);
}
}