前言
今天讲SpringBoot的最后一个漏洞点,就是JWT(JSON Web Token)身份权鉴。这是Java特有的一种身份鉴别技术,类似于PHP中的Cookie和Session。
什么是JWT
从这张图可以看出来浏览器通过POST请求发送账号和密码到服务端,服务端创建一个JWT并且返回给浏览器。浏览器访问某些页面时通过发送JWT到服务端,服务端检验JWT的合法性,返回对应的功能或者页面。

JWT(JSON Web Token)是由服务端用加密算法对信息签名 来保证其完整性和不可伪造;
Token里可以包含所有必要信息,这样服务端就无需保存任何关于用户或会话的信息;
JWT用于身份认证、会话维持等。由三部分组成,header、payload与signature。
Header(头部): JWT 的头部通常包含两部分信息:声明类型(typ)和使用的签名算法(alg) 。这些信息以 JSON 格式存在,然后进行 Base64 编码,形成 JWT 的第一个部分。头部用于描述关于该 JWT 的元数据信息。
{
"alg": "HS256",
"typ": "JWT"
}
Payload(负载): JWT 的负载包含有关 JWT 主题(subject)及其它声明的信息。与头部一样,负载也是以 JSON 格式存在,然后进行 Base64 编码,形成 JWT 的第二个部分。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature(签名): JWT 的签名是由头部、负载以及一个密钥生成的,用于验证 JWT 的真实性和完整性。签名是由指定的签名算法对经过 Base64 编码的头部和负载组合而成的字符串进行签名生成的。
例如,使用 HMAC SHA-256 算法生成签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
项目搭建
新建一个项目名为Java-demo。

依赖选择。

这里要引入一下依赖,引入3.4.0版本的JWT。
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
安全问题
创建一个Java类叫JWTcontroller,并且写入以下代码。
package com.sf.maven.jwtdemo.demos.web;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
public class JWTcontroller {
public static void main(String[] args) {
String JWTstring = JWT.create()
//创建header部分
//.withHeader()
//创建payload部分
.withClaim("userid",1)
.withClaim("username","admin")
.withClaim("password","123456")
//创建signature,并且设置签名算法为HMAC256,密钥为wlwnb
.sign(Algorithm.HMAC256("wlwnb"));
System.out.println(JWTstring);
}
}
直接运行一下代码,看看输出的JWT是啥样子的,可以看到生成了一个JWT数据。

这个JWT数据被两个点隔成三部分,分别是
Header:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload:eyJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiJ9
**Signature:**C2bW9w4bU3UGFTCpf9x247-DqUeso9TbIB6KeGzmJdM
我们拿到JWT解密网站去看看,可以看到我们解密的payload和刚刚我们设置的对得上。

有人可能说那么我把admin改为其它的用户不就可以实现任意用户登录了?其实我们往下可以看到签名这里显示密钥为空,你修改后加密出来的JWT是不可被服务端识别的,除非你知道加密的密钥。

直接做个实验就懂了,我们先写一个表单提交的页面,在resource/static/index.html里面写入以下代码,创建一个登录提交表单。
<form action="../jwtcreate" method="post">
id:<input type="text" name="id"><br>
user:<input type="text" name="username"><br>
password:<input type="text" name="password"><br>
<input type="submit" value="create">
</form>

接着我们在创建JWT的代码里面添加一下解密JWT的代码,并且提取数据。
package com.sf.maven.jwtdemo.demos.web;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class JWTcontroller {
//创建JWT
@PostMapping("/jwtcreate")
@ResponseBody
public static String main(Integer id,String username,String password) {
String JWTstring = JWT.create()
.withClaim("userid",id)
.withClaim("username",username)
.withClaim("password",password)
.sign(Algorithm.HMAC256("wlwnb"));
System.out.println(JWTstring);
return JWTstring;
}
//模拟用户身份检测,JWT数据解密
@PostMapping("/JWTcheck")
@ResponseBody
public static void JWTcheck(String JWTdata) {
//构建解密注册
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("wlwnb")).build();
//解密注册数据
DecodedJWT jwt = verifier.verify(JWTdata);
//提取注册解密数据,
Integer userid = jwt.getClaim("userid").asInt();
String username = jwt.getClaim("username").asString();
String password = jwt.getClaim("password").asString();
System.out.println(userid+" "+username+" "+password);
//获取头部数据
//jwt.getHeader();
//获取签名数据
//jwt.getSignature();
}
}
运行JwtDemoApplication,部署页面,可以看到这是我们刚刚写的表单页面。

填入数据点击create,可以看到返回了生成的JWT。

现在我们来模拟攻击者,先在原有的代码上添加一段判断的代码。

接着在index页面写个JWT数据的提交表单,使其可以解密JWT数据。

运行代码访问页面,把生成的JWT数据提交解密。

可以看到如果是admin用户的话就会返回you are admin。

如果不是admin,返回如下。

OK,那么我们现在来做个实验,可以看到我们现在JWT解密的payload为wlwnb。

那么我们现在把wlwnb改为admin,再在这个网站加密成JWT数据去提交,看看是否返回admin页面,可以看到是返回了一个报错页面。

这是为啥呢,其实上面我们讲过了,就是没有密钥的问题,你没有密钥那么加密出来的数据代码这边就无法解密,自然就是返回报错页面。


也就是说JWT不需要考虑你的账号密码正确性,只需看你传过来的JWT是否能解密即可,因为你不知道密钥,伪造的数据无法解密直接报错。我们添加密钥再去伪造admin的JWT的数据试试。

结果是代码判定我们是admin,这里更加验证了我们上面的说法。所以JWT的安全问题主要是算法泄露或者在开发的时候alg设置为"none",这就意味着不用算法对数据进行加密,那么此时没有密钥也可以解密。

打包部署
这个其实没啥可说的,就是顺便讲一下Java的项目该如何在服务器上面部署。其实是非常简单,就是把项目打包成jar包,再放到服务器上面运行即可。
来到我们Maven这里,点击生命周期,点击clean,清除一下缓存。

接着再点击package。

此时在目录这里便生成了一个jar包。

总结
基本关于SpringBoot的知识点都讲完了,后续会讲一下该如何利用,Java安全估计还得要十几篇文章。
最后,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。