如何在Delphi中向Jira传递RSA密钥并解决OAuth登录令牌获取失败问题
我之前帮不少开发者搞定过Delphi对接Jira OAuth的问题,你碰到的RSA私钥编码和签名问题确实是个常见卡点,下面我给你一步步拆解实现方法,保证能跑通:
核心原理先搞懂
Jira OAuth采用的是OAuth 1.0a的RSA-SHA1签名机制,核心逻辑是:
- 按照规则构造「签名基串」
- 用你的RSA私钥对这个基串进行SHA1签名
- 将签名结果Base64编码后,放到HTTP请求的
Authorization头中
Delphi具体实现步骤
1. 加载RSA私钥(PEM格式)
Jira要求的私钥一般是PEM格式,我们可以用Delphi自带的Indy库TIdRSA组件来加载,注意要先去掉PEM的头尾标记和换行符:
function LoadRSAPrivateKey(const PrivateKeyPath: string): TIdRSA; var SL: TStringList; PEMContent: string; begin Result := TIdRSA.Create; SL := TStringList.Create; try SL.LoadFromFile(PrivateKeyPath); // 清理PEM格式的头尾和换行 PEMContent := StringReplace(SL.Text, '-----BEGIN RSA PRIVATE KEY-----', '', [rfReplaceAll]); PEMContent := StringReplace(PEMContent, '-----END RSA PRIVATE KEY-----', '', [rfReplaceAll]); PEMContent := StringReplace(PEMContent, sLineBreak, '', [rfReplaceAll]); // 解码Base64后加载私钥 Result.LoadPrivateKeyFromBlob(TIdDecoderMIME.DecodeBytes(PEMContent)); finally SL.Free; end; end;
2. 生成OAuth签名基串
签名基串是OAuth签名的核心,必须严格按照「HTTP方法&编码后的请求URL&编码后的参数串」格式构造,还要对参数按字典序排序:
function GenerateSignatureBaseString(const HTTPMethod, RequestURL: string; Params: TStringList): string; var EncodedParams: string; I: Integer; ParamPairs: TStringList; begin ParamPairs := TStringList.Create; try // 对每个参数名和值做URL编码,然后排序 for I := 0 to Params.Count - 1 do begin ParamPairs.Add(TIdURI.URLEncode(Params.Names[I]) + '=' + TIdURI.URLEncode(Params.ValueFromIndex[I])); end; ParamPairs.Sort; // 用&连接排序后的参数 EncodedParams := ParamPairs.CommaText.Replace(',', '&'); // 构造最终的签名基串,每个部分都要URL编码 Result := TIdURI.URLEncode(HTTPMethod) + '&' + TIdURI.URLEncode(RequestURL) + '&' + TIdURI.URLEncode(EncodedParams); finally ParamPairs.Free; end; end;
3. 用RSA私钥签名并编码
用私钥对签名基串做SHA1签名,然后将签名结果Base64编码:
function GenerateRSASignature(const SignatureBaseString: string; RSA: TIdRSA): string; var SignatureBytes: TBytes; HashBytes: TBytes; begin // 先对签名基串做SHA1哈希 HashBytes := THashSHA1.GetHashBytes(SignatureBaseString); // 用RSA私钥签名(Jira要求PKCS#1 v1.5填充,TIdRSA默认支持) SignatureBytes := RSA.SignBytes(HashBytes, SHA1); // 签名结果Base64编码 Result := TIdEncoderMIME.EncodeBytes(SignatureBytes); end;
4. 构造Authorization请求头
把所有OAuth参数和签名组合成符合规范的请求头,注意每个参数值都要URL编码:
function BuildAuthorizationHeader(const ConsumerKey, Nonce, Timestamp, Signature: string): string; begin Result := Format('OAuth oauth_consumer_key="%s", oauth_nonce="%s", oauth_signature="%s", oauth_signature_method="RSA-SHA1", oauth_timestamp="%s", oauth_version="1.0"', [TIdURI.URLEncode(ConsumerKey), TIdURI.URLEncode(Nonce), TIdURI.URLEncode(Signature), Timestamp]); end;
完整流程示例(获取请求令牌)
把上面的步骤串起来,实现Jira请求令牌的获取:
procedure GetJiraRequestToken; var RSA: TIdRSA; SignatureBase: string; Signature: string; AuthHeader: string; Params: TStringList; HTTP: TIdHTTP; Response: string; Nonce: string; Timestamp: string; begin RSA := LoadRSAPrivateKey('C:\your\private\key\path\privatekey.pem'); try // 生成唯一的Nonce和Unix时间戳 Nonce := THashMD5.GetHashString(IntToStr(Random(MaxInt)) + Now.ToString); Timestamp := IntToStr(Trunc(Now - EncodeDate(1970, 1, 1)) * 86400 + Trunc(Time * 86400)); // 构造OAuth参数列表 Params := TStringList.Create; try Params.Add('oauth_consumer_key=YOUR_JIRA_CONSUMER_KEY'); Params.Add('oauth_nonce=' + Nonce); Params.Add('oauth_signature_method=RSA-SHA1'); Params.Add('oauth_timestamp=' + Timestamp); Params.Add('oauth_version=1.0'); // 生成签名基串和签名 SignatureBase := GenerateSignatureBaseString('POST', 'https://your-jira-instance.atlassian.net/oauth/request-token', Params); Signature := GenerateRSASignature(SignatureBase, RSA); // 构造Authorization头 AuthHeader := BuildAuthorizationHeader('YOUR_JIRA_CONSUMER_KEY', Nonce, Timestamp, Signature); // 发送HTTP请求 HTTP := TIdHTTP.Create; try HTTP.Request.CustomHeaders.Add('Authorization: ' + AuthHeader); // Jira请求令牌接口需要POST空内容 Response := HTTP.Post('https://your-jira-instance.atlassian.net/oauth/request-token', TStream(nil)); // 解析响应得到oauth_token和oauth_token_secret ShowMessage('Request Token Response: ' + Response); finally HTTP.Free; end; finally Params.Free; end; finally RSA.Free; end; end;
关键注意事项
- 确保你在Jira的应用链接设置中,已经上传了对应私钥的公钥
- 使用较新版本的Indy库,避免旧版本RSA组件的兼容性问题
- Nonce必须保证每次请求唯一,时间戳是标准Unix时间戳(秒数)
- 所有URL编码要严格遵循RFC 3986标准,
TIdURI.URLEncode符合要求
内容的提问来源于stack exchange,提问作者Kees Vermeulen




