编辑
概述
简介
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的声明规范。
定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息,特别适用于分布式站点的单点登录(SSO)场景
session认证的缺点
1.安全性:CSRF攻击因为基于cookie来进行用户识别, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
2.扩展性:对于分布式应用,需要实现 session 数据共享
3.性能:每一个用户经过后端应用认证之后,后端应用都要在服务端做一次记录,以方便用户下次请求的鉴别, 通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
JWT的优点
1.无状态
2.适合移动端应用
3.单点登录友好
流程
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage,此后,客户端每次与服务器通信,都要带上这个JWT。 你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
编写JWT的流程
第一步:头部header
JSON对象,描述 JWT 的元数据 { "alg": "HS256", "typ": "JWT" }
alg属性表示签名的算法,默认是 HMAC SHA256(写成 HS256 )
typ属性表示这个令牌(token)的类型(type),统一写为 JWT
第二步:载荷payload
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
内容又可以分为3中标准:标准中注册的声明/公共的声明/私有的声明
1.payload-标准中注册的声明 (建议但不强制使用):
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 2.payload-公共的声明 : 公共的声明可以添加任何的信息。一般这里我们会存放一下用户的基本信息(非敏感信息)
3.payload-私有的声明 : 私有声明是提供者和消费者所共同定义的声明。需要注意的是,不要存放敏感信息!!!
第三步:签证signature
签证的值是对头部和载荷进行base64UrlEncode后使用指定算法签名生成【私钥签名,到时候用公钥解密】
1.放入头部
2.放入载荷
3.使用私【公】钥签署
4.设置签名算法
如:以默认HS256为例,指定一个密钥(secret),就会按照如下公式生成
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret, )
第四步
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户
示例
编辑
依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>java_sc_alibaba</artifactId> <groupId>jkw.life</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>test-higress-jwt-8007</artifactId> <dependencies> <!-- nacos-discovery --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- SpringMVC--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JWT--> <dependency> <groupId>org.bitbucket.b_c</groupId> <artifactId>jose4j</artifactId> <version>0.7.0</version> </dependency> </dependencies> </project>
application.yml
server: port: 8007 spring: application: name: test-higress-jwt-8007 cloud: nacos: discovery: # 配置 nacos注册中心地址 server-addr: 192.168.66.103:8848
启动类
package jkw; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @Slf4j @EnableDiscoveryClient @SpringBootApplication public class Main8007 { public static void main(String[] args) { SpringApplication.run(Main8007.class, args); log.info("************** 服务提供者 8001 启动成功 ************"); } }
工具类【utils.JWTUtil】
package jkw.utils; import org.jose4j.json.JsonUtil; import org.jose4j.jwa.AlgorithmConstraints; import org.jose4j.jwk.JsonWebKey; 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.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.lang.JoseException; import java.security.PrivateKey; import java.security.PublicKey; /** * JWT工具类 */ public class JWTUtil { public static void main(String[] args) throws JoseException { RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); //生成公钥 //{"kty":"RSA","n":"ziX1yaqbQWGGto1B4NxvmYifUTSigM2LEN0KubXoxt7t9Nz9NaqES4Y36e_v_DhT_v0mKC74pReTWcDVSXZE49jFWphBNlcsnWOsjMlntVlZ_rOQyLZEMhcqshQVvBU8UPFoc77UYBddAjnnShdrSsP5e9qMeMAJVsRCEJZ3Y1IkwRmUGThhmqXNGn1UEhtMSrXDewkre7AWNVkixky7SV-0WhdA6QrEPtLfXoXBQseO2QgRAA73Gc7rs1hF89lKphcBx_mtngonAltNtGGuDhXriBCnt_zuUx8Bt7S-XlECxjSFtHbWKsgOuWTXxMIOVMHoerinsDP1AKmIqPo5xw","e":"AQAB"} final String publicKey = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY); System.out.println(publicKey); //生成私钥 //{"kty":"RSA","n":"ziX1yaqbQWGGto1B4NxvmYifUTSigM2LEN0KubXoxt7t9Nz9NaqES4Y36e_v_DhT_v0mKC74pReTWcDVSXZE49jFWphBNlcsnWOsjMlntVlZ_rOQyLZEMhcqshQVvBU8UPFoc77UYBddAjnnShdrSsP5e9qMeMAJVsRCEJZ3Y1IkwRmUGThhmqXNGn1UEhtMSrXDewkre7AWNVkixky7SV-0WhdA6QrEPtLfXoXBQseO2QgRAA73Gc7rs1hF89lKphcBx_mtngonAltNtGGuDhXriBCnt_zuUx8Bt7S-XlECxjSFtHbWKsgOuWTXxMIOVMHoerinsDP1AKmIqPo5xw","e":"AQAB","d":"LclImg4GhbL_lLQzGZpcPyGVIRgrr6f3ZztxEmZQ2TrSZzxeEPlagNvCt3bPOpnYLh5Tx0EHgMOHuruVo8dc7a5Lxx9h_IvIIPzuaiahninGT0fatHmnE-kJVpwXZ7rftqqnpG2SBfWqdsAdmtswvV5hnxyfboJYkKjuc3i385r4s1s0pTZp33C6adHWB7B_dyVouQyjKQfUu_hToD32omJcTmJNcsTIKOR2Lztx-2Dzc4V99-3qDVbwXbBTfle_1tIeHYtSOsaBqVWpqdPOXSw5D4QuFjqUIXEVhIpTw5qNIejlAJ6wSHKUpU7DRm7t7Yl3yH5TqGr1WRB5eA6cYQ","p":"91hbnLKTPYeZrIq77YoSO0mzu8Q-rdJOv9Nece_oTL9zxLyV0JdzBWRkCUEqoaWSb6j1oeXC1qliHOYyls3Jf1e7bjFvJVedPGRtmEBWW_-nRFrIIYKswcco5_qRy4fHtOdNqwObyO1-8hLlu46kU5pxujlHKz0DAclLVOG8Wjs","q":"1VySxagoE5SDnhb2PDmNRF1uXrFHo1bg62KqAfCaws-MtNxaC9dtfBAHKH-s2QEfMYpiLv1i0IkronhvUZug0L-DzLemdwCyV98naaBElMzkTsC2hZpkmqR95HTACFSzpC5KRspl1ZvIxq-U-n5BY9asqpkhoyn7dCpkRy9tWeU","dp":"R5SEfqaXQdk6Odq0ZBvvBsVfhFlYokkYjR8IWATLv1owkKDa4lDR8p-I67y2L62Q4UuOOloZtrGyORbNUSMgyv-CuHMJ7U6brFyL8uG7nEgyCfATts7wW-vdBLVY-APFYa8GpRUYQl-ouzmIzmyLVb5-ZxwoYnT3p86vRFNHhP0","dq":"i9qIYoNc8aixtVh7wvI-hQdxJySxPoHeIKyln2vlJbkCFDMz2vs0ytN-va8iz4OKvOBmh0KUGPkw3uhun2GRwgMnE3N17B9Kx4qAvR3OlnLPXEe53E1dkHguBSf6D_vlXMLy8QAOTDw3GPVSg_dqSVUYDSMfB2KnbnezD24pEXk","qi":"kSGhqTuRjKESkVeJdWipF9kFy5-1p6T2Ym-S74PVVjBChuasvkkJtfLppw-yb0fX504TjoLDJIvYNp2fDY3Gv8CVr_W4cQjViEONrbGspzQTZmPAVEH0TiovFZ7z_KNp3P5Pl6JXMhOwxKsEM3hGj8IL6-4Bkh9-wOHG1mOi4sQ"} final String privateKey = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE); System.out.println(privateKey); } //公钥 private static final String publicKeyString = "{\"kty\":\"RSA\",\"n\":\"ziX1yaqbQWGGto1B4NxvmYifUTSigM2LEN0KubXoxt7t9Nz9NaqES4Y36e_v_DhT_v0mKC74pReTWcDVSXZE49jFWphBNlcsnWOsjMlntVlZ_rOQyLZEMhcqshQVvBU8UPFoc77UYBddAjnnShdrSsP5e9qMeMAJVsRCEJZ3Y1IkwRmUGThhmqXNGn1UEhtMSrXDewkre7AWNVkixky7SV-0WhdA6QrEPtLfXoXBQseO2QgRAA73Gc7rs1hF89lKphcBx_mtngonAltNtGGuDhXriBCnt_zuUx8Bt7S-XlECxjSFtHbWKsgOuWTXxMIOVMHoerinsDP1AKmIqPo5xw\",\"e\":\"AQAB\"}"; //私钥 private static final String privateKeyString = "{\"kty\":\"RSA\",\"n\":\"ziX1yaqbQWGGto1B4NxvmYifUTSigM2LEN0KubXoxt7t9Nz9NaqES4Y36e_v_DhT_v0mKC74pReTWcDVSXZE49jFWphBNlcsnWOsjMlntVlZ_rOQyLZEMhcqshQVvBU8UPFoc77UYBddAjnnShdrSsP5e9qMeMAJVsRCEJZ3Y1IkwRmUGThhmqXNGn1UEhtMSrXDewkre7AWNVkixky7SV-0WhdA6QrEPtLfXoXBQseO2QgRAA73Gc7rs1hF89lKphcBx_mtngonAltNtGGuDhXriBCnt_zuUx8Bt7S-XlECxjSFtHbWKsgOuWTXxMIOVMHoerinsDP1AKmIqPo5xw\",\"e\":\"AQAB\",\"d\":\"LclImg4GhbL_lLQzGZpcPyGVIRgrr6f3ZztxEmZQ2TrSZzxeEPlagNvCt3bPOpnYLh5Tx0EHgMOHuruVo8dc7a5Lxx9h_IvIIPzuaiahninGT0fatHmnE-kJVpwXZ7rftqqnpG2SBfWqdsAdmtswvV5hnxyfboJYkKjuc3i385r4s1s0pTZp33C6adHWB7B_dyVouQyjKQfUu_hToD32omJcTmJNcsTIKOR2Lztx-2Dzc4V99-3qDVbwXbBTfle_1tIeHYtSOsaBqVWpqdPOXSw5D4QuFjqUIXEVhIpTw5qNIejlAJ6wSHKUpU7DRm7t7Yl3yH5TqGr1WRB5eA6cYQ\",\"p\":\"91hbnLKTPYeZrIq77YoSO0mzu8Q-rdJOv9Nece_oTL9zxLyV0JdzBWRkCUEqoaWSb6j1oeXC1qliHOYyls3Jf1e7bjFvJVedPGRtmEBWW_-nRFrIIYKswcco5_qRy4fHtOdNqwObyO1-8hLlu46kU5pxujlHKz0DAclLVOG8Wjs\",\"q\":\"1VySxagoE5SDnhb2PDmNRF1uXrFHo1bg62KqAfCaws-MtNxaC9dtfBAHKH-s2QEfMYpiLv1i0IkronhvUZug0L-DzLemdwCyV98naaBElMzkTsC2hZpkmqR95HTACFSzpC5KRspl1ZvIxq-U-n5BY9asqpkhoyn7dCpkRy9tWeU\",\"dp\":\"R5SEfqaXQdk6Odq0ZBvvBsVfhFlYokkYjR8IWATLv1owkKDa4lDR8p-I67y2L62Q4UuOOloZtrGyORbNUSMgyv-CuHMJ7U6brFyL8uG7nEgyCfATts7wW-vdBLVY-APFYa8GpRUYQl-ouzmIzmyLVb5-ZxwoYnT3p86vRFNHhP0\",\"dq\":\"i9qIYoNc8aixtVh7wvI-hQdxJySxPoHeIKyln2vlJbkCFDMz2vs0ytN-va8iz4OKvOBmh0KUGPkw3uhun2GRwgMnE3N17B9Kx4qAvR3OlnLPXEe53E1dkHguBSf6D_vlXMLy8QAOTDw3GPVSg_dqSVUYDSMfB2KnbnezD24pEXk\",\"qi\":\"kSGhqTuRjKESkVeJdWipF9kFy5-1p6T2Ym-S74PVVjBChuasvkkJtfLppw-yb0fX504TjoLDJIvYNp2fDY3Gv8CVr_W4cQjViEONrbGspzQTZmPAVEH0TiovFZ7z_KNp3P5Pl6JXMhOwxKsEM3hGj8IL6-4Bkh9-wOHG1mOi4sQ\"}"; /** * 生成token * * @param userId 用户id * @param username 用户名 * @return */ public static String sign(Long userId, String username) throws JoseException { // 第一步:载荷payload JwtClaims claims = new JwtClaims(); // 注册的声明 1.jwt签发者 claims.setIssuer("user"); // 注册的声明 2.jwt所面向的用户 claims.setSubject("subject"); // 注册的声明 3.接收jwt的一方 claims.setAudience("Audience"); // 注册的声明 4.jwt的过期时间,这个过期时间必须要大于签发时间【从现在开始10分钟】 claims.setExpirationTimeMinutesInTheFuture(10000); // 注册的声明 5.定义在什么时间之前,该jwt都是不可用的【2分钟前】 claims.setNotBeforeMinutesInThePast(2); // 注册的声明 6.jwt的签发时间 claims.setIssuedAtToNow(); // 注册的声明 7.jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 claims.setGeneratedJwtId(); // 公共的声明:可可以添加任何的信息,一般这里我们会存放一下用户的基本信息 claims.setClaim("userId", userId); claims.setClaim("username", username); // 第二步:签证signature:其值是对头部header和载荷payload进行base64UrlEncode后使用指定算法签名生成 JsonWebSignature jws = new JsonWebSignature(); // 1.放入头部 jws.setKeyIdHeaderValue("keyId"); // 2.放入载荷 jws.setPayload(claims.toJson()); // 3.使用私钥签名 PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyString)).getPrivateKey(); jws.setKey(privateKey); // 4.设置签名算法 jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256); //第三步:算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户 String jwt = jws.getCompactSerialization(); return jwt; } /** * 验证jwt * * @param jwt */ public static void checkJwt(String jwt) throws MalformedClaimException, JoseException { //1.引入公钥,使用公钥对私钥的签名解密 PublicKey publicKey = new RsaJsonWebKey(JsonUtil.parseJson(publicKeyString)).getRsaPublicKey(); //2.使用JwtConsumer解密 JwtConsumer jwtConsumer = new JwtConsumerBuilder().setRequireExpirationTime() .setAllowedClockSkewInSeconds(30) // 允许在验证基于时间的令牌时留有一定的余地,以计算时钟偏差,单位/秒 .setRequireSubject() // 主题声明 .setExpectedIssuer("user") // 验证 jwt签发者 .setExpectedAudience("Audience") // 验证 接收jwt的一方 .setVerificationKey(publicKey) // 用公钥验证签名 ,验证私钥 .setJwsAlgorithmConstraints( // 使用生成jwt的签名算法解密 new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST, // 白名单 AlgorithmIdentifiers.RSA_USING_SHA256)) .build(); try { // 验证JWT并将其处理为jwtClaims JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt); //如果JWT失败的处理或验证,将会抛出InvalidJwtException,希望能有一些有意义的解释关于哪里出了问题 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()); } } } }
状态码枚举类
package jkw.vo; import lombok.AllArgsConstructor; import lombok.Getter; /** * 状态码枚举类 */ @Getter @AllArgsConstructor public enum CodeEnum { SUCCESS(200, "OK"), SYSTEM_ERROR(500, "系统异常"), ; private final Integer code; private final String message; }
返回结果封装类
package jkw.vo; import lombok.AllArgsConstructor; import lombok.Data; import java.io.Serializable; /** * 返回结果封装类 */ @Data @AllArgsConstructor public class BaseResult<T> implements Serializable { private Integer code;//状态码(成功:200,失败:其他) private String message;//提示信息 private T data;//返回数据 //构建成功结果 public static <T> BaseResult<T> ok() { return new BaseResult(CodeEnum.SUCCESS.getCode(), CodeEnum.SUCCESS.getMessage(), null); } //构建带有数据的成功结果 public static <T> BaseResult<T> ok(T data) { return new BaseResult(CodeEnum.SUCCESS.getCode(), CodeEnum.SUCCESS.getMessage(), data); } }
用户服务
package jkw.service; import com.alibaba.nacos.common.utils.StringUtils; import jkw.utils.JWTUtil; import jkw.vo.BaseResult; import org.jose4j.lang.JoseException; import org.springframework.stereotype.Service; /** * 用户服务 */ @Service public class UserService { /** * 登录 * * @param username * @param password * @return * @throws JoseException */ public BaseResult login(String username, String password) throws JoseException { // 1.用户名或者密码校验 if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { return new BaseResult(301, "用户名或者密码为空", null); } // 2.判断用户名和密码是否正确 if (username.equals("admin") && password.equals("123456")) { // 颁发登录token String token = JWTUtil.sign(1001L, "admin"); return BaseResult.ok(token); } else { return new BaseResult(301, "用户名或者密码不对", null); } } }
用户控制器
package jkw.controller; import jkw.service.UserService; import jkw.vo.BaseResult; import org.jose4j.jwt.MalformedClaimException; import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 用户控制器 */ @RequestMapping("/user") @RestController public class UserCon { @Autowired private UserService userService; @PostMapping("/login") public BaseResult login(String username, String password) throws MalformedClaimException, JoseException { return userService.login(username, password); } }
higress配置
文档
https://higress.io/zh-cn/docs/plugins/jwt-auth/
1.路由配置【8007-higress-jwt 精确匹配 | /user/login test-higress-jwt-8007.DEFAULT-GROUP.public.nacos】
编辑
2.插件配置【全局,配置】:策略:JWT Auth
consumers: - issuer: "user" jwks: | { "keys": [ { "kty":"RSA", "n":"ziX1yaqbQWGGto1B4NxvmYifUTSigM2LEN0KubXoxt7t9Nz9NaqES4Y36e_v_DhT_v0mKC74pReTWcDVSXZE49jFWphBNlcsnWOsjMlntVlZ_rOQyLZEMhcqshQVvBU8UPFoc77UYBddAjnnShdrSsP5e9qMeMAJVsRCEJZ3Y1IkwRmUGThhmqXNGn1UEhtMSrXDewkre7AWNVkixky7SV-0WhdA6QrEPtLfXoXBQseO2QgRAA73Gc7rs1hF89lKphcBx_mtngonAltNtGGuDhXriBCnt_zuUx8Bt7S-XlECxjSFtHbWKsgOuWTXxMIOVMHoerinsDP1AKmIqPo5xw", "e":"AQAB", "kid": "keyId", } ] } name: "consumer1" global_auth: false
3.路由配置【路由级别,使用】:策略:JWT Auth
allow: - "consumer1"
4.测试步骤:开启服务test-higress8006和test-higress-jwt-8007
编辑
首先访问http://www.jkw.com/user/login【admin/123456】获取jwt,
编辑
然后在访问配置jwt Auth的http://www.jkw.com/test/index
【请求头中添加Authorization,值为Bearer jwt的值,前面一定要加Bearer 】
编辑