最近運維同學為了提高安全性,用Google Authenticator對服務器加了雙重認證,此后登錄服務器需要先輸入動態密碼,在輸入服務器密碼。Google Authenticator相當於軟token,對他不了解的同學可以看下這篇文章:谷歌驗證 (Google Authenticator) 的實現原理是什么?。
運維同學的出發點是好的,但是我原來寫的各種自動登錄服務器的腳本統統失效了。蛋疼的是我現在登錄服務器的流程變成了:
- 掏手機(我的是iPhone)
- 解鎖,碰上指紋解鎖失敗的情況還需要輸入密碼解鎖
- 打開Authenticator客戶端,等待Verification Code更新大概1s
- 記住Verification Code,然后到Mac端輸入
- 輸入服務器密碼,登錄...
原本我只執行自己搞的一個命令就完事了,由於我經常需要登錄各種不同的服務器,這種方式對工作效率的影響是可想而知的。
目前Google官方的客戶端只有Android和iOS的,於是開始找找看有沒有針對PC,發現有個針對Windows系統的WinAuth支持Google Authenticator,我無論工作、在家基本都用Mac,所以這個WinAuth我是沒法用了,后來在GitHub上找到一個MacAuthenticator的工具,下載下來試用了一下基本能用,至少Mac端可以得到Verification Code不需要依賴手機了,但是依然解決不了效率問題,而且那個工具居然沒法退出...
沒個順手的工具,看來還得我親手開發個了,於是簡單了設計了下我需要的功能:
- 支持從二維碼中直接識別Authentication Code,也就是otpauth協議中的那個secret
- 支持Authentication Code管理,保存、添加、刪除這些基本功能得有
- 能夠非常方便的得到我想要的Verification Code
- 不需要手抄驗證碼,點擊自動復制
- 支持全局快捷鍵直接填充驗證碼,不需要麻煩的點鼠標(我工作用觸摸板,不用鼠標,比較依賴鍵盤)
- 支持在shell腳本中獲取驗證碼(只有這樣,才能讓我以前寫的自動化工具正常工作)
技術調研
GitHub上已經有個MacAuthenticator開源項目了(基於OC的),所以技術實現上應該沒什么障礙。
語言方面,因為14年的時候參與過《The Swift Programming Language》翻譯(現在已經成為蘋果官方指定的中文版本了),但是還從來沒用過Swift,所以決定采用Swift開發,就當學習了。
otp協議方面,Google開源了其算法:google-authenticator,剛好也有個iOS版本的,是基於OC的,不過給Swift調用沒啥問題,所以核心協議的處理直接拿來用就可以了。
如何將生成的Verification Code給其他應用調用?想來想去還是基於HTTP的調用起來比較簡單,所以還需要實現一個內嵌的HTTP服務器,到cocoapods上找了下,發現Swifter比較適合。
macOS上的Application我確實是第一次接觸,不過在Windows平台上開發過不少桌面類的應用,這塊邊學邊做感覺問題不大(實際做的時候發現各種踩坑),在網上找了些快速入門的資料,發現一個非常棒的資料推薦一下:WeatherBar
最終成果
GoldenPassport已經放到GitHub上了,項目主頁有一個簡單的使用說明,我這里就不介紹具體功能了,基本照着我的需求實現的。
幾乎所有功能都在這個菜單里搞定了:

從二維碼中識別OTP地址,沒有二維碼,自己手動輸入也可以:

和Shell腳本集成,全靠這個HTTP接口啦:

# you can get the url from `http://localhost:17304/`
code=$(curl 'http://localhost:17304/code/test@stanzhai.site')
# ues the verification code
echo $code
技術點
開發過程中,踩了很多坑,遇到不少難點(主要是可參考的資料少),我這里簡單的梳理下,對源碼感興趣的同學,直接去GitHub上Fork吧。
基於Google的OTP庫生成Verification Code
let data = OTPAuthURL.base32Decode(otpData.secret)
let gen = TOTPGenerator(secret: data,
algorithm: TOTPGenerator.defaultAlgorithm(),
digits: TOTPGenerator.defaultDigits(),
period: TOTPGenerator.defaultPeriod())
let code = gen?.generateOTP(for: Date()) // 這個code就是最終的結果啦
狀態欄圖標不清晰的問題
如果你的statusIcon是個18*18的png,參照網上的例子去弄的話,你會發現狀態欄圖標相當模糊,遠不如系統自帶的清晰,如果你用的png是個比較大的圖片,你會發現狀態欄中根本顯示不下,解決這個問題的關鍵點是需要指定圖片的大小。
statusIcon = NSImage(named: "statusIcon") // 48 * 48的大小就可以了
statusIcon.size = NSMakeSize(20, 20) // 這是保證高清又能正常顯示的關鍵
給狀態欄按鈕綁定事件
獲取到系統狀態欄按鈕對象后,我需要綁定下點擊事件,來顯示菜單,折騰了許久才搞定,主要卡在action這個地方,網上關於這方面的資料是相當少,在Swift3中,我們創建一個Selector的正確姿勢是#selector(方法名)同時必須指定statusItem.target = self才行。
statusItem = NSStatusBar.system().statusItem(withLength: NSSquareStatusItemLength)
statusItem.target = self
statusItem.action = #selector(openMenu)
綁定全局快捷鍵
這方面的資料真的好少~
let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
guard AXIsProcessTrustedWithOptions(opts) == true else { return }
monitor = NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handleKeydownEvent)
窗口默認居中顯示
httpPortConfigWindow.showWindow(nil)
httpPortConfigWindow.window?.makeKeyAndOrderFront(nil)
httpPortConfigWindow.window?.center()
NSApp.activate(ignoringOtherApps: true)
不同組件間消息交互
Foundation庫為我們提供了一個基於觀察者模式的NotificationCenter,用起來相當方便。
// 組件A監聽消息
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,
selector: #selector(verifyCodeAdded),
name: NSNotification.Name(rawValue: "VerifyKeyAdded"),
object: nil)
// 組件B發送消息
let notificationCenter = NotificationCenter.default
notificationCenter.post(name: NSNotification.Name(rawValue: "VerifyKeyAdded"), object: nil)
復制內容到剪貼板
let pasteboard = NSPasteboard.general()
pasteboard.clearContents()
pasteboard.setString(codeInfo.value, forType: NSStringPboardType)
調用系統打開窗口,只允許選擇圖片類型
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = NSImage.imageTypes()
從文件中識別二維碼
網上大部分都是iOS掃二維碼的示例,OSX下從文件中識別的方法,摸索了好一陣子才實現。
let ciImage = CIImage(contentsOf: openPanel.url!)
let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyLow])
let results = detector?.features(in: ciImage!)
if (results?.count)! > 0 {
let qrFeature = results?.last as! CIQRCodeFeature
let data = qrFeature.messageString // 識別后的數據
...
}
收獲
GoldenPassport是我開發的第一個macOS Application,對桌面應用的開發流程算是清楚了,搞個窗口類的應用已無大礙。和Windows的桌面應用開發體驗相比,感覺OSX的還是差了不少,這也跟自己不熟悉OSX有關吧。
踩了不少Swift語法的坑,現在用的是Swift3,網上找的一些資料不一定是針對Swift3的代碼,拿過來不一定用,Swift的這種兼容性問題還是挺讓人討厭的。Swift4也快要出來了,依然有兼容性問題。
由於對Cocoa框架不熟悉,不少NSXXX的API不知道咋用,另外NS的不少API在Swift下用法變掉了,多虧了GitHub,通過GitHub的代碼搜索功能,可以找到很多別人項目里的示例代碼,在結合Swift的語法,開發過程中碰到的一些功能性問題基本都能解決。
熟悉了Xcode的項目依賴管理工具:cocoapods和Swift Package Manager,對於子子孫孫無窮盡也的項目依賴,熟悉下項目依賴管理工具還是非常有必要的。在GoldenPassport項目因為Swift Package Manager不支持混合語言的項目依賴管理,所以就用了cocoapods來管理項目依賴了。
GoldenPassport的核心功能是我利用周末整整2天多時間折騰出來的,有種參加黑馬的感覺,逼着自己做自己不熟悉的東西,現學現做,看看短時間內到底能做成什么樣,搞完那一刻成就感滿滿。
結語
源碼地址:GoldenPassport,歡迎Star。
編譯好的工具可以到GitHub的releases中下載,如果這個工具能幫助到其他人,那就再好不過了。
引入Google Authenticator,導致效率變差的問題得以完美解決,我原來的自動化腳本也能正常使用了,這個項目算是告一段了。回過神,該繼續研究大數據的東西去了 )逃...
