本文介绍的是单个token 的生成、认证过程,如果想要了解双token解决token刷新问题可以看下:accessToken refreshToken简单使用源码demo,双token刷新及有效时间设置
如果对JWT JWS不了解的可以看下这篇博文: JWT,JWS与JWE区别
注:本文主要为了介绍JWS实现token认证过程,其他地方写的随意,若要用到生成环境需要自行修改,当然JWS token部分可以搬过去。
本文用的JWT库是jose4j,还有其他的JWT库 参考各类JWT库的对比
废话不多说,直接上源码
demo目录结构 只增加了3个类文件,如下:
- AccountController 测试类,有3个方法:用户注册、登录、获取用户信息
- User 用户实体类
- AuthorizationService 认证服务,提供3个方法:用唯一id生成keyPair , 用私钥生成token,用公钥验证token
JWT库 jose4j依赖
<!-- https://mvnrepository.com/artifact/org.bitbucket.b_c/jose4j -->
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.5</version>
</dependency>
AuthorizationService
注意:这个认证服务类中方法 createKeyPair 用来生成唯一的keyId 通过uuid 创建公钥和私钥。这个方法只要在本地运行就好了不用在线上执行。只要keyId、公钥和私钥没有被泄漏可以一直用,不用再次执行这个方法。
代码中keyId、privateKeyStr 和 publicKeyStr 是在本地运行createKeyPair 方法生成的,记录下来。
package com.example.jwt_token_demo.service;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;
import org.springframework.stereotype.Service;
import java.security.PrivateKey;
import java.util.UUID;
@Service
public class AuthorizationService {
/**
* keyId,公钥,私钥 都是用 createKeyPair 方法生成
*/
private static String keyId = "fa677d525c0e4ee485a61543937794af";
private static String privateKeyStr = "{\"kty\":\"RSA\",\"kid\":\"fa677d525c0e4ee485a61543937794af\",\"alg\":\"RS256\",\"n\":\"ki6NKW2ow53FBjWf21xNGF0v-Fzv9R4-vu5tz7LHz4vZ7TE2Lp7Xx0N2vFIH-HLZPLfAYW35W5iV29sW_MkbhVlh6f0q4AeCIeYrVjBGbcYTK5g-Sb8i9sO78DkGivryKTU4tUnOjqar2bfobwXScFhAgc4-BjIcvZ9V8LEzcAW76he500-sqekXqvYv7LbxIMlbadGEwbBqjscE83hiYjk1KSFrEeNWKP6E0X_cHVGEEGys8IKlBcfwOOCgaJ0sCFxvN3M54V33jSUknFzHAi1qJRsOI87-Fk1oYS-aniQOTfm5y5x1syTIgWEX9JvXCQgTxjp2kMItuoL2G2faoQ\",\"e\":\"AQAB\",\"d\":\"OmSUCN-AEZv9LwzOrW6CcWAQIHLne4-4WsadYOE2hcaEqAYHcboL0dI2JOXTv0AJXQK9u22VtSwPeMJcvV-MOclJnpF9xf3Z0rbByuz_xSvhToHDJ-xNCCuJ8FynK28wuptC6s_vzfXwIckf9PFrbWsjYXbEOe9cobZ7Ould9boHkEq-x0eKkDFfcQbevuQeM54FCk5FGl_D4wpbuqudZiT8FYCJPO7m-MTnND6xzdEVuApuQUPCjAin34ygp9QPk2VfQaM1z-ZHchTaXtsv5gyLwu3bqA91vugmrtGuSBG8pFaN6mYL_ivt1Q_7QDLqXZ5_WBhkoXO5DZ4-6ApLvQ\",\"p\":\"58fTzOMjU1zJ42ZdUkJjSpRlZXdlWx3ILStrvVw7kCHH1iBZ4Lckd7SdqJ9DUfdIBFTNPaAKfDqpoz7fWOUZyyUpy7_8qaDagFi53pAjuCGUuKnOV5hbSPBIOvGi9iTHwLNz6Om5eRV6lBXoTRJgT7g8u88HeSyl29RmYt-N7rs\",\"q\":\"oXTxPC0BFDxpFjk2Jpuj6hBkioD3Kiv7HEJjxRoOL3TEAAc3811f7V7bO5FWK2L_2g8I5YiZhsyh0SyxjPBfuIzcbAfGSTAsFqKiIOGZ7fqiFTOV319FJIkcMOrT2dbvGxNeGgTJpWB1vcX6C8i3ytzPFto1H9Bl3xAPVFflHFM\",\"dp\":\"RU-eaLCryav_u37K_WRY6N6Di9oudxbq24cWiuPf8_QGHGREPEzIHPvoAZrOuN4nrRPm5DzNpeStAeI1TBIGqpcMbp-U4Oz3KlZeDs4vwEpafPZafBtVgPRJxUapIs5Q5bFEQixSiIEBzPLYKuQJ5Q0FLGx2oafWWWykyYBsoy0\",\"dq\":\"SS2_yQ581rcqyi_UI1uXx5b2evBJFowonH5ayhMtKsU5sOmUqnE_8U50_2K4M6IDZMo7tg1byIUnMq-XKdIpEHSH008SyElVMk00PsMCCaL3o7Rl0YBUzmJ2rJVCwBFy_kqg9BoHazV1KDZ7RqwK4Z-DHVB5k5nZEmktCYVtCpE\",\"qi\":\"jDbVP3xGaYx2wgAGJT7KNjdoC4dYbl5ajp9saRCgAih3TYr1CjZSrLRm9L90UukgwajU0pHaffP74epVx0RBhnS5GtZhCoGitpLwYSDkZ9qTMTVnFPHrg6M1OYEEXjZ_UKlnYCrzrDe6tfHcO1UTzMzsuOgHjRAV7IS6ImfzwVw\"}";
private static String publicKeyStr = "{\"kty\":\"RSA\",\"kid\":\"fa677d525c0e4ee485a61543937794af\",\"alg\":\"RS256\",\"n\":\"ki6NKW2ow53FBjWf21xNGF0v-Fzv9R4-vu5tz7LHz4vZ7TE2Lp7Xx0N2vFIH-HLZPLfAYW35W5iV29sW_MkbhVlh6f0q4AeCIeYrVjBGbcYTK5g-Sb8i9sO78DkGivryKTU4tUnOjqar2bfobwXScFhAgc4-BjIcvZ9V8LEzcAW76he500-sqekXqvYv7LbxIMlbadGEwbBqjscE83hiYjk1KSFrEeNWKP6E0X_cHVGEEGys8IKlBcfwOOCgaJ0sCFxvN3M54V33jSUknFzHAi1qJRsOI87-Fk1oYS-aniQOTfm5y5x1syTIgWEX9JvXCQgTxjp2kMItuoL2G2faoQ\",\"e\":\"AQAB\"}";
public static long accessTokenExpirationTime = 60 * 60 * 24;
//jws创建token
public String createToken(String account) {
try {
//Payload
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
//expire time
NumericDate date = NumericDate.now();
date.addSeconds(accessTokenExpirationTime);
claims.setExpirationTime(date);
claims.setNotBeforeMinutesInThePast(1);
claims.setSubject("YOUR_SUBJECT");
claims.setAudience("YOUR_AUDIENCE");
//添加自定义参数,必须是字符串类型
claims.setClaim("account", account);
//jws
JsonWebSignature jws = new JsonWebSignature();
//签名算法RS256
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
jws.setKeyIdHeaderValue(keyId);
jws.setPayload(claims.toJson());
/*
RsaJsonWebKey jwk = null;
try {
jwk = RsaJwkGenerator.generateJwk(2048);
} catch (JoseException e) {
e.printStackTrace();
}
jwk.setKeyId(keyId); */
//PrivateKey privateKey = jwk.getPrivateKey();
PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyStr)).getPrivateKey();
jws.setKey(privateKey);
//get token
String idToken = jws.getCompactSerialization();
return idToken;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* jws校验token
*
* @param token
* @return 返回 用户账号
* @throws JoseException
*/
public String verifyToken(String token) {
try {
JwtConsumer consumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setMaxFutureValidityInMinutes(5256000)
.setAllowedClockSkewInSeconds(30)
.setRequireSubject()
//.setExpectedIssuer("")
.setExpectedAudience("YOUR_AUDIENCE")
/*
RsaJsonWebKey jwk = null;
try {
jwk = RsaJwkGenerator.generateJwk(2048);
} catch (JoseException e) {
e.printStackTrace();
}
jwk.setKeyId(keyId); */
//.setVerificationKey(jwk.getPublicKey())
.setVerificationKey(new RsaJsonWebKey(JsonUtil.parseJson(publicKeyStr)).getPublicKey())
.build();
JwtClaims claims = consumer.processToClaims(token);
if (claims != null) {
System.out.println("认证通过!");
String account = (String) claims.getClaimValue("account");
System.out.println("token payload携带的自定义内容:用户账号account=" + account);
return account;
}
} catch (JoseException e) {
e.printStackTrace();
} catch (InvalidJwtException e) {
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 创建jwk keyId , 公钥 ,秘钥
*/
public static void createKeyPair(){
String keyId = UUID.randomUUID().toString().replaceAll("-", "");
RsaJsonWebKey jwk = null;
try {
jwk = RsaJwkGenerator.generateJwk(2048);
} catch (JoseException e) {
e.printStackTrace();
}
jwk.setKeyId(keyId);
//采用的签名算法 RS256
jwk.setAlgorithm(AlgorithmIdentifiers.RSA_USING_SHA256);
String publicKey = jwk.toJson(RsaJsonWebKey.OutputControlLevel.PUBLIC_ONLY);
String privateKey = jwk.toJson(RsaJsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
System.out.println("keyId="+keyId);
System.out.println();
System.out.println("公钥 publicKeyStr="+publicKey);
System.out.println();
System.out.println("私钥 privateKeyStr="+privateKey);
}
public static void main(String[] args){
createKeyPair();
}
}
User
package com.example.jwt_token_demo.entity;
public class User {
public String account;
public String password;
public String lastLoginTime;
public User(){
}
public User(String account, String password) {
this.account = account;
this.password = password;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getLastLoginTime() {
return lastLoginTime;
}
public void setLastLoginTime(String lastLoginTime) {
this.lastLoginTime = lastLoginTime;
}
@Override
public String toString() {
return "User{" +
"account='" + account + '\'' +
", lastLoginTime='" + lastLoginTime + '\'' +
'}';
}
}
AccountController
注意: 每次重启项目测试都应该 从注册开始执行,因为懒没有些持久化,仅仅在本地map保存了用户信息 ,重启项目就没了。
package com.example.jwt_token_demo.Controller;
import com.example.jwt_token_demo.entity.User;
import com.example.jwt_token_demo.service.AuthorizationService;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.HashMap;
@RestController
public class AccountController {
@Autowired
AuthorizationService authorizationService;
//用一个hashmap存储注册用户,演示用
private static HashMap<String,User> accountUserMap = new HashMap<>();
//注册
@GetMapping("/register")
public String register(@RequestParam(name = "account") String account,
@RequestParam(name = "password") String password){
JSONObject ret = new JSONObject();
//简单校验下非空
if(account == null || account.equals("") || password == null || password.equals("")){
ret.put("code","1");
ret.put("desc","account | password can not empty");
return ret.toString();
}
//注册成功
accountUserMap.put(account,new User(account,password));
ret.put("code","0");
ret.put("desc","ok");
return ret.toString();
}
//登录
@GetMapping("/login")
public String login(@RequestParam(name = "account") String account,
@RequestParam(name = "password") String password){
JSONObject ret = new JSONObject();
User user = accountUserMap.get(account);
if(user != null && password != null && password.equals(user.getPassword())){
//登录成功
//创建token
String token = authorizationService.createToken(account);
//更新用户最近登录时间
user.setLastLoginTime(new Date().toLocaleString());
accountUserMap.put(account,user);
ret.put("code","0");
ret.put("desc","ok");
ret.put("token",token);
return ret.toString();
}
ret.put("code","1");
ret.put("desc","failed");
return ret.toString();
}
//查询用户信息
@GetMapping("/user")
public String queryUser(@RequestParam(name = "token") String token){
JSONObject ret = new JSONObject();
//识别token
try {
String account = authorizationService.verifyToken(token);
if(account != null) {
System.out.println();
ret.put("code","0");
ret.put("desc","ok" );
ret.put("user",accountUserMap.get(account).toString());
return ret.toString();
}else {
//正常情况,token认证失败基本都是token过期,其他错误信息在上线前就调试正常了
//token刷新问题,后面博文再做介绍
ret.put("code","2");
ret.put("desc","token认证失败,请刷新token");
return ret.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
ret.put("code","1");
ret.put("desc","failed");
return ret.toString();
}
}
关于生成 keyId 、privateKeyStr 和 publicKeyStr, 直接在本地运行 认证服务类即可,如下图:
测试
注意:我提供的代码里面web服务端口是8097,你们如果没有设置端口默认8080
每次重启项目测试都应该 从注册开始执行 ,因为用户信息没做持久化,重启了就没了。
1 注册 一个账号 zhangsan/123456
浏览器输入 http://127.0.0.1:8097/register?account=zhangsan&password=123456
2 登录, 登录成功会返回token
浏览器输入 http://127.0.0.1:8097/login?account=zhangsan&password=123456
3 获取用户信息 登录的用户获取自己的信息,需要验证token
浏览器输入 127.0.0.1:8097/user?token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImN…
注意:用自己刚才登录成功返回的token
源码
链接:https://pan.baidu.com/s/1R9MnBhNgOJN-N66Pk4S8aA
提取码:epon
JWT调试工具 可以看下我的这篇博文: JWT token调试工具,JWT Debugger
下一章介绍token刷新问题