单点登录 SSO(Single Sign On)是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。打通所有系统的账户密码,只需要记住一个就行,而且登录一个系统后,打开其他系统不需要再登录。广义的单点登录概念还涵盖了第三方登录:己方应用信任第三方应用,用户只需在第三方应用上登录一次,就可以访问己方应用。
更高阶的产品 IAM(Identity and Access Management),即“身份识别与访问管理”,具有单点登录、强大的认证管理、基于策略的集中式授权和审计、动态授权、企业可管理性等功能。参考开源项目 Keycloak。
OIDC
OIDC 是 OpenID Connect,官网介绍。
感觉 OAuth 太负盛名了,以至于后来在 OIDC 反而难以企及前辈 OAuth。倒是大家谈论比较多的是 JWT(例如https://www.cnblogs.com/lyzg/p/6132801.html),——实际谈 JWT 就是在实现 OIDC,反而 OIDC 大家不怎么爱谈!但我们要知道的是,真正诠释这些的,做点单点登录的,——是 OIDC 规范,JWT 只是 OIDC 规范下的一种 Token 协议,再说句难听的,如果 JWT 不满足或者有问题,换别的 Token 实现规则也行。
不过话说回来,OIDC 与 OAuth 看上去大体是相近,只是把应用场景稍作转换,另外就是返回 Token 的不同,OAuth 不限定 Token 具体实现如何,而 OIDC 推荐带用户信息的 JWT。所以,这么说,也不能怪人们总爱谈 JWT 而忽视 OIDC
部署
目标是部署简单,且灵活多种方式。下面是 SSO 中心的部署方式。
单独部署 | 整合部署 | |
---|---|---|
部署形态 | WAR/JAR 包,单独进程运行 | JAR 包引入依赖,或源码整合 |
优点 | 独立维护 | 占资源更低 |
除了 SSO 中心的部署,还有客户端以 JAR 包的形式提供。
无状态设计
传统认证基于 Cookie+Session 的方式,是有状态的;单机时代问题不大,但到了集群的时候,如何同步 Session 是件麻烦的事情。另外一个方法是绑定 Session 到指定某一台机器,但这样不仅带来复杂性,而且还不能彻底解决问题。因此,渐渐有了以下分野:
- 服务端是无状态的:服务端组件不保存会话状态。
- 服务端是有状态的:服务端组件保存了会话状态。
我们主张服务端无状态的设计。当服务端组件不保存任何会话状态时,伸缩将比较容易,直接增加/减少物理服务器的台数即可。《Web应用中的状态(会话状态、应用状态、有状态协议、无状态协议、REST无状态约束)》这文章分析得很透彻了。
关于 Token 的设计
Token 从简单到复杂,可以采取下面几种方案。
- 最简单的 Token,就是随机字符串,那么我们用 UUID/GUID 即可
- 如果有验证 AuthToken 合法性需求,可以将 UserName+ 时间戳加密生成,服务端解密之后验证合法性
- JWT。我们后面重点说 JWT
可配置
强调系统的鲁棒性,从最简单的组件到复杂的组件,均可支持,例如,缓存组件,简单点的你可以使用本地的 Servlet Session,高级一点的可以采用 Redis。
应用 App
每个想要接入OAuth 授权服务的第三方客户端都需要事先在服务端这里“备案”,这样可以更好的管理接入的第三方应用。App 多数时候与 Client 的概念是一致的。
注册应用的时候一般需要提供一些基本信息,比如应用名称、网址、logo 等。主要需要以下几个字段:
- name:应用的名称,这个便于后台管理用的
- client_id:每个应用客户端的 client_id 是唯一的,相当于用户名,通常是一个随机生成的字符串,client_id 可以直接写在 Javascript 或者源码页面里面
- client_secret:这个秘钥是应用客户端和 OAuth2.0 服务端共同持有,用于鉴别请求中的身份,通常也是一个随机生成的字符串。client_secret 必须保证绝对机密,不能泄露给其他人。如果你部署的应用无法保证 client_secret 安全的话,比如Javascript 应用或者 Native APP,那么则不能使用 client_secret。一般来说,只有服务器端才可以保存 client_secret
- redirect_uri:第三方应用的地址。Redirect URI 可以在用户授权完成之后重定向回你的应用。Redirect URI 授权服务器只会重定向用户到已经注册过的 URI,以避免一些恶意攻击
模块设计
表设计及界面设计如下
CREATE TABLE `app` (
`id` INT(10) NOT NULL AUTO_INCREMENT COMMENT '主键 id,自增',
`pid` INT(10) NOT NULL DEFAULT '0' COMMENT '父级 id',
`name` VARCHAR(20) NOT NULL COMMENT '名称' COLLATE 'utf8mb4_unicode_ci',
`content` VARCHAR(256) NULL DEFAULT NULL COMMENT '简介' COLLATE 'utf8mb4_unicode_ci',
`client_id` VARCHAR(256) NOT NULL COMMENT '客户端 id' COLLATE 'utf8mb4_unicode_ci',
`client_secret` VARCHAR(256) NOT NULL COMMENT '客户端秘钥' COLLATE 'utf8mb4_unicode_ci',
`redirect_uri` VARCHAR(256) NOT NULL COMMENT '用户授权完成之后重定向回你的应用' COLLATE 'utf8mb4_unicode_ci',
`type` VARCHAR(20) NULL DEFAULT 'MISC' COMMENT '应用类型:HTML, APP,API_SERVICE, RPC_SERVICE, MISC' COLLATE 'utf8mb4_unicode_ci',
`logo` VARCHAR(200) NULL DEFAULT NULL COMMENT '图标' COLLATE 'utf8mb4_unicode_ci',
`stat` TINYINT(3) NULL DEFAULT NULL COMMENT '数据字典:状态',
`extend` TEXT NULL DEFAULT NULL COMMENT '扩展 JSON 字段' COLLATE 'utf8mb4_unicode_ci',
`creator` VARCHAR(50) NULL DEFAULT NULL COMMENT '创建人名称(可冗余的)' COLLATE 'utf8mb4_bin',
`creator_id` INT(10) NULL DEFAULT NULL COMMENT '创建人 id',
`create_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
`updater` VARCHAR(50) NULL DEFAULT NULL COMMENT '修改人名称(可冗余的)' COLLATE 'utf8mb4_bin',
`updater_id` INT(10) NULL DEFAULT NULL COMMENT '修改人 id',
`update_date` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `id_UNIQUE` (`id`) USING BTREE
)
COMMENT='应用/客户端'
计划
抽象 App 概念,可纳入 System,利用 pid 做层级。
授权模式对比总结
本文只对前后端授权常见的授权码模式做详细介绍,其他授权模式可自行了解,这里只做简单介绍。
配置
用户密码的加密规则
用户密码当然不能明文保存到数据库中,因为一旦泄露密码,危害的不止系统本身,还有危害用户的切身利益(因为用户不同系统的密码可能是一样的)。那么怎么实现一个加密规则呢?
AJ-IAM 把这个问题开放出来,允许用户自定义自己的加密规则。下面是一个简单的 MD5 加盐配置例子。
/**
* 指定密码的加密规则
*/
@Bean("passwordEncode")
Function<String, String> passwordEncode() {
return PasswordEncoder::md5salt;
}
因为比较简单的缘故,我们并没有设计一个 Java Interface 去定义,而是一个 Lambda:Function<String, String>
搞定。输入参数是明文,返回值是密文。要知道,Spring 对 Lambda 也是可以注入的。PasswordEncoder::md5salt
参见:
package com.ajaxjs.user.service.business;
import com.ajaxjs.util.Digest;
/**
* 密码加密规则
*/
public class PasswordEncoder {
/**
* 盐值
*/
private static final String SALT = "@#D2s!As12";
/**
* 基本的密码加密
*
* @param psw 明文
* @return 密文
*/
public static String md5salt(String psw) {
return Digest.md5(psw + SALT);
}
}
- 认证(Authentication),识别你是谁。即在网站上用来识别某个用户是否是注册过的合法用户。本文前半段皆在讲认证;
- 授权(Authorization),识别你能做什么。即在网站上用来识别某个用户是否有某方面的权限。本文后半段皆在讲授权。
开源参考
- 符节开源 JAP https://gitee.com/fujieid/jap、https://gitee.com/fujieid/jap-ids-oauthserver
- ArkID : 企业级IDaaS/IAM平台系统
- TopIAM 数字身份管控平台
- 推荐七个非常实用的OAuth开源项目
- MITREid Connect
安全考量
Insufficient Redirect URI validation: The risk of allowing to dynamically add arbitrary query parameters and fragments to the redirect_uri。这是一种 OAuth 2.0 和 OpenID Connect 1.0 实现缺陷模式,允许动态添加查询参数和片段到 redirect_uri。如果 redirect_uri 没有得到适当的验证,攻击者可以构造一个包含指向攻击者控制的服务器的 URL 的链接。这可以用来欺骗 AS 将授权代码发送给攻击者。如果用户在用户代理中打开此链接,AS 将重定向用户代理到恶意 URL。攻击者可以捕获伪造 URL 中传递的代码值,然后将其提交给 AS 令牌端点。如果您想测试 AS 是否容易受到不足的重定向 URI 验证,请使用 HTTP 拦截代理(例如 ZAP)捕获流量。启动 OAuth 流并在授权请求处暂停它。更改 redirect_uri 的值并观察响应。调查响应并确定是否接受了任意 redirect_uri 参数。如果 AS 将用户代理重定向到您指定的 redirect_uri,则 AS 未正确验证 redirect_uri。