本篇内容:JWT 工作原理及其应用 从0~0.5 快速整合SpringBoot以及Mybatis 二刷绝对适合你!
文章专栏:前端知识(后端需掌握知识点)
前后端分离项目(Vue + SpringBoot)
最近更新:2022年2月2日 Vue中的路由 Router 从0 ~ 0.5 基础通晓到使用 (后端人员需要掌握的基础使用)
个人简介:一只二本院校在读的大三程序猿,本着注重基础,打卡算法,分享技术作为个人的经验总结性的博文博主,虽然可能有时会犯懒,但是还是会坚持下去的,如果你很喜欢博文的话,建议看下面一行~(疯狂暗示QwQ)
点赞 收藏 ⭐留言 一键三连 关爱程序猿,从你我做起
什么是 JWT ?
JWT(全称JSON Web Tokens
)我们通常称之为 JSON Web 令牌
,它是个十分流行的跨域认证的一种解决方案
。
Token 又是什么呢?
Token是服务端生成的一串字符串
,以作客户端进行请求的
一个令牌
,当第一次登录后
,服务器生成一个Token便将此Token返回给客户端
,以后客户端只需带上这个Token
前来请求数据即可
,无需
再次带上用户名和密码
。
使用Token的目的:Token的目的是为了减轻服务器的压力
,减少
频繁的查询数据库
,使服务器更加健壮
。
JWT
实际上是 token 的一种具体实现方式
。
官方参考文献:
JSON Web Tokens - jwt.io
官方给出的概念:
Json web token(JWT)
是为了网络应用环境间传递声明而执行的一种基于JSON
的开发标准(RFC 7519
),用于对双方之间以JSON对象安全的传输信息
。
简单的来说:对于学过信息安全技术的同学就知道,为保证了数据的发送方不可抵赖
,以及数据的接收方能够安全并且进行验证获取信息
,通常都是要对数据进行加密
、签名等
相关操作。
JWT
就是用于在各个用户之间通过JSON对象数据
进行的安全传输
的令牌。
再简单点理解就是:我们把用户提出请求中的数据
保存到一个JSON字符串
中,然后通过某些算法
进行编码
,得到
了一个JWT
,此时这个 JWT 已经被加密签名
这些操作了, 然后服务器对其进行接收
,进行验证签名
,并且对之解密获取用户提交的数据
。
其工作流程一般分为如下6步:
将Token的类型和使用的算法作为Header
通过Base64
进行编码
生成 JWT 的第一部分结构,xxxxx
,将含有用户信息的数据作为Payload(负载)
进行Base64的编译
生成 JWT 的第二部分结构,得到的编码作为yyyyy
,随后与签名
作为 JWT 的第三部分结构 (zzzzz)进行拼接
,形成一个JWT Token的字符串:xxxxx,yyyyy,zzzzz
JWT Token
字符串 作为用户认证成功后的请求响应返回给前端
。前端可以将返回的结果保存在浏览器当中
,退出登录后会删除所对应的 JWT Token
。每次请求
时,将上面后端传给浏览器的JWT Token
一并放到HTTP请求头中
的 **Authorization
**属性 (解决恶意跨域、跨站请求访问的问题)。JWT Token
其有效性
,签名正确性
以及是否过期等
验证操作。解析出 JWT Token中的用户信息
,返回结果。注意:因为Base64是开放加密算法,所以一定不要在负载中存放敏感信息(用户密码等…)
步骤1:创建一个SpringBoot项目,整合 JWT 相关依赖
pom.xml
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
步骤2:编写一个 JWT 工具类用于生成令牌
JWTUtils.java
/**
* 功能描述
* JWT的工具类
* @author Alascanfu
* @date 2022/2/3
*/
public class JWTUtils {
/**
* 功能描述
* 生成令牌
* @date 2022/2/3
* @author Alascanfu
*/
public static String createToken(){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,180);
String token = JWT.create()//生成令牌
.withClaim("username", "Alascanfu")//设置payload
.withExpiresAt(instance.getTime())//设置过期时间
.sign(Algorithm.HMAC256("token!Q#28$5RCCF"));//设置signature
return token;
}
/**
*功能描述
* 令牌验证
* @date 2022/2/3
* @author Alascanfu
*/
public static DecodedJWT requireToken(String token){
JWTVerifier jwtVerifier = JWT
.require(Algorithm.HMAC256("token!Q#28$5RCCF"))
.build();
return jwtVerifier.verify(token);
}
}
步骤3:创建一个测试类进行测试
JWTJunitTest.java
@SpringBootTest
public class JWTJunitTest {
@Test
public void test(){
//创建一个token
String token = JWTUtils.createToken();
System.out.println("Token :" + token);
//拿着获取的token得到decodedJWT
DecodedJWT decodedJWT = JWTUtils.requireToken(token);
//通过decodedJWT获取数据
Claim username = decodedJWT.getClaim("username");
System.out.println(username);
String username = decodedJWT.getClaim("username").asString();
System.out.println(username);
}
}
执行结果:
返回的Token令牌:
Token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDM4NTk5MDQsInVzZXJuYW1lIjoiQWxhc2NhbmZ1In0.HYPycutSIfgfh_vNFL6fp2P3mzlBiAmuH7vAy0D-64k
com.auth0.jwt.impl.JsonNodeClaim@5bc7e78e
Alascanfu
我们可以根据jwt的依赖包找到对应的exceptions
SignatureVerificationException
: 签名认证错误异常TokenExpiredException
: 令牌过期错误异常。AlgorithmMismatchException
:校验算法不匹配的错误异常。InvalidClaimException
:失效负载数据异常。JWTUtil.java
/**
* 功能描述
* JWT封装工具类
* @author Alascanfu
* @date 2022/2/3
*/
public class JWTUtil {
private static String TOKEN = "&$s78"+UUID.randomUUID().toString()+"#34%6";
/**
* 功能描述
* 生成Token令牌
* @date 2022/2/3
* @author Alascanfu
*/
public static String getToken(Map<String ,String> map){
JWTCreator.Builder builder = JWT.create();
//向其中添加负载数据
//23种设计模式中典型的建造者模式
map.forEach((key,val)->{
builder.withClaim(key,val);
});
//设置过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,90);
builder.withExpiresAt(instance.getTime());
//进行签名拼接
return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
}
/**
* 功能描述
* 验证Token令牌的合法性
* @date 2022/2/3
* @author Alascanfu
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
/**
* 功能描述
* 获取DecodedJWT 用于获取负载数据
* @date 2022/2/3
* @author Alascanfu
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
}
用于测试封装类的测试类
JWTJunitTest.java
@SpringBootTest
public class JWTJunitTest {
@Test
public void test(){
HashMap<String, String> map = new HashMap<>();
map.put("username","Alascanfu");
map.put("id","201901094106");
String token = JWTUtil.getToken(map);
System.out.println(token);
JWTUtil.verify(token);
DecodedJWT token1 = JWTUtil.getToken(token);
System.out.println(token1.getClaim("username").asString());
System.out.println(Long.parseLong(token1.getClaim("id").asString()));
}
}
步骤1:创建一个数据库并且生成一张user表填入部分数据
CREATE DATABASE jwt;
CREATE TABLE user (
`id` int not null primary key auto_increment,
`username` VARCHAR(64) not null ,
`password` VARCHAR(64) not null
)engine = innodb default charset =utf8;
INSERT INTO `jwt`.`user`(`id`, `username`, `password`) VALUES (1, 'Alascanfu', '123456');
步骤2:创建SpringBoot项目导入相关所需依赖及部分配置
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
配置好application.yaml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
#Spring Boot默认是不注入这些属性值的,需要自我绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙,此处是filter修改的地方
filters: stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
配置好log4j.properties
log4j.rootLogger=DEBUG, Console,file
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
#文件输出的相关配置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File = ./log/DataLog.log
log4j.appender.file.MaxFileSize = 10mb
log4j.appender.file.Threshold = DEBUG
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = [%p][%d{yy-MM-dd}][%c]%m%n
#日志输出级别
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.org.apache=INFO
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
步骤3:编写用户类及其Mapper、service、controller等实例数据业务
User.java
public class User {
private int id ;
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
//...get/set方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
UserMapper.java
@Mapper
@Repository
public interface UserMapper {
/**
* 功能描述
* 通过用户名和密码校验用户进行查询信息
* @date 2022/2/3
* @author Alascanfu
*/
public User queryUser(User user);
}
UserMapper.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alascanfu.mapper.UserMapper">
<cache readOnly="false" flushInterval="60000" size="512" eviction="LRU"/>
<select id="queryUser" resultType="com.alascanfu.pojo.User">
select id, username, password from user where user.username = #{username} and user.password = #{password}
select>
mapper>
UserService.java
@Service
public class UserService {
@Autowired
UserMapper userMapper;
@Transactional(propagation = Propagation.REQUIRED)
public User queryUser(User user) {
if(userMapper.queryUser(user) != null){
return userMapper.queryUser(user);
}else {
throw new RuntimeException("用户名或密码错误~");
}
}
}
注意:此时进行单元测试检阅是否能获得得到数据库中的数据
HelloJwtApplicationTests.java
@SpringBootTest
class HelloJwtApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
System.out.println(userService.queryUser(new User("Alascanfu", "123456")).toString());
}
}
如果出现了绑定错误记得要去pom.xml中进行配置yaml、xml、properties等文件也要被打包到target中哦~标签中进行修改。
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
<include>**/*.yamlinclude>
<include>**/*.ymlinclude>
<include>**/*.propertiesinclude>
includes>
<filtering>truefiltering>
resource>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.xmlinclude>
<include>**/*.yamlinclude>
<include>**/*.ymlinclude>
<include>**/*.propertiesinclude>
includes>
<filtering>truefiltering>
resource>
步骤4:编写Controller
UserController.java
@RestController
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/user/login")
public Map<String,Object> login(User user){
Map<String ,Object > map = new HashMap<>();
try {
User queryUser = userService.queryUser(user);
Map<String ,String > payload = new HashMap<>();
payload.put("id",String.valueOf(queryUser.getId()));
payload.put("username",queryUser.getUsername());
String token = JWTUtil.getToken(payload);
map.put("status",true);
map.put("msg","认证成功!");
map.put("token",token);
}catch (Exception e){
map.put("status",false);
map.put("msg","认证失败!");
}
return map;
}
}
进行测试
额外接口
@RequestMapping("/user/index")
public Map<String,Object> userIndex(String token){
System.out.println(token);
Map<String ,Object > map = new HashMap<>();
try {
JWTUtil.verify(token);
DecodedJWT verify = JWTUtil.getToken(token);
map.put("state",true);
map.put("msg","请求成功!");
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("msg",e.toString());
}
map.put("state",false);
return map;
}
接口测试
注意
:如果给每个业务API编写上述会使得代码冗余,为了减少代码冗余,我们可以将这个公共的操作放在单体JavaSpringBoot应用中的拦截器中去做,如果是SpringCloud分布式服务模块咱们就可以去网关进行配置。
编写拦截器
interceptors/JWTInterceptor.java
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
//验证
JWTUtil.verify(token);
//如果能验证成功直接放行
return true;
} catch (TokenExpiredException e) {
map.put("msg", "Token已经过期~");
} catch (SignatureVerificationException e){
map.put("msg", "签名错误~");
} catch (AlgorithmMismatchException e){
map.put("msg", "加密算法不匹配~");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "无效token~");
}
map.put("state", false);
//通过jackson转化map为json数据
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
将我们的拦截器注入到Spring容器当中,我们需要写一个配置类继承WebMvcConfigurer对SpringMVC进行扩展
config/intercceptorConfig.java
@Configuration
public class IntercsptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/**")//需要拦截的路径
.excludePathPatterns("/user/login");//但是生成token的路径不能拦截
}
}
进行测试验证
POSTMAN测试
DispatcherServlet拦截器爆出的异常错误
:本次整理的是前后端分离项目或者单体SpringBoot应用中,基于JWT令牌认证的知识总结,可以有效避免了使用之前较为复杂的Session校验
,同时安全性便捷性都也有一定的提高
,对于每个业务API都有token认证
,可以为我们省去开发中很多问题
,同时后续结合SpringSecurity可以让系统更加的安全可靠,大概就是这么多了,大概一两个小时就可以入门到使用
,也了解其中的工作原理
,这就是后端人员需要掌握的JWT的知识咯
~