初探系列 — Pharbers用於單點登陸的權限架構

一. 前言

就任公司

法伯科技是一家以數據科技爲驅動, 專一於醫藥健康領域的循證諮詢公司. 以數據科學家身份, 賦能醫藥行業. 讓每位客戶都能享受數據帶來的價值, 洞察業務, 不止於數據, 讓決策更精彩。html

法伯擁有多套自主研發的數據分析工具, 爲企業帶來高效, 便捷, 實用的解決方案.前端

  • MAX © :市場動態監測工具
  • TMIST © :鐵馬區域管理模擬平臺
  • PET © :推廣評估優化工具

適用人羣

本篇文章, 全部實例代碼, 均爲Scala, 適用於以Scala系列技術棧和微服務架構的初期開發團隊. 其餘技術請自行斟酌修改.
原文地址http://www.javashuo.com/article/p-upfcxizd-ce.htmljava

軟件架構做用

公司的每一個產品都有各自應用的領域和範疇, 但都是醫藥業務擴展中的必經一換. 因此經常會有公司同時使用咱們多個產品的狀況. 而咱們每一個產品, 在一些數據和邏輯使用上, 有很大的相通性.
而如何更好的保護客戶數據, 怎樣提供更好的用戶體驗, 就是本篇文章的重點內容了.算法

加密安全

爲了保障用戶帳號密碼的安全性, 在先後端交互中, 所傳輸的密碼, 均爲RSA規範的非對稱加密. 同時, 數據庫中存儲的用戶密碼, 也爲MD5序列化的密文形式.數據庫

公司獨立祕鑰

爲每一個公司, 生成單獨的祕鑰對, 每對祕鑰有本身的過時時間, 過時時間爲公司購買產品的使用時間.後端

會話管理

在用戶登陸成功後, 會將該用戶的全部權限信息存入Redis中, 同時生成一個ObjectId做爲token返回給前端. 同時, token有本身的有效時間.安全

開放受權(單點登陸)

用戶在任意位置登陸法伯帳號後, 在token有效期內, 能夠不用輸入帳號密碼, 直接登陸該帳號所擁有的其餘產品中.架構

視圖組件

用戶成功登陸, 會根據用戶當前的權限和角色, 前端決定渲染的組件和佈局dom

接口安全

後端暴露給前端的接口(登陸, 註冊等無須登陸接口除外), 都須要有一個有效token才能夠正確調用.微服務

二. 技術實現

Cryptography(加密部分)

加密算法, 使用的類庫是java自帶的java.security庫.
Base64庫使用的是commons-codec, MVN以下:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.10</version>
</dependency>

祕鑰對建立

公鑰祕鑰建立:

// create by ClockQ
trait RSACryptogram extends PhCryptogram {
    val puk: String
    val prk: String
    val ALGORITHM_RSA: String
    val TRANSFORMS_RSA: String
    val CHARSET_NAME_UTF_8: String
    val KEY_SIZE: Int

    def createKey(): (String, String) = {
        val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_RSA)
        keyPairGenerator.initialize(KEY_SIZE, new SecureRandom())
        val keyPair = keyPairGenerator.generateKeyPair()

        val publicKey = Base64.encodeBase64String(keyPair.getPublic.getEncoded)
        val privateKey = Base64.encodeBase64String(keyPair.getPrivate.getEncoded)

        (publicKey, privateKey)
    }
}

這一段代碼很簡單:

  1. 實例化祕鑰工廠;
  2. 設置KEY_SIZE和隨機碼;
  3. 生成祕鑰;
  4. 將公鑰和祕鑰按照Base64編碼.

上面的常量以下:

val ALGORITHM_RSA = "RSA"
val TRANSFORMS_RSA = "RSA/ECB/PKCS1PADDING"
val CHARSET_NAME_UTF_8 = "UTF-8"
val KEY_SIZE = 512

加密

加密流程:
image

代碼以下:

trait RSAEncryptTrait { this: RSACryptogram =>
    def encrypt(cleartext: String): String = {

        if(puk.isEmpty) throw new Exception("public key is empty")

        val originKey = Base64.decodeBase64(puk)
        val keySpec = new X509EncodedKeySpec(originKey)
        val publicKey = KeyFactory.getInstance(ALGORITHM_RSA).generatePublic(keySpec)

        val cipher = Cipher.getInstance(TRANSFORMS_RSA)
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)

        val inputBytes = URLEncoder.encode(cleartext, CHARSET_NAME_UTF_8).getBytes(CHARSET_NAME_UTF_8)
        val inputLength = inputBytes.length

        val MAX_ENCRYPT_BLOCK = (KEY_SIZE >> 3) - 11
        var offset = 0
        var cache: Array[Byte] = Array()

        while (inputLength - offset > 0) {
            val tmp = if (inputLength - offset > MAX_ENCRYPT_BLOCK)
                cipher.doFinal(inputBytes, offset, MAX_ENCRYPT_BLOCK)
            else
                cipher.doFinal(inputBytes, offset, inputLength - offset)

            cache ++= tmp
            offset += MAX_ENCRYPT_BLOCK
        }

        Base64.encodeBase64String(cache)
    }
}

解密

解密的過程與加密相反, 流程圖就不畫了, 代碼以下:

trait RSADecryptTrait { this: RSACryptogram =>
    def decrypt(ciphertext: String): String = {

        if(prk.isEmpty) throw new Exception("private key is empty")

        val originKey = Base64.decodeBase64(prk)
        val keySpec = new PKCS8EncodedKeySpec(originKey)
        val privateKey = KeyFactory.getInstance(ALGORITHM_RSA).generatePrivate(keySpec)

        val cipher = Cipher.getInstance(TRANSFORMS_RSA)
        cipher.init(Cipher.DECRYPT_MODE, privateKey)

        val inputBytes = Base64.decodeBase64(ciphertext)
        val inputLength = inputBytes.length

        val MAX_DECRYPT_BLOCK = KEY_SIZE >> 3
        var offset = 0
        var cache: Array[Byte] = Array()

        while (inputLength - offset > 0) {
            val tmp = if (inputLength - offset > MAX_DECRYPT_BLOCK)
                cipher.doFinal(inputBytes, offset, MAX_DECRYPT_BLOCK)
            else
                cipher.doFinal(inputBytes, offset, inputLength - offset)

            cache ++= tmp
            offset += MAX_DECRYPT_BLOCK
        }

        URLDecoder.decode(new String(cache, CHARSET_NAME_UTF_8), CHARSET_NAME_UTF_8)
    }
}

注意事項

  • 上面代碼中的while循環, 是用來處理加密解密的內容過長時, 用來分段加密的. 但請記住, 因爲RSA非對稱加密的效率問題, 不建議加密過長的內容, 能夠考慮採用對稱加密, 而後對於對稱加密的祕鑰, 使用RSA加密, 而後將密文和以前對稱加密的密文共同傳輸.
  • 對於RSA加密的明文長度計算公式, 假如咱們的KEY_SIZE = 512, 而且採用RSA/ECB/PKCS1PADDING協議加密, 則咱們的最大加密長度爲(KEY_SIZE >> 3) - 11, 爲何減11呢? 由於RSA/ECB/PKCS1PADDING是一種加密協議, 它爲了保證相同公鑰加密相同內容, 出現密文同樣, 因此在明文的中間部分, 加入了11位隨機的混淆碼.
  • 關於變態對接問題, 我在和Golang和Js聯調的時候, 發現個人密文他們能夠解析, 而他們的密文我沒法解析, 緣由在於, Java的解密庫中, 只能使用PKCS8解密協議.關於PKCS的信息, 能夠查看百度百科

公司獨立祕鑰

上面的代碼中, 已經寫了如何建立一對指定KEY_SIZE大小的祕鑰對, 咱們只須要將其存入MongoDB中, 並和Company關聯便可. 爲了實現每一個祕鑰對有本身的過時時間, 我想到了Redis的TTL, 慶幸MongoDB有相似的技術, 文章我就不抄了, 有興趣的朋友能夠查看這篇文章.

MongoDB TTL索引技術自動刪除過時數據

登陸驗證

image

開放受權

前端得到登陸token後, 對以後的每次請求, 都將如下面形式寫入Headers中,

{
    "key":"Authorization",
    "value":"bearer 5bc58327c8f5e406a2b57394"
}

後端驗證token是否過時, 以及該用戶token中所記錄的權限, 決定本次請求的合法性和返回內容.

小結

以上就是咱們法伯科技的一個簡單的權限管理系統, 經過OAuth的特性, 能夠實現單點登陸, 利用token在Redis中存放用戶相關的角色和產品, 能夠決定用戶在登陸某一產品時, 是否有進入權限, 進入產品後, 決定能夠顯示哪些組件, 可使用哪些功能.

參考資料

相關文章
相關標籤/搜索