JWT(json web tokens)是目前比较流行的跨域认证解决方案;说通俗点就是比较流行的token生成和校验的方案。碰巧公司有个app的项目的token采用了jwt方案,因此记录下后端项目集成jwt的过程,方便后续查阅。

一、jwt的简单介绍

    jwt生成的token是一种无状态的token,服务端不需要对该token进行保存;它一般由客户端保存。客户端访问请求服务时,服务端对token进行校验,然后进行各种控制。下面直接拿一个生成好的token来讲解
        
    通过上图我们可以发现jwt生成的token是非常长的字符串,并且字符串中有2个小点("."),通过这2个小点我们可以把这token分成3部分。
      • header:头部,是用来描述这个token是什么类型,采用了何种加密算法;token中header是经过base64编码的
      • payload:荷载,用来存放需要传递的数据。官方提供的几个标准字段,同时也可以自己往里面加自定义的字段和内容,用来存放一些不敏感的用户信息。可以简单的把它想像成一个Map集合;token中payload也是经过base64编码的
      • signature:签名,主要是将header和payload的base64编码后内容用点拼接在一起然后进行加密生成签名。服务端需要利用这签名来校验token是否被篡改(验签)
    所以通俗的来讲,token = base64(header) + "." + base64(payload) + "." + 签名
    网上很多博文对jwt的介绍都比较详细,因此本文就不再详细的介绍jwt相关细节,重点放在java代码该怎么写。jwt相关详细介绍可以参考如下链接:
    下面直接上代码

二、Maven依赖版本说明

    pom中部分重要jar包依赖版本如下:
    <!-- SpringBoot 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent> <!--jwt 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
14
 
1
    <!-- SpringBoot 版本 -->
2
    <parent>
3
        <groupId>org.springframework.boot</groupId>
4
        <artifactId>spring-boot-starter-parent</artifactId>
5
        <version>2.1.4.RELEASE</version>
6
        <relativePath/> <!-- lookup parent from repository -->
7
    </parent>
8

9
        <!--jwt 依赖-->
10
        <dependency>
11
            <groupId>io.jsonwebtoken</groupId>
12
            <artifactId>jjwt</artifactId>
13
            <version>0.9.1</version>
14
        </dependency>

三、token生成和解析工具类及token认证拦截器的编写

(1)token生成和解析工具类编写

    该工具类需要具有如下功能
      • 生成jwt标准的token;生成token时支持把不敏感的用户信息放在token里面,后续解析token后可以直接使用这些用户信息
      • 解析token,校验token是否过期和篡改
        直接看下面代码
package com.psx.gqxy.web.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; /**
* JwtUtils
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @version 1.0
*/
@Slf4j
public final class JwtUtils { /** 存放token的请求头对应的key的名字 */
private static String headerKey = "token";
/** 加密的secret */
private static String secret = "zxyTestSecret";
/** 过期时间,单位为秒 */
private static long expire = 1800L; static {
// TODO 上面变量的值应该从配置文件中读取,方便测试这里就不从配置文件中读取
// 利用配置文件中的值覆盖静态变量初始化的值
} /**
* 生成jwt token
*/
public static String generateToken(Map<String, Object> userInfoMap) {
if (Objects.isNull(userInfoMap)) {
userInfoMap = new HashMap<>();
}
// 过期时间
Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT") // 设置头部信息
.setClaims(userInfoMap) // 装入自定义的用户信息
.setExpiration(expireDate) // token过期时间
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
} /**
* 校验token并解析token
* @param token
* @return Claims:它继承了Map,而且里面存放了生成token时放入的用户信息
*/
public static Claims verifyAndGetClaimsByToken(String token) {
try {
/* 如果过期或者是被篡改,则会抛异常.
注意点:只有在生成token设置了过期时间(setExpiration(expireDate))才会校验是否过期,
可以参考源码io.jsonwebtoken.impl.DefaultJwtParser的299行。
拓展:利用不设置过期时间就不校验token是否过期的这一特性,我们不设置Expiration;
而采用自定义的字段来存放过期时间放在Claims(可以简单的理解为map)中;
通过token获取到Claims后自己写代码校验是否过期。
通过这思路,可以去实现对过期token的手动刷新
*/
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
return null;
}
} public static String getHeaderKey() {
return headerKey;
} }
83
 
1
package com.psx.gqxy.web.jwt;
2
import io.jsonwebtoken.Claims;
3
import io.jsonwebtoken.Jwts;
4
import io.jsonwebtoken.SignatureAlgorithm;
5
import lombok.extern.slf4j.Slf4j;
6
import org.apache.commons.lang3.exception.ExceptionUtils;
7
import java.util.Date;
8
import java.util.HashMap;
9
import java.util.Map;
10
import java.util.Objects;
11

12
/**
13
 * JwtUtils
14
 * @author ZENG.XIAO.YAN
15
 * @blog https://www.cnblogs.com/zeng1994/
16
 * @version 1.0
17
 */
18
@Slf4j
19
public final class JwtUtils {
20

21
    /** 存放token的请求头对应的key的名字 */
22
    private static String headerKey = "token";
23
    /** 加密的secret */
24
    private static String secret = "zxyTestSecret";
25
    /** 过期时间,单位为秒 */
26
    private static long expire = 1800L;
27

28
    static {
29
        // TODO 上面变量的值应该从配置文件中读取,方便测试这里就不从配置文件中读取
30
        // 利用配置文件中的值覆盖静态变量初始化的值
31
    }
32

33

34
    /**
35
     * 生成jwt token
36
     */
37
    public static String generateToken(Map<String, Object> userInfoMap) {
38
        if (Objects.isNull(userInfoMap)) {
39
            userInfoMap = new HashMap<>();
40
        }
41
        //  过期时间
42
        Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
43
        return Jwts.builder()
44
                .setHeaderParam("typ", "JWT")   // 设置头部信息
45
                .setClaims(userInfoMap)               // 装入自定义的用户信息
46
                .setExpiration(expireDate)            // token过期时间
47
                .signWith(SignatureAlgorithm.HS512, secret)
48
                .compact();
49
    }
50

51
    /**
52
     * 校验token并解析token
53
     * @param token
54
     * @return Claims:它继承了Map,而且里面存放了生成token时放入的用户信息
55
     */
56
    public static Claims verifyAndGetClaimsByToken(String token) {
57
        try {
58
            /* 如果过期或者是被篡改,则会抛异常.
59
                注意点:只有在生成token设置了过期时间(setExpiration(expireDate))才会校验是否过期,
60
                可以参考源码io.jsonwebtoken.impl.DefaultJwtParser的299行。
61
                拓展:利用不设置过期时间就不校验token是否过期的这一特性,我们不设置Expiration;
62
                      而采用自定义的字段来存放过期时间放在Claims(可以简单的理解为map)中;
63
                      通过token获取到Claims后自己写代码校验是否过期。
64
                      通过这思路,可以去实现对过期token的手动刷新
65
            */
66
            return Jwts.parser()
67
                    .setSigningKey(secret)
68
                    .parseClaimsJws(token)
69
                    .getBody();
70
        }catch (Exception e){
71
            log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
72
            return null;
73
        }
74
    }
75

76
    public static String getHeaderKey() {
77
        return headerKey;
78
    }
79

80

81
}
82

83

(2)token身份认证拦截器的编写

    拦截器主要作用如下:
      • 1)拦截器拦截到请求后,拿请求头中的token,如果不存在只直接response输出token不能为空
      • 2)拿到token后,进行token的解析,校验是否篡改或者过期。如果被篡改或者过期只直接response输出token已失效
      • 3)如果校验都通过了,则把token中解析出的用户信息放在request请求域中,方便后续Controller方法取用户信息
        直接看参考下面代码
package com.psx.gqxy.web.jwt;
import com.alibaba.fastjson.JSON;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; /**
* jwtToken校验拦截器
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @version 1.0
*/
public class JwtInterceptor extends HandlerInterceptorAdapter { public static final String USER_INFO_KEY = "user_info_key"; @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取用户 token
String token = request.getHeader(JwtUtils.getHeaderKey());
if (StringUtils.isBlank(token)) {
token = request.getParameter(JwtUtils.getHeaderKey());
}
// token为空
if(StringUtils.isBlank(token)) {
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
JwtUtils.getHeaderKey() + " can not be blank",
response);
return false;
}
// 校验并解析token,如果token过期或者篡改,则会返回null
Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
if(null == claims){
this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
JwtUtils.getHeaderKey() + "失效,请重新登录",
response);
return false;
}
// 校验通过后,设置用户信息到request里,在Controller中从Request域中获取用户信息
request.setAttribute(USER_INFO_KEY, claims);
return true;
} /**
* 利用response直接输出错误信息
* @param code
* @param msg
* @param response
* @throws IOException
*/
private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
ModelResult<Void> result = new ModelResult<>();
result.setCode(code);
result.setMsg(msg);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(result));
} }
65
 
1
package com.psx.gqxy.web.jwt;
2
import com.alibaba.fastjson.JSON;
3
import com.psx.gqxy.common.base.CommonConstant;
4
import com.psx.gqxy.common.base.ModelResult;
5
import io.jsonwebtoken.Claims;
6
import org.apache.commons.lang3.StringUtils;
7
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
8
import javax.servlet.http.HttpServletRequest;
9
import javax.servlet.http.HttpServletResponse;
10
import java.io.IOException;
11

12
/**
13
 * jwtToken校验拦截器
14
 * @author ZENG.XIAO.YAN
15
 * @blog https://www.cnblogs.com/zeng1994/
16
 * @version 1.0
17
 */
18
public class JwtInterceptor extends HandlerInterceptorAdapter {
19

20
    public static final String USER_INFO_KEY = "user_info_key";
21

22
    @Override
23
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
24
        //  获取用户 token
25
        String token = request.getHeader(JwtUtils.getHeaderKey());
26
        if (StringUtils.isBlank(token)) {
27
            token = request.getParameter(JwtUtils.getHeaderKey());
28
        }
29
        //  token为空
30
        if(StringUtils.isBlank(token)) {
31
            this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
32
                    JwtUtils.getHeaderKey() + " can not be blank",
33
                    response);
34
            return false;
35
        }
36
        //  校验并解析token,如果token过期或者篡改,则会返回null
37
        Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
38
        if(null == claims){
39
            this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
40
                    JwtUtils.getHeaderKey() + "失效,请重新登录",
41
                    response);
42
            return false;
43
        }
44
        //  校验通过后,设置用户信息到request里,在Controller中从Request域中获取用户信息
45
        request.setAttribute(USER_INFO_KEY, claims);
46
        return true;
47
    }
48

49
    /**
50
     * 利用response直接输出错误信息
51
     * @param code
52
     * @param msg
53
     * @param response
54
     * @throws IOException
55
     */
56
    private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
57
        ModelResult<Void> result = new ModelResult<>();
58
        result.setCode(code);
59
        result.setMsg(msg);
60
        response.setContentType("application/json;charset=UTF-8");
61
        response.getWriter().write(JSON.toJSONString(result));
62
    }
63

64
}
65

四、拦截器的配置和功能测试

(1)编写一个Controller

    写一个Controller,里面包含一登录方法和一个test方法
      • 登录方法用来实现登录,登录成功后返回token
      • test方法,主要通过拦截器拦截该方法的请求,当用户带有效的token访问时才允许访问该方法
        代码如下:
package com.psx.gqxy.web.controller;
import com.psx.gqxy.common.base.CommonConstant;
import com.psx.gqxy.common.base.ModelResult;
import com.psx.gqxy.domain.dto.UserLoginDTO;
import com.psx.gqxy.web.jwt.JwtInterceptor;
import com.psx.gqxy.web.jwt.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map; /**
* TestJwtController
* @author ZENG.XIAO.YAN
* @blog https://www.cnblogs.com/zeng1994/
* @Date 2019-07-14
* @version 1.0
*/ @RestController
@RequestMapping("jwt")
public class TestJwtController { @PostMapping("login")
public ModelResult<String> login(@RequestBody UserLoginDTO dto) {
ModelResult<String> result = new ModelResult<>();
// 这里登录就简单的模拟下
if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
Map<String, Object> userInfoMap = new HashMap<>();
userInfoMap.put("userName", "隔壁老王");
String token = JwtUtils.generateToken(userInfoMap);
result.setData(token);
} else {
result.setCode(CommonConstant.FAIL);
result.setMsg("用户名或密码错误");
}
return result;
} @GetMapping("test")
public String test(HttpServletRequest request) {
// 登录成功后,从request中获取用户信息
Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
if (null != claims) {
return (String) claims.get("userName");
} else {
return "fail";
}
} }
53
 
1
package com.psx.gqxy.web.controller;
2
import com.psx.gqxy.common.base.CommonConstant;
3
import com.psx.gqxy.common.base.ModelResult;
4
import com.psx.gqxy.domain.dto.UserLoginDTO;
5
import com.psx.gqxy.web.jwt.JwtInterceptor;
6
import com.psx.gqxy.web.jwt.JwtUtils;
7
import io.jsonwebtoken.Claims;
8
import org.springframework.web.bind.annotation.*;
9
import javax.servlet.http.HttpServletRequest;
10
import java.util.HashMap;
11
import java.util.Map;
12

13
/**
14
 * TestJwtController
15
 * @author ZENG.XIAO.YAN
16
 * @blog https://www.cnblogs.com/zeng1994/
17
 * @Date 2019-07-14
18
 * @version 1.0
19
 */
20

21
@RestController
22
@RequestMapping("jwt")
23
public class TestJwtController {
24

25
    @PostMapping("login")
26
    public ModelResult<String> login(@RequestBody UserLoginDTO dto) {
27
        ModelResult<String> result = new ModelResult<>();
28
        // 这里登录就简单的模拟下
29
        if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
30
            Map<String, Object> userInfoMap = new HashMap<>();
31
            userInfoMap.put("userName", "隔壁老王");
32
            String token = JwtUtils.generateToken(userInfoMap);
33
            result.setData(token);
34
        } else {
35
            result.setCode(CommonConstant.FAIL);
36
            result.setMsg("用户名或密码错误");
37
        }
38
        return result;
39
    }
40

41
    @GetMapping("test")
42
    public String test(HttpServletRequest request) {
43
        // 登录成功后,从request中获取用户信息
44
        Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
45
        if (null != claims) {
46
            return (String) claims.get("userName");
47
        } else {
48
            return "fail";
49
        }
50
    }
51

52
}
53

(2)拦截器的配置

    拦截器拦截需要身份认证的请求,同时放行登录接口
    代码如下:
/**
* web相关的定制化配置
* @author ZENG.XIAO.YAN
* @version 1.0
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
// WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现WebMvcConfigurer 这个接口 @Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
} @Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
// 配置拦截器的拦截规则和放行规则
jwtInterceptorRegistration.addPathPatterns("/jwt/**")
.excludePathPatterns("/jwt/login");
}
}
x
 
1
/**
2
 * web相关的定制化配置
3
 * @author ZENG.XIAO.YAN
4
 * @version 1.0
5
 */
6
@Configuration
7
public class WebConfig implements WebMvcConfigurer {
8
    // WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现WebMvcConfigurer 这个接口
9

10
    @Bean
11
    public JwtInterceptor jwtInterceptor() {
12
        return new JwtInterceptor();
13
    }
14

15
    @Override
16
    public void addInterceptors(InterceptorRegistry registry) {
17
        InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
18
        // 配置拦截器的拦截规则和放行规则
19
        jwtInterceptorRegistration.addPathPatterns("/jwt/**")
20
                .excludePathPatterns("/jwt/login");
21
    }
22
}    

(3)相关测试

    • 不带token访问 /jwt/test接口,被拦截器拦截;返回token不能为空; 效果如下图
                

    • 访问登录接口,进行登录;登录成功,同时返回生成的token;效果如下图
                

    • 带上登录成功返回的token访问/jwt/test接口,拦截器放行了请求,成功请求到了test方法;效果如下图
                

    • 当token被篡改或者已过期时,访问/jwt/test接口,拦截器拦截了该请求,返回token已失效;效果如下图
                

    进行完上述测试后,说明jwt的集成已经大功告成了。

五、小结

    jwt集成不麻烦,但是也有很多不完善的地方,后续再想办法把它完善。
    在我的JwtUtils的verifyAndGetClaimsByToken方法里提到了相关扩展的思路,可以通过该思路来实现token的刷新及其他的骚操作。
    当然,jwt也有很多缺点,这里就不在赘述了。


SpringBoot集成JWT的更多相关文章

  1. SpringBoot集成JWT 实现接口权限认证

    JWT介绍 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的, 特别适用于分布式站点 ...

  2. SpringBoot集成JWT实现token验证

    原文:https://www.jianshu.com/p/e88d3f8151db JWT官网: https://jwt.io/ JWT(Java版)的github地址:https://github. ...

  3. SpringBoot集成JWT实现权限认证

    目录 一.JWT认证流程 二.SpringBoot整合JWT 三.测试 上一篇文章<一分钟带你了解JWT认证!>介绍了JWT的组成和认证原理,本文将介绍下SpringBoot整合JWT实现 ...

  4. springboot之Jwt验证

    简介 什么是JWT(Json Web Token) jwt是为了在网络应用环境间传递声明而执行的一种基于json的开放标准.该token被设计紧凑且安全的,特别适用于SSO场景. jwt的声明一般被用 ...

  5. SpringBoot系列 - 集成JWT实现接口权限认证

    会飞的污熊 2018-01-22 16173 阅读 spring jwt springboot RESTful API认证方式 一般来讲,对于RESTful API都会有认证(Authenticati ...

  6. SpringBoot 集成SpringSecurity JWT

    目录 1. 简介 1.1 SpringSecurity 1.2 OAuth2 1.3 JWT 2. SpringBoot 集成 SpringSecurity 2.1 导入Spring Security ...

  7. Springboot shiro JWT集成总结

    SpringBoot Shiro JWT 1.建表 DDL.sql CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, ` ...

  8. spring boot 2 集成JWT实现api接口认证

    JSON Web Token(JWT)是目前流行的跨域身份验证解决方案.官网:https://jwt.io/本文使用spring boot 2 集成JWT实现api接口验证. 一.JWT的数据结构 J ...

  9. 轻松上手SpringBoot+SpringSecurity+JWT实RESTfulAPI权限控制实战

    前言 我们知道在项目开发中,后台开发权限认证是非常重要的,springboot 中常用熟悉的权限认证框架有,shiro,还有就是springboot 全家桶的 security当然他们各有各的好处,但 ...

随机推荐

  1. 使用hangfire在xunit中

    框架为:abp hangfire配置连接:https://aspnetboilerplate.com/Pages/Documents/Hangfire-Integration 在单元测试中如何配置呢? ...

  2. [POJ1952]BUY LOW, BUY LOWER

    题目描述 Description The advice to "buy low" is half the formula to success in the bovine stoc ...

  3. 集合 List ,Set

    https://www.cnblogs.com/jmsjh/p/7740123.html

  4. 跨交换机VLAN之间的通信(基于Cisco模拟器)

    实验要求: 拓扑结构如下 1.交换机2台:主机4台:网线若干. 2.把主机.交换机进行互联. 3.给2台交换机重命名为A.B. 4.设置2台交换机及主机的ip.注意IP要不冲突 5.在2台交换机上分别 ...

  5. cmd 批处理创建 IIS 站点

    windows 创建站点命令 appcmd C:\Windows\System32\inetsrv\appcmd.exe SITE 虚拟站点的管理 APP 管理应用程序 VDIR 管理虚拟目录 APP ...

  6. echarts 中 柱图 、折线图、柱图层叠

    app.title = '折柱混合'; option = { tooltip: { trigger: 'axis', axisPointer: { type: 'cross', crossStyle: ...

  7. MySQL实战45讲学习笔记:第十讲

    一 .本节内容概要 前面我们介绍过索引,你已经知道了在 MySQL 中一张表其实是可以支持多个索引的.但是,你写 SQL 语句的时候,并没有主动指定使用哪个索引.也就是说,使用哪个索引是由MySQL ...

  8. Web协议详解与抓包实战:HTTP1协议-请求与响应的上下文(7)

    一.请求的上下文: User-Agent 指明客户端的类型信息,服务器可以据此对资源的表述做抉择 二.请求的上下文: Referer 浏览器对来自某一页面的请求自动添加的头部 截图2 这对于我们的防盗 ...

  9. [LeetCode] 901. Online Stock Span 股票价格跨度

    Write a class StockSpanner which collects daily price quotes for some stock, and returns the span of ...

  10. js中的super

    1.this和super的区别: this关键词指向函数所在的当前对象 super指向的是当前对象的原型对象 2.super的简单应用 const person = { name:'jack' } c ...