内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

Pharbers用于单点登录的权限架构

2018-11-06 20:14 出处:清屏网 人气: 评论(0

软件架构作用

公司的每个产品都有各自应用的领域和范畴, 但都是医药业务扩展中的必经一换. 所以常常会有公司同时使用我们多个产品的情况. 而我们每个产品, 在一些数据和逻辑使用上, 有很大的相通性.

而如何更好的保护客户数据, 怎样提供更好的用户体验, 就是本篇文章的重点内容了.

加密安全

为了保障用户账号密码的安全性, 在前后端交互中, 所传输的密码, 均为RSA规范的非对称加密. 同时, 数据库中存储的用户密码, 也为MD5序列化的密文形式.

公司独立秘钥

为每个公司, 生成单独的秘钥对, 每对秘钥有自己的过期时间, 过期时间为公司购买产品的使用时间.

会话管理

在用户登录成功后, 会将该用户的所有权限信息存入Redis中, 同时生成一个ObjectId作为token返回给前端. 同时, token有自己的有效时间.

开放授权(单点登录)

用户在任意位置登录法伯账号后, 在token有效期内, 可以不用输入账号密码, 直接登录该账号所拥有的其他产品中.

视图组件

用户成功登录, 会根据用户当前的权限和角色, 前端决定渲染的组件和布局

接口安全

后端暴露给前端的接口(登录, 注册等无须登录接口除外), 都需要有一个有效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

加密

加密流程:

代码如下:

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索引技术自动删除过期数据

登录验证

开放授权

前端获得登录token后, 对之后的每次请求, 都将以下面形式写入Headers中,

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

后端验证token是否过期, 以及该用户token中所记录的权限, 决定本次请求的合法性和返回内容.

小结

以上就是我们 法伯科技 的一个简单的权限管理系统, 通过OAuth的特性, 可以实现单点登录, 利用token在Redis中存放用户相关的角色和产品, 可以决定用户在登录某一产品时, 是否有进入权限, 进入产品后, 决定可以显示哪些组件, 可以使用哪些功能.

参考资料

分享给小伙伴们:
本文标签: Pharbers单点登录

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号