JWT(JSON Web Token) 学习笔记(整合Spring Boot)
1、了解 JWT
1.1、什么是 JWT ?
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
1.2、什么时候使用 JWT ?
- 授权:这是使用 JWT 的最常见方案。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够跨不同域轻松使用。
- 信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确定发送方就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
1.3、JWT 的结构?
JWT 由三个部分组成,由点( . )分隔,分别是:
- Header(头部)
- Payload(有效荷载)
- Signature(签名)
格式如下:
aaaaa.bbbbb.cccccc
1.3.1、Header
头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA)。
{ "alg": "HS256", "typ": "JWT" }
然后通过Base64Url 编码这段 JSON ,从而形成 JWT 的第一部分。
1.3.2、PayLoad
令牌的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明:注册声明、公共声明和私人声明。
注册声明:这些是一组预定义的声明,这些声明不是必需的,但建议提供一组有用的、可互操作的声明。其中一些是:iss(发行人),exp(到期时间),sub (主题),aud(受众)等。
请注意,声明名称的长度只有三个字符,因为 JWT 应该是紧凑的。
公共声明:这些可以由使用JWT的人随意定义。但为避免冲突,应在 IANA JSON Web 令牌注册表中定义它们,或将其定义为包含抗冲突命名空间的 URI。
私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公开声明。
PayLoad 示例:
{ "iss": "songshu", "sub": "1234567890", "name": "haha", "admin": true }
然后对 PayLoad 进行 Base64Url 编码,以形成 JSON Web 令牌的第二部分。
警告:
请注意,对于已签名的令牌,此信息虽然受到保护以防止篡改,但任何人都可以读取。不要将机密信息放在 JWT 的有效负载或标头元素中,除非它已加密。
1.3.3、Signature
要创建签名部分,您必须获取编码的标头、编码的有效负载、机密、标头中指定的算法,并对其进行签名。
例如,如果要使用 HMAC SHA256 算法,将按以下方式创建签名:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在此过程中未发生更改,并且,对于使用私钥签名的令牌,它还可以验证 JWT 的发送者是否是它所说的发件人。
1.3.4、完整的 JWT
将所有的内容放在一起,输出是三个 Base64-URL 字符串,由点分隔,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更紧凑。
下面显示了一个 JWT,它具有编码的先前标头和有效负载,并使用密钥对其进行签名。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA
1.4、JSON Web 令牌如何工作?
在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web 令牌。
每当用户想要访问受保护的路由或资源时,用户代理都应发送 JWT,通常在授权标头中使用持有者架构。标头的内容应如下所示:
Authorization: Bearer <token>
如果令牌在标头中发送,则跨源资源共享 (CORS) 不会成为问题,因为它不使用 Cookie。
下图显示了如何获取 JWT 并用于访问 API 或资源:
- 应用程序或客户端请求对授权服务器进行授权。这是通过不同的授权流之一执行的。例如,典型的 OpenID Connect 兼容 Web 应用程序将使用授权代码流通过终结点。
/oauth/authorize
- 授予授权后,授权服务器将向应用程序返回访问令牌。
- 应用程序使用访问令牌访问受保护的资源(如 API)。
1.5、为什么要使用 JWT ?
1.5.1、基于传统的 Session 认证
认证方式:
由于 http 是一种无状态的协议,这就意味着如果用户向应用提供了用户名和密码来进行认证,那么下一次请求时,用户还需要再进行用户认证才行,根据 http 协议,我们并不知道是哪个用户发出的请求,所以为了识别出是哪一个用户发出的请求,我们只能在服务器中存储一份用户信息,这份用户信息会返回给浏览器,告诉其保存为 cookie ,以便下次请求的时候发给服务器,这样就能够识别是哪一个用户发出的请求。
认证流程:
1、客户端向 Web Application 发出认证请求
2、Web Application 响应客户端 cookie
3、客户端向 Web Application 发送携带 cookie 的请求,找到对应的 session
2、JWT 的第一个程序(Spring Boot)
2.1、引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
2.2、创建Token并对其进行签名
@SpringBootTest
class JwtApplicationTests {
@Test
void contextLoads() {
//创建Algorithm对象,设置秘钥(“secret”)
Algorithm algorithm = Algorithm.HMAC256("secret");
//创建日期类
Calendar calendar = Calendar.getInstance();
//对当前时间加90秒
calendar.add(Calendar.SECOND,90);
//创建token实例
String token = JWT.create()
.withIssuer("songshu") //设置发行人
.withClaim("username", "lisi") //设置自定义用户名
.withExpiresAt(calendar.getTime()) //设置过期时间
.sign(algorithm); //设置签名,保密,且需要秘钥复杂
System.out.println(token);
}
}
生成 token 结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA
2.3、根据Token解析
@SpringBootTest
class JwtApplicationTests {
@Test
void VerifierTest() {
String token ="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA";
//创建Algorithm对象,设置秘钥(“secret”)
Algorithm algorithm = Algorithm.HMAC256("secret");
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
DecodedJWT jwt = jwtVerifier.verify(token);
System.out.println(jwt.getClaim("username"));
System.out.println(jwt.getIssuer());
}
}
如果令牌具有无效的签名或未满足声明要求,则会引发。JWTVerificationException
3、JWT 使用方法
3.1、Header
Algorithm (“alg”)
返回"Algorithm"值,如果未在标头中定义,则返回 null。
String algorithm = jwt.getAlgorithm();
Type (“typ”)
返回 Type 值,如果未在 Header 中定义,则返回 null。
String type = jwt.getType();
Content Type (“cty”)
返回"Content Type"值,如果未在标头中定义,则返回 null。
String contentType = jwt.getContentType();
Key Id (“kid”)
返回 Key Id 值,如果未在 Header 中定义,则返回 null。
String keyId = jwt.getKeyId();
Private Claims
可以通过调用和传递声明名称来获取令牌标头中定义的其他声明。即使找不到报销申请,也始终会退回报销申请。您可以通过调用 来检查声明的值是否为空。getHeaderClaim()
claim.isNull()
Claim claim = jwt.getHeaderClaim("owner");
使用 创建令牌时,可以通过调用和传递声明映射来指定标头声明。JWT.create()
withHeader()
Map<String, Object> headerClaims = new HashMap();
headerClaims.put("owner", "auth0");
String token = JWT.create()
.withHeader(headerClaims)
.sign(algorithm);
3.2、PayLoad
Issuer (“iss”)
返回"Issuer"值或 null(如果未在有效负载中定义)。
String issuer = jwt.getIssuer();
Subject (“sub”)
返回 Subject 值或 null(如果未在有效负载中定义)。
String subject = jwt.getSubject();
Audience (“aud”)
返回 Audience 值,如果未在有效负载中定义,则返回 null。
List<String> audience = jwt.getAudience();
Expiration Time (“exp”)
返回"Expiration Time"值,如果未在"有效负载"中定义,则返回 null。
Date expiresAt = jwt.getExpiresAt();
Not Before (“nbf”)
返回"Not Before"值,如果未在有效负载中定义,则返回 null。
Date notBefore = jwt.getNotBefore();
Issued At (“iat”)
返回"Issued At"值,如果未在有效负载中定义,则返回 null。
Date issuedAt = jwt.getIssuedAt();
JWT ID (“jti”)
返回 JWT ID 值,如果未在有效负载中定义,则返回 null。
String id = jwt.getId();
Private Claims
令牌的有效负载中定义的其他声明可以通过调用或传递声明名称来获取。即使找不到报销申请,也始终会退回报销申请。您可以通过调用 来检查声明的值是否为空。getClaims()
getClaim()
claim.isNull()
Map<String, Claim> claims = jwt.getClaims(); //Key is the Claim name
Claim claim = claims.get("isAdmin");
或
Claim claim = jwt.getClaim("isAdmin");
使用 创建令牌时,可以通过调用并传递名称和值来指定自定义声明。JWT.create()
withClaim()
String token = JWT.create()
.withClaim("name", 123)
.withArrayClaim("array", new Integer[]{1, 2, 3})
.sign(algorithm);
还可以通过调用声明名称的映射并将其传递给值来创建 JWT:withPayload()
Map<String, Object> payloadClaims = new HashMap<>();
payloadClaims.put("@context", "https://auth0.com/");
String token = JWT.create()
.withPayload(payloadClaims)
.sign(algorithm);
还可以通过调用和传递名称和所需值来验证 上的自定义声明。JWT.require()
withClaim()
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("name", 123)
.withArrayClaim("array", 1, 2, 3)
.build();
DecodedJWT jwt = verifier.verify("my.jwt.token");
4、自定义简单的 JWTUtils 类
4.1、创建 JWTUtils 类
public class JWTUtils {
//设置秘钥
private static final String SING = "secret";
/**
* 获取 token
* @param map
* @return
*/
public static String getToken(Map<String,String> map) {
//创建Algorithm对象,设置秘钥(“secret”)
Algorithm algorithm = Algorithm.HMAC256(SING);
//创建日期类
Calendar calendar = Calendar.getInstance();
//对当前时间加90秒
calendar.add(Calendar.DATE,7);
//创建JWT build
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v) -> {
builder.withClaim(k,v);
});
//指定过期时间,并得到token
String token = builder.withExpiresAt(calendar.getTime()).sign(algorithm);
return token;
}
/**
* 验证 token
* @param token
* @return
*/
public static DecodedJWT verify(String token) {
try {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
return decodedJWT;
} catch (JWTVerificationException exception){
//Invalid signature/claims
throw exception;
}
}
}
4.2、测试
@Test
void JWTUtilsTest() {
HashMap<String,String> map = new HashMap<>();
map.put("username","songshu");
//获取 token
String token = JWTUtils.getToken(map);
System.out.println(token);
//验证
DecodedJWT verify = JWTUtils.verify(token);
if (verify != null) {
String username = verify.getClaim("username").asString();
System.out.println(username);
}
}
5、SpringBoot 整合 JWT
5.1、准备工作
5.1.1、引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
5.1.2、创建数据库表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
5.1.3、配置文件
server.port=8081
#mysql配置
spring.datasource.url=jdbc:mysql://localhost:3306/school?characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.password=root
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
5.2、编写代码
5.2.1、创建 User 类
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
private String id;
private String name;
private String password;
}
5.2.2、创建 UserMapper 接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
启动类上配置包扫描
@SpringBootApplication
@MapperScan("com.songshu.jwt.mapper")
public class JwtApplication {
public static void main(String[] args) {
SpringApplication.run(JwtApplication.class, args);
}
}
5.2.3、创建 UserService 接口和 UserServiceImpl 类
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public User login(User user) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",user.getUsername());
queryWrapper.eq("password",user.getPassword());
//根据用户名和密码进行验证
User user1 = userMapper.selectOne(queryWrapper);
if (user1 != null) {
return user1;
}
throw new RuntimeException("账号或密码不存在");
}
}
5.2.4、创建 UserController 类
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 登录
* @param user
* @return
*/
@PostMapping("/login")
public Map<String,Object> login(User user) {
Map<String,Object> map = new HashMap<>();
try {
Map<String,String> payLoad = new HashMap<>();
User user1 = userService.login(user);
payLoad.put("id",user1.getId().toString());
payLoad.put("username",user1.getUsername());
//生成token
String token = JWTUtils.getToken(payLoad);
map.put("state",true);
map.put("msg","登录成功");
map.put("token",token);
return map;
} catch (Exception e) {
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
/**
* 测试 token
* @param token
* @return
*/
@PostMapping("/testToken")
public Map<String,Object> testToken(String token) {
Map<String,Object> map = new HashMap<>();
try {
DecodedJWT decodedJWT = JWTUtils.verify(token);
map.put("state",true);
map.put("msg","令牌认证成功");
return map;
} catch (SignatureVerificationException e) {
//签名验证异常
map.put("msg","签名验证异常");
} catch (TokenExpiredException e) {
//token过期异常
map.put("msg","token过期异常");
} catch (AlgorithmMismatchException e) {
//算法不一致异常
map.put("msg","算法不一致异常");
} catch (Exception e) {
map.put("msg","签名验证异常");
}
map.put("state",false);
return map;
}
}
5.3、通过 Postman 进行测试
接口一:http://localhost:8081/user/login
Body中的 K/V:
username: lishi password: 123456
响应结果:
{ "msg": "登录成功", "state": true, "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleHAiOjE2NDUxMDU1MTksInVzZXJuYW1lIjoibGlzaGkifQ.0L7WPFBrcrajzRlRJcWLQm0_OoqS_vLmlE2Kpviv2c0" }
接口二:http://localhost:8081/user/testToken
Body中的 K/V:
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleHAiOjE2NDUxMDU1MTksInVzZXJuYW1lIjoibGlzaGkifQ.0L7WPFBrcrajzRlRJcWLQm0_OoqS_vLmlE2Kpviv2c0
响应结果:
{ "msg": "令牌认证成功", "state": true }
5.4、拦截器
使用上述方式每次都需要传递 token 数据,每个方法都需要验证 token ,导致代码冗余,应该如何优化?
- 使用拦截器进行优化
5.4.1、创建 JWTInterceptor 拦截器
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
//JWT官网建议把 token 放入 header 中
//从请求头中获取 token
String token = request.getHeader("token");
try {
DecodedJWT decodedJWT = JWTUtils.verify(token);
return true;
} catch (SignatureVerificationException e) {
//签名验证异常
e.printStackTrace();
map.put("msg","签名验证异常");
} catch (TokenExpiredException e) {
//token过期异常
e.printStackTrace();
map.put("msg","token过期异常");
} catch (AlgorithmMismatchException e) {
//算法不一致异常
e.printStackTrace();
map.put("msg","算法不一致异常");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","签名验证异常");
}
map.put("state",false);
//将Map转化为Json
String data = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(data);
return false;
}
}
5.4.2、创建 InterceptorConfig 配置类,并且配置拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/testToken")
.excludePathPatterns("/user/login");
}
}
5.4.3、修改 UserController 类
/**
* 测试 token
* @return
*/
@PostMapping("/testToken")
public Map<String,Object> testToken() {
Map<String,Object> map = new HashMap<>();
//处理业务逻辑
map.put("state",true);
map.put("msg","令牌认证成功");
return map;
}
5.4.4、测试
与上面相同。