基于session认证所显露的问题:
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
传统的Token,例如:用户登录成功生成对应的令牌,key为令牌 value:userid,隐藏了数据真实性 ,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。 客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
原理:jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。
下列场景中使用JSON Web Token是很有用的:
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
因此,一个典型的JWT看起来是这个样子的:
xxxxx.yyyyy.zzzzz
接下来,具体看一下每一部分:
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:
然后,用Base64对这个JSON编码就得到JWT的第一部分
也可以是下列中的算法:
JWS | 算法名称 | 描述 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
下面是一个例子:
对payload进行Base64编码就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
看一张官网的图就明白了:
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header应该看起来是这样的:
Authorization: Bearer <token>
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。
下面这张图显示了如何获取JWT以及使用它来访问APIs或者资源:
在讨论基于Token的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证
传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。
这种基于服务器的身份认证方式存在一些问题:
相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
注意:
还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。
https://tools.ietf.org/html/rfc7519#section-3
http://blog.leapoahead.com/2015/09/06/understanding-jwt/
https://cnodejs.org/topic/557844a8e3cc2f192486a8ff
http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
源码:https://github.com/jwtk/jjwt
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use
RSASSA-PSS (PS256, PS384, PS512) algorithms. JDK 11 or later does not require it for those algorithms:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
<scope>runtime</scope>
</dependency>
-->
如果您使用的是JDK 10或更早版本,并且还希望使用RSASSA-PSS(PS256、PS384、PS512)算法。JDK 11或更高版本不需要这些算法,需要依赖bcprov-jdk15on。
package com.myproject.studentmis4;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
@SpringBootTest
public class JWTTest {
//过期毫秒时长
public static final long Expiration=24*60*60*1000;
//密钥
private static final String secretString="Zd+kZozTI5OgURtbegh8E6KTPghNNe/tEFwuLxd2UNw=";
//生成安全密钥
private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
/**生成密钥*/
@Test
public void genKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
}
@Test
public void creatJWT(){
//创建一个Jwt构造器
JwtBuilder builder = Jwts.builder();
//设置签发时间
builder.setIssuedAt(new Date());
//设置过期时间
builder.setExpiration(new Date(System.currentTimeMillis()+Expiration));
//设置Id
builder.setId(UUID.randomUUID().toString());
//设置主题
builder.setSubject("auth");
//设置自定义信息
builder.claim("username","zhangsan");
builder.claim("role","admin");
//设置签名
builder.signWith(KEY);
//生成token字符串
String token=builder.compact();
System.out.println(token);
}
}
运行结果:
eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NDQ0MjksImV4cCI6MTY2ODczMDgyOSwianRpIjoiYTQ4NjJiNWYtZTg3NS00ZGQ5LTg1M2ItZTJmZjAyY2Y1NDViIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4ifQ.VlEr3LZNc941vugUU8Cvxh5DX7h6rL1T3WSVZA81080
自定义密钥
您的密钥字符串是 Base64 编码的吗?如果是这样,请执行以下操作:
@Value("${jwt.token.secret}")
private String secret;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(this.secret);
return Keys.hmacShaKeyFor(keyBytes);
}
JwtToken.builder().value(Jwts.builder()
.setClaims(createClaims(account))
.setSubject(subject.toString())
.setIssuedAt(Date.from(createdDateTime))
.setExpiration(Date.from(expirationDateTime))
.signWith(getSigningKey())
.compact()).expiration(expirationDateTime.toString()).build()
如果您的密钥不是 base64 编码的(它可能应该是,因为例如,如果您使用原始密码,您的密钥可能不正确或格式不正确),您可以通过以下方式执行此操作:
private Key getSigningKey() {
byte[] keyBytes = this.secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
但是,通常不建议使用第二个示例,因为这可能意味着您的密钥格式不佳。格式良好的安全随机密钥不是人类可读的,因此要将其存储为字符串,密钥字节通常首先进行 base64 编码。
从文档https://github.com/jwtk/jjwt#jws-key-create:
如果要生成足够强的 SecretKey 以用于 JWT HMAC-SHA 算法,请使用
Keys.secretKeyFor(SignatureAlgorithm)
辅助方法: SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512 在幕后,JJWT 使用 JCA 提供程序的 KeyGenerator 为给定算法创建具有正确最小长度的安全随机密钥。 如果您有现有的 HMAC SHA SecretKey 的编码字节数组,则可以使用Keys.hmacShaKeyFor
辅助方法。例如: byte[] keyBytes = getSigningKeyFromApplicationConfiguration(); SecretKey key = Keys.hmacShaKeyFor(keyBytes);
package com.myproject.studentmis4;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
@SpringBootTest
public class JWTTest {
//过期毫秒时长
public static final long Expiration=24*60*60*1000;
//密钥
private static final String secretString="Zd+kZozTI5OgURtbegh8E6KTPghNNe/tEFwuLxd2UNw=";
//生成安全密钥
private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
/**生成密钥*/
@Test
public void genKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
}
@Test
public void parseJWT(){
String token="eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NDQwNjMsImV4cCI6MTY2ODczMDQ2MywianRpIjoiODI3N2FhMjgtMGJmOC00YjY0LWE3M2ItMjk3YWIyY2JhNDZmIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4iLCJyb2xlIjoiYWRtaW4ifQ.oUX0iRjKMANNdFUmJdHgq3BJ_d4q54928p_leBx_JU0";
//创建解析器
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
//设置签名密钥
jwtParserBuilder.setSigningKey(KEY);
//解析token获得payload
Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(token);
System.out.println(claimsJws.getHeader());
System.out.println(claimsJws.getBody());
System.out.println(claimsJws.getSignature());
}
}
生成结果:
从5.7.0开始,Hutool提供了零依赖的JWT(JSON Web Token)实现。
相关资料网络上非常多,可以自行搜索,简单点说JWT就是一种网络身份认证和信息交换格式。
整体结构是:
header.payload.signature
JWT模块的核心主要是两个类:
JWT
类用于链式生成、解析或验证JWT信息。JWTUtil
类主要是JWT的一些工具封装,提供更加简洁的JWT生成、解析和验证工作// 密钥
byte[] key = "1234567890".getBytes();
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();
生成的内容为:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40
// 密钥
byte[] key = "1234567890".getBytes();
// SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 随机生成密钥对,此处用户可自行读取`KeyPair`、公钥或私钥生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()
示例代码:
package com.myproject.studentmis4;
import cn.hutool.jwt.JWT;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.UUID;
@SpringBootTest
public class HutoolJWTTest {
byte[] key="+6P1SPDJKQltJdSJq2W4IqPRLon/gQg2Z+dAMoqfYtU=".getBytes();
@Test
public void createJWT(){
String token=JWT.create()
.setHeader("alg","HS256") //加密算法
.setHeader("typ","JWT") //类型
.setIssuedAt(new Date()) //签发日期
.setExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*24)) //过期时间
.setKey(key) //密钥
.setJWTId(UUID.randomUUID().toString()) //JWT编号
.setSubject("auth") //主题
.setPayload("username","zhangsan123") //自定义信息
.setPayload("role","admin") //自定义信息
.sign(); //生成token
System.out.println(token);
}
}
运行结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
JWT jwt = JWT.of(rightToken);
// JWT
jwt.getHeader(JWTHeader.TYPE);
// HS256
jwt.getHeader(JWTHeader.ALGORITHM);
// 1234567890
jwt.getPayload("sub");
// looly
jwt.getPayload("name");
// true
jwt.getPayload("admin");
@Test
public void parseJWTInfo()
{
String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0";
JWT jwt = JWT.of(token); //解析jwt
//getAlgorithm:HS256
System.out.println("getAlgorithm:"+jwt.getAlgorithm()); //算法
//typ:JWT
System.out.println("typ:"+jwt.getHeader("typ")); //类型
//{"iat":1668656516,"exp":1668742916,"jti":"68dfb600-0a6c-469e-8736-45b0640cd842","sub":"auth","username":"zhangsan123","role":"admin"}
System.out.println(jwt.getPayloads());
}
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
// 密钥
byte[] key = "1234567890".getBytes();
// 默认验证HS265的算法
JWT.of(rightToken).setKey(key).verify()
package com.myproject.studentmis4;
import cn.hutool.jwt.JWT;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.UUID;
@SpringBootTest
public class HutoolJWTTest {
byte[] key="+6P1SPDJKQltJdSJq2W4IqPRLon/gQg2Z+dAMoqfYtU=".getBytes();
@Test
public void parseJWTVerify()
{
String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2Njg2NTY1MTYsImV4cCI6MTY2ODc0MjkxNiwianRpIjoiNjhkZmI2MDAtMGE2Yy00NjllLTg3MzYtNDViMDY0MGNkODQyIiwic3ViIjoiYXV0aCIsInVzZXJuYW1lIjoiemhhbmdzYW4xMjMiLCJyb2xlIjoiYWRtaW4ifQ.Ar2u1pZfLrj8cpvzCsgPp8u6gxA0I97jUp-uHorU2d0";
JWT.of(token).setKey(key).verify(); //验证是否正确
}
}
除了验证签名,Hutool提供了更加详细的验证:validate
,主要包括:
使用方式如下:
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNb0xpIiwiZXhwIjoxNjI0OTU4MDk0NTI4LCJpYXQiOjE2MjQ5NTgwMzQ1MjAsInVzZXIiOiJ1c2VyIn0.L0uB38p9sZrivbmP0VlDe--j_11YUXTu3TfHhfQhRKc";
byte[] key = "1234567890".getBytes();
boolean validate = JWT.of(token).setKey(key).validate(0);
这里使用Vue3+TypeScript+VueRouter+Spring Boot+JWT实现一个简单的用户登录功能。
创建一个Vue3项目,使用vue-cli,选择TypeScript,Babel
依赖vue-router,在命令行中使用npm i vue-router@next即可