这篇文章深入探讨了JWT(JSON Web Token)的概念、应用场景、原理及其与SpringBoot的集成方法。JWT通过JSON格式安全地传输信息,支持数据加密和签名,广泛应用于授权、信息交换和单点登录场景。最后,文章展示了如何在SpringBoot项目中集成JWT,并强调了JWT的安全性考虑和配置要点。
前言
JWT官网
JSON Web Token (JWT) 是一个通过JSON形式作为WEB应用的令牌,用于在各方之间以 JSON 对象的形式安全传输信息。在传输过程中可以完成数据加密、签名等操作。
以下是JWT使用的一些场景:
- 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
- 信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以您可以确定发件人就是他们所说的那个人。此外,由于使用标头和有效负载计算签名,您还可以验证内容没有被篡改。
- 单点登录:“一次登录,处处登录”,所有应用系统共享一个身份认证系统,如我们在百度旗下的产品:“百度搜索” 的网页进行登录了,那么当我们访问百度旗下的其它产品网址,如 “百度网盘” 就无需再次登录,利用JWT就可以很好的实现此功能
原理
传统的Seesion认证
客户端发送登录请求,服务器端认证通过后将用户信息保存在session中,然后返回给客户端sessionID(JSESSIONID),客户端将sessionID用cookie保存起来,下次用户登陆时会携带cookie,通过cookie中的sessionID(JSESSIONID)获取服务器端session中的用户信息。
cookie是储存在客户端的,session是储存在服务器端的,由于cookie储存在本地,所以更容易被破解
缺点:
可以看到传统的session登录,每次用户认证登陆时,都会在服务器端进行记录保存,而通常session都是储存在内层中的,当登录用户过多时,无疑会增加服务器消耗。
由于用户在做认证后,用户认证信息被保存在内存中,意味着下次用户再次请求时,仍然需要请求服务器,才能拿到授权资源,这样的处理方式在分布式应用中,相应的限制了负载均衡等一些扩展操作的能力。
由于session是基于cookie进行用户识别的,如果cookie被截获,用户就会很容易受到跨站请求伪造的攻击,并且由于sessionID只是一个特征值,表达的信息不够丰富,也大大限制了扩展操作。
JWT认证
客户端将用户名及密码发送给服务器端做校验,服务器端校验通过后,将用户ID及其它信息作为JWT的负载(PayLoad),将其与头部(Header)分别进行base64编码拼接后签名(Signature),形成一个JWT【所以JWT中是包含了用户信息的(即自包含),也就不需要向传统的Session认证一样,去服务器端请求用户信息】,并返回给客户端进行本地保存(cookie或者localStorage)。
客户端在每次请求时将JWT放入HTTP Header中的Authorization位(用以解决XSS和XSRF问题)。服务器端检查是否存在,若存在则验证JWT的有效性(检查签名是否正确,Token是否过期,Token身份信息等),验证通过后,服务器端执行相应的操作,并返回给客户端。
JWT 的原理是:服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2022年2月2日2点2分"
}
之后的用户与服务器端通信,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
基于此特性,服务器就不再需要保存任何 session 数据了,也就是说,服务器变成无状态了(Http协议就是一种无状态协议),从而比较容易实现扩展。
结构
JSON Web Tokens 由以点(.
)分隔的三部分组成,它们是:
- 标头(Header)
- 负载(Payload)
- 签名(Signature)
因此,JWT 通常是这样的:xxxxx.yyyyy.zzzzz
Header
标头通常由两部分组成:令牌的类型,即 JWT,以及正在使用的签名算法,例如 HMAC SHA256 或 RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个 JSON 被Base64Url编码以形成 JWT 的第一部分。
Payload
令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和附加数据的陈述。
示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后对有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。
请注意,对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。
Signation
要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建(即:对 base64编码的标头+base64编码的负载+盐 进行加密签名)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
最后一步的签名其实就是对头部和负载内容进行签名,防止内容信息被篡改。并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者就是它所说的那个人。
JWT 规定了7个官方字段,供选用:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
总结
输出是三个用点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更紧凑。
下面显示了一个 JWT,该 JWT 具有先前的标头和有效负载编码,并使用秘密签名(下面只是为了演示效果,实际是没有换行的)
可以使用jwt.io Debugger来解码、验证和生成 JWT。
总结
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
整合SpringBoot
简单使用
创建一个SpringBoot项目
引入JWT:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;
import java.util.Calendar;
import java.util.HashMap;
public class JwtApplicationTests {
/**
* 生成令牌
*/
@Test
public void contextLoads() {
HashMap<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
// 生成过期时间为600秒后
instance.add(Calendar.SECOND,600);
// 生成过期时间为1天后
// instance.add(Calendar.DATE,1);
String token = JWT.create()
// 标头
.withHeader(map)
// 负载
.withClaim("userId", 1001)
.withClaim("userName", "ahzoo")
// 过期时间
.withExpiresAt(instance.getTime())
// 签名
.sign(Algorithm.HMAC256("999"));
System.out.println(token);//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9.yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8
}
/**
* 验证令牌
*/
@Test
public void verify(){
// 创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("999")).build();
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9.yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8";
// 验签,验签失败会抛异常
DecodedJWT verify = jwtVerifier.verify(token);
System.out.println(verify.getHeader());
System.out.println(verify.getPayload());
System.out.println(verify.getSignature());
System.out.println(verify.getClaims());
// 默认只能拿String类型数据,获取其它类型数据时需要调用额外的方法
System.out.println(verify.getClaim("userId").asInt());
// 获取String类型数据,就不用调用额外的方法
System.out.println(verify.getClaim("userName"));
System.out.println(verify.getExpiresAt());
/*
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
eyJ1c2VyTmFtZSI6ImFoem9vIiwiZXhwIjoxNjQzNTI2NzUxLCJ1c2VySWQiOjEwMDF9
yHGbp1cxM0xCA9aKjCZtSgwo4OI6UNYjonxsCLAQyv8
{userName="ahzoo", exp=1643526751, userId=1001}
1001
"ahzoo"
Sun Jan 30 15:12:31 CST 2022
*/
}
}
封装JWT工具类:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtil {
/**
* 密钥
*/
private static String SECRET = "privatekeyahzoo";
/**
* 传入payload信息获取token
* @param map payload
* @return token
*/
public static String getToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach(builder::withClaim);
Calendar instance = Calendar.getInstance();
// 默认7天过期
instance.add(Calendar.DATE, 7);
// 指定令牌的过期时间
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证token
*/
public static DecodedJWT verify(String token) {
// 验证失败会抛异常
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
/**
* 获取token中的payload
*/
public static Map<String, Claim> getPayloadFromToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token).getClaims();
}
}
这里还没有评论哦
快来发一条评论抢占前排吧