背景:应项目组要求,以后需整合cs、bs、app等,使用jwt作为权限验证
官网 :https://jwt.io/
GitHub:https://github.com/jwtk/jjwt
other: https://blog.usejournal.com/sessionless-authentication-withe-jwts-with-node-express-passport-js-69b059e4b22c
https://github.com/dwyl/learn-json-web-tokens/blob/master/README.md
1.JWT是一个开放标准(RFC 7519)
2.JWT定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息
3.JWT传输信息通过数字签名进行验证和信任
4.JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名
1. 授权:允许用户访问该令牌允许的路由,服务和资源
2.信息交换:安全传输信息
1. JWT由dots (.
)分隔得三个部分组成:头部元素(Header)、有效载荷(Payload)、签名(Signature)
2. 对应的格式:xxxxx.yyyyy.zzzzz
3. 头部:头部一般由token类型(即JWT)、正在使用的签名算法(如:HMAC SHA256或RSA)
头部元素案例:
{
"alg": "HS256",
"typ": "JWT"
}
4.有效载荷:包含声明(注:声明只有三个字符),是关于实体(通常是用户)和其他数据的声明,有效负载最终会经过Base64Url编码
声明有三种类型:已注册的声明、公开声明、私有声明
已注册的声明:是预定义的、非强制性的,一般建议使用,用来提供一组有用的、可互操作的声明,如:iss(发行人), exp(到期时间),子(主题), aud(观众)等
公开声明:可以随意定义,但是为了避免冲突,应该在 IANA JSON Web Token Registry中定义,或者将其定义为包含冲突命名空间的URI
私有声明:是为了在双方之间共享信息而创建的自定义声明
有效载荷案例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注:对于已签名的令牌,尽管这些信息受到保护以防篡改,但任何人都可以读取。除非JWT是加密的,否则不要将机密信息放在它的有效载荷或头元素中。
签名:要创建签名部分,必须获取编码的头部元素、编码的有效负载、密码、头部元素指定的算法,并对其进行签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
JWT解密地址:jwt.io Debugger ,下面是官方提供的JWT解码案例
1. 在身份验证中,当用户使用凭证成功登陆后,将返回JWT(JSON Web Token)并且必须在本地保存(通常在本地存储中,但也可以使用cookie),而不是传统的在服务器创建会话并返回一个cookie的方法。
2. 由于token是重要的凭证,所以必须非常小心,以防止出现安全问题
3. 一般情况下,token保留时长不要超过要求的时间
4. 当用户想要访问受保护的路由或资源时,用户代理应该使用承载(Bearer)模式发送JWT,header应如下所示
Authorization: Bearer
5. 在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization header中是否存在有效的JWT,如果存在,则允许用户访问受保护的资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。
6. 如果在Authorization header中发送token,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。
下图显示了如何获取JWT并用于访问API或资源:
1. 由于JSON比XML更简洁,因此在编码时它的大小也更小,使得JWT比SAML(Simple Web Tokens)更紧凑
2. 在安全方面,SWT只能使用HMAC算法通过共享密钥对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公钥/私钥对进行签名。与签名JSON的简单性相比,使用XML数字签名对XML进行签名而不会引入模糊的安全漏洞非常困难。
3. JSON解析器在大多数编程语言中很常见,因为它们直接映射到对象,相反,XML没有自然的文档到对象映射,这使得使用JWT比使用SAML断言更容易。
4. 关于使用,JWT在互联网的使用,突出了在多个平台(尤其是移动平台)上客户端处理JWT(JSON Web token)的便利性。
1. 创建一个简单的maven工程,pom文件依赖如下
org.springframework.boot
spring-boot-starter-parent
2.1.4.RELEASE
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt-api
${jjwt.version}
io.jsonwebtoken
jjwt-impl
${jjwt.version}
runtime
io.jsonwebtoken
jjwt-jackson
${jjwt.version}
runtime
org.junit.platform
junit-platform-launcher
test
org.junit.jupiter
junit-jupiter-engine
test
2. 创建测试类JWTTest.java
package com.hegret.test;
import java.security.Key;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.junit.jupiter.api.Test;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class JWTTest {
// @Test
// void test() {
// String header = "{\"alg\":\"HS256\"}";
// String claims = "{\"sub\":\"Joe\"}";
// String encodedHeader = base64URLEncode(header.getBytes("UTF-8"));
// String encodedClaims = base64URLEncode(claims.getBytes("UTF-8"));
// String concatenated = encodedHeader + '.' + encodedClaims;
// Key key = getMySecretKey()
// byte[] signature = hmacSha256(concatenated, key);
// String jws = concatenated + '.' + base64URLEncode(signature);
// }
@Test
void testCreateJWTAndParseJWT() {
String token = createJWT("1", "myCompany", "zhangsan", 10000);
System.out.println(token);
parseJWT(token);
}
@Test
void testTtlMillis() throws InterruptedException {
String token = createJWT("1", "myCompany", "zhangsan", 10000);
System.out.println(token);
Thread.sleep(10000);
parseJWT(token);
}
// Sample method to construct a JWT
private String createJWT(String id, String issuer, String subject, long ttlMillis) {
// The JWT signature algorithm we will be using to sign the token
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// We will sign our JWT with our ApiKey secret
// byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey.getSecret());
byte[] apiKeySecretBytes = DatatypeConverter
.parseBase64Binary("mySecretaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbcccccccb");
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id).setIssuedAt(now).setSubject(subject).setIssuer(issuer)
.signWith(signingKey);
// .signWith(signingKey, signatureAlgorithm);
// if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
// Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
// Sample method to validate and read the JWT
private void parseJWT(String jwt) {
// This line will throw an exception if it is not a signed JWS (as expected)
// Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary("mySecretaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbcccccccb"))
.parseClaimsJws(jwt).getBody();
System.out.println("ID: " + claims.getId());
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration: " + claims.getExpiration());
}
}
3.运行测试方法testCreateJWTAndParseJWT,结果显示如下,表示创建token和token验证成功
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTYxNDU2ODAxLCJzdWIiOiJ6aGFuZ3NhbiIsImlzcyI6Im15Q29tcGFueSIsImV4cCI6MTU2MTQ1NjgxMX0.EyAYyTAFXTK04P3sYfQn8rlRngSswQT_1p9DEWphyyU
ID: 1
Subject: zhangsan
Issuer: myCompany
Expiration: Tue Jun 25 18:00:11 CST 2019
4. 运行测试方法createJWT,结果显示如下,从结果可以看出,token失效后,再进行验证不通过
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTYxNDU2OTU3LCJzdWIiOiJ6aGFuZ3NhbiIsImlzcyI6Im15Q29tcGFueSIsImV4cCI6MTU2MTQ1Njk2N30.TaCgyoXQRhATLPNeo_pKfX80WXZ8tv1Vw6lph_Wfj0Q
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2019-06-25T10:02:47Z. Current time: 2019-06-25T10:02:48Z, a difference of 1234 milliseconds. Allowed clock skew: 0 milliseconds.
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:411)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:513)
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:573)
at com.hegret.test.JWTTest.parseJWT(JWTTest.java:83)
at com.hegret.test.JWTTest.testTtlMillis(JWTTest.java:42)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:628)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:117)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:184)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:180)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:127)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:137)
at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:89)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)
测试案例到此结束
1. 在上面的基础上引入mysql驱动、数据库连接池HikariCP、mybatis依赖
mysql
mysql-connector-java
runtime
com.zaxxer
HikariCP
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.1
2. 在项目资源文件夹src/main/resources下创建application.yml文件,内容如下:
debug:
true
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://127.0.0.1:3306/jwt?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
maximum-pool-size: 20
max-lifetime: 1800000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
3. 创建用户实体类User.java
package com.hegret.entity;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String password;
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", password=" + password + ", email=" + email + "]";
}
}
4. 创建UserDao接口
package com.hegret.dao;
import org.springframework.stereotype.Repository;
import com.hegret.entity.User;
@Repository
public interface UserDao {
User findByName(String name);
}
5. 在src/main/resources/mybatis/mapper目录下添加user-mapper.xml文件,内容如下:
6. 在application.yml文件中添加mapper映射文件读取路径
mybatis:
mapper-locations: classpath:mybatis/mapper/*-mapper.xml
7. 编写启动器引导类,并添加dao接口注解扫描
package com.hegret;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan(value = "com.hegret.dao")
@SpringBootApplication
public class JWTApplication {
public static void main(String[] args) {
SpringApplication.run(JWTApplication.class, args);
}
}
8. 编写UserService业务接口
package com.hegret.service;
import com.hegret.entity.User;
public interface UserService {
User getByName(String name);
}
9. 编写实现类UserServiceImpl.java
package com.hegret.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.hegret.dao.UserDao;
import com.hegret.entity.User;
import com.hegret.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User getByName(String name) {
return userDao.findByName(name);
}
}
10. 自定义异常类CustomException
package com.hegret.exception;
import com.hegret.enums.CodeMsgEnum;
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
private CodeMsgEnum codeMsg;
public CustomException(CodeMsgEnum codeMsg) {
this.codeMsg = codeMsg;
}
public CodeMsgEnum getCodeMsg() {
return codeMsg;
}
}
11. 自定义结果类型JsonResult.java
package com.hegret.util;
import java.io.Serializable;
import com.hegret.enums.CodeMsgEnum;
public class JsonResult implements Serializable {
private static final long serialVersionUID = 1L;
private static final int SUCCESS_CODE = 200;
private static final String SUCCESS_MSG = "success";
private int code;
private String msg;
private T data;
public static JsonResult error(CodeMsgEnum codeMsg) {
return new JsonResult(codeMsg);
}
public static JsonResult success(T data) {
return new JsonResult(data);
}
public static JsonResult success() {
return new JsonResult();
}
private JsonResult() {
this.code = SUCCESS_CODE;
this.msg = SUCCESS_MSG;
}
private JsonResult(T data) {
this.code = SUCCESS_CODE;
this.msg = SUCCESS_MSG;
this.data = data;
}
private JsonResult(CodeMsgEnum codeMsg) {
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
@Override
public String toString() {
return "JsonResult [code=" + code + ", msg=" + msg + ", data=" + data + "]";
}
}
12. 定义CodeMsg枚举类
package com.hegret.enums;
public enum CodeMsgEnum {
USERNAME_OR_PASSWORD_ERROR(500100, "用户名或密码错误"),
UNKNOWN_ERROR(500200, "发生未知错误,请联系管理员");
private final Integer code;
private final String msg;
private CodeMsgEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
13. 统一异常处理
package com.hegret.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.hegret.enums.CodeMsgEnum;
import com.hegret.exception.CustomException;
import com.hegret.util.JsonResult;
@ControllerAdvice
@ResponseBody
public class WebExceptionHandler {
Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler
public JsonResult usernameOrPasswordError(CustomException e) {
logger.error(e.getCodeMsg().toString());
return JsonResult.error(e.getCodeMsg());
}
@ExceptionHandler
public JsonResult unknownException(Exception e) {
logger.error("发生了未知异常", e);
// 发送邮件通知技术人员.
return JsonResult.error(CodeMsgEnum.UNKNOWN_ERROR);
}
}
14. 编写api控制器类UserController.java
package com.hegret.api.controller;
import java.security.Key;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.hegret.entity.User;
import com.hegret.enums.CodeMsgEnum;
import com.hegret.exception.CustomException;
import com.hegret.service.UserService;
import com.hegret.util.JsonResult;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
@RestController
@RequestMapping("/user")
public class UserController {
private volatile Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
@Autowired
private UserService userService;
@PostMapping("/sessions")
public JsonResult
15. 打开post,发送post请求http://localhost:8080/user/sessions
16.发送post请求http://localhost:8080/user/info
从结果可以看出通过http方式验证成功