jwt使用原理
概念:
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。JWT将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证;应用场景如用户登录。
JWT可通过URL、POST参数或HTTP header发送,数据量小;负载中可包含用户信息,避免多次查询数据库。
JWT定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
springboot-jwt-2020
JWT原理
服务器认证以后会生产一个json对象,服务器完全只靠这个json对象校验用户身份,
为了防止json串被篡改,服务器在生成这个json对象时会进行签名
也就是说服务器端不保存这个数据,每次客户端请求时需要带着这个json对象
JWT数据结构
形如 xxxx.yyy.zzz 由三部分组成,每部分用英文句号连接
JWT的三个部分:
header 头部
payload 负载
signature 签名
也就是 Header.Payload.Signature
1、Header 头部
是一个JSON 对象, 描述JWT的元数据,形如:
{"alg": "HS256", "typ": "JWT"}
alg属性表示签名的算法(algorithm),默认是 HMAC SHA256
typ属性表示这个令牌的类型(type),JWT 令牌统一写为JWT
2、payload 负载
是一个JSON 对象, 用来存放实际需要传递的数据,形如:
{"sub": "1234567890", "name": "John Doe","admin": true}
一般是在这个部分定义私有字段:
例如{"userId":"1","userName":"jack"}
其中payload官方规定了7个字段:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把机密信息放在这个部分。
3、signature 签名
signature 是对前两部分的签名,防止数据篡改
1、需要指定一个密钥(secret)
2、这个密钥只有服务器才知道,不能泄露给客户端
3、使用 Header 里面指定的签名算法,按照下面的公式产生签名。
`HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)`
也就是signature等于上面公式算出来的
把 Header、Payload、Signature 三个部分拼成一个字符串: xxxx.yyy.zzz
其中base64UrlEncode是串型化算法,处理特殊字符,=被省略、+替换成-,/替换成_
JWT 使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage
以后客户端每次与服务器通信,都要带上这个 JWT
方式1、可以放在 Cookie 里面自动发送,但是这样不能跨域
方式2、更好的做法是放在 HTTP 请求的头信息Authorization字段里面
Authorization: Bearer <token>
方式3、JWT放在POST请求的数据体body里面
JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
jwt工具类
package com.chilly.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static String SECRET = "token!Q@W#E$R";
/**
* 生产token
*/
public static String getToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7); //默认7天过期
builder.withExpiresAt(instance.getTime());//指定令牌的过期时间
String token = builder.sign(Algorithm.HMAC256(SECRET));//签名
return token;
}
/**
* 验证token
*/
public static DecodedJWT verify(String token) {
//如果有任何验证异常,此处都会抛出异常
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
return decodedJWT;
}
// /**
// * 获取token中的 payload
// */
// public static DecodedJWT getToken(String token) {
// DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
// return decodedJWT;
// }
}
测试controller
package com.chilly.controller;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.chilly.entity.User;
import com.chilly.service.UserService;
import com.chilly.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Resource
private UserService userService;
@GetMapping("/user/login")
public Map<String, Object> login(User user) {
log.info("用户名:{}", user.getName());
log.info("password: {}", user.getPassword());
Map<String, Object> map = new HashMap<>();
try {
User userDB = userService.login(user);
Map<String, String> payload = new HashMap<>();
payload.put("id", userDB.getId());
payload.put("name", userDB.getName());
String token = JWTUtils.getToken(payload);
map.put("state", true);
map.put("msg", "登录成功");
map.put("token", token);
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", e.getMessage());
map.put("token", "");
}
return map;
}
@PostMapping("/user/test")
public Map<String, Object> test(HttpServletRequest request) {
String token = request.getHeader("token");
DecodedJWT verify = JWTUtils.verify(token);
String id = verify.getClaim("id").asString();
String name = verify.getClaim("name").asString();
log.info("用户id:{}", id);
log.info("用户名: {}", name);
//TODO 业务逻辑
Map<String, Object> map = new HashMap<>();
map.put("state", true);
map.put("msg", "请求成功");
return map;
}
}
项目springboot 定义一个JWT拦截器
package com.chilly.interceptors;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.chilly.utils.JWTUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//获取请求头中的令牌
String token = request.getHeader("token");
log.info("当前token为:{}", token);
Map<String, Object> map = new HashMap<>();
try {
JWTUtils.verify(token);
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "签名不一致");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "令牌过期");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "算法不匹配");
} catch (InvalidClaimException e) {
e.printStackTrace();
map.put("msg", "失效的payload");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "token无效");
}
map.put("state", false);
//响应到前台: 将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
springboot 通过配置类 注入对应的拦截器,spirngboot会自动加载配置类
package com.chilly.config;
import com.chilly.interceptors.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")
.excludePathPatterns("/user/login")
;
}
}
Cookie+Session与JWT对比
- Cookie+Session:
用户登录认证中,因为http是无状态的,所以采用session方式。用户登录成功,服务端会保存一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
Cookie+session这种模式通常是保存在服务器内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。
JWT:只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。简单便捷,无需通过Redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验。
- JWT优点
占资源少:不在服务端保存信息。
扩展性:分布式中,Session需要做多机数据共享,通常存在数据库或者Redis里面。而JWT不需要。
跨语言:Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持;
- JWT缺点
安全性差:payload使用Base64编码,没有加密,因此JWT不能存敏感数据。而Session存在服务端,相对更安全。
无法废弃已颁布的令牌
一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。例如在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但由于旧的jwt还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。
解决方案:服务端部署额外的逻辑,例如:设置黑名单,一旦签发了新的JWT,旧的就加入黑名单(比如存到Redis里面),避免被再次使用。(违背JWT初衷)
- 过期
Cookie续签方案一般都是框架自带的,如:Session有效期30分钟,若30分钟内有访问,有效期刷新至30分钟。对于JWT,改变JWT的有效时间,就要签发新的JWT。
解决方案1::每次请求刷新JWT。即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。
解决方案2:在Redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。引入 Redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了JWT的初衷。而且这个方案和Session都差不多了
网友评论