JWT实现信息安全传输

     背景

        这两天在整理之前研发的单体服务时,偶然发现了一个好玩的东西:JWT。该服务中集成了用户登陆和权鉴功能,用户登陆后,通过JWT将用户的基本信息进行签名,存储到cookie里,后面的请求自动带上cookie,通过过滤器检查cookie里的用户信息是否存在,是否过期,是否被篡改,从而达到校验用户身份的效果。而这些信息的签名,解析,校验,仅通过JWT用几行代码就可以实现。我当时看到这个只觉眼前一亮:嘿,这小东西还挺别致哈,必须得拿来给宝子们分享下!

     JWT介绍

        JWT是JSON Web Token的缩写。它是一种用于在网络应用之间安全传递信息的方式。JWT由三部分组成,分别是Header(头部),Payload(负载)和Signature(签名)。

        Header(头部)用于描述关于该JWT的最基本的信息,比如JWT的类型(typ)和签名算法(alg)等。JWT的头部信息和负载信息都是经过Base64编码的,并通过"."连接起来形成最终的JWT。设置头部信息的作用有以下几点:
        1) 指定JWT的类型:JWT可以分为JWS(JSON Web Signature)和JWE(JSON Web Encryption),JWT三种类型。JWS用于签名,JWE用于加密。在头部信息中,可以通过设置typ字段来指定JWT的类型,方便客户端进行相应的处理。

        2) 指定JWT的签名算法:JWT的签名算法决定了JWT的安全性和可靠性。在头部信息中,可以通过设置alg字段来指定JWT的签名算法。常见的签名算法包括HS256、RS256等。(一般不在header里设置,而是用.sign(Algorithm)来设置签名算法)。

        3) 添加自定义头部信息:在头部信息中,还可以添加自定义的字段,用于传递额外的信息。比如,可以添加kid字段来指定签名密钥的ID,方便客户端进行密钥管理。

        Payload(负载)包含了需要传递的用户信息,如用户ID、角色、权限等。Signature(签名)则是对Header和Payload进行签名,防止信息被篡改。

        JWT的优点是易于使用和传递,由于信息已经被签名,可以防止信息篡改,因此可以实现无状态的分布式系统。但是,由于JWT中包含了用户信息,因此一旦被窃取,可能会导致安全问题。因此在使用JWT时需要注意安全性,并采取相应的措施,如使用HTTPS协议等。
        在使用JWT时,服务端会生成一个JWT并将其返回给客户端。客户端之后每次请求时都会将该JWT加入到请求头部中,服务端会验证该JWT的合法性,并根据其中的信息进行相应的操作。由于JWT中已包含了用户信息,因此无需再次查询数据库,可以提高系统的性能。

     JWT实践

        闲话不多说,咱们这就来实践JWT的数据签名和信息获取。

        1、引入依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>

        2、编写JWT工具类

// 用户类
@Data
public class UserVO {

    private String userAccount;

    private String password;

    private String userName;
}


// JWT工具类
public class JwtUtils {

    // token密钥
    private static final String TOKEN_SECRET = "leixiyueqi";

    public static final String USER_INFO_KEY = "userInfo";

    /**
     * 将数据转换成JWT
     *
     * @param subject 主题
     * @param obj 数据
     * @return 转换后的token值
     */
    public static String token(String subject, Object obj) {
        String token = "";
        try {
            //秘钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            //设置头部信息
            Map<String, Object> header = new HashMap<>();
            header.put("typ", "JWT");
            token = JWT.create()
                    // 设置主题
                    .withSubject(subject)
                    // 设置头
                    .withHeader(header)
                    // 设置数据
                    .withClaim("data", JSON.toJSONString(obj))
                    // 设置签发时间
                    .withIssuedAt(new Date())
                    // 设置过期时间(expiration) 为1小时
                    .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
                    // 设置签名算法
                    .sign(algorithm);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return token;
    }


    /**
     * @desc 验证token,通过返回true
     **/
    public static void verify(String token) {
        try {
            JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).build().verify(token);
        } catch (Exception e) {
            throw e;
        }
    }

    public static String getTokenStr(String token, String data) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(data).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 从token中获取信息,并进行转码
     *
     * @param token
     * @param clazz
     * @return
     * @param <T>
     */
    public static <T> T getTokenData(String token, Class<T> clazz) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            Map<String, Claim> claims = jwt.getClaims();
            String data = claims.get("data").asString();
            return JSON.parseObject(data, clazz);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 从cookie中获取信息并解码
     *
     * @param request
     * @return
     */
    public static <T> T getDataFromCookie(HttpServletRequest request, String cookieName, Class<T> clazz) {
        String cookieLoginValue = getCookieFromRequest(request, cookieName);
        try {
            verify(cookieLoginValue);
            return getTokenData(cookieLoginValue, clazz);
        } catch (Exception e) {
            return null;
        }
    }


    public static String getCookieFromRequest(HttpServletRequest request, String cookieName) {
        String cookieLoginValue = Strings.EMPTY;
        Cookie[] cookies = request.getCookies();
        if (cookies == null || cookies.length == 0) {
            return cookieLoginValue;

        }
        for (Cookie cookie : cookies) {
            if (cookieName.equals(cookie.getName())) {
                cookieLoginValue = cookie.getValue();
                break;
            }
        }
        return cookieLoginValue;
    }
}

        3、添加controller

@RestController
public class UserController {

    @PostMapping("/user/login")
    public Object login(@RequestBody UserVO user, HttpServletResponse response) throws Exception {
        if (!user.getPassword().equals("123456")) {
            throw new Exception("用户密码错误,登陆失败");
        }
        // 将用户信息转成token,存到key为userInfo的cookie中
        Cookie cookie = new Cookie(JwtUtils.USER_INFO_KEY, JwtUtils.token("LOGIN", user));
        cookie.setPath("127.0.0.1");
        response.addCookie(cookie);
        return "登陆成功";
    }


    @GetMapping("/getUserInfo")
    public Object getUserInfo(HttpServletRequest request){
        return JwtUtils.getDataFromCookie(request, JwtUtils.USER_INFO_KEY, UserVO.class);
    }

}

        4、postman执行测试

        用postman执行了login之后,自动添加了cookie到工具上,直接运行查询接口,就可以查询到用户信息。

        5、添加拦截器:如果想要每次发送请求时,都对cookie进行校验,检查cookie中的数据是否准确,有没有过期,则可以通过添加一个拦截器来实现。如下:

// 配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor()).
                excludePathPatterns("/user/login")  // 放行
                .addPathPatterns("/**"); // 拦截除了"/user/**的所有请求路径
    }
}

//拦截器类
public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = JwtUtils.getCookieFromRequest(request, JwtUtils.USER_INFO_KEY);
        Map<String,Object> map = new HashMap<>();
        try {
            JwtUtils.verify(token);
            return true;
        } catch (TokenExpiredException e) {
            map.put("state", false);
            map.put("msg", "Token已经过期!!!");
        } catch (SignatureVerificationException e){
            map.put("state", false);
            map.put("msg", "签名错误!!!");
        } catch (AlgorithmMismatchException e){
            map.put("state", false);
            map.put("msg", "加密算法不匹配!!!");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state", false);
            map.put("msg", "无效token~~");
        }
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }
}

       我们把JwtUtils里面的过期时间设置为8000ms,重新启动服务,先调/user/login刷新cookie,等8秒后再调/getUserInfo,结果如下图,说明拦截器是成功的。

     扩展测试

        在实践JWT的功能时,我突发其想,既然token是密文,我能不能用什么办法将密文给破解了呢?于是我从postman中拿到response里返回的cookie,直接对token进行解析,然后再进行验证,结果如下:

        事实证明,JWT加密后的数据可以直接通过JWT.decode()方法进行解密,根本不需要知道加密时用的什么签名key和算法,所以JWT加密的数据是非常不安全的,千万别像雷袭那样把用户密码都放到token里。但是,在verify时,会用生成token的算法和secretKey对token进行检查,如果发现token时间过期,或者算法、secretKey不符合预设,内容不一致,均会报错。所以verify()方法防止了信息被篡改,实现了数据的安全传输。在进行token解析之前,一定要先对token执行verify()方法。

        最后,在进行JWT实践时,我参考了这篇文章jwt原理_bigrobin的技术博客_51CTO博客

        感谢大佬引路之恩。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值