SpringBoot2.x简单使用JWT
什么是JWT
JSON Web Token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于作为JSON对象在各方之间安全地传输信息。
关于JWT的更多的介绍可以参考JWT的官网:https://jwt.io/
基于SpringBoot 2.x来使用JWT
Java使用jwt可以参考GitHub上官方的介绍:https://github.com/jwtk/jjwt
- 创建一个SpringBoot工程
相关目录结构如下图所示 - 添加相关依赖及其版本
该项目用到的依赖如下图所示:
pom.xml文件中依赖部分代码为:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
说明:
(1)lombok是用于减少显示地编写实体类地getter/setter、构造器等方法的工具,除了要引入依赖,所用的IDE也需要安装相关插件,安装Lombok插件可参考–网址–。
(2)java-jwt、jjwt是使用jwt的相关依赖,我用的jjwt是0.9.0版本的,目前最新的是0.10.5版本,有些方法有所变动。(若使用了最新版本,最好是参考GitHub上的官方教程)
(3)fastjson是阿里开源的处理json数据于Java对象之间转换的Java库。
- springboot使用mybatis
数据库中数据
(1) 创建User实体类
package com.springboot.jwtdemo2.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Description: 用户实体类,使用Lombok相关注解来实现构造器和getter、setter
* @Author 傅琦
* @Date 2019/4/12 21:30
* @Version V1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
/**
* 用户id
*/
private int userId;
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
}
(2) 编写持久层接口UserMapper.java
package com.springboot.jwtdemo2.mapper;
import com.springboot.jwtdemo2.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @Description: 用户数据持久层接口,其实现为userMapper.xml文件
* @Author 傅琦
* @Date 2019/4/12 21:39
* @Version V1.0
*/
@Mapper
public interface UserMapper {
/**
* 根据用户姓名进行查找
* @param username
* @return User
*/
User findByUsername(@Param("username") String username);
/**
* 根据用户id进行查找
* @param userId
* @return User
*/
User findById(@Param("userId") int userId);
}
(3) 编写mybatis的SQL映射文件
<?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.springboot.jwtdemo2.mapper.UserMapper">
<select id="findByUsername" resultType="com.springboot.jwtdemo2.pojo.User">
SELECT
id AS "userId",
user_name AS "username",
password AS "password"
FROM jwt_user
WHERE
user_name = #{username}
LIMIT 1
</select>
<select id="findById" resultType="com.springboot.jwtdemo2.pojo.User">
SELECT
id AS "userId",
user_name AS "username",
password AS "password"
FROM jwt_user
WHERE
id = #{userId}
LIMIT 1
</select>
</mapper>
(4) 编写项目配置文件中的相关配置,并对UserMapper进行单元测试
application.yml
# 配置提供服务的端口
server:
port: 9000
# 数据库的相关配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://172.16.9.96:3307/springboot?useSSL=false&useUnicode=true&characterEncoding=utf8
username: root
password: 123456
# mybatis的相关配置
mybatis:
mapper-locations: classpath:mapper/*.xml
现在就可对UserMapper进行单元测试
package com.springboot.jwtdemo2.mapper;
import com.springboot.jwtdemo2.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
/**
* @Description: UserMapper的测试类
* @Author 傅琦
* @Date 2019/4/12 22:00
* @Version V1.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testMapper(){
assert userMapper != null;
}
@Test
public void findByUsername() {
String name = "李雷";
User user = userMapper.findByUsername(name);
if (user == null){
System.out.println("该用户不存在");
}else {
System.out.println("the user is: " + user.getUsername());
}
}
@Test
public void findById() {
User user = userMapper.findById(2);
if (user == null){
System.out.println("该用户不存在");
}else {
System.out.println("the user is: " + user.getUsername());
}
}
}
单元测试完全通过,说明UserMapper没有问题,可再往上对service层进行开发。
(5) 编写UserResult.java和UserService.java、UserServiceImpl.java,在service层对查询数据进行封装,并对UserService进行单元测试
这里在service层对Mapper层的返回结果进行封装,需要一个结果封装类,所以先白那些UserResult.java
package com.springboot.jwtdemo2.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Description: 用户查询结果的封装
* @Author 傅琦
* @Date 2019/4/12 22:56
* @Version V1.0
*/
@AllArgsConstructor
@Data
@NoArgsConstructor
public class UserResult<T> {
private String state;
private T data;
}
再编写UserService.java
package com.springboot.jwtdemo2.service;
import com.springboot.jwtdemo2.pojo.User;
import com.springboot.jwtdemo2.dto.UserResult;
/**
* @Description: 用户服务层接口
* @Author 傅琦
* @Date 2019/4/12 22:46
* @Version V1.0
*/
public interface UserService {
/**
* 根据用户名查询用户
* @param username
* @return User
*/
UserResult<User> findByUsername(String username);
/**
* 根据用户id查询用户
* @param userId
* @return User
*/
UserResult<User> findById(int userId);
}
以及UserService.java的实现类UserServiceImpl.java
package com.springboot.jwtdemo2.service.serviceimpl;
import com.springboot.jwtdemo2.mapper.UserMapper;
import com.springboot.jwtdemo2.pojo.User;
import com.springboot.jwtdemo2.service.UserService;
import com.springboot.jwtdemo2.dto.UserResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Description: 用户服务层接口的实现
* @Author 傅琦
* @Date 2019/4/12 22:51
* @Version V1.0
*/
@Service("UserService")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserResult<User> findByUsername(String username) {
User user = userMapper.findByUsername(username);
if (user == null){
return new UserResult<>("fail", null);
}else {
return new UserResult<>("success", user);
}
}
@Override
public UserResult<User> findById(int userId) {
User user = userMapper.findById(userId);
if (user == null){
return new UserResult<>("fail", null);
}else {
return new UserResult<>("success", user);
}
}
}
由于这里逻辑比较简单,可以不用进行单元测试,如果嫌麻烦的话,可以跳过UserService的单元测试。
- JwtUtil.java的编写及说明
在编写了对用户的查询操作后,可开始JWT相关操作的开发,编写JwtUtil.java
package com.springboot.jwtdemo2.utils;
import com.springboot.jwtdemo2.commons.Constant;
import com.springboot.jwtdemo2.pojo.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @Description: JWT工具类
* @Author 傅琦
* @Date 2019/4/13 9:29
* @Version V1.0
*/
public class JwtUtil {
/**
* 生成**
* @return SecretKey
*/
private static SecretKey generalKey(){
String stringKey = Constant.JWT_SECRET;
byte[] encodedKey = Base64.decodeBase64(stringKey);
SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return secretKey;
// SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// return key;
}
/**
* 根据用户信息为其签发tocken
* @param user
* @return String
*/
public static String generalTocken(User user){
try {
// 设置签发算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成**
SecretKey key = generalKey();
// System.out.println("签发时生成的key为:" + key);
// 设置私有声明
Map<String, Object> claims = new HashMap<>(16);
claims.put("userId", user.getUserId());
claims.put("username", user.getUsername());
// 记录生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date nowTime = new Date(nowMillis);
// 设置过期时间
long expMillis = nowMillis + Constant.EXP_TIME_LENGTH;
Date expTime = new Date(expMillis);
// 创建tocken构建器实例
JwtBuilder jwtBuilder = Jwts.builder()
// 设置自己的私有声明
.setClaims(claims)
// 设置该tocken的Id,用于防止tocken重复
.setId(UUID.randomUUID().toString())
// 设置签发者
.setIssuer("FUQI-PC")
// 设置签发时间
.setIssuedAt(nowTime)
// 设置过期时间
.setExpiration(expTime)
// 设置tocken的签发对象
.setSubject("users")
// 设置签发算法和**
.signWith(signatureAlgorithm, key);
return jwtBuilder.compact();
} catch (Exception e) {
e.printStackTrace();
return "生成tocken失败";
}
}
/**
* 解析tocken,从中提取出声明信息
* @param tocken
* @return Claims
* @throws Exception
*/
public static Claims parseTocken(String tocken) throws Exception{
SecretKey key = generalKey();
// System.out.println("解析tocken时生成的key为:" + key);
// 获取tocken中的声明部分
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(tocken).getBody();
return claims;
}
}
由于我对**、证书等概念还不熟,所以签发tocken使用的**是用方法生成的。
- 控制器UserController.java的编写
package com.springboot.jwtdemo2.controller;
import com.alibaba.fastjson.JSONObject;
import com.springboot.jwtdemo2.dto.UserResult;
import com.springboot.jwtdemo2.pojo.User;
import com.springboot.jwtdemo2.service.UserService;
import com.springboot.jwtdemo2.utils.JwtUtil;
import com.springboot.jwtdemo2.vo.UserViewObject;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @Description: 用户控制器
* @Author 傅琦
* @Date 2019/4/13 15:21
* @Version V1.0
*/
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public UserViewObject<Object> userLongin(@RequestBody User user){
UserViewObject<Object> result = new UserViewObject<>();
UserResult<User> userResult = userService.findByUsername(user.getUsername());
if ("fail".equals(userResult.getState())){
result.setState("fail");
result.setData("用户不存在,请先注册");
}else {
User user1 = userResult.getData();
if (!user.getPassword().equals(user1.getPassword())){
result.setState("fail");
result.setData("密码错误,请重新输入");
}else {
String tocken = JwtUtil.generalTocken(user1);
JSONObject jsonObject = new JSONObject();
jsonObject.put("user", user1);
jsonObject.put("tocken", tocken);
result.setState("sucees");
result.setData(jsonObject);
}
}
return result;
}
@PostMapping("/whoami")
public String getMessage(HttpServletRequest httpServletRequest) {
try {
String tocken = httpServletRequest.getHeader("tocken");
Claims claims = JwtUtil.parseTocken(tocken);
String username = claims.get("username", String.class);
System.out.println("执行处理器中的控制器中的对应方法中的方法体。");
return username + ",你已通过验证";
} catch (Exception e) {
e.printStackTrace();
return "处理tocken出现错误。";
}
}
}
- 拦截器AuthenticationInterceptor.java的编写
package com.springboot.jwtdemo2.interceptor;
import com.springboot.jwtdemo2.dto.UserResult;
import com.springboot.jwtdemo2.pojo.User;
import com.springboot.jwtdemo2.service.UserService;
import com.springboot.jwtdemo2.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description: 自定义的拦截器,用于对tocken以及其中所包含的信息进行校验
* @Author 傅琦
* @Date 2019/4/13 19:56
* @Version V1.0
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法,则直接通过
if (!(handler instanceof HandlerMethod)){
return true;
}
// 从请求头中获取tocken
String tocken = request.getHeader("tocken");
// System.out.println(tocken);
// 当没有获取到tocken时的处理
if (tocken == null){
response.sendError(403, "无tocken,请先登录");
return false;
}else {
try {
// tocken还未过期时的处理
// 获取声明部分
Claims claims = JwtUtil.parseTocken(tocken);
String username = claims.get("username", String.class);
// userService导入存在问题时的处理
if (userService == null){
System.out.println("无法导入userService。");
response.sendError(500, "服务器发生错误");
return false;
}
// 查询用户信息
UserResult<User> userResult = userService.findByUsername(username);
if ("success".equals(userResult.getState())){
return true;
}else {
response.sendError(401, "用户不存在,请先注册");
return false;
}
}catch (ExpiredJwtException exp){
// tocken过期时的处理
response.sendError(40102, "tocken过期,请重新登录");
return false;
}
}
// return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("处理器完成后的方法,此时说明控制器已经执行完毕。");
}
}
拦截器将请求拦截下来,并取出tocken进行解析,从而判断是否放行该请求。
实现拦截器就需要实现HandlerInterceptor
接口,该接口是Java 8 的接口,所以其中3个方法都被声明为default,并且提供了空实现。当我们需要自己编写拦截判断逻辑时,只需实现HandlerInterceptor,覆盖对应的方法即可。
其中比较重要的是preHandle方法。preHandle方法是在处理器(包含了控制器的功能)执行前执行的方法,返回值是bool值。若返回true,则放行请求,由处理器对请求进行处理;若返回false,则拦住该请求,结束所有流程。
- 注册拦截器
package com.springboot.jwtdemo2.config;
import com.springboot.jwtdemo2.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description: 注册验证tocken的拦截器
* @Author 傅琦
* @Date 2019/4/13 20:41
* @Version V1.0
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/user/whoami");
}
}
InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。
在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内。
- 使用postman进行测试
经过前面几个步骤之后,一个简单的基于springboot 2.x来使用JWT的工程就已经完成了。接下来就是运行起该工程,使用postman对其进行测试。
(1)先测试没有tocken请求/user/whoami
不带tocken发起请求的结果为:
由于我对统一异常处理还不太熟悉,所以只是在拦截器的preHandle方法中对response进行了简单的信息填写。但是从message的值可以看出,拦截器发挥了作用。
(2)再测试登录接口:/user/login,获取tocken
登录请求的结果截图
可以看到返回的数据中有最新的tocken,说明JWT工作也成功了。
(3)再测试使用正确且未过期的tocken请求/user/whoami接口
将登录后返回的额tocken复制过来放在/user/whoami请求的header中,并且设置其键名为tocken(也可命名为别的名字,但是拦截器中获取tocken所用的getHeader()方法中的参数值要保证与这里设定的键名一致)。请求的返回结果截图:
说明tocken验证成功。
(4)最后测试使用过期的tocken请求/user/whoami接口
这里使用过期的tocken发起请求
其返回结果为 - 遇到的问题及解决办法
(1)由于我在配置注册自定义的拦截器时 ,一开始偷懒,这样注册:
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(new AuthenticationInterceptor())
.addPathPatterns("/user/whoami");
}
}
这就导致了自定义的拦截器中UserService一致导入不进来,百度了一下发现是由于不用@bean注解,是的拦截器没有添加进Spring的上下文之间,才会导致在拦截器中注入service时,报空指针异常。解决方法就是使用@bean注解提前将拦截器注册到Spring上下文中。
解决方法参考自:拦截器中无法注入service
-
可再进一步的地方
(1)使用非对称**来签发tocken
(2)还可结合shiro和redis使用
(3)用https携带tocken
(4)tocken加密加言(盐),具体哪个yan,我目前还不清楚。 -
项目源码
GitHub:https://github.com/fanfanfufu/JWTlearning
码云Gitee:https://gitee.com/fanfanfufu/JWTlearning