再传统的基于session的用户身份认证方式之中,用户相关信息存储与后端,通常基于cookie来携带用户的会话id,然后后端在基于会话id查到对应的用户身份信息;区别于session的身份认证方式,jwt作为一个基于RFC 7519的开发标准,提供了一种通过JSON形式的web令牌,用于在各系统之间的安全可信的数据传输、身份标识
本文将主要介绍jwt的相关知识点,以及如何基于jwt来实现一个简单的用户鉴权方案
I. JWT知识点
jwt,全称 json web token, JSON Web 令牌是一种开放的行业标准 RFC 7519 方法,用于在两方之间安全地表示声明。
详情可以参考: hhttps://jwt.io/introduction
1. 数据结构
JSON Web Token由三部分组成,它们之间用圆点.
进行分割, 一个标准的JWT形如 xxx.yyy.zzz
即第一部分,由两部分组成:token的类型(JWT
)和算法名称(比如:HMAC
SHA256
或者RSA
等等)。
一个具体实例如
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
然后,用Base64对这个JSON编码就得到JWT的第一部分
1.2 Payload
第二部分具体的实体,可以写入自定义的数据信息,有三种类型
Registered claims
: 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer 签发者), exp (expiration time 有效期), sub (subject), aud (audience)等。
Public claims
: 可以随意定义。
Private claims
: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
如一个具体实例
1 2 3 4 5 6 7
| { "iss": "一灰灰blog", "exp": 1692256049, "wechat": "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg", "site": "https://spring.hhui.top", "uname": "一灰" }
|
对payload进行Base64编码就得到JWT的第二部分
1.3 Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
如 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
1.4 具体实例
下面给出一个基于 java-jwt
生成的具体实例
1 2 3 4 5 6
| public static void main(String[] args) { String token = JWT.create().withIssuer("一灰灰blog").withExpiresAt(new Date(System.currentTimeMillis() + 86400_000)) .withPayload(MapUtils.create("uname", "一灰", "wechat", "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg", "site", "https://spring.hhui.top")) .sign(Algorithm.HMAC256("helloWorld")); System.out.println(token); }
|

II. 使用实例
接下来我们基于jwt方案实现一个用户鉴权的示例demo
1. 项目搭建
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
添加web支持,用于配置刷新演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>
|
我们采用thymeleaf来进行前端页面的渲染,添加一些相关的配置 application.yml
1 2 3 4 5 6 7 8 9 10
| server: port: 8080
spring: thymeleaf: mode: HTML encoding: UTF-8 servlet: content-type: text/html cache: false
|
2. JWT鉴权流程
一个简单的基于jwt的身份验证方案如下图

基本流程分三步:
用户登录成功之后,后端将生成的jwt返回给前端,然后前端将其保存在本地缓存;
之后前端与后端的交互时,都将jwt放在请求头中,我们这里借助Http的身份认证的请求头Authorization
后端接收到用户的请求,从请求头中获取jwt,然后进行校验,通过之后,才响应相关的接口;否则表示未登录
3. 实现方案
基于上面的流程,我们可以实现一个非常简单的登录认证演示工程
首先在内存中,维护几个简单用户名/密码信息,用于模拟用户名+密码的校验
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Controller public class LoginController { private Map<String, String> userCache = create("一灰灰", "123", "yihui", "hello");
private static <K, V> Map<K, V> create(K k, V v, Object... kvs) { Map<K, V> map = new HashMap<>(kvs.length + 1); map.put(k, v); for (int i = 0; i < kvs.length; i += 2) { map.put((K) kvs[i], (V) kvs[i + 1]); } return map; } }
|
然后提供登录接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @PostMapping(path = "/login") @ResponseBody public String login(String uname, String pwd, HttpServletResponse response) { if (!userCache.containsKey(uname) || !Objects.equals(pwd, userCache.get(uname))) { return "用户名密码错误,登录失败"; }
String token = JWT.create() .withIssuer("一灰灰blog") .withExpiresAt(new Date(System.currentTimeMillis() + 86400_000L)) .withPayload(create("uname", uname, "wechat", "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg", "site", "https://spring.hhui.top")) .sign(Algorithm.HMAC256("helloWorld"));
response.addCookie(new Cookie("Authorization", token)); return token; }
|
上面的接口实现,接收两个请求参数: 用户名 + 密码
当用户身份校验通过之后,将生成一个jwt,这里直接使用开源项目java-jwt
来生成(当然有兴趣的小伙伴也可以自己来实现)
需要注意的一点是,我们在上面的实现中,除了直接返回jwt之外,也将这个jwt写在cookie中,这种将jwt写入cookie的方案,主要的好处就是前端不需要针对jwt进行特殊处理
当然对应的缺点也和直接使用session的鉴权方式一样,存在csrf风险,以及对于跨资源共享时的资源共享问题(CORS)
本项目的实际演示中,采用前端存储返回的jwt,然后通过请求头方式来传递jwt
上面登录完成之后,再提供一个简单的要求登录之后才能查看的查询接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @GetMapping("query") @ResponseBody public Object queryInfo(HttpServletRequest request) { String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) { return "未登录"; } token = token.substring(token.indexOf(" ")).trim();
try { JWTVerifier verifier = JWT.require(Algorithm.HMAC256("helloWorld")).withIssuer("一灰灰blog").build(); DecodedJWT decodedJWT = verifier.verify(token); HashMap pay = JSONObject.parseObject(new String(Base64Utils.decodeFromString(decodedJWT.getPayload())), HashMap.class); pay.put("query", "查询成功!"); return pay; } catch (Exception e) { e.printStackTrace(); return "鉴权失败: " + e.getMessage(); } }
|
最后再写一个前端页面来完成整个测试
1 2 3 4
| @GetMapping(path = {"/", "", "/index"}) public String index() { return "index"; }
|
对应的前端页面如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="SpringBoot thymeleaf"/> <meta name="author" content="YiHui"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>JWT示例demo</title> </head> <body>
<div> <div>准备登录</div> <br/> <div> 用户名: <input type="text" id="uname"> </div> <div> 密码: <input type="password" id="pwd"> </div> <div> <button onclick="login()">登录</button> </div> <div id="tip"></div> </br> <hr/> <div> --- 分割线 ---</div> </br> <button onclick="query()">查询用户信息</button> </br> <div> <pre id="res"></pre> </div> </div> <script> function login() { const uname = document.getElementById("uname").value; const pwd = document.getElementById("pwd").value; console.log("开始登录", uname, pwd); var httpRequest = new XMLHttpRequest();//第一步:创建需要的对象 httpRequest.open('POST', '/login', true); //第二步:打开连接 httpRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");//设置请求头 注:post方式必须设置请求头(在建立连接后设置请求头) httpRequest.send(`uname=${uname}&pwd=${pwd}`);//发送请求 将情头体写在send中 /** * 获取数据后的处理程序 */ httpRequest.onreadystatechange = function () {//请求后的回调接口,可将请求成功后要执行的程序写在其中 if (httpRequest.readyState == 4 && httpRequest.status == 200) {//验证请求是否发送成功 const res = httpRequest.responseText;//获取到服务端返回的数据 console.log(res); document.getElementById("tip").innerText = "登录完成:" + res; window.sessionStorage.setItem("jwt", res); } }; }
function query() { var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象 httpRequest.open('GET', '/query', true);//第二步:打开连接 httpRequest.setRequestHeader("Authorization", "Bearer " + window.sessionStorage.getItem("jwt")); httpRequest.send();//第三步:发送请求 将请求参数写在URL中 /** * 获取数据后的处理程序 */ httpRequest.onreadystatechange = function () { if (httpRequest.readyState == 4 && httpRequest.status == 200) { var json = httpRequest.responseText;//获取到json字符串,还需解析 console.log(json); document.getElementById("res").innerText = json; } }; } </script> </body> </html>
|
4. 实例演示
基于上面的实现,接下来我们看一下具体表现情况


从上面的两张图也可以看出,登录成功之后,jwt写入到本地的session storage中,再后续的请求中,若请求头Authroization
中携带了jwt信息,则后端可以进行正常校验
有兴趣的小伙伴可以尝试修改一下本地存储中的jwt值,看一下非法或者过期的jwt会怎么表现
5. 小结
本文主要介绍了jwt的基本知识点,并给出了一个基于jwt的使用实例,下面针对jwt和session做一个简单的对比
jwt |
session |
前端存储,通用的校验规则,后端再获取jwt时校验是否有效 |
前端存索引,后端判断session是否有效 |
验签,不可篡改 |
无签名保障,安全性由后端保障 |
可存储非敏感信息,如用户名,头像等 |
一般不存储业务信息 |
jwt生成时,指定了有效期,本身不支持续期以及提前失效 |
后端控制有效期,可提前失效或者自动续期 |
通常以请求头方式传递 |
通常以cookie方式传递 |
可预发csrf攻击 |
session-cookie方式存在csrf风险 |
关于上面的两个风险,给一个简单的扩展说明
csrf攻击
如再我自己的网站页面上,添加下面内容
1
| <img src="https://paicoding.com/logout" style="display:none;"/>
|
然后当你访问我的网站时,结果发现你在技术派上的登录用户被注销了!!!
使用jwt预防csrf攻击的主要原理就是jwt是通过请求头,由js主动塞进去传递给后端的,而非cookie的方式,从而避免csrf漏洞攻击
III. 不能错过的源码和相关知识点
0. 项目
1. 微信公众号: 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
一灰灰blog
Related Issues not found
Please contact @liuyueyi to initialize the comment