漫谈JSON Web Token(JWT)
一、背景
传统的单体应用基于cookie-session的身份验证流程一般是这样的:
- 用户向服务器发送账户和密码。
- 服务器验证账号密码成功后,相关数据(用户角色、登录时间等)都保存到当前会话中。
- 服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中。
- 以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid作为索引获取到具体session。
但是这种模式存在如下几个问题:
- 没有分布式架构无法支持横向扩展,例如站点A和站点B提供同一公司的相关服务。现在要求用户只需要登录其中一个网站,然后它就会自动登录到另一个网站,满足不了这种需求。
- 如果用户很多,这些信息存储在服务器内存中会给服务器增加负担。
- 还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
针对问题1,有两种解决方案,第一种就是使用session共享,比如使用redis实现,第二种解决方案就是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
二、什么是JWT
JSON WEB TOKEN(以下称JWT),JWT也是一种token,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
2.1JWT的特点
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
自包含(Self-contained): 负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。
2.2JWT的消息结构
实际的JWT数据结构就像这样。

它是一个很长的字符串,中间用点分隔成三个部分,红色是header,紫色部分是Payload,蓝色部分是Signature。即格式都是Header.Payload.Signature。
Header(头部)
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{ "alg": "HS256", "typ": "JWT" }
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的JSON对象使用Base64URL算法转成字符串。
Payload(负载)
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段我们还可以自定义字段,比如:
{ "userId": 1234567890, "name": "John Doe" }
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。然后这个JSON对象也要使用Base64URL算法转成字符串。
Signature(签名)
Signature是对前两个部分的签名,作用是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
Base64URL算法
Base64URL算法与Base64算法有一些区别,作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_" 替换,这就是Base64URL算法。
三、JWT的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信都要带上JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。
还可以将JWT放在POST请求的数据体中,或者跟在URL后面。
所以使用JWT实现单点登录,也可以放在Cookie中或者Header中,基于Cookie的实现可以参考这篇文章《八幅漫画理解使用 JWT设计的单点登录系统》,之前公司的项目是基于Header的Authorization字段字段实现的。
四、JWT的缺点
当然,JWT并不是完美的,它也有一些缺点。
1.JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
2.JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。所以为了避免盗用,可以将有效期设置的短一些,使用HTTPS协议加密传输。
《理解JWT的使用场景和优劣》这篇文章介绍的很详细,但是我不太认同他说的使用jwt做单点登录+会话管理没有传统的cookie-session 机制工作得更好。
五、代码示例
JWT一般是在网关服务配合Filter来实现认证的,考虑再弄个Zuul网关比较麻烦这里就简化了下。
JwtTestController
/** * Created by 2YSP on 2019/8/26. */ @Slf4j @RestController @RequestMapping("/jwt") public class JwtTestController { @GetMapping("/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletResponse response) throws Exception { if (!checkUserNameAndPwd(username, password)) { return "login error:invalid username or password"; } // 过期时间 Long exp = System.currentTimeMillis() + 20 * 1000; PayLoad payLoad = new PayLoad(1L, username, exp); String token = JwtUtils.generateToken(payLoad); Cookie cookie = new Cookie("token", token); // HttpOnly属性来防止Cookie被JavaScript读取,从而避免跨站脚本攻击 cookie.setHttpOnly(true); // 30秒 cookie.setMaxAge(30); response.addCookie(cookie); return token; } @GetMapping("/verify") public Boolean verifyToken(HttpServletRequest request) { String token = getTokenFromCookie(request); if (StringUtils.isBlank(token)){ return false; } // 验证签名 if (!JwtUtils.checkSignature(token)){ return false; } PayLoad payLoad = JwtUtils.getPayLoad(token); if (payLoad.getExp() < System.currentTimeMillis()){ // 已过期 return false; } Gson gson = new Gson(); log.info("verify successfully ,payLoad:{} ", gson.toJson(payLoad)); return true; } private String getTokenFromCookie(HttpServletRequest request){ Cookie[] cookies = request.getCookies(); for(Cookie cookie : cookies){ if (cookie.getName().equals("token")){ return cookie.getValue(); } } return null; } private boolean checkUserNameAndPwd(String username, String pwd) { if (StringUtils.isBlank(username)) { return false; } if (StringUtils.isBlank(pwd)) { return false; } if (username.equals("admin") && pwd.equals("1234")) { return true; } return false; } }
这里提供了两个接口,一个是模拟登录(登录一般是POST的这里方便测试就改为GET方式了),登录成功后返回一个token同时将token保存在cookie中。另一个是校验token的接口,依次进行验签、过期时间等校验。
PayLoad
@Data @AllArgsConstructor @NoArgsConstructor public class PayLoad { private Long userId; private String name; private Long exp; }
保存一些用户信息和过期时间的实体类。
JwtUtils
- public class JwtUtils {
- /**
- * 加密算法
- */
- public static final String HEADER_ALG = "HS256";
- public static final String HEADER_TYP = "JWT";
- /**
- * 加密串
- */
- public static final String SECRET = "d5ec0a02";
- public static String generateToken(PayLoad payLoad) throws Exception {
- Gson gson = new Gson();
- Header header = new Header(HEADER_ALG, HEADER_TYP);
- String encodedHeader = Base64Utils.encodeToUrlSafeString(gson.toJson(header).getBytes(
- Charsets.UTF_8));
- String encodePayLoad = Base64Utils.encodeToUrlSafeString(gson.toJson(payLoad).getBytes(
- Charsets.UTF_8));
- String signature = HMACSHA256(encodedHeader + "." + encodePayLoad, SECRET);
- return encodedHeader + "." + encodePayLoad + "." + signature;
- }
- public static boolean checkSignature(String token) {
- String[] array = token.split("\\.");
- if (array.length != 3) {
- throw new IllegalArgumentException("token error");
- }
- try {
- String signature = HMACSHA256(array[0] + "." + array[1], SECRET);
- return signature.equals(array[2]);
- } catch (Exception e) {
- }
- return false;
- }
- public static PayLoad getPayLoad(String token) {
- String[] array = token.split("\\.");
- if (array.length != 3) {
- throw new IllegalArgumentException("token error");
- }
- String payLoad = new String(Base64Utils.decodeFromUrlSafeString(array[1]), Charsets.UTF_8);
- Gson gson = new Gson();
- return gson.fromJson(payLoad, PayLoad.class);
- }
- public static String HMACSHA256(String data, String key) throws Exception {
- Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
- SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
- sha256_HMAC.init(secret_key);
- byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
- StringBuilder sb = new StringBuilder();
- for (byte item : array) {
- sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
- }
- return sb.toString().toUpperCase();
- }
- static class Header {
- private String alg;
- private String typ;
- }
- }
JWT的工具类,提供了一些生成token,验证签名等方法。代码地址。
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/11413478.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步