Node 环境调通 Jira rest api 要踩多少坑

1,780 阅读6分钟

我司是使用 Jira 管理项目的 tasks、subtasks、issues。近期我要做一个 cli 工具,需要调用 jira rest api 获取某个 task 以及它下面的 subtasks 信息。查询 Jira 官网可知它提供了以下鉴权方式:

我选择的是 OAuth。简单解释下 OAuth:资源的拥有者(Owner)通过账号密码去访问资源(tasks、subtasks、issues),现在有一个第三方(consumer)也想访问这些资源,直接把账号密码给它的话,首先是不安全,其次没法控制第三方的访问权限。所以生成一个 token,在 token 上做权限控制,把它给到第三方,实现安全且有控制的访问。

Jira OAuth 鉴权 4 步骤

  1. 配置 Jira
    • 生成一个 RSA 公钥/私钥对
    • 创建一个 Application Link
  2. 创建 Client
  3. 授权
  4. 发送请求

注意:Application links use OAuth(version 1.0a) with RSA-SHA1 signing for authentication。

我先在 postman 上试了试:Authorization Type 选择 OAuth1.0(为什么不选 OAuth1.0a?因为没有🐶),发现 Signature Method 中没有 RSA-SHA1 选项,查了下发现 postman 版本低了,升级到最新版本(v9.29.6)后有了 RSA-SHA1 选项(此为第 1 坑),如下图,输入相关字段成功获取 tasks 信息。我发现 Version 字段是可以编辑的,试着改成 1.0a 发现 response 没了!

image.png

RSA Private Key

Jira OAuth 鉴权步骤中生成的 RSA 私钥 .pem 内容如下,也就是 consumerSecret

-----BEGIN RSA PRIVATE KEY-----
sa1R8yi9lnZW3+FzxChaniztcs6P3qKX77h8ab54U0+ke7AMNZ1gTdZCCCrXiUkG
6dTi4L58qoj1EpN12A4lBe12Az/zKp2cN4Kh+atKDtyCOFHkc0gkbJMeAQIDAQAB
NEKpbKoRaraHHN7CsN2iEvcxvxcvxcv4545ghghgfhKFKDWDSCIvQHyFIfH0PIr5
GIf1ys2uFj79V/cjxfresgqz2vjvKIaB2dj5nTaw5riBbNkCQQDQ9oSmA/95vsPt
8cX9LF2lzJ2OpSYfII3u4SR/EBED5wtp6eeBWXV67Q==
-----END RSA PRIVATE KEY-----

该文件的格式如下:每一行后面都有一个换行符

-----BEGIN RSA PRIVATE KEY-----\n
xxx\n
yyy\n
ddd\n
-----END RSA PRIVATE KEY-----\n

当我直接把 .pem 复制到 js 对象属性时,如下图: image.png secret 的格式变成了下面这样,从第二行开始前面多了两个空格,打印出来就很明显了

-----BEGIN RSA PRIVATE KEY-----\n
  xxx\n
  yyy\n
  ddd\n
  -----END RSA PRIVATE KEY-----\n

image.png

当使用 crypto 创建 RSA-SHA1 签名就报了 error:0908F066:PEM routines:get_header_and_data:bad end line此处是第 2 坑

import crypto from 'crypto';
crypto.createSign('RSA-SHA1').update(baseString).sign(JIRA_CONFIG.consumerSecret, 'base64');

image.png

下图是第 3 坑,然鹅,我写这篇的时候已经忘了当时怎么踩坑里了(过了快两周了) image.png

Axios with OAuth

Jira OAuth 鉴权步骤中发送请求使用的是 java 的例子,我们来看看 Axios + OAuth 如何鉴权?

Axios 鉴权信息是放在 headers 中的,如下:

axios.get(url, {
  headers: {
    Authorization: `OAuth ${token}` // token 怎么生成呢?
  }
})

经过 Jira OAuth 鉴权步骤后,我们可以得到 consumerKey, consumerSecret, token, tokenSecret。接下来,我们需要生成 Axios 需要的 token:

Jira 使用的是 OAuth-1.0a,我查到的 npm 包有 oauth, oauth-1.0a

npm oauth 如何生成 token?
import { OAuth } from 'oauth';
// 以下 key, token, secret 都是乱敲的,别妄想了。。。
export const JIRA_CONFIG = {
  consumerKey: 'OAuthKey',
  consumerSecret:
    '-----BEGIN RSA PRIVATE KEY-----\nsa1R8yi9lnZW3+FzxChaniztcs6P3qKX77h8ab54U0+ke7AMNZ1gTdZCCCrXiUkG\n6dTi4L58qoj1EpN12A4lBe12Az/zKp2cN4Kh+atKDtyCOFHkc0gkbJMeAQIDAQAB\nNEKpbKoRaraHHN7CsN2iEvcxvxcvxcv4545ghghgfhKFKDWDSCIvQHyFIfH0PIr5\nGIf1ys2uFj79V/cjxfresgqz2vjvKIaB2dj5nTaw5riBbNkCQQDQ9oSmA/95vsPt\n8cX9LF2lzJ2OpSYfII3u4SR/EBED5wtp6eeBWXV67Q==\n-----END RSA PRIVATE KEY-----',
  authToken: 'esgqz2vjvKIaB2dj5nTaw5riBbNkCQQDQ9',
  authTokenSecret: 'lBe12Az/zKp2cN4Kh+atKDtyCOFHkc0g',
};

export const JIRA_CONSUMER = new OAuth(
  'https://jdog.atlassian.com/plugins/servlet/oauth/request-token',
  'https://jdog.atlassian.com/plugins/servlet/oauth/access-token',
  JIRA_CONFIG.consumerKey,
  JIRA_CONFIG.consumerSecret,
  '1.0',
  null,
  'RSA-SHA1',
);

const oauthHeader = JIRA_CONSUMER.authHeader(
  url,
  JIRA_CONFIG.authToken,
  JIRA_CONFIG.authTokenSecret,
  'GET',
);
axios.get(url, {
  headers: {
    Authorization: oauthHeader,
  }
})
  • consumerSecret 内容
npm oauth-1.0a 如何生成 token?
import OAuth from 'oauth-1.0a';
import crypto from 'crypto';

export const JIRA_CONFIG = {
  consumerKey: 'OAuthKey',
  consumerSecret:
    '-----BEGIN RSA PRIVATE KEY-----\nsa1R8yi9lnZW3+FzxChaniztcs6P3qKX77h8ab54U0+ke7AMNZ1gTdZCCCrXiUkG\n6dTi4L58qoj1EpN12A4lBe12Az/zKp2cN4Kh+atKDtyCOFHkc0gkbJMeAQIDAQAB\nNEKpbKoRaraHHN7CsN2iEvcxvxcvxcv4545ghghgfhKFKDWDSCIvQHyFIfH0PIr5\nGIf1ys2uFj79V/cjxfresgqz2vjvKIaB2dj5nTaw5riBbNkCQQDQ9oSmA/95vsPt\n8cX9LF2lzJ2OpSYfII3u4SR/EBED5wtp6eeBWXV67Q==\n-----END RSA PRIVATE KEY-----',
  authToken: 'esgqz2vjvKIaB2dj5nTaw5riBbNkCQQDQ9',
  authTokenSecret: 'lBe12Az/zKp2cN4Kh+atKDtyCOFHkc0g',
};

function hashFunctionSha1(baseString: string, key: string) {
  return crypto
    .createSign('RSA-SHA1')
    .update(baseString)
    .sign(JIRA_CONFIG.consumerSecret, 'base64');
}

export const JIRA_CONSUMER = new OAuth({
  consumer: {
    key: JIRA_CONFIG.consumerKey,
    secret: JIRA_CONFIG.consumerSecret,
  },
  signature_method: 'RSA-SHA1',
  hash_function: hashFunctionSha1,
  version: '1.0',
});

const oauthHeader = JIRA_CONSUMER.toHeader(
  JIRA_CONSUMER.authorize(
    {
      url,
      method: 'GET',
    },
    {
      key: JIRA_CONFIG.authToken,
      secret: JIRA_CONFIG.authTokenSecret,
    },
  ),
);
  
axios.get(url, {
  headers: {
    ...oauthHeader,
  },
})

注意:private key 最好保密,写在 js 里打包之后也是能被发现滴。但我们这个 cli 只在内网用,所以就这么着了!

题外话

为啥 Authorization 里加个 Bearer 呢?
什么是 RSA-SHA1 签名?

RSA

  • RSA 是一种非对称加密算法,1977 年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的,RSA 就是他们三人姓氏开头字母拼在一起组成的。
  • 非对称加密算法需要两个密钥:公开密钥(publickey: 简称公钥)和私有密钥(privatekey: 简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
  • 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将公钥公开,需要向甲方发送信息的其他角色(乙方)使用该密钥(甲方的公钥)对机密信息进行加密后再发送给甲方;甲方再用自己私钥对加密后的信息进行解密。甲方想要回复乙方时正好相反,使用乙方的公钥对数据进行加密,同理,乙方使用自己的私钥来进行解密。
  • 另一方面,甲方可以使用自己的私钥对机密信息进行签名后再发送给乙方;乙方再用甲方的公钥对甲方发送回来的数据进行验签。

SHA-1

  • 英语:Secure Hash Algorithm 1,中文名:安全散列算法1,是一种密码散列函数。SHA-1 将任意大小的输入转化为固定长度的输出(160 位),通常的呈现形式为 40 个十六进制数。基于散列的算法是不可逆的,即无法通过散列值倒推出原始数据。
  • 散列函数存在碰撞,碰撞即有两个不同的数据,他们的散列值相同(SHA1值相同),但是很少得,达到千万分之一。
  • Hash:散列,或音译为哈希。
SHA1('irene') -> 2ec1ddb5b29133e23d1e8722efead92bce315ef7 // 40 个十六进制数

RSA vs SHA-1

  • SHA-1 (can't be reversed)无法被解密,RSA 是可以被解密的。查资料的时候发现一些在线解密网站声称能解密 SHA-1,对于一些简单的原始数据可能能被解密出来,但是复杂的原始数据就不一定了,see
  • RSA 的效率不高,但 SHA 的效率高。

RSA-SHA1

先对输入进行散列,然后用私钥进行加密,输出一个固定长度的数字签名。

---------------------------------这是一条分界线--------------------------------

用周董骗骗赞🥺

疯狂赶今年的 KPI 😓......掐指一算,还有大半(单鸭)