一、JWT是什么?
JWT是一种基于JSON的轻量级身份验证和授权方案,全称为JSON Web Token。它可以在网络应用之间传递信息,用于身份认证和授权。
JWT包含三部分:头部、载荷和签名。最后由这三者组合进行base64url编码得到JWT,JWT为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
- 头部指定了JWT类型,以及所用的加密算法
{ "alg": "HS256", "typ": "JWT" } 在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256) typ属性表示令牌的类型,JWT令牌统一写为JWT 最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
- 载荷则包含了需要传递的信息,是JWT的主体内容部分,也是一个JSON对象,JWT指定七个默认字段供选择。
iss: jwt签发者 sub: 主题 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 { "name": "Helen", "role": "editor", "avatar": "helen.jpg" } 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。 JSON对象也使用Base64 URL算法转换为字符串保存。
- 签名则用于是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret) ==> 签名hash
- Base64URL算法,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法。
JWT的优点是不需要在服务端存储会话信息,减轻了服务器的负担。而且JWT本身已经包含了授权信息,减少了对服务器频繁查询数据库的需要,提高了应用性能。
JWT最重要的作用就是对 token信息的防伪作用。
二、使用JWT
1.引入Maven依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
2.创建JWT工具类
@Component
public class JwtUtil {
private static String secretKey = "123456";
public String createJWT(String id, String subject, long ttlMillis, Map<String, Object> 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();
}
}
3.Controll、ServiceImpl
@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);
}
}
}
@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);
}
}
4.Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.atguigu.system.mapper.SysMenuMapper">
<resultMap id="sysMenuMap" type="com.atguigu.model.system.SysMenu" autoMapping="true"></resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
</sql>
<select id="findListByUserId" resultMap="sysMenuMap">
select
distinct <include refid="columns" />
from sys_menu m
inner join sys_role_menu rm on rm.menu_id = m.id
inner join sys_user_role ur on ur.role_id = rm.role_id
where
ur.user_id = #{userId}
and m.status = 1
and rm.is_deleted = 0
and ur.is_deleted = 0
and m.is_deleted = 0
</select>
</mapper>
5.接收JWT
@RestController
@RequestMapping("/jwt")
public class TestController {
@Resource
private JwtUtil jwtUtil;
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 的缺点,因为任何技术都不是完美的,所以我们得用辩证思维去看待任何一项技术。
- 安全性没法保证,所以 jwt 里不能存储敏感数据。因为 jwt 的 payload 并没有加密,只是用 Base64 编码而已。
- 无法中途废弃。因为一旦签发了一个 jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的 jwt 过期后重新签发新的 jwt。
- 续签问题。当签发的 JWT 保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当 JWT有效期到了就会导致用户需要重新登录。那么怎么为JWT续签呢?最简单粗暴就是每次签发新的 JWT,但是由于过于暴力,会影响性能。如果要优雅一点,又要引入 Redis 解决,但是这又把无状态的 JWT 硬生生变成了有状态的,违背了初衷。