初探系列 — Pharbers用于单点登录的权限架构
一. 前言
就职公司
法伯科技是一家以数据科技为驱动, 专注于医药健康领域的循证咨询公司. 以数据科学家身份, 赋能医药行业. 让每位客户都能享受数据带来的价值, 洞察业务, 不止于数据, 让决策更精彩。
法伯拥有多套自主研发的数据分析工具, 为企业带来高效, 便捷, 实用的解决方案.
- MAX © :市场动态监测工具
- TMIST © :铁马区域管理模拟平台
- PET © :推广评估优化工具
适用人群
本篇文章, 所有实例代码, 均为Scala, 适用于以Scala系列技术栈和微服务架构的初期开发团队. 其他技术请自行斟酌修改.
原文地址https://www.cnblogs.com/clockq/p/9908742.html
软件架构作用
公司的每个产品都有各自应用的领域和范畴, 但都是医药业务扩展中的必经一换. 所以常常会有公司同时使用我们多个产品的情况. 而我们每个产品, 在一些数据和逻辑使用上, 有很大的相通性.
而如何更好的保护客户数据, 怎样提供更好的用户体验, 就是本篇文章的重点内容了.
加密安全
为了保障用户账号密码的安全性, 在前后端交互中, 所传输的密码, 均为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)
}
}
这一段代码很简单:
- 实例化秘钥工厂;
- 设置KEY_SIZE和随机码;
- 生成秘钥;
- 将公钥和秘钥按照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有类似的技术, 文章我就不抄了, 有兴趣的朋友可以查看这篇文章.
登录验证

开放授权
前端获得登录token后, 对之后的每次请求, 都将以下面形式写入Headers中,
{
"key":"Authorization",
"value":"bearer 5bc58327c8f5e406a2b57394"
}
后端验证token是否过期, 以及该用户token中所记录的权限, 决定本次请求的合法性和返回内容.
小结
以上就是我们法伯科技的一个简单的权限管理系统, 通过OAuth的特性, 可以实现单点登录, 利用token在Redis中存放用户相关的角色和产品, 可以决定用户在登录某一产品时, 是否有进入权限, 进入产品后, 决定可以显示哪些组件, 可以使用哪些功能.
参考资料
初探系列 — Pharbers用于单点登录的权限架构的更多相关文章
- springBoot整合spring security+JWT实现单点登录与权限管理--筑基中期
写在前面 在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与 ...
- 029.[转] SSO单点登录的通用架构实现
单点登录的通用架构实现 pphh发布于2018年4月26日 http://www.hyhblog.cn/2018/04/26/single_sign_on_arch/ 目录 1. 什么是单点登录 2. ...
- cas系列(一)--cas单点登录基本原理
(这段时间打算做单点登录,因此研究了一些cas资料并作为一个系列记录下来,一来可能会帮助一些人,二来对我自己所学知识也是一个巩固.) 一.为什么要实现单点登录 随着信息化不断发展,企业的信息化过程是一 ...
- 七、spring boot 1.5.4 集成shiro+cas,实现单点登录和权限控制
1.安装cas-server-3.5.2 官网:https://github.com/apereo/cas/releases/tag/v3.5.2 下载地址:cas-server-3.5.2-rele ...
- OAuth2.0 原理流程及其单点登录和权限控制
2018年07月26日 07:21:58 kefeng-wang 阅读数:5468更多 所属专栏: Java微服务构架 版权声明:[自由转载-非商用-非衍生-保持署名]-转载请标明作者和出处. h ...
- Office 365实现单点登录系列(5)—配置单点登录
这是单点登录系列的最后一篇文章,前面4篇文章其实都是在为这篇文章的内容做准备,我把这四篇文章的链接放在下面,如果大家有需要,可以参考我以下的链接: Office 365实现单点登录系列(1)—域环境搭 ...
- 看图理解JWT如何用于单点登录
单点登录是我比较喜欢的一个技术解决方案,一方面他能够提高产品使用的便利性,另一方面他分离了各个应用都需要的登录服务,对性能以及工作量都有好处.自从上次研究过JWT如何应用于会话管理,加之以前的项目中也 ...
- JAVA架构之单点登录 任务调度 权限管理 性能优化大型项目实战
单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任.单点登录在大型网站里使用得 ...
- cas sso单点登录系列6_cas单点登录防止登出退出后刷新后退ticket失效报500错
转(http://blog.csdn.net/ae6623/article/details/9494601) 问题: 我登录了client2,又登录了client3,现在我把client2退出了,在c ...
随机推荐
- Spring 事物Transaction
日常开发中Spring 为我们提供了两种事物的定义方式 XML 配置 方式 :这种方式配置起来比较麻烦,但后期比较好进行维护 注解方式:配置起来比较方便,也是日常开发常用的: 我们这里进行第二种注解的 ...
- 51单片机学习笔记(清翔版)(21)——ADDA数模转换
A:anolog模拟的 D:digital数字的 AD模拟转数字,DA数字转模拟 生活中的基本都是模拟量,如温度,可以是10℃,10.1℃等 手机的背光亮度自动调节,拿到太阳光下,亮度 ...
- 解析ReentrantLock实现原理
在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock(Lock的实现主要有ReentrantLock.ReadLock和WriteLock).synchronize ...
- 【Rails App】 应用服务器从Passenger切换为Puma, Grape出现线程安全问题
Grape中的代码如下: def market @market ||= Market.find(params[:id]) end @market基于类层次的实例变量,属于非线程安全,如果一直使用多线程 ...
- php核心技术与最佳实践(笔记一)
1.1面向对象的型与本 类是对象的抽象组织,对象是类的具体存在. 1.1.1对象的形 <?php class Person{ public $name; public $gender; publ ...
- 命令行连WiFi
命令行连WiFi sudo iw dev wlan0 scan |grep SSID wpa_cli -iwlan0 add_network wpa_cli -iwlan0 set_network 4 ...
- 把多个字符串里面的项写到不同的对象中,然后在push到一个数组中
otherUserNames: "甲,乙,丙,丁"otherUserIds: "10008750,10008711,10003348,10008747" oth ...
- RNAseq测序reads定位
RNAseq测序reads定位 发表评论 3,210 A+ 所属分类:Transcriptomics 收 藏 获得RNA-seq的原始数据后,首先需要将所有测序读段通过序列映射(mapping) ...
- 第二阶段第六次spring会议
昨天我将对初始页面进行加工和修改. 我用两个小动物作为按钮分别进入动物便签界面和植物便签界面,可以让用户自由选择. 今天我将尝试对软件进行添加搜索引擎的界面. private void linkLab ...
- Mybatis中的CDATA标签
术语 CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data). 在 XML 元素中,"<" 和 "&& ...