1、JWT是什么?
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
2、JWT和传统Session的优劣
2.1、什么时候使用JWT?
Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
2.2、JWT和Session的比较
1、无状态:
token 自身包含了身份验证所需要的所有信息,使得我们的服务器不需要存储 Session 信息,这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
也导致了它最大的缺点:当后端在token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户 Logout 的话,token 也还有效。除非,我们在后端增加额外的处理逻辑。
2、避免CSRF 攻击:
攻击者就可以通过让用户误点攻击链接,达到攻击效果。防止误触操作,避免请求直接获取本地的session值进行请求访问。
3、适合移动端应用
使用 Session 进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到 Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。
但是,使用 token 进行身份认证就不会存在这种问题,因为只要 token 可以被客户端存储就能够使用,而且 token 还可以跨语言使用。
4、单点登录友好
使用 Session 进行身份认证的话,实现单点登录,需要我们把用户的 Session 信息保存在一台电脑上,并且还会遇到常见的 Cookie 跨域的问题。但是,使用 token 进行认证的话, token 被保存在客户端,不会存在这些问题。
3、JWT的结构
JWT的结构由三部分组成,分别是标头、有效负载、签名算法,中间使用 点 进行隔开。
header标头:记录了token的类型和加密算法的josn。
像是这样:{ “typ”: “JWT”, “alg”: “HS256” }。然后要转成base64字符串。
payload有效负载:记录了用户信息的json。
同样要转成base64字符串。
signatur:签名算法
head和payload的json,转为base64字符串后,再加密,得到的。
通过这个加密,就可以验证jwt是不是对的了。
4、JWT的工作流程
在身份验证中,当用户成功登录系统时,授权服务器将会把 JSON Web Token(JWT)返回给客户端,用户需要将此凭证信息存储在本地(cookie或浏览器缓存)。当用户发起新的请求时,需要在请求头中附带此凭证信息,当服务器接收到用户请求时,会先检查请求头中有无凭证,是否过期,是否有效。如果凭证有效,将放行请求;若凭证非法或者过期,服务器将回跳到认证中心,重新对用户身份进行验证,直至用户身份验证成功。
步骤如下:
5、获取Token和对token进行验证
在进行使用JWT的时候,我们首先还是引入依赖。
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
之后我们先进行生成token的测试。如下代码所示:我们首先使用JWT对象的create方法进行创建对象,之后我们分别将Header标头、payload有效负载、signature签名算法进行指定,而这里的密钥也由自己指定。
public static final String SIGN ="@lzq4585#$^"; // 密钥
@Test
void token() {
Map map = new HashMap();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE,7);
String token = JWT.create()
.withHeader(map) // header
.withClaim("id", 1) // payload
.withClaim("name", "月月")
.withExpiresAt(calendar.getTime()) // 有效时长
.sign(Algorithm.HMAC256(SIGN)) // signature
;
System.out.println(token);
}
运行Test进行测试,我们可以看到token的结构,也就是x.y.z
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5pyI5pyIIiwiaWQiOjEsImV4cCI6MTYyNjcwMDAzOH0.PvUR8Qk9l_ct4xHfrl18cPs7NfNq9OIk4yyvTJmgmeY
既然生成了token,那么同样的我们也可以对这个token进行解析出来,如下代码所示:进行解析token值的时候我们需要给定对应的加密算法和密钥以及token值,进行解密后获取的token值分别getHeader、getClaim就可以获取到标头和有效负载了。
@Test
void verify(){
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("@lzq4585#$^")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi5pyI5pyIIiwiaWQiOjEsImV4cCI6MTYyNjAwMTkxOH0.4Q9RBX-YT2MQe2-RnlxquSdHj6WHNWgu3QJ91muk6qY");
System.out.println(verify.getHeader());
System.out.println(verify.getClaim("id").asString());
System.out.println(verify.getClaim("id").asInt());
System.out.println(verify.getPayload());
System.out.println("过期时间:"+verify.getExpiresAt());
}
注意:在这里的有效负载的值对于相对应的数据类型应使用对应的asString或者asInt等等。比如上面的id就是int类,而进行asString就得不到值,也就是null。
6、方法的封装
在前面的测试JWT的生成以及验证我们写了两个对应的test测试,而在项目当中我们进行使用这两个方法也是必不可少的,所以我们可以对上面两个方法进行封装一下,用来直接处理JWT的生成和验证。
第一个就是生成JWT的方法,首先我们生成JWT先获取对于参数,这里使用map进行传参,这里的参数我们要写入payload有效负载当中,返回回去,之后我们添加一个时间,用来作为token过期时间,而后我们创建一个JWT对象,将对应的有效负载、过期时间、加密算法进行写进去给到token。这里入参用的map,而进行写入直接forEach进行循环写入,这是Java8的语法,可以了解一下。
public static String getToken(Map<String, String> map) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 7);
JWTCreator.Builder jwt = JWT.create();
map.forEach((k, v) -> {
jwt.withClaim(k, v);
});
jwt.withExpiresAt(calendar.getTime());
String token = jwt.sign(Algorithm.HMAC256(SIGN));
return token;
}
第二个方法就是验证token的方法。这个就比较简单了,直接将token值传进来,通过密钥进行判断,最后直接返回。
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
方法封装之后,我们使用一个Utils类进行保存和统一管理,新建一个JwtUtil类,后面当我们需要使用到token的生成和验证的时候,我们就可以直接进行调用这里面的静态方法即可。
7、登录验证案例
最后离不开的还是案例,这里用一个登录进行验证。首先还是整合mybatis,这里的整合mybatis也就不做详细的说明了,SpringBoot详情可以参考:SpringBoot 详解
并且我们添加一个用来获取所有信息的接口:其中mapper查询语句如下:
<select id="login" parameterType="User" resultType="User">
select * from user where name = #{name} and password = #{password}
select>
之后添加一个请求,用来模拟登录,这里入参给到name和password。之后查询数据库,返回值用userDB进行保存,将id和name写进有效负载当中,然后调用前面封装的方法,也就是jwtUtil.getToken方法,返回一个token值,最后将数据写进map当中进行返回。
@GetMapping("/user/login1")
public Map<String,Object> login1(User user){
Map map = new HashMap();
try {
User userDB =userService.login(user);
Map payload = new HashMap();
payload.put("id",userDB.getId());
payload.put("name",userDB.getName());
String token = jwtUtil.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);
}catch (Exception e){
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
接口有了,之后我们就直接发送请求看一下效果,首先是账号密码都正确的,可以看到是会有token值进行返回的。
随后发送一个账号密码不对的,也就验证通不过,不会生成token值,查看返回结果。
同样的,在生成了token之后,我们可以调用验证token的方法,对生成的token进行验证
@PostMapping("/user/test")
public Map<String,Object> token(String token){
Map map = new HashMap();
try {
DecodedJWT verify =jwtUtil.verify(token);
map.put("token",verify);
}catch (Exception e){
map.put("e",e.getMessage());
}
return map;
}
而这里我们同样的发送一条post请求进去查看结果,首先是有效的token,我们可以获取到这个token当中的信息。
而后,当token被修改了,也就是这个token是通不过的,捕获异常。
当然了,我们这里的token验证是很多请求接口都需要用到,这里的话我们可以使用一个拦截器进行统一管理,同样的可以参考:SpringBoot 详解