转自:java技术爱好者
链接:https://www.zhihu.com/question/485758060/answer/2257869896
来源:知乎
需要了解一门技术,首先从为什么产生开始说起是最好的。JWT 主要用于用户登录鉴权,所以我们从最传统的 session 认证开始说起。
众所周知,http 协议本身是无状态的协议,那就意味着当有用户向系统使用账户名称和密码进行用户认证之后,下一次请求还要再一次用户认证才行。因为我们不能通过 http 协议知道是哪个用户发出的请求,所以如果要知道是哪个用户发出的请求,那就需要在服务器保存一份用户信息(保存至 session ),然后在认证成功后返回 cookie 值传递给浏览器,那么用户在下一次请求时就可以带上 cookie 值,服务器就可以识别是哪个用户发送的请求,是否已认证,是否登录过期等等。这就是传统的 session 认证方式。
session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。于是乎为了解决 session 共享的问题,又引入了 redis,接着往下看。
这种方式跟 session 的方式流程差不多,不同的地方在于保存的是一个 token 值到 redis,token 一般是一串随机的字符(比如UUID),value 一般是用户ID,并且设置一个过期时间。每次请求服务的时候带上 token 在请求头,后端接收到token 则根据 token 查一下 redis 是否存在,如果存在则表示用户已认证,如果 token 不存在则跳到登录界面让用户重新登录,登录成功后返回一个 token 值给客户端。
优点是多台服务器都是使用 redis 来存取 token,不存在不共享的问题,所以容易扩展。缺点是每次请求都需要查一下redis,会造成 redis 的压力,还有增加了请求的耗时,每个已登录的用户都要保存一个 token 在 redis,也会消耗 redis 的存储空间。
有没有更好的方式呢?接着往下看。
JWT (全称:Json Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
上面说法比较文绉绉,简单点说就是一种认证机制,让后台知道该请求是来自于受信的客户端。
首先我们先看一个流程图:
流程描述一下:
可以看出与token方式有一些不同的地方,就是不需要依赖 redis,用户信息存储在客户端。所以关键在于生成 JWT 和解析 JWT 这两个地方。
JWT 一般是这样一个字符串,分为三个部分,以 “.” 隔开:
xxxxx.yyyyy.zzzzz
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v1Odcreb-1653810812357)(什么是JWT.assets/v2-f999f0dd631b4dac907a29ed77fb1634_720w-16526077033222.jpg)]
JWT 第一部分是头部分,它是一个描述 JWT 元数据的 Json 对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为HS256),typ 属性表示令牌的类型,JWT 令牌统一写为JWT。
最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。
JWT 第二部分是 Payload,也是一个 Json 对象,除了包含需要传递的数据,还有七个默认的字段供选择。
iss (issuer):签发人/发行人
sub (subject):主题
aud (audience):用户
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
iat (Issued At):签发时间
jti (JWT ID):用于标识该 JWT
如果自定义字段,可以这样定义:
{
//默认字段
"sub":"主题123",
//自定义字段
"name":"java技术爱好者",
"isAdmin":"true",
"loginTime":"2021-12-05 12:00:03"
}
需要注意的是,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此一些敏感信息不要存放于此,以防信息泄露。
JSON 对象也使用 Base64 URL 算法转换为字符串后保存,是可以反向反编码回原样的,这也是为什么不要在 JWT 中放敏感数据的原因。
header (base64URL 加密后的)
payload (base64URL 加密后的)
secret
JWT 第三部分是签名。是这样生成的,首先需要指定一个 secret,该 secret 仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用 . 连接组成的字符串,然后通过header 中声明的加密算法 进行加盐secret组合加密,然后就得出一个签名哈希,也就是Signature,且无法反向解密。
那么 Application Server 如何进行验证呢?可以利用 JWT 前两段,用同一套哈希算法和同一个 secret 计算一个签名值,然后把计算出来的签名值和收到的 JWT 第三段比较,如果相同则认证通过。
首先引入Maven依赖。
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
创建工具类,用于创建(生成) jwt 字符串和解析 jwt。
@Component
public class JwtUtil {
@Value("${jwt.secretKey}")
private String secretKey;
public String createJWT(String id, String subject, long ttlMillis, Map map) throws Exception {
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 发行者
.setIssuedAt(new Date()) // 发行时间
.signWith(SignatureAlgorithm.HS256, secretKey) // 签名类型 与 密钥
.compressWith(CompressionCodecs.DEFLATE);// 对载荷进行压缩
if (!CollectionUtils.isEmpty(map)) {
builder.setClaims(map);
}
if (ttlMillis > 0) {
builder.setExpiration(new Date(System.currentTimeMillis() + ttlMillis));
}
return builder.compact();
}
public Claims parseJWT(String jwtString) {
return Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(jwtString)
.getBody();
}
}
接着在application.yml配置文件配置jwt.secretKey
。
## 用户生成jwt字符串的secretKey
jwt:
secretKey: ak47
接着创建一个响应体。
public class BaseResponse {
private String code;
private String msg;
public static BaseResponse success() {
return new BaseResponse("0", "成功");
}
public static BaseResponse fail() {
return new BaseResponse("1", "失败");
}
//构造器、getter、setter方法
}
public class JwtResponse extends BaseResponse {
private String jwtData;
public static JwtResponse success(String jwtData) {
BaseResponse success = BaseResponse.success();
return new JwtResponse(success.getCode(), success.getMsg(), jwtData);
}
public static JwtResponse fail(String jwtData) {
BaseResponse fail = BaseResponse.fail();
return new JwtResponse(fail.getCode(), fail.getMsg(), jwtData);
}
//构造器、getter、setter方法
}
接着创建一个UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public JwtResponse login(@RequestParam(name = "userName") String userName,
@RequestParam(name = "passWord") String passWord){
String jwt = "";
try {
jwt = userService.login(userName, passWord);
return JwtResponse.success(jwt);
} catch (Exception e) {
e.printStackTrace();
return JwtResponse.fail(jwt);
}
}
}
还有UserService:
@Service
public class UserServiceImpl implements UserService {
@Resource
private JwtUtil jwtUtil;
@Resource
private UserMapper userMapper;
@Override
public String login(String userName, String passWord) throws Exception {
//登录验证
User user = userMapper.findByUserNameAndPassword(userName, passWord);
if (user == null) {
return null;
}
//如果能查出,则表示账号密码正确,生成jwt返回
String uuid = UUID.randomUUID().toString().replace("-", "");
HashMap<String, Object> map = new HashMap<>();
map.put("name", user.getName());
map.put("age", user.getAge());
return jwtUtil.createJWT(uuid, "login subject", 0L, map);
}
}
还有UserMapper.xml:
@Mapper
public interface UserMapper {
User findByUserNameAndPassword(@Param("userName") String userName, @Param("passWord") String passWord);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.github.yehongzhi.jwtdemo.mapper.UserMapper">
<select id="findByUserNameAndPassword" resultType="io.github.yehongzhi.jwtdemo.model.User">
select * from user where user_name = #{userName} and pass_word = #{passWord}
select>
mapper>
user 表结构如下:
启动项目,然后用 postman 请求 login 接口。
返回的 jwt 字符串如下:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.qib2DrjRKcFnY77Cuh_b1zSzXfISOpCA-g8PlAZCWoU
接着我们写一个接口接收这个 jwt,并做验证。
@RestController
@RequestMapping("/jwt")
public class TestController {
@Resource
private JwtUtil jwtUtil;
@RequestMapping("/test")
public Map<String, Object> test(@RequestParam("jwt") String jwt) {
//这个步骤可以使用自定义注解+AOP编程做解析jwt的逻辑,这里为了简便就直接写在controller里
Claims claims = jwtUtil.parseJWT(jwt);
String name = claims.get("name", String.class);
String age = claims.get("age", String.class);
HashMap<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("age", age);
map.put("code", "0");
map.put("msg", "请求成功");
return map;
}
}
像这样能正常解析成功的话,就表示该用户登录未过期,并且已认证成功,所以可以正常调用服务。那么有人会问了,这个 jwt 字符串能不能被伪造呢?
除非你知道 secretKey,否则是不能伪造的。比如客户端随便猜一个 secretKey 的值,然后伪造一个jwt:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.bHr9p3-t2qR4R50vifRVyaYYImm2viZqiTlDdZHmF5Y
然后传进去解析,会报以下错误:
还记得原理吧,是根据前面两部分(Header、Payload)加上 secretKey 使用 Header 指定的哈希算法计算出第三部分(Signature),所以可以看出最关键就是 secretKey。secretKey只有服务端自己知道,所以客户端不知道 secretKey 的值是伪造不了jwt字符串的。
最后讲讲 JWT 的缺点,因为任何技术都不是完美的,所以我们得用辩证思维去看待任何一项技术。
的,所以我们得用辩证思维去看待任何一项技术。
所以印证了那句话,没有最好的技术,只有适合的技术。