一、概述
JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于各方之间安全地将信息作为JSON对象传输,在数据传输的过程中还可以完成数据加密、签名等相关处理。
二、优势所在
在JavaWeb阶段,经常使用session来存储,以方便用来判断用户是否操作等等。但这又恰巧暴露了一个问题,用session存储是需要占用服务器内存的。当用户只有一两个的时候没什么问题,但是当用户成千上万的话,服务器就很难招架得住了。并且session是基于cookie来实现的,因此数据很容易被截获,遭遇CSRF跨域伪造攻击,因此不太安全。这时JWT的优势就突显出来的:
- 简洁(Compact):可以通过URL、POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所有JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务
三、结构组成
1.标头(Header)
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256(默认)或RSA。它会使用Base64编码组成WT结构的第一部分。值得注意的是,Base64是一种编码,也就是说,它是可以被翻译回原来的样子的,它并不是一种加密过程。
JSON
{
"alg":"HS256",
"typ":"JWT"
}
2.有效负载(Payload)
有效负荷是包含声明,通常将需要传递的数据存放在Payload中,同样的是使用Base64编码,但这同时说明这一块是可以被反编译的,这是什么意思呢?就是太敏感的信息存放在这里的话存在被捕获的可能,因此官方推荐在Payload中不要存放敏感的信息,例如住址、密码、手机号等。
3.签名(Signature)
签名是由编码后的标头和有效负荷以及我们提供的一个密钥,然后使用标头规定的签名算法进行签名。因此,签名的主要作用是保证JWT没有被篡改过,保证token的合法性。
这里特别注意的是,密钥secret 是验证 token 是否合法的重要凭证,如果用于验证的密钥被外界所获取到的话,我们所建立的验证防线将如同虚设!
四、开始整合JWT
1.添加核心依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
2.编写 JwtUtils,提高代码重用率
package com.example.springboot_jwt.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtils {
//首先顶一个token失效时间 2分钟
private static final Long EXPIRATION=60 * 1000l* 2;
//设置一个密钥,一般长度较长,比较复杂
private static final String SECRET="3idi83rie9";
/**
* 生成token的方法
* @param claimMap
* @return
*/
public static String creatToken(Map<String,String> claimMap){
//设置正式的失效时间=当前时间+失效时间戳
Date expiration=new Date(new Date().getTime()+EXPIRATION);
//设置JWT的头部
Map<String, Object> map=new HashMap<>();
map.put("alg","hs256");
map.put("typ","JWT");
// 创建token
JWTCreator.Builder builder = JWT.create();
// 使用lambda创建playload:有效负载,存放需要传递的数据,通过base64编码,也可以被反编码。
claimMap.forEach((k,v)->{
builder.withClaim(k,v);
});
return builder.withHeader(map)//添加头部,可省略保持默认
.withExpiresAt(expiration)//设置过期时间
.sign(Algorithm.HMAC256(SECRET));//设置签名解密算法
}
/**
* 对token进行验证
* @param token
* @return
*/
public static DecodedJWT verifyToken(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
System.out.println("验证token的函数得到的token"+verify.getToken());
System.out.println("验证token的函数得到的header"+verify.getHeader());
System.out.println(verify.getPayload());
return verify;
}
}
3.编写过滤器
为了不用在每一个controller控制器访问之前都写一遍获取token,将token放在请求头中,简化开发。
package com.example.springboot_jwt.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.springboot_jwt.util.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HashMap<String, Object> map = new HashMap<>();
//获取请求头中令牌 获取请求头中的token,每次前端发送请求都要给我们传递token
String token = request.getHeader("token");
System.out.println("拦截器中的token是:" + token);
try {
//验证令牌
JWTUtils.verifyToken(token);
//验证成功,请求放行
return true;
}catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签证");
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token过期");
}catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token算法不一致!");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效!!");
}
//设置状态
map.put("state",false);
//将map转换为json
String json= new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
4.配置过滤器
package com.example.springboot_jwt.config;
import com.example.springboot_jwt.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/**")//添加拦截路径
.excludePathPatterns("/login/user","/login/other");//添加放行路径
}
}
** 注意:添加的放行路径会影响到后面测试接口。
声明该类是一个配置类,注入到容器中。
5.编写yaml配置文件
spring:
datasource: #数据源配置
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///mybatis
username: root
password: root
mybatis: #mapper访问路径
mapper-locations: classpath:mapper/*.xml
configuration: #sql日志输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
6.创建实体类
package com.example.springboot_jwt.entity;
import java.io.Serializable;
public class User implements Serializable {
private Integer id;
private String username;
private String password;
public User() {
}
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
7.编写mapper
package com.example.springboot_jwt.mapper;
import com.example.springboot_jwt.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
User login(User user);
}
8.编写mapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springboot_jwt.mapper.UserMapper">
<select id="login" resultType="com.example.springboot_jwt.entity.User">
select * from user where username=#{username} and password=#{password}
</select>
</mapper>
9.编写service
package com.example.springboot_jwt.service;
import com.example.springboot_jwt.entity.User;
public interface UserService {
User longin(User user);
}
10.编写service实现
package com.example.springboot_jwt.service.impl;
import com.example.springboot_jwt.entity.User;
import com.example.springboot_jwt.mapper.UserMapper;
import com.example.springboot_jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User longin(User user) {
return userMapper.login(user);
}
}
11.编写控制器
package com.example.springboot_jwt.handler;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.springboot_jwt.entity.User;
import com.example.springboot_jwt.service.UserService;
import com.example.springboot_jwt.util.JWTUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/login")
public class UserHandler {
@Resource
UserService userService;
@GetMapping("/user")
public Map<String,Object> login(User user){
System.out.println(user.getUsername());
System.out.println(user.getPassword());
HashMap<String, Object> map = new HashMap<>();
try {
HashMap<String, String> payload = new HashMap<>();
User login=userService.longin(user);
if (null!=login){
payload.put("id",user.getId()+"");
payload.put("username",user.getUsername());
payload.put("password",user.getPassword());
// 生成JWT的令牌
String s = JWTUtils.creatToken(payload);
map.put("state",true);
map.put("msg","认证成功");
//相应token
map.put("token",s);
}
}catch (Exception e){
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
@PostMapping("/other")
public Map<String,Object> other(HttpServletRequest request){
Map<String,Object> map=new HashMap<>();
String token = request.getHeader("token");
DecodedJWT decodedJWT = JWTUtils.verifyToken(token);
System.out.println(decodedJWT.getClaim("username").asString());
System.out.println(decodedJWT.getClaim("password").asString());
map.put("state",true);
map.put("msg","请求成功");
return map;
}
}
12.数据库表设计
表名为 user
列名 | 数据类型 | 是否为空 | 主键 | 备注 |
---|---|---|---|---|
id | int | 否 | 是 | 主键id |
username | varchar(50) | 否 | 否 | 用户名 |
password | varchar(50) | 否 | 否 | 密码 |
五、测试
1.登录测试
左边是参数传递,右边是响应结果,如下所示:
{
"msg": "认证成功",
"state": true,
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6ImFkbWluIiwiaWQiOiJudWxsIiwiZXhwIjoxNjkyMzU3NTQxLCJ1c2VybmFtZSI6ImFkbWluIn0.PPC1tClbLGDuc61kaSmXr11fQ-s2DvNOGxc0KBxPCEc"
}
2.其它接口验证(token验证)
将第一接口传递回来的token作为参数,传递至token验证的请求头中,通过调用接口来验证token的是否正确
在本次案例中,本人设置token的有效时长为两分钟,两分钟之后的测试结果如下:
控制台输出结果如下:
token已经于某某某时间失效...