掌握JWT:解密身份验证和授权的关键技术

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 掌握JWT:解密身份验证和授权的关键技术


1、什么是JWT

官方文档解释:JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

官网地址: https://jwt.io/introduction/

通俗来讲,JWT是一个含签名并携带用户相关信息的加密串,页面请求校验登录接口时,客户端请求头中携带JWT串到后端服务,后端通过签名加密串匹配校验,保证信息未被篡改。校验通过则认为是可靠的请求,将正常返回数据。

2、JWT解决了什么问题

  • 授权:这是最常见的使用场景,解决单点登录问题。因为JWT使用起来轻便,开销小,服务端不用记录用户状态信息(无状态),所以使用比较广泛;
  • 信息交换:JWT是在各个服务之间安全传输信息的好方法。因为JWT可以签名,例如,使用公钥/私钥是以对儿 - 可以确定请求方是合法的。此外,由于使用标头和有效负载计算签名,还可以验证内容是否未被篡改。

3、早期的SSO认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

4、JWT认证

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议)。
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同aaaa.bbb.cc的字符串。 token head.payload.singurater。
  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  • 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER。
  • 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  • 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

5、JWT优势

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  • 简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快。

  • 自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库。

  • 自校验

对token可以自己校验是否过期。

6、JWT结构

令牌组成

  • 标头(Header)
  • 有效载荷(Payload)
  • 签名(Signature)
Header 标头

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法。它会使用 Base64 对header做编码,组成而来JWT结构的第一部分。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

{
  "alg": "HS256", # 签名算法
  "typ": "JWT" # 类型
}
Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。同样的,它也会使用 Base64 编码组成 JWT 结构的第二部分。

{
    "iss": "demo JWT",
    "iat": 1342513302,
    "exp": 1342513302,
    "name": "admin",
    "sub": "dev"
}

Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了

签名的目的:

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息安全性:

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

7、代码实现

添加依赖

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

生成Token

@Test
  void testCreateToken() {
    // 1.设置超时时间
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.SECOND,30); // 超时时间是30s
    // 2.创建JWTbuilder
    JWTCreator.Builder builder = JWT.create();
    // 3.设置头,负载,签名
    String token = builder
//        .withHeader(map) 设置头信息,可以不设置有默认值
        .withClaim("name", "admin")
        .withClaim("id", 10) // 设置用户自定义属性
        .withExpiresAt(calendar.getTime()) // 设置令牌超时时间
        .sign(Algorithm.HMAC256("dalaoshi"));// 设置用户签名
    // 4.输出结果
    System.out.println(token);
  }

认证token

String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6MTAsImV4cCI6MTU5OTQwNTQ2NH0.7YFYieOC-ChS32He7DqyVtECCvM4nFWmb7hKLiPAIXY\n";
        // 1.根据用户签签名获取JTW校验器
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("dalaoshi")).build();
        // 2.验证token
        DecodedJWT verify = jwtVerifier.verify(token);
        // 3.获取token的数据
        System.out.println(verify.getClaim("name").asString()); // 字符串使用asString()
        System.out.println(verify.getClaim("id").asInt()); // int使用asInt
        System.out.println(verify.getExpiresAt()); // 获取过期时间

认证常见的异常

- SignatureVerificationException:   签名不一致异常
- TokenExpiredException:          令牌过期异常
- AlgorithmMismatchException:     算法不匹配异常
- InvalidClaimException:        失效的payload异常

8、工具类

public class JWTUtils {
    private static String sign = "dalaoshi";
    public static String createToken(Map<String, String> map) {
        // 1.设置超时时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, 7); // 7天
        // 2.创建JWTbuilder
        JWTCreator.Builder builder = JWT.create();
        // 设置负载数据
        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> entrie : entries) {
            builder.withClaim(entrie.getKey(), entrie.getValue());
        }
        // 3.设置签名,过期时间
        String token = builder
                .withExpiresAt(calendar.getTime()) // 设置令牌超时时间
                .sign(getSignature());// 设置用户签名
        // 4.返回
        return token;
    }
    // 获取起签名
    public static Algorithm getSignature() {
        return Algorithm.HMAC256(sign);
    }
    // 校验
    public static DecodedJWT require(String token) {
        return JWT.require(getSignature()).build().verify(token);
    }
    // 获取token中的数据
    public static Claim getPayload(String token, String key) {
        return require(token).getClaim(key);
    }
}

9、JWT整合Web

@Autowired
    private IUserService userService;
    @RequestMapping("/login")
    public ResultEntity login(String username,String password){
        ResultEntity resultEntity = userService.login(username, password);
        if(ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
            Map<String,String> map = new HashMap<>();
            map.put("id","10");
            map.put("username",username);
            String token = JWTUtils.createToken(map);
            return ResultEntity.success(token);
        }else{
            return ResultEntity.error("登录失败");
        }
    }
    @RequestMapping("/require")
    public ResultEntity require(String token){
        try {
            DecodedJWT require = JWTUtils.require(token);
            return ResultEntity.response(require);
        }catch (TokenExpiredException e){
            return ResultEntity.error("token过期");
        }catch (SignatureVerificationException e){
            return ResultEntity.error("用户签名不一致");
        } catch (InvalidClaimException e){
            return ResultEntity.error("payload数据有误");
        }catch (Exception e){
            return ResultEntity.error("校验失败");
        }
    }
    @RequestMapping(value = "/getPayLoad")
    public ResultEntity getPayLoad(String token){
        DecodedJWT decodedJWT = JWTUtils.require(token);
        Map<String, Claim> claims = decodedJWT.getClaims();
        Map<String,String> map = new HashMap<>();
        Set<Map.Entry<String, Claim>> entries = claims.entrySet();
        for (Map.Entry<String, Claim> entrie:entries) {
            map.put(entrie.getKey(),entrie.getValue().asString());
        }
        return ResultEntity.success(map);
    }

10、拦截器校验

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 1.获取token
  String token = request.getHeader("token");
  Map<String,Object> map = new HashMap<>();
  try {
      // 2.校验
    JWTUtils.verify(token);
    return true;
  }catch (TokenExpiredException e){
      return ResultEntity.error("token过期");
  }catch (SignatureVerificationException e){
      return ResultEntity.error("用户签名不一致");
  } catch (InvalidClaimException e){
      return ResultEntity.error("payload数据有误");
  }catch (Exception e){
      return ResultEntity.error("校验失败");
  }
    // 3.校验失败响应数据
  String json = new ObjectMapper().writeValueAsString(map);
  response.setContentType("application/json;charset=UTF-8");
  response.getWriter().println(json);
  return false;
}

11、网关路由校验

@Component
public class SSOFilter extends ZuulFilter{
    @Autowired
    private ISSOService ssoService;
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
    }
    @Override
    public boolean shouldFilter() {
        return true;
    }
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        StringBuffer requestURL = request.getRequestURL();
        System.out.println(requestURL);
        // 1.该服务是否需要验证
        if("http://localhost/shop-back/user/getUserPage".equals(requestURL.toString())){
            String token = request.getHeader("token");
            // 2.验证服务
            ResultEntity resultEntity = ssoService.require(token);
            System.out.println(resultEntity);
            if(!ResultEntity.SUCEESS.equals(resultEntity.getStatus())){
                requestContext.setSendZuulResponse(false); // 不能往下执行了
                HttpServletResponse response = requestContext.getResponse();
                response.setContentType("application/json;charset=utf-8"); // 设置响应数据类型
                requestContext.setResponseBody(JSON.toJSONString(ResultEntity.error("校验未通过"))); // 设置响应数据
            }
        }
        return null;
    }
}

12、解决多用户登录的问题

如果一个用户登录在多个设备登录,就会出现一个用户多个token在多个设备上同时登录。如果要解决这个问题就要判断用户操作的token是否是最新的,只有是最新的token才能认证成功。

// 伪代码
// login
public String login(String name,String password){
    // 1.查询数据库认证
    // 2.生成token
    String token = "";
    // 3.把用户最新的token放入到reids中
    redisTemp.set(username,token); // username作为key,多次登录key会被覆盖
}
// 路由校验
// 1.获取用户token
// 2.根据用户名查询用户最新的token
// 3.对比两个token是否一致,如果不一致就说明用户进行了第二次登陆,就不让认证通过。

13、客户端保存/携带token

// 登录获取token,保存到本地  
function login(){
            var username ="admin";
            var password ="123";
            var param = new Object();
            param.username=username;
            param.password=password;
            $.post("http://localhost/shop-sso/sso/login",param,function (data) {
                if(data.status ="success"){
                    // 获取token
                    var token = data.data;
                    // 保存toke到客户端
                    localStorage.setItem("login-token",token);
                }
            },"JSON");
        }
// 发送请求是把token放到请求头中保存
   function sendRequest(){
            $.ajax({
                url: "http://localhost/shop-sso/addXxxxx",
                type: "post",
                dataType: 'json',
                beforeSend: function (XMLHttpRequest) {
                    // 获取本地储存的token,添加到请求头中
                    XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
                },
                success: function (result) {
                }
            });
        }

为什么要把token放在请求头中的Authorization中?

a)保存在请求头中方便和其他参数区分

b)保存在请求头中可以解决跨域的问题,比如cookie是存在跨域的问题

c)Authorization header就是为用户认证而生的。

d)解决XSS和XSRF问题

14、抽取ajax工具类

window.utils={
    ajax:function(param){
        $.ajax({
            url: param,
            type: "post",
            dataType: 'json',
            data:param.data,
            beforeSend: function (XMLHttpRequest) {
                XMLHttpRequest.setRequestHeader("Authorization", localStorage.getItem("login-token"));
            },
            success: function (result) {
                param.success(result);
            }
        });
    }
}
// 调用
utils.ajax({
    url:"http://localhost/shop-sso/sso/login",
    data:param,
    success:function(data){
        if(data.status ="success"){
            // 获取token
            var token = data.data;
            // 保存toke到客户端
            localStorage.setItem("login-token",token);
        }
    }
})

15、a标签跳转如何传递token

token只针对api设计,和原生标签的跳转没有直接的关系。如果请求跳转可以在url后面携带token。

后记
👉👉💕💕美好的一天,到此结束,下次继续努力!欲知后续,请看下回分解,写作不易,感谢大家的支持!! 🌹🌹🌹

相关文章
|
6月前
|
JSON 算法 安全
Nest.js JWT 验证授权管理
Nest.js JWT 验证授权管理
155 3
Nest.js JWT 验证授权管理
|
6月前
|
存储 JSON 算法
无懈可击的身份验证:深入了解JWT的工作原理
无懈可击的身份验证:深入了解JWT的工作原理
966 0
|
6月前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
207 0
|
2月前
|
存储 中间件 API
ThinkPHP 集成 jwt 技术 token 验证
本文介绍了在ThinkPHP框架中集成JWT技术进行token验证的流程,包括安装JWT扩展、创建Token服务类、编写中间件进行Token校验、配置路由中间件以及测试Token验证的步骤和代码示例。
ThinkPHP 集成 jwt 技术 token 验证
|
1月前
|
存储 JSON 算法
JWT令牌基础教程 全方位带你剖析JWT令牌,在Springboot中使用JWT技术体系,完成拦截器的实现 Interceptor (后附源码)
文章介绍了JWT令牌的基础教程,包括其应用场景、组成部分、生成和校验方法,并在Springboot中使用JWT技术体系完成拦截器的实现。
71 0
JWT令牌基础教程 全方位带你剖析JWT令牌,在Springboot中使用JWT技术体系,完成拦截器的实现 Interceptor (后附源码)
|
3月前
|
JSON 安全 算法
使用Go轻松实现JWT身份验证
使用Go轻松实现JWT身份验证
|
3月前
|
JSON 安全 数据安全/隐私保护
Python安全新篇章:OAuth与JWT携手,开启认证与授权的新时代
【8月更文挑战第6天】随着互联网应用的发展,安全认证与授权变得至关重要。本文介绍OAuth与JWT两种关键技术,并展示如何结合它们构建安全系统。OAuth允许用户授权第三方应用访问特定信息,无需分享登录凭证。JWT是一种自包含的信息传输格式,用于安全地传递信息。通过OAuth认证用户并获取JWT,可以验证用户身份并保护数据安全,为用户提供可靠的身份验证体验。
46 6
|
6月前
|
JSON 安全 程序员
[JavaWeb]——JWT令牌技术,如何从获取JWT令牌
[JavaWeb]——JWT令牌技术,如何从获取JWT令牌
104 0
|
4月前
|
存储 JSON 安全
OAuth2与JWT在API安全中的角色:技术深度解析
【7月更文挑战第20天】OAuth2和JWT作为两种重要的安全协议,在API安全中发挥着不可或缺的作用。OAuth2通过提供灵活的授权框架,实现了对资源的细粒度访问控制;而JWT则通过其紧凑性和自包含性,确保了身份验证和信息传输的安全性。在实际应用中,将OAuth2和JWT结合使用,可以构建出既强大又安全的API服务,为用户提供更加安全、可靠和便捷的数字体验。
|
3月前
|
安全 Nacos 数据安全/隐私保护
【技术干货】破解Nacos安全隐患:连接用户名与密码明文传输!掌握HTTPS、JWT与OAuth2.0加密秘籍,打造坚不可摧的微服务注册与配置中心!从原理到实践,全方位解析如何构建安全防护体系,让您从此告别数据泄露风险!
【8月更文挑战第15天】Nacos是一款广受好评的微服务注册与配置中心,但其连接用户名和密码的明文传输成为安全隐患。本文探讨加密策略提升安全性。首先介绍明文传输风险,随后对比三种加密方案:HTTPS简化数据保护;JWT令牌减少凭证传输,适配分布式环境;OAuth2.0增强安全,支持多授权模式。每种方案各有千秋,开发者需根据具体需求选择最佳实践,确保服务安全稳定运行。
320 0