文章目录
前文
JWT 请求流程
- 用户使用账号和密码发出 post 请求
- 服务器使用私钥创建一个 jwt
- 服务器返回这个 jwt 给浏览器
- 浏览器将该 jwt 串在请求头中向服务器发送请求
- 服务器验证该 jwt
- 返回响应的资源给浏览器
验证流程:
- ① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
- ② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
- ③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
- ④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密
项目目录结构
com.example.jwtdemo
config
InterceptConfig.java
controller
UserController.java
dao
UserDao.java
entity
User.java
intercepts
JwtIntercept.java
service
UserService.java
impl
UserServiceImpl.java
utils
JwtUtil.java
JwtDemoApplication.java
Maven 依赖
SpringBoot 使用的是 2.3.0 版本
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
application.properties
server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名?useSSL=false
spring.datasource.username=用户名
spring.datasource.password=密码
// 实体类包扫描
mybatis.type-aliases-package=com.example.jwtdemo.entity
mybatis.mapper-locations=classpath:mapper/*.xml
logging.level.com.example.jwtdemo.dao=debug
实体类
package com.example.jwtdemo.entity;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author Woo_home
* @create 2020/9/13 23:09
*/
@Data
@Accessors(chain = true)
public class User {
private String id;
private String username;
private String password;
}
Dao 层
用户登录接口
package com.example.jwtdemo.dao;
import com.example.jwtdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
/**
* @author Woo_home
* @create 2020/9/13 23:12
*/
@Mapper
@Repository
public interface UserDao {
User login(User user);
}
UserDao.xml
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.jwtdemo.dao.UserDao">
<select id="login" parameterType="User" resultType="User">
select * from user where username = #{username} and password = #{password}
</select>
</mapper>
Service 层
跟 UserDao 一样
package com.example.jwtdemo.service;
import com.example.jwtdemo.entity.User;
/**
* @author Woo_home
* @create 2020/9/13 23:16
*/
public interface UserService {
User login(User user);
}
实现类
package com.example.jwtdemo.service.impl;
import com.example.jwtdemo.dao.UserDao;
import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Woo_home
* @create 2020/9/13 23:16
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
// 根据接收用户名和密码查询数据库
User userDB = userDao.login(user);
if (userDB != null) {
return userDB;
}
throw new RuntimeException("认证失败~~");
}
}
JWT 封装类
主要是用于生成 token、验证 token、获取 token 信息
package com.example.jwtdemo.utils;
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.Calendar;
import java.util.Map;
import java.util.UUID;
/**
* @author Woo_home
* @create 2020/9/13 18:48
*/
public class JwtUtil {
/**
* 私钥
*/
private static final String SALT = UUID.randomUUID().toString().replace("-", "");
/**
* 生成 token header.payload.sing
* @param map
* @return
*/
public static String generToken(Map<String, String> map) {
Calendar instance = Calendar.getInstance();
// 默认 7 天过期
instance.add(Calendar.DATE, 7);
// 创建 JWT builder
JWTCreator.Builder builder = JWT.create();
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
// 指定令牌过期时间
String token = builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(SALT));
return token;
}
/**
* 验证 token 合法性
* @param token
*/
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SALT)).build().verify(token);
}
/**
* 获取 token 信息
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SALT)).build().verify(token);
return verify;
}
}
自定义 JWT 拦截器
package com.example.jwtdemo.intercepts;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.jwtdemo.utils.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @author Woo_home
* @create 2020/9/14 14:50
*/
public class JwtIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
Map<String, Object> map = new HashMap<>();
// 获取请求头中的令牌
String token = request.getHeader("token");
try {
// 验证令牌
JwtUtil.verify(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;
}
}
将拦截器注册到 SpringMVC
将我们自定义的拦截器注册到 SpringMVC
package com.example.jwtdemo.config;
import com.example.jwtdemo.intercepts.JwtIntercept;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author Woo_home
* @create 2020/9/14 14:57
*/
@Configuration
public class InterceptConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 将我们自定义的拦截器注册进来
registry.addInterceptor(new JwtIntercept())
// 其它接口均需 token 验证
.addPathPatterns("/user/test")
// 所有用户可访问
.excludePathPatterns("/user/login");
}
}
控制器
package com.example.jwtdemo.controller;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.jwtdemo.entity.User;
import com.example.jwtdemo.service.UserService;
import com.example.jwtdemo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @author Woo_home
* @create 2020/9/13 23:19
*/
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user/login")
public Map<String, Object> login(User user) {
log.info("用户名:[{}]", user.getUsername());
log.info("密码:[{}]", user.getPassword());
Map<String, Object> map = new HashMap<>();
try {
user = userService.login(user);
Map<String, String> payload = new HashMap<>();
payload.put("id", user.getId());
payload.put("username", user.getUsername());
// 生成 JWT 的令牌
String token = JwtUtil.generToken(payload);
map.put("state", true);
map.put("msg", "认证成功");
map.put("token", token);
} catch (Exception e) {
map.put("state", false);
map.put("msg", e.getMessage());
}
return map;
}
@PostMapping("/user/test")
public Map<String, Object> test(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
// 处理自己业务逻辑
String token = request.getHeader("token");
DecodedJWT verify = JwtUtil.verify(token);
log.info("用户 id:[{}]", verify.getClaim("id").asString());
log.info("用户 username:[{}]", verify.getClaim("username").asString());
map.put("state", true);
map.put("msg", "请求成功!");
return map;
}
}
测试
数据库中的数据如下:
我们启动项目,使用 postman 来测试一下,首先先输入一个错的用户名和密码
可以发现,认证失败了
我们再输入正确的用户名和密码
认证成功,并且给我们返回了一个 token
然后我们不带 token 去访问 localhost:8080/user/test
当我们在请求头 Header 加上正确的 token 时,再次验证
此时就能请求成功了
完整代码已上传到 Gitee, 下载地址