基于Token的身份验证——JWT(token介绍转载)
1. JWT介绍
初次了解JWT,很基础,高手勿喷。
基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。
1.1 JWT是啥?
JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:
A.B.C
A由JWT头部信息header加密得到
B由JWT用到的身份验证信息json数据加密得到
C由A和B加密得到,是校验部分
1.2 怎样生成A?
header格式为:
{
"typ": "JWT",
"alg": "HS256"
}
它就是一个json串,两个字段是必须的,不能多也不能少。alg
字段指定了生成C的算法,默认值是HS256
将header用base64加密,得到A
通常,JWT库中,可以把A部分固定写死,用户最多指定一个alg
的取值
1.3 怎样计算B?
根据JWT claim set[用base64]加密得到的。claim set是一个json数据,是表明用户身份的数据,可自行指定字段很灵活,也有固定字段表示特定含义(但不一定要包含特定字段,只是推荐)。
这里偷懒,直接用php中的代码来表示claim set了,重在说明字段含义:
$token = array(
"iss" => "http://example.org", #非必须。issuer 请求实体,可以是发起请求的用户的信息,也可是jwt的签发者。
"iat" => 1356999524, #非必须。issued at。 token创建时间,unix时间戳格式
"exp" => "1548333419", #非必须。expire 指定token的生命周期。unix时间戳格式
"aud" => "http://example.com", #非必须。接收该JWT的一方。
"sub" => "jrocket@example.com", #非必须。该JWT所面向的用户
"nbf" => 1357000000, # 非必须。not before。如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟。
"jti" => '222we', # 非必须。JWT ID。针对当前token的唯一标识
"GivenName" => "Jonny", # 自定义字段
"Surname" => "Rocket", # 自定义字段
"Email" => "jrocket@example.com", # 自定义字段
"Role" => ["Manager", "Project Administrator"] # 自定义字段
);
JWT遵循RFC7519,里面提到claim set的json数据中,自定义字段的key是一个string,value是一个json数据。因此随意编写吧,很灵活。
个人初学,认为一个最基本最简单最常用的claim set为:
$token=array(
"user_id" => 123456, #用户id,表明用户
"iat" => 1356999524, #token发布时间
"exp" => 1556999524, #token过期时间
);
将claim set加密后得到B
,学名payload
1.4 怎样计算C?
将A.B
使用HS256加密(其实是用header中指定的算法),当然加密过程中还需要密钥(自行指定的一个字符串)。
加密得到C
,学名signature
,其实就是一个字符串。作用类似于CRC校验,保证加密没有问题。
好了,现在A.B.C
就是生成的token了。
1.5 怎样使用token?
可以放到HTTP请求的请求头中,通常是Authorization
字段。
也有人说放到cookie。不过移动端app用cookie似乎不方便。
1.6 token应用流程?
- 初次登录:用户初次登录,输入用户名密码
- 密码验证:服务器从数据库取出用户名和密码进行验证
- 生成JWT:服务器端验证通过,根据从数据库返回的信息,以及预设规则,生成JWT
- 返还JWT:服务器的HTTP RESPONSE中将JWT返还
- 带JWT的请求:以后客户端发起请求,HTTP REQUEST HEADER中的Authorizatio字段都要有值,为JWT
2 jose.4.j 实现(2.0原创)
介绍
jose.4.j 库是一个健壮且易于使用的JSON Web Token(JWT)和JOSE规范套件(JWS、JWE和JWK)的开源实现。它是用Java编写的,并且完全依赖于JCA api来进行密码学。请参阅https://bitbucket.org/bc/jose4j/wiki/home,了解更多信息、示例等。
2.1 引入依赖
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.4</version>
</dependency>
2.2 秘钥生成类
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.lang.JoseException;
public class RsaJsonWebKeyUtil {
public static RsaJsonWebKey rsaJsonWebKey = null;
private RsaJsonWebKeyUtil() {
}
public static RsaJsonWebKey getInstance() {
// 生成一个RSA密钥对,用于签署和验证JWT,包装在JWK中
if (rsaJsonWebKey == null) {
try {
rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("jwt1");
} catch (JoseException e) {
e.printStackTrace();
}
}
// 给JWK一个关键ID(kid),这是礼貌的做法
return rsaJsonWebKey;
}
}
2.3 jwt生产与消费 jws方式实现jwt签发
import java.security.Key;
import java.util.Arrays;
import java.util.List;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwa.AlgorithmConstraints.ConstraintType;
import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers;
import org.jose4j.jwe.JsonWebEncryption;
import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.ErrorCodes;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.keys.AesKey;
import org.jose4j.lang.ByteUtil;
import org.jose4j.lang.JoseException;
/**
* JSON Web令牌是一种紧凑的url安全方法,表示在两方之间传输的声明/属性。 这个例子演示了生产和消费一个签名的JWT
*/
public class JWT {
// 生成一个RSA密钥对,用于签署和验证JWT,包装在JWK中
public static RsaJsonWebKey rsaJsonWebKey = RsaJsonWebKeyUtil.getInstance();
public static void main(String[] args) throws Exception {
// simpleJwt();
String jwtsign = jwtsign();
checkJwt(jwtsign);
}
public static String jwtsign() throws JoseException, MalformedClaimException {
// 创建claims,这将是JWT的内容 B部分
JwtClaims claims = new JwtClaims();
claims.setIssuer("Issuer"); // 谁创建了令牌并签署了它
claims.setAudience("Audience"); // 令牌将被发送给谁
claims.setExpirationTimeMinutesInTheFuture(10); // 令牌失效的时间长(从现在开始10分钟)
claims.setGeneratedJwtId(); // 令牌的唯一标识符
claims.setIssuedAtToNow(); // 当令牌被发布/创建时(现在)
claims.setNotBeforeMinutesInThePast(2); // 在此之前,令牌无效(2分钟前)
claims.setSubject("subject"); // 主题 ,是令牌的对象
claims.setClaim("email", "mail@example.com"); // 可以添加关于主题的附加 声明/属性
List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
claims.setStringListClaim("groups", groups); // 多个属性/声明 也会起作用,最终会成为一个JSON数组
// JWT是一个JWS和/或一个带有JSON声明的JWE作为有效负载。
// 在这个例子中,它是一个JWS,所以我们创建一个JsonWebSignature对象。
JsonWebSignature jws = new JsonWebSignature();
// JWS的有效负载是JWT声明的JSON内容
jws.setPayload(claims.toJson());
// JWT使用私钥签署
jws.setKey(rsaJsonWebKey.getPrivateKey());
/*
* 设置关键ID(kid)头,因为这是一种礼貌的做法。 在这个例子中,我们只有一个键但是使用键ID可以帮助 促进平稳的关键滚动过程
*/
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
// 在jw/jws上设置签名算法,该算法将完整性保护声明
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
/*
* 签署JWS并生成紧凑的序列化或完整的jw/JWS 表示,它是由三个点('.')分隔的字符串
* 在表单头.payload.签名中使用base64url编码的部件 如果你想对它进行加密,你可以简单地将这个jwt设置为有效负载
* 在JsonWebEncryption对象中,并将cty(内容类型)头设置为“jwt”。
*/
String jwt = jws.getCompactSerialization();
// 现在你可以用JWT做点什么了。比如把它寄给其他的派对
// 越过云层,穿过网络。
System.out.println("JWT: " + jwt);
return jwt;
}
public static void checkJwt(String jwt) throws MalformedClaimException {
/*
* 使用JwtConsumer builder构建适当的JwtConsumer,它将 用于验证和处理JWT。 JWT的具体验证需求是上下文相关的, 然而,
* 通常建议需要一个(合理的)过期时间,一个受信任的时间 发行人, 以及将你的系统定义为预期接收者的受众。
* 如果JWT也被加密,您只需要提供一个解密密钥对构建器进行解密密钥解析器。
*/
JwtConsumer jwtConsumer = new JwtConsumerBuilder().setRequireExpirationTime() //// JWT必须有一个有效期时间
.setAllowedClockSkewInSeconds(30) // 允许在验证基于时间的令牌时留有一定的余地,以计算时钟偏差。单位/秒
.setRequireSubject() // 主题声明
.setExpectedIssuer("Issuer") // JWT需要由谁来发布,用来验证 发布人
.setExpectedAudience("Audience") // JWT的目的是给谁, 用来验证观众
.setVerificationKey(rsaJsonWebKey.getKey()) // 用公钥验证签名 ,验证秘钥
.setJwsAlgorithmConstraints( // 只允许在给定上下文中预期的签名算法,使用指定的算法验证
new AlgorithmConstraints(ConstraintType.WHITELIST, // 白名单
AlgorithmIdentifiers.RSA_USING_SHA256))
.build(); // 创建JwtConsumer实例
try {
// 验证JWT并将其处理为jwtClaims
JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt);
// 如果JWT失败的处理或验证,将会抛出InvalidJwtException。
// 希望能有一些有意义的解释(s)关于哪里出了问题。
System.out.println("JWT validation succeeded! " + jwtClaims);
} catch (InvalidJwtException e) {
System.out.println("Invalid JWT! " + e);
// 对JWT无效的(某些)特定原因的编程访问也是可能的
// 在某些情况下,您是否需要不同的错误处理行为。
// JWT是否已经过期是无效的一个常见原因
if (e.hasExpired()) {
System.out.println("JWT expired at " + e.getJwtContext().getJwtClaims().getExpirationTime());
}
// 或者观众是无效的
if (e.hasErrorCode(ErrorCodes.AUDIENCE_INVALID)) {
System.out.println("JWT had wrong audience: " + e.getJwtContext().getJwtClaims().getAudience());
}
}
}
}
运行结果
JWT: eyJraWQiOiJqd3QxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJJc3N1ZXIiLCJhdWQiOiJBdWRpZW5jZSIsImV4cCI6MTUzNzk0ODUyOCwianRpIjoibXJmbGg4dGlFaWZUM1VjaERQM1lrUSIsImlhdCI6MTUzNzk0NzkyOCwibmJmIjoxNTM3OTQ3ODA4LCJzdWIiOiJzdWJqZWN0IiwiZW1haWwiOiJtYWlsQGV4YW1wbGUuY29tIiwiZ3JvdXBzIjpbImdyb3VwLW9uZSIsIm90aGVyLWdyb3VwIiwiZ3JvdXAtdGhyZWUiXX0.P3odsUv8ArhagkRxSMB0pjlQqXe5TVGjTN9yn92Ay0tkXtqz2_uvWvk4Gf89w7QSTllLztoxlOsRTI0vOoVdG7krVSqMyTxtkxo4qOdWtEgBo-6IXdFtVhQSd8qKMHkI2-Vnsjd_ZvHI-2Mb2-PjotqNhXhqNG3WEIsbCwy64t6A4gvEyEsLqD2HVnPunW9Dp3hkP63fi4fjFXHATrMmTeeFDV0RKfK6sSHWOZSAPXFBlDXLn9ln1apxfzQvZ7Ub7mXTeqYd6dPJUHwp-tPRixp1KPPZyBrsHtyfHCEvdREM464a9NimgjqU7ThTULODgxKFAVj-_6y1PvW36tsAOA
JWT validation succeeded! JWT Claims Set:{iss=Issuer, aud=Audience, exp=1537948528, jti=mrflh8tiEifT3UchDP3YkQ, iat=1537947928, nbf=1537947808, sub=subject, email=mail@example.com, groups=[group-one, other-group, group-three]}
校验成功了