阅读 165

OAuth 2.0(三):令牌机制

一、OAuth 2.0 的核心是什么?

回顾之前 OAuth 第一篇文章中用户访问客户的故事,有一个关键名词临时访问二维码

当用户出示一系列信息和操作之后拿到这个二维码,它就可以去见到客户了。

这个二维码回到 OAuth 中,其实就是令牌,那么具体来看令牌到底是什么呢?

其实就是授权之后的结果,前面做的一系列操作都是为了获取最终这个令牌,有了令牌凭据,才能去访问这个令牌能请求的路由、服务、资源等。

可见 OAuth 2.0 的核心就是颁发访问令牌,使用访问令牌。

二、OAuth 2.0 中的令牌

OAuth 官网没有限定令牌的格式,但目前只支持一种,就是 bearer 令牌。

可以是随机字符串,也可以是自定义的数据结构,但都要符合三个条件。

唯一性、不连续、不可猜。

三、JWT

1、是什么

JSON Web Token 是一种基于 RFC7519 协议的开放标准,定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全的传输数据。

 

简单理解:其实是一组自定义的结构化数据,通过结构化的方式去生成 token

一方面是使得返回给客户端的数据有意义

另外一方面是自编码能力,扩展性强

  此外还可以通过签名来保护数据安全,JWT 是需要在 HTML 和 HTTP 等环境下传输的

一方面是需要做 URLEncode(编码)保障不乱码且不丢失数据

另外一方面是做加密,避免失窃

官方文档:jwt.io/introductio…

橘长对其做了翻译:github.com/AFlymamba/t…

 

2、组成

内部包含了三部分,头部、有效负荷、签名,三部分之间通过 .(点) 连接

如下是一个简单的 jwt 示例:

header.payload.signature复制代码

1)header(头部)

JWT 的第一部分是 Header(头部),通常由两部分组成,type 和 sign

type 表示令牌类型,在此自然是”JWT“

此外 sign 表示采用的签名算法,可以是 HMAC SHA256 或 RSA 等

如下是一个简单示例:

{
  "type": "JWT",
  "sign": "HS256"
}复制代码

最后生成的头部是需要把这一串 JSON 串做 URLEncode 操作,得到一个字符串。

JSONObject headerJson = JSONUtil.createObj();
String headerStr = base64UrlEncode(headerJson);复制代码
  • payload(有效负荷)

JWT 的第二部分是 Payload(有效负荷),这是 JWT 最有价值的一部分

其中包含声明(Claims),声明是关于实体(通常是用户)和其他数据的声明

简单理解就是自定义的那部分数据。

JWT 官方有三种类型的声明,分别是 注册声明、公开声明、私有声明。

注册声明:一组框架预定义的声明,非强制性要求每个都实现,但建议使用,通过这个可以提供一组有用的、可操作的数据。

公开声明:JWT 使用者自定义的,但是需要注意不要和官方提供的数据相冲突。

私有声明:适用场景是数据传输,当通信双方约定好数据结构之后,做共享数据用。

【注意】Payload 的声明只有三个字符长度,符合 紧凑型 特点。

特别要来学习官方推荐的:注册声明

挑选了场景的一些数据定义做示例

iss:issuer(JWT 的发行者)

exp:expression time(JWT 过期时间)

sub:subject(主体)

aud:audience(受众)

如下给出一个 payload 的 json 结构:

{
    "sub": 1,
    "name": "John",
    "admin": true
}复制代码

最后生成的 payload 是需要把这一串 JSON 串做 URLEncode 操作,得到一个字符串。

 JSONObject payloadJson = JSONUtil.createObj();
 String payloadStr = base64UrlEncode(payloadJson); 
复制代码
  • signature(签名)

JWT 的第二部分是 Signature(签名),JWT 需要在网络上传输

有了头部和有效负荷携带数据,为了保证安全性,需要做相关加密算法

签名其实就是对 header.payload 数据做加密后的结果。

创建签名前,后续获取到 header、payload、做签名用到的 secret、签名算法,如下是一个生成签名的示例:

String headerStr = base64UrlEncode(header);
String payloadStr = base64UrlEncode(payload);
String secret = "xxxxx";
String headerPayload = headerStr + "." + payloadStr;


// 生成签名
String signature = HMACSHA256(headerPayload, secret);复制代码

签名作用有哪些?

①保证 header 和 payload 在传输过程中没有被纂改。

②如果签名算法用的是非对称加密(比方 RSA),可以通过验证签名,来确定谁是持有私钥的那方。

整合三部分信息,其实就回到了什么是 JWT,其实就是一个由 header.payload.signature 组成的字符串。

3、如何使用 JWT

假设是用户授权登录场景,当用户登录成功之后服务端会颁发 token 给到客户端

之后用户的每个请求都需要携带 token,如果没携带那么就认为没有凭据,无法访问,如果携带合法合理的,放行。

思考:token 的提交方式?

OAuth 官方有三种方式,如下

方式一:Form-Encoded Body Parameter(表单参数)

form.png

方式二:URI Query Parameter(URI查询参数)

uri.png

方式三:Authorization Request Header(授权请求头部字段)

header.png

4、JWT 的优缺点

万物都有两面性。

优点:

1)相比随机字符串,具有含义(自编码能力)

2)加密,保护了 header、payload 传输过程被盗窃

缺点:

最大缺点:覆水难收

JWT 从服务端颁发之后,在有效期内其实是横冲直撞的,它不存储在服务端,很难改变 JWT 的状态。

切忌在代码中不要开 api 提供给测试同事获取token,如果要获取,开发手动生成。

四、开源组件

1、为什么选用开源

JWT 中的 header、payload 都需要做 base64URLEncode 操作,为了保证传输过程的安全,还需要引入加密算法做签名,如果自行手写极容易出错,同时安全风险高。

开源组件有一个特点那就是开箱即用,它屏蔽了底层复杂的具体实现(比方说封装了 Base64 操作、对称/非对称一系列算法实现等),让开发者更专注于上层 api 调用和业务实现。

在 JWT 这块选用开源组件,我们会比较关心两块,一块是 api 层面,另外一块自然就是开源活跃度。

api 层面:是否提供了 生成 JWT 的接口、校验 JWT 的接口等。

开源活跃度:说明出了 bug 有处可寻。

2、JJWT

官网:github.com/jwtk/jjwt

JJWT.png

以下示例说明在 SpringBoot 中如何简单使用 JJWT:

1)引入依赖

<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>复制代码

2)生成 JWT

# 声明
Map<String, Object> claims = new HashMap<>(2);
claims.put("authId", activityUser.getId());
claims.put("authRole", "user");


String jwt = Jwts.builder()
          .setClaims(claims)
          .setSubject("api")
          .setIssuedAt(new Date())
          .setExpiration(expirationAt)
          .signWith(SignatureAlgorithm.HS512, signingKey)
          .compact();复制代码

3)使用 JWT

这里用 Authorization Header 头部的方式提交 jwt

postman.png

4)校验 JWT

①自定义 SpringMVC 拦截器,在 preHandler 方法中做拦截

  • 自定义拦截器

public class TokenInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandler(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {
    // 方便演示,硬编码
    String tokenKey = "Bearer";
    String tokenSecret = "xxx";
    // 1、从 request 头部获取 token
    String token = request.getHeader(tokenKey);
    // 方便演示,去掉了 null 等判断
    // 2、调用 Jwts 开源工具提供的方法,获取声明
    Claims claims = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(tokenKey).getBody();
    // 判断是否过期等,硬编码
    boolean judgeResult = this.isNullOrExpired(claims);
    
    // 后续业务逻辑
  }
}


/**
 * 判断令牌是否过期
 *
 * @param claims 令牌解密后的Map对象。
 * @return true 过期,否则false。
 */
public boolean isNullOrExpired(Claims claims) {
    return ObjectUtil.isNull(claims) || DateUtil.date().after(claims.getExpiration());
}复制代码
  • 注入 Spring 容器

@Component
public class InterceptorConfig implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor ())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/oauth/login");
    }
}复制代码

②校验不通过的时候,后端响应 401

{
    "message": "用户会话已过期,请重新登录!",
    "statusCode": 401
}复制代码

③校验通过,进入业务接口

五、总结

今天橘长带大家分析了 OAuth 2.0 的核心 以及 令牌机制,大家需要记住几点:

1、OAuth 2.0 的核心是颁发令牌,使用访问令牌。

2、令牌组成、payload 中的三种声明,JWT 的优缺点。

3、开源 JWT 组件 JJWT 的使用。


作者:橘长说Java
链接:https://juejin.cn/post/7031562514592694302

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐