步骤
- 申请建立新程序(账号需要绑定手机)
- 测试OAuth 1.0
各个变量的含义
原文地址:
POST oauth/request_token
Creating a signature
参数 | 含义 |
---|---|
oauth_consumer_key | 申请的程序的key |
oauth_nonce | base64编码的32字节的随机数据。Twitter使用它来查验信息是否属于重复提交 |
oauth_signature | 所有提交的信息的校验码,用于检验内容是否被篡改1 |
oauth_signature_method | 校验码的计算方法,固定为HMAC-SHA1 |
oauth_timestamp | Unix相对于GMT 1970年1月1日的秒数。 |
oauth_token | 代表当前用户的token,首次使用时需要向Twitter申请 |
oauth_version | oauth的版本,目前总是1.0 |
Consumer secret | 你申请App时获得的另外一段文本2 |
OAuth token secret | 调用API后从Twitter网站获得的当前用户的标志 |
Signing key | 计算校验码时的key,由Consumer secret和OAuth token secret加工而成 |
status | 用户发送的tweet的正文 |
创建校验码的方法
原文地址:
Creating a signature
写在前面
**一定要记住:只有上面原文的例子是真正可用的。**不过这个例子也有一些问题,后面我会提到。
按照流程我们一般会最先到POST oauth/request_token页面去。因为我们不申请授权的话,什么事都做不了。但这里是Twitter官方文档的第一个坑。用来计算oauth_signature
需要的key的一部分是Consumer secret
,而这个页面根本没有提供它。因此,这个例子里的值是没法计算的。
更坑的是,由于Consumer secret
和Consumer Key
非常像,而后者就是oauth_consumer_key
,这个值在这个页面有提供。因此,初学者如我,就很容易把oauth_consumer_key
作为Consumer secret
来计算oauth_signature
。得到的结果自然与例子不同。我在这里浪费了很多的时间。
另外一个页面Authorizing a request也是同样的问题,没有提供Consumer secret
,因此也是无法计算的。
结论:强烈建议你在算法设计完成时,使用Creating a signature这个页面提供的例子来检验你的算法是否正确。如果正确了,那么再进行下一步。
需要计算的量
在调用一次API时,需要有很多参数,有一些参数的值是可以直接获得的,比如oauth的版本,signature的校验方式,都是固定的,每次都一样。但也有一些参数需要计算,简单的timestamp的就不提了,下面提几个相对复杂的。
oauth_nonce
oauth_nonce
是一个应该每次都不一样的字符串,Twitter通过这个值来验证客户端是否发送了重复的请求。按照官方的说法,这个字符串的计算过程是取一段32个字节的随机数据,然后将这段数据采用base64编码的方式输出为字符串,之后过滤掉其中非词的字符。
and stripping out all non-word characters
这里的非词指的是大小写字母和数字之外的字符。^[a-z|A-Z|0-9]
实现这个的算法有很多,但是有一点我很疑惑。因为官方文档里的几个例子,字符都是42个,但是我按照算法说明生成的,一般都是43个。只有把32个字节,改成31个字节,得到的才是42个。
先说结论,这个字符串是42还是43其实无所谓。后来我的程序确定能通过之后,我发现,有时生成的字符串是45个。也就是说,其实这个字符串,本质上来讲,只要每次不一样就可以,长度其实没有限制。
附上我的算法:
private static func calculateOauthNonce() -> String {
let uuidString = UUID().uuidString
let randomStringIn32Bytes = uuidString.substring(to: uuidString.index(uuidString.startIndex, offsetBy: 32))
let dataOfRandomStringIn32Bytes = randomStringIn32Bytes.data(using: .utf8)!
let base64EncodedStringOfRandomStringIn32Bytes = dataOfRandomStringIn32Bytes.base64EncodedString()
let modifiedBase64EncodedString = base64EncodedStringOfRandomStringIn32Bytes.unicodeScalars
.filter {
CharacterSet.alphanumerics.contains($0)
}
.reduce("", { (result, unicodeScalar) -> String in
let character = Character(unicodeScalar)
return result + String(character)
})
return modifiedBase64EncodedString
}
percent encode
在计算header
和signature
等处,需要使用到percent encode
。百分比化,即把不在范围内的字符,转化为带%号的形式。按照官方的规则,percent encode
的计算方法如下:
let nonePercentedCharacterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~"))
private func percentedString(_ source:String) -> String {
return source.addingPercentEncoding(withAllowedCharacters: nonePercentedCharacterSet)!
}
signature base string
signature base string
是计算signature的中间步骤。这里有一点要注意,即键值对的key要按照字母顺序排列。这是为了计算校验值的唯一。
这里有一个坑,官方例子提供的值是
POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521
它去掉百分号实际上是
let s2 = "POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521"
print(s2.removingPercentEncoding!)
// POST&https://api.twitter.com/1/statuses/update.json&include_entities=true&oauth_consumer_key=xvz1evFS4wEEPTGEFPHBog&oauth_nonce=kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1318622958&oauth_token=370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb&oauth_version=1.0&status=Hello%20Ladies%20%2B%20Gentlemen%2C%20a%20signed%20OAuth%20request%21
可以看到,url实际是https://api.twitter.com/1/statuses/update.json
,但是官方文档中写url是https://api.twitter.com/1.1/statuses/update.json
。这里一个是1
,一个是1.1
,你按照文档的url来算,那么signature
肯定就和官方提供的tnnArxj06cWHq44gCs1OSKk/jLY=
不一样。
signing key
标准的signing key
由两部分组成,百分比化的Consumer secret
与百分比化的OAuth token secret
,二者按照先后顺序用&
链接,需要注意两点:
- 是
Consumer secret
而不是Consumer Key
&
不需要百分比化
当首次申请时,由于没有OAuth token secret
,此时的signing key
为百分比化的Consumer secret
之后加上&
。
signature
这里直接调用加密的函数就可以,值得说的部分是,官方这里要求使用HMAC-SHA1
,但是输出值不是普通的字符串,而是要转成base64编码的字符串输出。网上能找到的例子,一般都只是普通的字符串输出,这里提供我的算法,主要是增加了base64编码的输出,以及一些重构。
import Foundation
enum CryptoAlgorithm {
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512
var HMACAlgorithm: CCHmacAlgorithm {
var result: Int = 0
switch self {
case .MD5:
result = kCCHmacAlgMD5
case .SHA1:
result = kCCHmacAlgSHA1
case .SHA224:
result = kCCHmacAlgSHA224
case .SHA256:
result = kCCHmacAlgSHA256
case .SHA384:
result = kCCHmacAlgSHA384
case .SHA512:
result = kCCHmacAlgSHA512
}
return CCHmacAlgorithm(result)
}
var digestLength: Int {
var result: Int32 = 0
switch self {
case .MD5:
result = CC_MD5_DIGEST_LENGTH
case .SHA1:
result = CC_SHA1_DIGEST_LENGTH
case .SHA224:
result = CC_SHA224_DIGEST_LENGTH
case .SHA256:
result = CC_SHA256_DIGEST_LENGTH
case .SHA384:
result = CC_SHA384_DIGEST_LENGTH
case .SHA512:
result = CC_SHA512_DIGEST_LENGTH
}
return Int(result)
}
}
enum HashStyle {
case string, base64EncodedString
}
extension String {
func hmac(algorithm: CryptoAlgorithm, key: String, hashStyle:HashStyle = .base64EncodedString) -> String {
let str = self.cString(using:.utf8)
let strLen = self.lengthOfBytes(using:.utf8)
let digestLen = algorithm.digestLength
let hashBytes = UnsafeMutablePointer<UInt8>.allocate(capacity: digestLen)
let keyStr = key.cString(using:.utf8)
let keyLen = Int(key.lengthOfBytes(using:.utf8))
CCHmac(algorithm.HMACAlgorithm, keyStr!, keyLen, str!, strLen, hashBytes)
let digest:String
switch hashStyle {
case .string:
digest = calculateString(from: hashBytes, length: digestLen)
case .base64EncodedString:
digest = calculateBase64EncodedString(from: hashBytes, length: digestLen)
}
hashBytes.deallocate(capacity: digestLen)
return digest
}
private func calculateString(from hashBytes: UnsafeMutablePointer<UInt8>, length: Int) -> String {
let string = (0..<length).reduce("") { (result, index) -> String in
return result + String(hashBytes[index])
}
return string
}
private func calculateBase64EncodedString(from hashBytes: UnsafeMutablePointer<UInt8>, length: Int) -> String {
let data = Data(bytes:hashBytes, count:length)
return data.base64EncodedString()
}
}
计算header
原文:Authorizing a request
这个没啥好说的,按照说明就行了。header
和计算signature
不一样,header
里的键值对的顺序可以是任意的。
调用API
全都计算好了,就可以申请调用API了。简单的步骤如下:
let url = URL(string: "https://api.twitter.com/oauth/request_token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var tt = Twitter()
request.setValue(tt.postOauthRequestToken.header, forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
if error != nil {
print("error=\(error!.localizedDescription))")
return
}
print("response = \(response!.description)")
let responseString = String(data: data!, encoding: .utf8)!
print("responseString = \(responseString)")
}
task.resume()
如果看到输出的response里的代码是200,那么就证明你的程序是没有问题的了。