Json Web Token(JWT):JSON网络令牌,是为了在网络应用环境间传递声明而制定的一种基于JSON的开放标准((RFC 7519)。JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式用于通信双方之间以 JSON 对象行使安全的传递信息。因为数字签名的存在,这些信息是可信的。
jwt含有三个部分
头部(header)
头部一般有两部分信息:类型、加秘的算法(通常使用HMAC SHA256)
载荷(payload)
该部分一般存放一些有效的信息。jwt的标准定义包含五个字段:
签证(signature)
jwt最后一个部分。该部分是使用了HS256加密后的数据;包含了三个部分:
secret是保存在服务器端(server)的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证。所以,它就是服务端的密钥,z在任何场景都不应该流露出去。一旦客户端得知这个secret,那就有客户端自我签发jwt的安全危险了。在身份鉴定的实现中,传统的方法是在服务端存储一个 session,给客户端返回一个 cookie,而使用JWT之后,当用户使用它的认证信息登录系统之后,会返回给用户一个JWT(token), 用户只需要本地保存该 token(通常使用localStorage,也可以使用cookie)即可。
当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization头部使用 Bearer 模式添加JWT,其内容格式:
Authorization: Bearer <token>
因为用户的状态在服务端内容中是不存储的,所以这是一种无状态的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此,减少了需要查询数据库的需要。
JWT的这些特征使得我们可以完全依赖无状态的特性提供数据API服务。因为JWT并不使用Cookie的,所以你可以在任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)
下面的序列图展示了该过程,流程图如下:

中文流程介绍:
说了这么多JWT到底如何应用到我们的项目中,下面我们就使用SpringBoot 结合 JWT完成用户的登录验证。
初次登录生成jwt流程图

用户访问资源流程图

坏境:
下面通过代码来实现用户认证的功能,博主这里主要采用Spring Boot与JWT整合的方式实现
1、我的pom.xml相关依赖版本代码
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.2.4.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.jinzheyi</groupId>
- <artifactId>jwt</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>war</packaging>
- <name>jwt</name>
- <description>Spring Boot集成jwt</description>
-
- <properties>
- <java.version>1.8</java.version>
- </properties>
-
- <dependencies>
- <!--引入jwt依赖-->
- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt</artifactId>
- <version>0.9.1</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.60</version>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
- <version>3.9</version>
- </dependency>
-
- <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-tomcat</artifactId>
- <scope>provided</scope>
- </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>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
-
- </project>
2、在工程 application.yml 配置文件中添加JWT的配置信息:
- ##jwt配置
- audience:
- #代表这个jwt的接收对象,存入audience
- clientId: 098f6bcd4621d373cade4e832627b4f6
- #密钥,经过Base64加密,可自行替换
- base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
- # JWT的签发主体,存入issuer
- name: restapiuser
- # 过期时间,时间戳
- expiresSecond: 172800
-
- server:
- port: 8081
3、新建配置信息的实体类,以便获取JWT配置:
- package com.jinzheyi.jwt.entity;
-
- import lombok.Data;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import org.springframework.stereotype.Component;
-
- /**
- *@Description 创建的关于yml文件中jwt配置属性的实体类
- *@Author jinzheyi
- *@Date 2020/2/15 22:49
- *@version 0.1
- */
- @Data
- @ConfigurationProperties(prefix = "audience")
- @Component
- public class Audience {
-
- //客户端id
- private String clientId;
- //经过base64加密后密钥
- private String base64Secret;
- //签发主题
- private String name;
- //过期时间
- private int expiresSecond;
- }
JWT验证主要是通过过滤器验证,所以我们需要添加一个拦截器来演请求头中是否包含有后台颁发的 token
4、创建JWT验证拦截器(进行了一层封装):
- package com.jinzheyi.jwt.config;
-
- import com.jinzheyi.jwt.config.interceptor.JwtInterceptor;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.CorsRegistry;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
- /**
- *@Description 全局配置公共类,含拦截器、允许跨域请求处理
- *@Author jinzheyi
- *@Date 2020/2/16 14:01
- *@version 0.1
- */
- @Configuration
- public class WebConfig implements WebMvcConfigurer {
-
- /**
- * 添加拦截器
- * @param interceptorRegistry
- */
- @Override
- public void addInterceptors(InterceptorRegistry interceptorRegistry){
- //拦截路径可自行配置多个 可用,分隔开
- interceptorRegistry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
- }
-
- /**
- * 跨域支持
- * @param corsRegistry
- */
- @Override
- public void addCorsMappings(CorsRegistry corsRegistry){
- corsRegistry.addMapping("/**")
- .allowedOrigins("*")
- .allowCredentials(true)
- .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
- .maxAge(3600 * 24);
- }
- }
- package com.jinzheyi.jwt.config.interceptor;
-
- import com.jinzheyi.jwt.common.Constant;
- import com.jinzheyi.jwt.common.annotation.JwtIgnore;
- import com.jinzheyi.jwt.common.exception.CustomException;
- import com.jinzheyi.jwt.common.response.ResultCode;
- import com.jinzheyi.jwt.entity.Audience;
- import com.jinzheyi.jwt.utils.JwtUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang3.StringUtils;
- import org.springframework.beans.factory.BeanFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.HttpMethod;
- import org.springframework.web.context.support.WebApplicationContextUtils;
- import org.springframework.web.method.HandlerMethod;
- import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
-
- /**
- *@Description 封装好的jwt拦截器处理类
- *@Author jinzheyi
- *@Date 2020/2/16 14:00
- *@version 0.1
- */
- @Slf4j
- public class JwtInterceptor extends HandlerInterceptorAdapter {
-
- @Autowired
- private Audience audience;
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
- //忽略带JwtIgnore注解的请求,不做后续token认证校验
- if (handler instanceof HandlerMethod){
- HandlerMethod handlerMethod = (HandlerMethod) handler;
- JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
- if (jwtIgnore != null) {
- return true;
- }
- }
- if (HttpMethod.OPTIONS.equals(request.getMethod())) {
- response.setStatus(HttpServletResponse.SC_OK);
- return true;
- }
- //获取请求头信息authorization信息
- final String authHeader = request.getHeader(Constant.AUTH_HEADER_KEY);
- log.info("##authHeader = {}",authHeader);
- if (StringUtils.isBlank(authHeader)||!authHeader.startsWith(Constant.TOKEN_PREFIX)) {
- log.info("###用户未登录,请先登录###");
- throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
- }
- //获取token
- final String token = authHeader.substring(7);
- if (audience == null) {
- BeanFactory beanFactory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
- audience = (Audience) beanFactory.getBean("audience");
- }
- JwtUtil.parseJwt(token, audience.getBase64Secret());
- return true;
-
- }
- }
5、然后我们创建JWT工具类:
- package com.jinzheyi.jwt.utils;
-
- import com.jinzheyi.jwt.common.exception.CustomException;
- import com.jinzheyi.jwt.common.response.ResultCode;
- import com.jinzheyi.jwt.entity.Audience;
- import io.jsonwebtoken.*;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import javax.crypto.spec.SecretKeySpec;
- import javax.xml.bind.DatatypeConverter;
- import java.security.Key;
- import java.util.Date;
-
- /**
- *@Description jwt工具类
- *@Author jinzheyi
- *@Date 2020/2/16 14:03
- *@version 0.1
- */
- public class JwtUtil {
-
- //创建日志对象
- private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);
-
- /**
- * 解析 jwt
- * @param jsonWebToken token
- * @param base64Security
- * @return
- */
- public static Claims parseJwt(String jsonWebToken, String base64Security){
- try {
- Claims claims = Jwts.parser()
- .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
- .parseClaimsJws(jsonWebToken).getBody();
- return claims;
- } catch (ExpiredJwtException e){
- logger.error("===token过期===");
- throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED_EXCEPTION);
- } catch (MalformedJwtException e){
- logger.error("====json web token格式错误====");
- throw new CustomException(ResultCode.PERMISSION_TOKEN_MALFORMEDJWT_EXCEPTION);
- }catch (Exception e){
- logger.error("=====token解析异常=====");
- throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
- }
- }
-
- /**
- * 构建jwt
- * @param userId 用户id
- * @param username jwt的所有者
- * @param role 权限
- * @param audience 配置信息对象
- * @return
- */
- public static String createJwt(String userId, String username, String role, Audience audience){
- try {//使用HS256加密算法
- SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
- Long nowMillis = System.currentTimeMillis();
- Date now = new Date(nowMillis);
- //生成签名
- byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
- Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
- //userId是重要的信息,进行简要加密下
- String encryId = Base64Util.encode(userId);
- //添加构成JWT的参数
- JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("type", "JWT")
- //可以将基本不重要的对象信息放到claims
- .claim("role", role)
- .claim("userId", encryId)
- .setSubject(username) //代表这个jwt的主体,即它的所有人
- .setIssuer(audience.getClientId()) //代表这个jwt的签发主体
- .setIssuedAt(new Date()) //一个时间戳,代表签发时间
- .setAudience(audience.getName()) //代表jwt的接收对象
- .signWith(signatureAlgorithm, signingKey);
- //添加token过期时间
- int expiresMillis = audience.getExpiresSecond();
- if (expiresMillis >= 0) {
- long expMillis = nowMillis + expiresMillis;
- Date expireTime = new Date(expMillis);
- jwtBuilder.setExpiration(expireTime) //是一个时间戳,代表jwt的过期时间
- .setNotBefore(now); //是一个时间戳,代表这个jwt生效的开始时间,意味在这个时间之前验证jwt都是无效的
- }
- return jwtBuilder.compact();
- } catch (Exception e){
- logger.error("生成签名失败");
- throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
- }
- }
-
- /**
- * 获取用户
- * @param token
- * @param base64Security
- * @return
- */
- public static String getUsername(String token, String base64Security){
- return parseJwt(token, base64Security).getSubject();
- }
-
- /**
- * 获取用户id
- * @param token
- * @param base64Security
- * @return
- */
- public static String getUserId(String token, String base64Security){
- return Base64Util.decode(parseJwt(token, base64Security).get("userId",String.class));
- }
-
- /**
- * token是否过期
- * @param token
- * @param base64Security
- * @return
- */
- public static boolean isExpiration(String token, String base64Security){
- return parseJwt(token, base64Security).getExpiration().before(new Date());
- }
- }
其中创建jwt工具类的时候,内部使用到的一些封装方法,可以到我的gitee分享的代码里面进行查看。
6、添加全局异常处理
- package com.jinzheyi.jwt.common.exception;
-
- import com.jinzheyi.jwt.common.response.ResultCode;
- import com.jinzheyi.jwt.common.response.ResultData;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.validation.BindException;
- import org.springframework.validation.BindingResult;
- import org.springframework.validation.FieldError;
- import org.springframework.validation.ObjectError;
- import org.springframework.web.bind.MethodArgumentNotValidException;
- import org.springframework.web.bind.annotation.ExceptionHandler;
- import org.springframework.web.bind.annotation.RestControllerAdvice;
- import java.util.List;
-
- /**
- *@Description 定义全局异常处理类
- *@Author jinzheyi
- *@Date 2020/2/16 13:58
- *@version 0.1
- */
- @RestControllerAdvice
- public class GlobalExceptionHandler {
-
- public static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
-
- /**
- * 处理自定义异常
- * @param e
- * @return
- */
- @ExceptionHandler(CustomException.class)
- public ResultData handleException(CustomException e){
- // 打印异常信息
- logger.error("### 异常信息:{} ###", e.getMessage());
- return new ResultData(e.getResultCode());
- }
-
- /**
- * 参数错误异常
- * @param e
- * @return
- */
- @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
- public ResultData handleException(Exception e) {
- if (e instanceof MethodArgumentNotValidException) {
- MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
- BindingResult result = validException.getBindingResult();
- StringBuffer errorMsg = new StringBuffer();
- if (result.hasErrors()) {
- List<ObjectError> errors = result.getAllErrors();
- errors.forEach(p ->{
- FieldError fieldError = (FieldError) p;
- errorMsg.append(fieldError.getDefaultMessage()).append(",");
- logger.error("### 请求参数错误:{"+fieldError.getObjectName()+"},field{"+fieldError.getField()+ "},errorMessage{"+fieldError.getDefaultMessage()+"}");
- });
- }
- } else if (e instanceof BindException) {
- BindException bindException = (BindException)e;
- if (bindException.hasErrors()) {
- logger.error("### 请求参数错误: {}", bindException.getAllErrors());
- }
- }
- return new ResultData(ResultCode.PARAM_IS_INVALID);
- }
-
- /**
- * 处理所有不可知的异常
- * @param e
- * @return
- */
- @ExceptionHandler(Exception.class)
- public ResultData handleOtherException(Exception e){
- //打印异常堆栈信息
- e.printStackTrace();
- // 打印异常信息
- logger.error("### 不可知的异常:{} ###", e.getMessage());
- return new ResultData(ResultCode.SYSTEM_INNER_ERROR);
- }
-
- }
7、最后添加用户Controller进行测试
- package com.jinzheyi.jwt.web.controller;
-
- import com.alibaba.fastjson.JSONObject;
- import com.jinzheyi.jwt.common.Constant;
- import com.jinzheyi.jwt.common.annotation.JwtIgnore;
- import com.jinzheyi.jwt.common.response.ResultData;
- import com.jinzheyi.jwt.entity.Audience;
- import com.jinzheyi.jwt.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.HttpServletResponse;
- import java.util.UUID;
-
- /**
- *@Description 用户业务控制层
- *@Author jinzheyi
- *@Date 2020/2/16 14:03
- *@version 0.1
- */
- @Slf4j
- @RestController
- public class UserController {
-
- @Autowired
- private Audience audience;
-
- @PostMapping("/login")
- @JwtIgnore
- public ResultData login(HttpServletResponse response, String username, String password){
- //这里模拟测试,默认登录成功,返回用户id和角色信息
- String userId = UUID.randomUUID().toString();
- String role = "admin";
- //创建token
- String token = JwtUtil.createJwt(userId, username, role, audience);
- log.info("### 登录成功, token={} ###", token);
- response.setHeader(Constant.AUTH_HEADER_KEY, Constant.TOKEN_PREFIX + token);
- //将token响应给客户端
- JSONObject result = new JSONObject();
- result.put("token", token);
- return ResultData.successResultData(result);
- }
-
- @GetMapping("/users")
- public ResultData userList() {
- log.info("### 查询所有用户列表 ###");
- return ResultData.successResult();
- }
-
- }
8、接下来我们使用PostMan工具进行测试:
没有登录时候直接访问:http://localhost:8081/users 接口:

然后我们去执行登录接口:http://localhost:8081/login?username=zhangsan接口

此时我们获取到了后台服务器生成的token。将token带入到获取用户的接口中,如图

注意:这里选择 Bearer Token类型,就把不要在 Token中手动Bearer,postman会自动拼接。补充:如果带入错误的token会提示token已失效,或者超时之后带入token,会提示已超时。拦截器里面已经做了判断。登录接口不用验证token是因为我们在controller层login方法加了@JwtIgnore注解。