目录
JWT 最后的形式就是个字符串,它由头部、载荷与签名这三部分组成,中间以「.」分隔。像下面这样:

Header:
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
Payload:
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
Signature:
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
如:
第三部分内容:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),私钥);

签名目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
如果JWT的前2部分的内容被修改了,服务器接收到token后,会先获取前两部分的内容+私钥加密后和token中的第三部分的签名进行对比。如果一致就是合法的
信息安全问题:
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏 感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第 三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
1、在用户登录网站的时候,需要输入用户名、密码或者短信验证的方式登录,登录请求到达服务端的时候,服务端对账号、密码进行验证,然后计算出 JWT 字符串,返回给客户端。
2、客户端拿到这个 JWT 字符串后,存储到 cookie 或者 浏览器的 LocalStorage 中。
3、再次发送请求,比如请求用户设置页面的时候,在 HTTP 请求头中加入 JWT 字符串,或者直接放到请求主体中。
4、服务端拿到这串 JWT 字符串后,使用 base64的头部和 base64 的载荷部分加上私钥,通过HMACSHA256算法计算签名部分,比较计算结果和传来的签名部分是否一致,如果一致,说明此次请求没有问题,如果不一致,说明请求过期或者是非法请求。


保证安全性的关键就是 私钥和HMACSHA256 或者与它同类型的加密算法,因为加密过程是不可逆的,所以不能根据传到前端的 JWT 传反解到密钥信息。另外,不同的头部和载荷加密之后得到的签名都是不同的,所以,如果有人改了载荷部分的信息,那最后加密出的结果肯定就和改之前的不一样的,所以,最后验证的结果就是不合法的请求。
私钥如果不小心泄露会怎么样?
就算私钥泄露了,强盗也无法修改token携带的信息。如果修改,后台还是无法校验通过。如果不做更改,直接用呢,那就没有办法了,为了更大程度上防止被强盗盗取,应该使用 HTTPS 协议而不是 HTTP 协议,这样可以有效的防止一些中间劫持攻击行为。
- <!--引入jwt-->
- <dependency>
- <groupId>com.auth0</groupId>
- <artifactId>java-jwt</artifactId>
- <version>3.4.0</version>
- </dependency>
- @Test
- public void makeToken(){
- //1、日历类
- Calendar calendar=Calendar.getInstance();
- //2、设置时间为 1小时过期
- calendar.add(Calendar.SECOND,60);
- Map<String,Object> map=new HashMap<>();
- //3、生成令牌
- String token = JWT.create()
- .withHeader(map)//第一部分 头使用默认值 签名算法:HS256 type:jwt
- .withClaim("username", "rk")//第二部分
- .withClaim("age", 20)
- .withIssuedAt(new Date(System.currentTimeMillis()))//设置令牌发放时间
- .withExpiresAt(calendar.getTime())//设置过期时间
- .sign(Algorithm.HMAC256("LUOLIN!@$%#@"));//第三部分 设置签名,LUOLIN!@$%#@为秘钥
- //4、输出令牌
- System.out.println(token);
- }
- //解析token
- @Test
- public void parseToken(){
- try {
- String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTYzNzk5OTAsImlhdCI6MTY1NjM3OTkzMCwiYWdlIjoyMCwidXNlcm5hbWUiOiJyayJ9.-Pcm-dox8IG2ZT_gve7ApVHCOvM1M9mHDicgDBm8H0k";
- //1、创建验证对象
- JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("LUOLIN!@$%#@")).build();
- //2、校验签名(校验token是否合法)
- DecodedJWT jwt = jwtVerifier.verify(token);
-
- System.out.println("用户名:"+jwt.getClaim("username").asString());
- System.out.println("年龄:"+jwt.getClaim("age").asInt());
- SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
- System.out.println("令牌发放时间:"+sdf.format(jwt.getIssuedAt()));
- System.out.println("令牌过期时间:"+sdf.format(jwt.getExpiresAt()));
- }
- catch (TokenExpiredException e){
- System.out.println("token已过期!");
- }
- catch (JWTVerificationException e) {
- System.out.println("token不合法!");
- }
- }
常见异常:
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
- public class JWTUtils {
- private static String TOKEN = "token!Q@W3e4r";
- /**
- * 生成token
- * @param map //传入payload
- * @return 返回token
- */
- public static String getToken(Map<String,String> map){
- JWTCreator.Builder builder = JWT.create();
- map.forEach((k,v)->{
- builder.withClaim(k,v);
- });
- Calendar instance = Calendar.getInstance();
- instance.add(Calendar.SECOND,7);
- builder.withExpiresAt(instance.getTime());
- return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
- }
- /**
- * 验证token
- * @param token
- * @return
- */
- public static void verify(String token){
- JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
- }
- /**
- * 获取token中payload
- * @param token
- * @return
- */
- public static DecodedJWT getToken(String token){
- return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
- }
- }
登录Controller:
- @GetMapping("/user/login")
- public Map<String,Object> login(User user){
- 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());
- //生成JWT的令牌
- String token = JWTUtils.getToken(payload);
-
- //返回值赋值
- map.put("state",true);
- map.put("msg","登录成功!");
- map.put("token",token);//响应token
- }catch (Exception e){
- map.put("state",false);
- map.put("msg",e.getMessage());
- }
- return map;
- }
登录Service:
- @Autowired
- private UserDAO userDAO;
- @Override
- @Transactional(propagation = Propagation.SUPPORTS)
- public User login(User user) {
- //根据接收用户名密码查询数据库
- User userDB = userDAO.login(user);
- if(userDB!=null){
- return userDB;
- }
- throw new RuntimeException("登录失败");
- }
拦截器:
拦截器配置:
- @Configuration
- public class InterceptorConfig implements WebMvcConfigurer {
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new JWTInterceptor())//将jwt拦截器添加进去
- .excludePathPatterns("/user/login")//放行登录接口
- .addPathPatterns("/**"); //拦截接口
- }
- }
- public class JWTInterceptor implements HandlerInterceptor {
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- //拦截处理
- Map<String, Object> map = new HashMap<>();
- //获取请求头中令牌
- String token = request.getHeader("token");
- try {
- JWTUtils.verify(token);//验证令牌
- return true;//放行请求
- } catch (SignatureVerificationException e) {
- e.printStackTrace();
- map.put("msg","无效签名!");
- }catch (TokenExpiredException e){
- e.printStackTrace();
- map.put("msg","token过期!");
- }catch (AlgorithmMismatchException e){
- e.printStackTrace();
- map.put("msg","token算法不一致!");
- }catch (Exception e){
- e.printStackTrace();
- map.put("msg","token无效!!");
- }
- map.put("state",false);//设置状态
- //将map 转化为json jackson
- String json = new ObjectMapper().writeValueAsString(map);
- response.setContentType("application/json;charset=UTF-8");
- response.getWriter().println(json);
- return false;
- }
- }
拦截器的作用主要就是校验token 是否合法,每次发起请求前都会先通过拦截器校验token。
测试接口:
- @PostMapping("/user/test")
- public Map<String,Object> test(HttpServletRequest request){
- Map<String, Object> map = new HashMap<>();
- //处理自己业务逻辑
- String token = request.getHeader("token");
- DecodedJWT verify = JWTUtils.verify(token);
- log.info("用户id: [{}]",verify.getClaim("id").asString());
- log.info("用户name: [{}]",verify.getClaim("name").asString());
- map.put("state",true);
- map.put("msg","请求成功!");
- return map;
- }
拦截器用来校验token,其他接口就只需要获取token后来解析其中的值,处理自己的业务逻辑就行了

未登录的情况下访问其它接口会被拦截器拦截。

登录成功后返回token给前端界面。


登录成功后携带token继续访问其它接口,拦截器校验通过。test接口也获取解析出来了token中携带的值。
- //jwt的token解码方式:
- //首先拿到token码然后以点为分隔符转为数组
- let token=localStorage.getItem(‘token’).split(".");
- console.log(token);
- //拿到第二段token也就是负载的那段 进行window.atob方法的 base64的解算,
- 然后再用decodeURIComponent字符串解码方法 解析出字符串 然后再转成JSON对象
- 由于atob()方法解码无法对中文解析 所以要再用escape()方法对其重新编码
- 然后再用decodeURI解码方式解析出来
- let str=token[1];
- let user=JSON.parse(decodeURIComponent(escape(window.atob(str))));
- console.log(user.username);