登录认证(4):令牌技术:JWT令牌
如上文所说(登录认证(1):登录的基本逻辑及实现思路登录),因为 HTTP
协议是无状态的协议,我们需要使用会话跟踪技术实现同一会话中不同请求
之间的数据共享,但Cookie技术
和Session技术
都有各自的使用局限,所以说在当今的企业开发中,越来越青睐令牌技术
来进行会话跟踪。
令牌技术概览
在登录认证中,令牌是用户的身份标识,是合法的身份凭证,好像十分神秘和高大上,但其本质是一个字符串。如果使用令牌技术进行会话跟踪,在浏览器发起请求,请求登录接口
,如果登录成功,那么就在服务端生成一个令牌
,令牌就是用户的合法身份凭证,在响应数据的时候,就可以将令牌直接响应给前端。
在前端程序接收到令牌之后,就需要将令牌存储起来,可以存储在cookie
中,也可以存储在localStorage
这样的其他存储空间。之后,在后续的每一次请求中,都需要携带令牌
,服务端的统一拦截器
需要校验令牌的有效性。如果令牌有效,则说明用户已经执行了登录操作,拦截器就可以放行;如果令牌无效(解析令牌报错),那么则说明用户没有执行登录操作,拦截器就需要拦截,并返回错误代码,让用户登录。整个流程如下图所示:
此时,在同一个会话的多次请求之间,我们就通过将数据存储在令牌中的方式完成了数据共享
。令牌技术有很多优点,比如:支持多端,不但支持PC
端,而且支持移动
端;令牌技术也可以解决服务器集群
的认证问题,因为只需要成功解析令牌,就可以证明令牌是有效的,是无需在服务器存储的;令牌的安全性也非常强悍。但也是因为其强悍的性能,令牌使用起来会更加的复杂,但是这些劣势在优势面前就不值一提了。
JWT令牌
令牌的形式有很多,本文讲解功能强大、使用最广泛的JWT令牌。JWT令牌(JSON Web Token
),定义了一种简洁的、自包含的格式,可用于通信双方以Json
数据格式安全的传输信息。
特性
简洁
JWT令牌
的本质就是字符串
,可以作为请求参数
或者在请求头
中直接传递。
自包含
JWT令牌
虽然是一个字符串
,但是可以根据需求,在令牌中存储自定义的数据内容,比如在登录操作中,可以在JWT令牌中存储用户相关信息。
安全
简单理解,JWT令牌
就是将原本简单的Json
数据进行了安全的封装,这样就可以直接在JWT
令牌中进行信息传输了,并且由于数字签名
的存在,这些信息是安全可靠的。
组成
JWT令牌
由三个部分组成,每个部分之间使用.
进行分隔。一个完整的JWT
令牌如图所示:
第一部分:Header(头)
该部分主要是记录令牌类型、令牌使用的签名算法等。例如:{"alg":"HS256", "type":"JWT"}
,从这个Header
信息就可以看出:这是一个JWT
令牌,使用了HS256
签名算法。
第二部分:Payload(有效载荷)
该部分主要是携带一些自定义的信息,或者一些默认的信息等,例如:{"id":"1","username":"Tom"}
,从这个Payload
信息可以看出:这个令牌携带的数据是一个用户数据,id为1,username为Tom。
第三部分:Signature(签名)
这个部分主要是令牌的签名,签名可以防止Token
被篡改,可以提高令牌的安全性。其构成是将Header
和Payload
两个部分,加入指定密钥(Secret
),并通过指定的签名算法计算而来。正是因为数字签名
,所以说JWT令牌
是非常安全的,一旦令牌中的任何一个部分、任何一个字符被篡改了,整个令牌在校验时都会失效。
Base64编码
那么JWT令牌
是如何将原始的Json数据
,转变为字符串的呢?在生成JWT令牌
的时候,对原始数据进行了Base64编码
(这并非是一种加密方式,只是一种编码方式)。
Base64编码
:是一种基于64个可打印的字符来表示二进制数据的编码方式。所使用的64个字符分别是A到Z
、a到z
、0-9
,一个加号(+),一个斜杠(/)
,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。在有些情况下,Basae64编码
可能会出现一个等号(=
)。等号是一个补位的符号。
使用
想要使用JWT令牌
,首先需要引入JWT
对应的Maven坐标:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT令牌
在引入了JWT依赖之后,就可以使用对应的工具类Jwts
提供的API来完成JWT令牌
的生成与校验:
/**
* 生成JWT令牌
*/
@Test
public void testGenerateJWT() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 10);
claims.put("username", "wzb");
// 通过Jwts工具类中的builder方法构建一个JWT令牌
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "hello") // 加密算法和签名
.addClaims(claims) // 添加数据:键值对
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)) // 设置JWT令牌有效期
.compact(); // 生成令牌
log.info("jwt令牌是:{}", jwt);
}
这个代码主要是使用Jwts
工具类提供的API
生成一个JWT令牌
。使用signWith
方法,指定该令牌的加密算法为HS256
,并且指定签名(这里的签名指定的十分粗糙,实际开发中需要指定更加复杂的签名来增加令牌安全性。);Jwt令牌中的数据都是以键值对
的形式出现的,所以说可以直接把需要的数据封装为一个Map
,然后使用addClaims
方法,将Map中的数据封装到令牌中;然后再使用setExpiration
方法,设置令牌的有效时间
,此处设置的有效时间是12h;最后使用compact
方法生成JWT令牌
。运行这个测试方法,得到的JWT令牌
:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoid3piIiwiZXhwIjoxNzM3NTk0NDgxfQ.m4BT-AR8cMCEmMJoNNL0iuyNCHylKdasuu77ETsLHcw
将该令牌拿到JWT
令牌官网(JSON Web Tokens - jwt.io)解析:
JWT令牌的最后一个部分并非Base64编码,而是签名算法计算的,所以说最后一个部分不会解析。
成功解析令牌内容,加密算法、数据等都和代码封装得相同,说明我们成功生成了一个JWT令牌
。
校验JWT令牌
校验(解析)JWT令牌
和生成一样,同样需要使用Jwts
工具类提供的API
:
/**
* 解析JWT令牌
*/
@Test
public void testParseJWT() {
Claims claims = Jwts.parser().setSigningKey("hello")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoid3piIiwiZXhwIjoxNzM3NDI1MTg" +
"5fQ.cHdgtWD-BtrnM2Xnl418CcB_6h-rB2ogGSK8aT4T0dk")
.getBody();
log.info("JWT解析结果为:{}", claims);
}
解析JWT令牌必须要使用和生成时同样的签名
,通过Jwts
中的parser
方法解析一个JWT
令牌,必须在setSignKey
方法中传递和生成时一样的签名,否则解析失败;在parseClaimsJws
方法中传递需要解析的JWT令牌
,即可解析其Payload
部分,也就是令牌中的数据;最后通过getBody
方法即可获得令牌数据的键值对对象Claims,Claims
继承了Map
类,可以理解为一种特殊的Map
。让我们用该程序解析刚才生成的JWT令牌
:
成功解析令牌,说明解析令牌方法成功。JWT令牌
的解析(校验)十分严格,只要有一点错误,不论哪个部分,都无法解析,即使我们只是改变了一个字符的大小写,在运行解析方法的时候都会报错,可以看出JWT令牌
的可靠性。过期的JWT令牌
也无法解析,令牌过期之后,令牌就会马上失效,解析就会失败。
利用JWT令牌完善登录功能
上文已经对JWT令牌
做出了详细的解释与分析,JWT令牌最典型的应用就是登录,所以说让我们用JWT令牌
技术来完善登录功能。具体思路前面已经分析过了,主要分为两步操作:
生成令牌
在用户登录成功之后,生成一个JWT令牌
,并且把这个令牌直接返回给客户端,之后的每一个请求,都要携带这个令牌。
校验令牌
请求携带了JWT令牌
,统一拦截器需要拦截请求,从请求中获取令牌,并对令牌进行解析,如果成功解析,那么就放行;如果解析失败,则返回错误代码
。
我们先要创建一个JwtUtils
工具类,这个工具类提供生成、解析令牌的方法,以便程序使用:
package com.wzb.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JWTUtils {
// 独创数字签名
private static final String signKey = "hello";
// 令牌过期时间
private static final Long expire = 43200000L;
/**
* 生成的JWT令牌
* @param claims JWT令牌中的数据,键值对
* @return JWT令牌
*/
public static String generateJWT(Map<String, Object> claims) {
return Jwts.builder()
.signWith(SignatureAlgorithm.HS256, signKey)
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return Claims,JWT令牌中的数据
*/
public static Claims parseJWT(String jwt) {
return Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
}
}
然后改造登录方法:
/**
* 员工登录
*
* @param emp 登录请求数据封装的Emp实体对象
* @return LoginInfo员工登录信息
*/
@Override
public LoginInfo login(Emp emp) {
Emp empLogin = empMapper.getUserByUsernameAndPassword(emp);
Map<String, Object> claims = new HashMap<>();
if (empLogin != null) {
Integer id = empLogin.getId();
String username = empLogin.getUsername();
String name = empLogin.getName();
claims.put("id", id);
claims.put("username", username);
return new LoginInfo(id, username, name, JWTUtils.generateJWT(claims));
}
return null;
}
从数据库中查询到员工的信息之后,将其以键值对的方式封装到Map中,然后调用工具类中的方法生成JWT令牌之后,封装到登录信息类LoginInfo
中返回。此时,就成功给客户端返回了令牌,此时再次发起登录请求:
成功给客户端响应JWT令牌
。
总结
JWT令牌
是现在越来越流行,使用越来越广泛的会话跟踪技术,可以在多端使用
,并且有极强的安全性能
,还可以应对服务器集群问题,是解决登录认证问题的最佳选择。现在已经有了令牌
来标识用户已经登录,但是程序仍然没有对请求进行拦截,验证其是否登录,统一拦截器的部分且听下文分解。