- 为什么要用 JWT ? 认证在谈起 JWT 之前,咱们先理解一下什么是认证。在登录淘宝、微博等软件或者网站之前,咱们须要通过填写账号和明码来校验身份。认证是用来验证用户身份合法性的一种形式。
那咱们登录胜利之后,网站如何记录咱们的身份信息呢?后面咱们在学习 servlet 的时候,晓得了传统的零碎次要是通过 session 来存储用户的信息。session 将用户的信息存储在服务端。然而随着用户数量的增多,服务端就须要存一堆用户的认证信息,这种形式会一直减少服务端的压力。如果是分布式系统,用 session 存储用户信息就太拘谨了。因为分布式系统个别都会做负载平衡,如果这次认证胜利了,那么意味着下次申请必须仍要拜访这台服务器能力认证胜利。如果是前后端拆散的零碎就更好受了,因为前端代码和后端代码放在不同的服务器上,除了会减少服务器的压力,还会产生跨域等一系列问题,有点得失相当。那有没有一种工具能帮咱们解决这些认证问题?服务端不须要存储用户的认证信息防止跨域保证数据的安全性 JWT 闪亮退场。2. 什么是 JWTJWT 简称: JSON Web Token,又叫做 web 利用中的令牌。它能够帮忙咱们实现用户的认证、存储信息、加密数据等性能。那什么是令牌呢?令牌就相当于现代的虎符。
现代将军要想调兵遣将,必须手持虎符。
而用户要想拜访零碎中的某些页面,在发动的申请中必须携带应用 JWT 生成的令牌。令牌校验通过了,方可拜访零碎。这里的令牌简称为 token。3. JWT 的构造注:这里所说的 JWT 的构造,指的是用 JWT 生成令牌的构造,也就是 token 的构造。令牌的构造组成:标头 (Header) 载荷 (Payload) 签名 (Signature) 令牌最终的样子是由这三局部组成的字符串:Header.Payload.Signature
复制代码例如:hjYGH1dajUU.dajhjksfiu2h27jjghg2.kjbhjkf982bhh2lk2
复制代码 3.1 标头标头是应用 Base64 编码将令牌类型和签名算法通过加工后生成的一段字符串。标头蕴含两局部:令牌的类型:JWT(个别是默认的)签名算法:例如 SHA256、HMAC 等 {
“alg”: “HS256”,
“typ”: “JWT”
}
复制代码 3.2 载荷载荷次要存储一些自定义信息。它也是应用 Base64 编码加工后生成的一段字符串。3.3 签名签名是通过一个秘钥和标头中提供的算法再将标头和载荷进行加工后生成的一段字符串。例如:
-
JWT 的认证流程
用户点击登录,后盾接管用户申请并依据账号和明码从数据库查问用户信息。用户若存在,则应用 JWT 生成 token 并返回给前台。用户若不存在,则返回错误信息。前端在申请其余资源时将 token 放到申请头中。后盾从申请头中获取 token 信息,如果 token 校验失败,则返回错误信息。如果校验胜利,就将业务数据返回给前端。5. JWT 的应用 1. 引入依赖 <dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
复制代码 2. 生成 tokenpublic static void main(String[] args) {
Date date = new Date(System.currentTimeMillis() +1000);
Algorithm algorithm = Algorithm.HMAC256(“!Secret”);String token = JWT.create()
.withClaim("name", "张三") .withExpiresAt(date) // 设置过期工夫 .sign(algorithm); // 设置签名算法
System.out.println(token);
}
复制代码生成后果:”eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJuYW1lIjoi5byg5LiJIiwiZXhwIjoxNjUwNzE1OTg3fQ.
-hrxN6RwXCPnI-pmQaIsetx-8iN98XwZczmTFBoV1FI”
复制代码校验 token 校验 token,其实就是比拟 token 是否真确,如果不正确程序就会报错。String token = “”;
JWT.require(Algorithm.HMAC256(“!Secret”)).build().verify(token);
复制代码获取 token 中的载荷信息 String token=” “;
DecodedJWT jwt = JWT.decode(token);
String name = jwt.getClaim(“name”).asString();
复制代码判断 token 是否过期 public boolean isExpire(String token) {
DecodedJWT jwt = JWT.decode(token);
// 如果 token 的过期工夫小于以后工夫,则示意已过期,为 true
return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
}
复制代码 6. JWT 工具类因为 JWT 的作用次要是生成 token、校验 token、获取 token 中存储的自定义信息,所以咱们个别会把 JWT 封装成一个工具类。public class JwtUtil {
// 秘钥
private static final String SECRET = “SECRET_PRIVATE!”;
private static final long TIME_UNIT = 1000;// 生成蕴含用户 id 的 token
public static String createJwtToken(String userId, long expireTime) {Date date = new Date(System.currentTimeMillis() + expireTime * TIME_UNIT); Algorithm algorithm = Algorithm.HMAC256(SECRET); return JWT.create() .withClaim("userId", userId) .withExpiresAt(date) // 设置过期工夫 .sign(algorithm); // 设置签名算法
}
// 生成蕴含自定义信息的 token
public static String createJwtToken(Map<String, String> map, long expireTime) {JWTCreator.Builder builder = JWT.create(); if (MapUtil.isNotEmpty(map)) {map.forEach((k, v) -> {builder.withClaim(k, v); }); } Date date = new Date(System.currentTimeMillis() + expireTime * TIME_UNIT); Algorithm algorithm = Algorithm.HMAC256(SECRET); return builder .withExpiresAt(date) // 设置过期工夫 .sign(algorithm); // 设置签名算法
}
// 校验 token,其实就是比拟 token
public static DecodedJWT verifyToken(String token) {// 如果校验失败,程序会抛出异样 return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
// 从 token 中获取用户 id
public static String getUserId(String token) {try {DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userId").asString();} catch (JWTDecodeException e) {return null;}
}
// 从 token 中获取定义的荷载信息
public static String getTokenClaim(String token, String key) {try {DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(key).asString();} catch (JWTDecodeException e) {return null;}
}
// 判断 token 是否过期
public static boolean isExpire(String token) {DecodedJWT jwt = JWT.decode(token); // 如果 token 的过期工夫小于以后工夫,则示意已过期,为 true return jwt.getExpiresAt().getTime() < System.currentTimeMillis();
}
}
复制代码 7. JWT 案例这里咱们通过一个 springboot 我的项目来感受一下 JWT 的应用过程。开发工具:IDEA 技术栈:SpringBoot、MyBatisPlus、JWT 数据库:Mysql7.1 用户登录 7.1.1 创立 SpringBoot 我的项目
7.1.2 引入依赖 <!–web–>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!–myql–>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!–lombok–>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!–mybatis plus–>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!– 引入 jwt–>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<!–hutool–>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.7</version>
</dependency>
<!–test–>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
复制代码 7.1.3 application.ymlserver:
port: 8082
servlet:
context-path: /jwt-demo
spring:
# 数据源
datasource:
url: jdbc:mysql://localhost:3306/jwt_demo?allowPublicKeyRetrieval=true&useSSL=false
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
MybatisPlus
mybatis-plus:
global-config:
db-config:
field-strategy: IGNORED
column-underline: true
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
db-type: mysql
id-type: assign_id
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.zhifou.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志
复制代码 7.1.4 创立用户表 CREATE TABLE t_user
(
id
bigint unsigned NOT NULL AUTO_INCREMENT COMMENT ‘id’,
username
varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘ 姓名 ’,
password
varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘ 明码 ’,
PRIMARY KEY (id
) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
复制代码 7.1.5 用户实体类 @Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName(“t_user”)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 姓名
*/
private String username;
/**
* 明码
*/
private String password;
}
复制代码 7.1.6 Service 和实现类 UserServicepublic interface UserService extends IService<User> {
/**
* 登录
* @param user
* @return
*/
User login(User user);
}
复制代码 @Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User login(User user) {User userOne = this.getOne(new QueryWrapper<User>().eq("username", user.getUsername())
.eq("password", user.getPassword()));
return null == userOne ? null : userOne;
}
}
复制代码 7.1.7 Dao 和 mapper.xmlUserMapperpublic interface UserMapper extends BaseMapper<User> {
}
复制代码 UserMapper.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.zhifou.mapper.UserMapper”>
<!-- 通用查问映射后果 -->
<resultMap id="BaseResultMap" type="com.zhifou.entity.User">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
</resultMap>
<!-- 通用查问后果列 -->
<sql id="Base_Column_List">
id, username, password, sex, age
</sql>
</mapper>
复制代码 7.1.8 登录接口 @PostMapping(“/login”)
public Map<String, Object> login(@RequestBody User user) {
Map<String, Object> data = new HashMap<>();
User userOne = userService.login(user);
if (null != userOne) {data.put("code", 200);
data.put("msg", "登陆胜利");
data.put("token", JwtUtil.createJwtToken(userOne.getId().toString(), 24 * 10));
} else {data.put("code", 400);
data.put("msg", "账号或者明码谬误");
}
return data;
}
复制代码 7.1.9 测试登录胜利
登录失败
7.2 登录胜利拜访其余资源用户登录胜利后,咱们把 token 返回给了前端。用户再次拜访该网站的其余资源,咱们怎么判断以后的用户和上次登录胜利后的用户是同一个用户呢?只须要两步:前端:申请头中携带 token 后端:配置拦截器,校验 token7.2.1 创立拦截器 MyInterceptor/**
- @Desc:
- @Author: 知否技术
- @date: 下午 7:11 2022/4/24
*/
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");
Map<String, Object> result = new HashMap<>();
try {
// 校验 token, 校验失败会抛出异样
JwtUtil.verifyToken(token);
return true;
} catch (TokenExpiredException e) {e.printStackTrace();
result.put("code", "500");
result.put("msg", "token 已过期");
} catch (Exception e) {e.printStackTrace();
result.put("code", "500");
result.put("msg", "token 有效");
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(JSONUtil.parse(result));
return false;
}
}
复制代码 7.2.2 拦截器配置类 /**
- @Desc:
- @Author: 知否技术
- @date: 下午 7:18 2022/4/24
*/
@Component
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyInterceptor())
// 拦挡所有申请
.addPathPatterns("/**")
// 排除门路,比方用户登录、退出等
.excludePathPatterns("/user/login");
}
}
复制代码 7.2.3 测试 token 校验失败
token 校验胜利
7.2.4 残缺代码链接: https://pan.baidu.com/s/1sVRX…
提取码: wvr7