关于cas:实践篇基于CAS的单点登录实践之路

作者:京东物流 赵勇萍 前言上个月我负责的零碎SSO降级,对接京东ERP零碎,这也让我想起了之前我做过一个单点登录的我的项目。想来单点登录有很多实现计划,不过最支流的还是基于CAS的计划,所以我也就分享一下我的CAS实际之路。 什么是单点登录单点登录的英文名叫做:Single Sign On(简称SSO)。SSO的定义是在多个利用零碎中,用户只须要登录一次就能够拜访所有相互信任的利用零碎。之前我做的零碎,须要须要设计一套反对单点登录的鉴权认证零碎,所有零碎都基于一套鉴权零碎进行登录,并且能够实现各个系统之间的互信和跳转。所以就采纳了CAS架构。 什么是CASCAS架构的外围是须要搭建一个CAS Server,该服务独立部署,领有独立三级域名,次要负责对用户的认证工作。他次要组成包含WEB前端提供登录页面,票据模块,认证模块。 外围票据: a. TGT(Ticket Grangting Ticket):TGT是CAS为用户签发的登录票据,有TGT就表明用户在CAS上胜利登录过。用户在CAS认证胜利后,会生成一个TGT对象,放入本人的缓存中(Session),同时生成TGC以cookie的模式写入浏览器。当再次拜访CAS时,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及拜访起源生成针对起源的ST,用户就不必再次登录,以此来实现单点登录。 b. TGC(Ticket-granting cookie):TGC就是TGT的惟一标识,以cookie的模式存在在CAS Server三级域名下,是CAS Server 用来明确用户身份的凭证。 c. ST(Service Ticket):ST是CAS为用户签发的拜访某一客户端的服务票据。用户拜访service时,service发现用户没有ST,就会重定向到 CAS Server 去获取ST。CAS Server 接管到申请后,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及拜访起源生成针对起源的ST。用户凭借ST去拜访service,service拿ST 去CAS Server 上进行验证,验证通过service生成用户session,并返回资源。 基于CAS的零碎实际计划1. 业务背景在我负责的我的项目零碎中,后盾业务采纳的是微服务架构,有对立的业务网关,所以基于对立的业务网关,整合客户其余零碎登录鉴权流程。具体业务架构图如下: 在此阐明一下,因为登录零碎的用户体系在不同的零碎中,所以我在设计SSO对立登录认证的时候,把SSO零碎与业务系统结构进去。而用户体系有两套,一套叫做采方用户体系,一套叫做供方用户体系。所以才会有如图所示的SSO Server服务,他自身不负责用户治理,但会通过对立标准接口的形式实现管制反转,实现对用户服务的调用。 2. 单点登录时序图时序图如下: 如图所示,时序图标识的是两个零碎通过SSO服务,实现了单点登录。 3. 单点登录外围接口阐明3.1 sso认证跳转接口调用阐明: 由利用侧发动调用认证核心的接口。 URL地址: https:// sso.com?appId=***&tenantType=1&redirectUri=***申请形式:302重定向 参数阐明: appId: 对接SSO认证核心的利用惟一标识,由SSO认证核心通过线下的形式颁发给各个利用零碎。 tenantType: 标记是供方登录还是采方登录。采方为1,供方为2. RedirectUri: 利用回调地址。 3.2 重定向获取长期令牌code接口调用阐明: 有认证核心发动,利用侧需实现的接口。认证核心通过302重定向,将code传给利用侧,利用侧自行发动通过长期令牌code换取accessTokenInfo。 URL地址: https://利用域名?code=***申请形式:GET 参数阐明: Code: 长期令牌,无效工夫5min 3.3 获取accessTokenInfo接口调用阐明 由利用侧发动调用认证核心的接口。通过该接口能够获取accessTokenInfo信息,而后零碎自行生成本零碎session信息。 URL地址: https://sso.com/api/token/create?grantType=authorization_code&appId=yuncai&code=***申请形式:GET 参数阐明: appId: 对接SSO认证核心的利用惟一标识,由SSO认证核心通过线下的形式颁发给各个利用零碎。 code: 长期令牌,需加密 ...

April 13, 2023 · 1 min · jiezi

关于cas:MaxKey单点登录认证系统v358GA发布

English|中文 概述MaxKey单点登录认证零碎,谐音马克思的钥匙寓意是最大钥匙,是业界当先的IAM身份治理和认证产品,反对OAuth 2.x/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供平安、规范和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站官网|官网二线 官网QQ:1054466084 邮箱email:[email protected] 代码托管Gitee|GitHub 产品个性1.标准协议 序号协定反对1.1OAuth 2.0/OpenID Connect高1.2SAML 2.0高1.3JWT高1.4CAS高1.5FormBased中1.6TokenBased(Post/Cookie)中1.7ExtendApi低1.8EXT低2.登录反对 序号登录形式2.1动静验证码 字母/数字/算术2.2双因素认证2.3短信认证 腾讯云短信/阿里云短信/网易云信2.4登录易/Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP2.5Kerberos/SPNEGO/AD域2.6OpenLDAP/ActiveDirectory/规范LDAP服务器2.7社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余2.8扫码登录 企业微信/钉钉/飞书扫码登录3.提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。 4.提供用户生命周期治理,反对SCIM 2协定;开箱即用的连接器(Connector)实现身份供应同步。 5.简化微软Active Directory域控、规范LDAP服务器机构和账号治理,明码自助服务重置明码。 6.IDaas多租户性能,反对团体下多企业独立治理或企业下不同部门数据隔离的,升高运维老本。 7.认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。 8.基于Java EE平台,微服务架构,采纳Spring、MySQL、Tomcat、Redis、MQ等开源技术,扩展性强。 9.开源、平安、自主可控,许可证 Apache 2.0 License &MaxKey版权申明。 界面 下载以后版本百度网盘下载,历史版本 版本日期Docker网盘网盘提取码v 3.5.82022/10/10链接下载mxk9版本阐明MaxKey v 3.5.8 GA 2022/10/10 *(MAXKEY-221301) 新版LOGO更新 *(MAXKEY-221302) 角色属性dynamic - >category *(MAXKEY-221303) 角色列表减少"成员"和"拜访权限"按钮,不便管理员操作 *(MAXKEY-221304) 新增组织类型type=department,sortIndex=11 *(MAXKEY-221305) 组织列表编码和名称左对齐 *(MAXKEY-221306) 社交服务优化,socialsProvider 状态 hidden-->display, scanCode *(MAXKEY-221307) 提交登记地址回调内部URL地址 *(MAXKEY-221308) LDAP配置的测试性能 *(MAXKEY-221309) ActiveDirectory组织同步默认类型department,fullName=orgName *(MAXKEY-221310) LDAP组织同步默认类型department,fullName=orgName *(MAXKEY-221311) 拜访权限治理appName显示问题 *(MAXKEY-221312) 修复MFA的MfaAuthenticationProvider的getProviderName的BUG *(MAXKEY-221313) Cannot call sendRedirect() after the response has been committed 无权限拜访利用 *(MAXKEY-221314) 资源权限治理中的角色列表调整到右边,资源树列表调整到左边 *(MAXKEY-221315) 优化docker-compose部署计划,反对最新版v3.5.7疾速启动部署 @xiaojian *(MAXKEY-221316) 优化v3.3docker-compose部署计划,启动时不从新打包镜像,mysql服务名取代localhost @xiaojian *(MAXKEY-221317) 依赖项援用、更新和降级 springBoot 2.7.4 kafkaclients 2.8.9 hibernate 6.2.5.Final jackson 2.13.4 commonsbeanutils 1.9.4 commonsdbcp2 2.9.0 commonscompress 1.21 postgresql 42.4.1 minidevjsonsmart 2.4.5 snakeyaml 1.32 xmlsec 2.1.7 xstream 1.4.19

October 13, 2022 · 1 min · jiezi

关于cas:使用CAS技术实现无锁并发保证线程安全之单例模式应用

CAS是项乐观锁技术,当多个线程尝试应用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并能够再次尝试。实现单例的形式如下: import java.util.concurrent.atomic.AtomicReference;/** * 应用CAS保障线程平安 * @author shixiangcheng * 2019-12-20 */public class Singleton { private static final AtomicReference<Singleton> INSTANCE=new AtomicReference<Singleton>(); private Singleton(){} public static Singleton getInstance(){ for(;;){ Singleton singleton=INSTANCE.get(); if(singleton!=null){ return singleton; } singleton=new Singleton(); if(INSTANCE.compareAndSet(null, singleton)){ return singleton; } } }}用CAS的益处在于不须要应用传统的锁机制来保障线程平安,CAS是一种基于忙期待的算法,依赖底层硬件的实现,绝对于锁它没有线程切换和阻塞的额定耗费,能够反对较大的并行度。CAS的一个重要毛病在于如果忙期待始终执行不胜利(始终在死循环中),会对CPU造成较大的执行开销。另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创立,很可能导致内存溢出。

May 6, 2022 · 1 min · jiezi

关于cas:docker部署cas

一、首先装置cas镜像1.拉取casdocker镜像 docker pull apereo/cas2.启动容器: docker run--name cas -p 8443:8443 -p 8442:8080apereo/cas /bin/sh /cas-overlay/bin/run-cas.sh期待一会之后,启动失败了,报错是没有证书,局部谬误如下: Caused by: java.lang.IllegalArgumentException: /etc/cas/thekeystore (No such file or directory)at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:99)at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:71)at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:216)at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1159)at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1245)at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:603)at org.apache.catalina.connector.Connector.startInternal(Connector.java:1064)... 27 moreCaused by: java.io.FileNotFoundException: /etc/cas/thekeystore (No such file or directory)at java.base/java.io.FileInputStream.open0(Native Method)at java.base/java.io.FileInputStream.open(Unknown Source)二、制作证书 keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -validity 10000输出密钥库口令:再次输出新口令:您的名字与姓氏是什么[Unknown]:sso.castest.com您的组织单位名称是什么[Unknown]:sso您的组织名称是什么[Unknown]:cas您所在的城市或区域名称是什么[Unknown]:beijing您所在的省/市/自治区名称是什么[Unknown]:beijing该单位的双字母国家/地区代码是什么[Unknown]:CNCN=sso.castest.com, OU=sso, O=cas, L=beijing, ST=beijing, C=CN是否正确[否]:y正在为以下对象生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 10,000 天):CN=sso.castest.com, OU=sso, O=cas, L=beijing, ST=beijing, C=CN[正在存储debug.keystore]其中密钥库命令输出的是changeit ...

July 26, 2021 · 1 min · jiezi

关于单点登录:MaxKey单点登录认证系统-v270GA-发布

English | 中文 概述MaxKey单点登录认证零碎(Single Sign On System),中文谐音马克思的钥匙寓意是最大钥匙,是业界当先的企业级开源IAM身份治理和身份认证产品,国内开源IAM第一品牌;反对OAuth 2.0/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供简略、规范、平安和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站 官网 | 官网二线 QQ交换群:434469201 邮箱email: maxkeysupport@163.com 代码托管 GitHub | 码云(Gitee) 什么是单点登录(Single Sign On),简称为SSO? 用户只须要登录认证核心一次就能够拜访所有相互信任的利用零碎,无需再次登录。 次要性能: 1) 所有利用零碎共享一个身份认证零碎 2) 所有利用零碎可能辨认和提取ticket信息 产品个性规范认证协定:序号协定反对1.1OAuth 2.0/OpenID Connect高1.2SAML 2.0高1.3JWT高1.4CAS高1.5FormBased中1.6TokenBased(Post/Cookie)中1.7ExtendApi低1.8EXT低登录反对序号登录形式2.1动静验证码 字母/数字/算术2.2双因素认证2.3短信认证 腾讯云短信/阿里云短信/网易云信2.4登录易/Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP2.5Kerberos/SPNEGO/AD域2.6OpenLDAP/ActiveDirectory/规范LDAP服务器2.7社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。提供用户生命周期治理,反对SCIM 2协定,基于Apache Kafka代理,通过连接器(Connector)实现身份供应同步。认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。多种认证机制并存,各利用零碎可保留原有认证机制,同时集成认证核心的认证;利用具备高度独立性,不依赖认证核心,又可用应用认证核心的认证,实现单点登录。基于Java平台开发,采纳Spring、MySQL、Tomcat、Apache Kafka、Redis等开源技术,反对微服务,扩展性强。开源、平安、自主可控,许可证 Apache 2.0 License & MaxKey版权申明。界面MaxKey认证 登录界面   主界面   MaxKey治理 拜访报表   用户治理   利用治理   下载以后版本百度网盘下载, 历史版本 版本日期下载地址提取码v 2.7.0 GA2021/04/15链接下载hf73Roadmap1.MaxKey Cloud(微服务版)-2021年 2.零信赖场景整合 版本阐明MaxKey v 2.7.0 GA 2021/04/15 ...

April 15, 2021 · 1 min · jiezi

关于golang:Go同步原语的基石

Go是一门以并发编程见长的语言,它提供了一系列的同步原语不便开发者应用,例如sync包下的Mutex、RWMutex、WaitGroup、Once、Cond,以及形象层级更高的Channel。然而,它们的实现基石是原子操作。须要记住的是:软件原子操作离不开硬件指令的反对。本文拟通过探讨原子操作——比拟并替换(compare and swap, CAS)的实现,来了解Go是如何借助硬件指令来实现这一过程的。 什么是CAS在看源码实现之前,咱们先了解一下CAS。 维基百科定义:CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而防止多线程同时改写某一数据时因为执行程序不确定性以及中断的不可预知性产生的数据不统一问题。 该操作通过将内存中的值与指定数据进行比拟,当数值一样时将内存中的数据替换为新的值。 CAS的实现思维能够用以下伪代码示意 bool Cas(int *val, int old, int new) Atomically: if(*val == old){ *val = new; return 1; } else { return 0; }在sync/atomic/doc.go中,定义了一系列原子操作函数原型。以CompareAndSwapInt32为例,有以下代码 package mainimport ( "fmt" "sync/atomic")func main() { a := int32(10) ok := atomic.CompareAndSwapInt32(&a, 10, 100) fmt.Println(a, ok) ok = atomic.CompareAndSwapInt32(&a, 10, 50) fmt.Println(a, ok)}它的执行后果如下 $ go run main.go100 true100 falseCAS能做什么CAS从线程层面来说,它是非阻塞的,其乐观地认为在数据更新期间没有其余线程影响,因而也经常被称为是一种轻量级的乐观锁。它关注的是并发平安,而并非并发同步。 在文章结尾时,咱们就曾经提到原子操作是实现下层同步原语的基石。以互斥锁为例,为了不便了解,咱们在这里将它的状态定义为0和1,0代表目前该锁闲暇,1代表已被加锁。那么,这个时候,CAS就是治理状态的最佳抉择,以下是sync.Mutex中Lock办法的局部实现代码。 func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow()}在atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)中,m.state代表锁的状态,通过CAS函数,判断锁此时的状态是否闲暇(m.state==0),是,则对其加锁(这里mutexLocked的值为1)。 ...

April 7, 2021 · 2 min · jiezi

关于cas:MaxKey单点登录认证系统-v260GA-发布

English | 中文 概述MaxKey单点登录认证零碎(Single Sign On System),中文谐音马克思的钥匙寓意是最大钥匙,是业界当先的企业级开源IAM身份治理和身份认证产品,国内开源IAM第一品牌;反对OAuth 2.0/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供简略、规范、平安和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站 官网 | 官网二线 QQ交换群:434469201 邮箱email: maxkeysupport@163.com 代码托管 GitHub | 码云(Gitee) 什么是单点登录(Single Sign On),简称为SSO? 用户只须要登录认证核心一次就能够拜访所有相互信任的利用零碎,无需再次登录。 次要性能: 1) 所有利用零碎共享一个身份认证零碎 2) 所有利用零碎可能辨认和提取ticket信息 产品个性规范认证协定:序号协定反对1.1OAuth 2.0/OpenID Connect高1.2SAML 2.0高1.3JWT高1.4CAS高1.5FormBased中1.6TokenBased(Post/Cookie)中1.7ExtendApi低1.8EXT低登录反对序号登录形式2.1动静验证码 字母/数字/算术2.2双因素认证2.3短信认证 腾讯云短信/阿里云短信/网易云信2.4登录易/Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP2.5Kerberos/SPNEGO/AD域2.6社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。提供用户生命周期治理,反对SCIM 2协定,基于Apache Kafka代理,通过连接器(Connector)实现身份供应同步。认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。多种认证机制并存,各利用零碎可保留原有认证机制,同时集成认证核心的认证;利用具备高度独立性,不依赖认证核心,又可用应用认证核心的认证,实现单点登录。基于Java平台开发,采纳Spring、MySQL、Tomcat、Apache Kafka、Redis等开源技术,反对微服务,扩展性强。开源、平安、自主可控,许可证 Apache 2.0 License & MaxKey版权申明。界面MaxKey认证 登录界面   主界面   MaxKey治理 拜访报表   用户治理   利用治理   下载以后版本百度网盘下载, 历史版本 版本日期下载地址提取码v 2.6.0 GA2021/03/05链接下载0x24Roadmap1.MaxKey Cloud(微服务版)-2021年 2.零信赖场景整合 版本阐明MaxKey v 2.6.0 GA 2021/03/05 ...

March 4, 2021 · 1 min · jiezi

关于cas:SSO单点登录

单点登录(Single Sign On)是指一次登录即可拜访相干的零碎网站。而实现的技术计划有很多种,针对不同场景需要计划也不一样。 场景1、只有一个域名只有一个零碎平台一个域名,如: example.com  登录流程: 登录生成Session,将SessionId设置到以后域名的cookie中毛病: 只限于单个零碎平台2、二级域名雷同多个零碎平台,然而二级域名雷同,如: xxx.example.com 、 yyy.example.com ,二级域名都是 example.com  登录流程: 登录生成Session,将SessionId设置到二级域名的cookie中同一个二级域名下的零碎,只有登录一次其余零碎即可免登毛病: 如果公司的二级域名比拟多,不同二级域之间无奈免登3、二级域名不同多零碎平台二级域名也不同,如: xxx.example.com 、 xxx.test.com 、 demo.com ,二级域名都不同,跨域无奈读取cookie。实现计划有很多种,咱们看下cas实现,这里就须要一个sso认证核心服务来实现单点登录。 登录流程: xxx.example.com 未登录跳转到sso服务的对立登录页 www.sso.com/login ,输出账号密码创立全局会话,设置tgc到cookie,并将生成tgt参数增加 xxx.example.com URL上跳回xxx.example.com 服务拿tgt参数去获取st,做这一步次要为了平安,而后用st去获取用户信息,st是一次性的,应用后生效,校验通过后设置Session,并设置SeesionId到cookiexxx.test.com 校验没有cookie跳到sso服务,sso服务发现用户曾经登录,生成tgt参数增加到 xxx.test.com 回跳登出流程: 登记以后服务session,革除cookie,并重定向sso服务登出页面sso服务登记全局会话,告诉其余服务销毁session登录页面不对立问:如果登录页面各个系统想不一样,二级域名也应用各个系统本人的,怎么办?答:能够每个零碎服务调用sso认证核心登录,登录后带着tgt参数重定向到不同二级域名去写入cookie,登出也须要重定向这些域名。然而如果二级域名很多体验就很差了。

January 25, 2021 · 1 min · jiezi

关于cas:Cas单点登录剖析

CAS简介CAS(Central Authentication Service) 是 Yale 大学发动的构建 Web SSO 的 开源我的项目SSO 是什么?SSO-Single Sign On就是 单点登录 也就是 多个网站程序 对立到一个网址进行登录身份验证次要特点是:SSO 利用之间应用 Web 协定 (如HTTPS) ,并且只有一个登录入口。咱们所讲的SSO,指 Web SSO 。SSO 的体系中,有上面三种角色:User(多个)Web利用(多个)SSO认证核心(一个)所有的登录都在 SSO 认证核心进行。SSO 认证核心通过一些办法来通知 Web 利用以后拜访用户到底是不是通过认证的用户。SSO 认证核心和所有的 Web 利用建设一种信赖关系。就是能达到 很多不同服务器的或者雷同服务器的网站 对立到某个中央登录 如果你是应用同一个浏览器登录的 那么 在这个浏览器再关上其余的网站 这个网站 就不必再登录了 CAS 的构造体系CAS ServerCAS Server 负责实现对用户信息的认证,须要独自部署,CAS Server 会解决用户名 / 明码等凭证 (Credentials) 。就是 装置在服务器端的一个web程序 目前有耶鲁大学的 也有其它机构开发的,它是复制认证的服务器CAS ClientCAS Client部署在应用程序中,当有对本地 Web 利用受爱护资源的拜访申请,并且须要对申请方进行身份认证,重定向到 CAS Server 进行认证。 术语解释TGT:用来存储登入用户身份的的重要票据,一旦用户胜利登入到CAS服务器后,CAS服务器就会生成一个TGT,并存储在CAS服务器端。最为重要的是确保TGT是全局惟一的TGC:是一个存储TGT的Cookie。当登入到CAS服务器后,CAS会在浏览器中存储它,这样下次登入时传回CAS服务器进行登入验证,只有借助Https传输通道,TGC才会被传回CAS服务器术语解释SSO-Single Sign On,单点登录TGT-Ticket Granting Ticket,用户身份认证凭证票据ST-Service Ticket,服务许可凭证票据TGC-Ticket Granting Cookie,寄存用户身份认证凭证票据的cookie ...

January 18, 2021 · 1 min · jiezi

关于cas:CAS单点登录浅析一实现思路

CAS是Central Authentiction Service 的缩写,地方认证服务,一种独立凋谢指令协定.CAS是Yale大学发动的开源我的项目.旨在为Web利用零碎提供一种牢靠的单点登录办法,CAS在2004年12月正式称为JA-SIG的一个我的项目. 特点: 1.开源的企业级单点登录解决方案. 2.CAS Service为须要独立部署的Web利用. 3.CAS Client 反对十分多的客户端(这里指单点登录零碎中的各个Web利用),包含Java .NET PHP等 首次登录验证,其原理如下图所示: 1、第一次跳转:客户端拜访利用零碎,利用零碎判断Session发现为定路,返回302跳转到sso登录页面,并传递service参数给sso,该service参数有两个作用:2、service个别传递利用零碎url地址,用于sso认证通过后回跳到利用零碎;3、service参数同时会被cas服务端的作为cas客户端的惟一标记记录下来,用于前期匹配相应的认证凭据;4、第二次跳转:浏览器显示登录页面,用户输出账号密码登录胜利后,sso会返回302跳转回到原来申请的利用零碎页面,并携带ticket参数,作为认证票据,同时通过Set-Cookie向浏览器记录TGT,(TGT的作用将在下一个利用零碎须要登录的时候体现进去,是防止反复登录的要害)5、一次后盾验证:利用零碎接管到带有ticket的申请后,向从后盾间接向sso服务器发动一个http申请,将service和ticket作为参数,用于验证ticket的有效性;如果ticket无效,sso服务器将返回该ticket对应的登录用户名。 已实现登录验证切换到其余零碎时,其原理如下图所示:1、当用户曾经登录过一个利用零碎当前,在同一个浏览器上拜访第二个利用零碎,依据单点登录的要求此时不应该再登录,而是间接进入第二个零碎。然而实际上还是须要通过两次前端跳转、一次后端验证,只不过此时的两次跳转是间断的,两头不会再呈现登陆页面,用户感触不到。判断的根据就是后面第4步通过Set-Cookie保留到客户端的TGT(Ticket Granted Cookie )。2、相比首次拜访,少了之前的第3步(不须要再呈现登录页面),因为此时在第二步跳转时,携带了之前保留的TGT,cas服务端通过TGT能够得悉用户信息,因而间接生成ticket返回给利用零碎。所以此时是两次间断的302跳转,用户看到的成果就是间接进入第二个利用零碎了。 CAS源码解析-cas相干的jar包CAS源码-cas配置文件1、spring-configuration是spring相干的外围配置,蕴含读取配置文件办法、springbean注册配置、ticket票据配置、Cookie配置等。2、view 页面目录:蕴含jsp、js、css等。3、cas.properties文件:cas服务的配置文件,能够寄存数据库配置、redis配置等。4、deployerConfigContext.xml:用于连贯数据库的配置。1、零碎接管到申请首先通过web.xml,通过web.xml中SafeDispatcherServlet进行映射找到cas-servlet.xml.2、cas-servlet.xml中次要蕴含了两种管制层映射形式。handlerMappingC和FlowHandlerMaping。依据两种不同的映射形式找到对应的服务。3、以退出登陆为例/logout,通过springbean关系能够找到退出的登陆页面。退出胜利后进入/login登陆页面。4、/login 采纳了FlowHandlerMaping形式,通过login-webflow.xml文件能够看到cas登陆的整个过程。on-start(start-state)流程开始,end-state流程完结 decision-state判断,相似于if,view-state对应jsp页面 action-state对应执行程序的某段。

December 24, 2020 · 1 min · jiezi

关于cas:Java中CAS原理分析volatile和synchronized浅析

CAS是什么?CAS英文解释是比拟和替换,是cpu底层的源语,是解决共享变量原子性实现计划,它定义了三个变量,内存地址值对应V,期待值E和要批改的值U,如下图所示,这些变量都是在高速缓存中的,如果两个线程A,B别离通过cas形式同时批改共享变量,假如当A线程先获取工夫片,如果发现V的值和E相等就将主内存值更新为U,如果不相等阐明线程B在线程A更新之前曾经胜利更新过,线程A会失败重试,此时依据缓存一致性协定,线程A的本地正本会生效,须要从主内存再同步最新的变量到本地内存正本,在Java中通过调用UnSafe的compareAndSet相似形式调用,底层是c,反编译后操作系统指令是cmpxchg指令。 保障i++原子性你肯定会有一个疑难,被volatile润饰的变量i,i++为什么会有线程平安问题呢,也就是原子性的问题,咱们还是举一个经典的i++案例一步步剖析吧!咱们晓得在多线程状况下volatile保障了共享变量的可见性,程序行,但唯独不能保障原子性,起因是i++是一个复合操作,大抵能够分成3步,1.先从主内存拿到最新的i值,2.将i加1这个操作保留到操作数栈,3.从栈中取出i加1的值写回到主内存。OK,当线程AB同时执行i++操作时,比方线程A先获取工夫片,执行完第2步,这是线程A还未执行完,工夫片调配给线程B,B顺利执行完所有操作后并同步了主内存,假如咱们i的初始值是1,那么此时主内存值是2,因为线程B执行结束,cpu工夫片又回到线程A手上,做第3步操作,此时同步到主内存的值还是2,看,线程A,B各做了一次加1的操作,但最终后果可能是2,cas的作用就来了,他能保障i++操作的原子性,为什么能保障原子性呢?cas能够把下面三个操作合并成一个操作,是原子的。 有什么益处?大家都晓得解决多线程平安须要用到锁的,能够用synchronized来解决,然而synchronized也有它的劣势,最次要是它是阻塞的,阻塞会有什么问题?性能啊,这是计算机人不能忍的,频繁内核外核切换,会重大节约系统资源,所以就提了cas这个乐观锁概念,它是非阻塞的,操作系统不必在内核态与用户态来回切换,相当于用while循环形式获取锁,在性能上有肯定晋升。即便这样,也会有肯定问题,上面咱们来看看。 有什么问题?1.ABA问题。 这个案例比较简单,线程A把共享变量i,从1变成2,再变成1,线程B想把i变成2,原本应该是不会胜利,因为即时变量i当初是1,然而它的状态变动了,他的解决方案是版本号。相当于批改胜利一次版本号减少1,就能够解决了,已经被面试官问到一个问题,cas是线程平安的吗?答案不是线程平安的。 2.自旋工夫过长。 如果一个线程拿到锁后,始终不开释,其余线程就只能始终循环期待。 3.只能保障一个共享变量的原子性。 像Automic包上面的基本上都只能保障一个变量的原子性。 JUC包上面应用!可能有些童鞋看JDK源码会比拟纠结一个点,发现volatile关键字个别都会和cas连用,如果不要volatile会怎么样呢?cas自身只作用于办法,cas对共享变量没有束缚,如果不对共享变量做volatile润饰,是不可见的,不可能保障共享变量的实效性,须要期待共享变量被动同步到主内存,这是须要花工夫的,效率更低下,所有在JUC并发包里始终能够看到这样的volatile关键字个别都会和cas组合。 总结这篇文章,咱们先引出了cas概念,并且阐明了它的优缺点,做了案例介绍,简略的和synchronized关键字做了比拟,最初,深刻的阐明了volatile关键字和cas连用的效率,这是我在深刻思考后失去的论断,分享给大家,文章有肯定浏览门槛,如果有想搞清楚童鞋,能够1v1私聊探讨交换。心愿大家喜爱。点赞哦! 我是叫练,边叫边练,欢送点赞和评论。

December 14, 2020 · 1 min · jiezi

关于cas:单点登录认证系统-MaxKey-v-210GA发布

MaxKey(马克思的钥匙)单点登录认证零碎(Sigle Sign On System),寓意是最大钥匙,是业界当先的企业级IAM身份治理和身份认证产品,反对OAuth 2.0/OpenID Connect、SAML 2.0、JWT、CAS、SCIM等标准协议,提供简略、规范、平安和凋谢的用户身份治理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC权限治理和资源管理等。 官方网站 官方网站 | 官方网站二线 代码托管 GitHub | 码云(Gitee) 什么是单点登录(Single Sign On),简称为SSO? 用户只须要登录认证核心一次就能够拜访所有相互信任的利用零碎,无需再次登录。 次要性能: 1.所有利用零碎共享一个身份认证零碎 2.所有利用零碎可能辨认和提取ticket信息 规范认证协定:序号协定反对1OAuth 2.0/OpenID Connect高2SAML 2.0高3JWT高4CAS高5FormBased中6TokenBased(Post/Cookie)中7ExtendApi低8EXT低登录反对序号登录形式1动静验证码 字母/数字/算术2双因素认证3短信认证 腾讯云短信/阿里云短信/网易云信4Google/Microsoft Authenticator/FreeOTP/反对TOTP或者HOTP5Kerberos/SPNEGO/AD域6社交账号 微信/QQ/微博/钉钉/Google/Facebook/其余提供规范的认证接口以便于其余利用集成SSO,平安的挪动接入,平安的API、第三方认证和互联网认证的整合。提供用户生命周期治理,反对SCIM 2协定,基于Apache Kafka代理,通过连接器(Connector)实现身份供应同步。认证核心具备平台无关性、环境多样性,反对Web、手机、挪动设施等, 如Apple iOS,Andriod等,将认证能力从B/S到挪动利用全面笼罩。多种认证机制并存,各利用零碎可保留原有认证机制,同时集成认证核心的认证;利用具备高度独立性,不依赖认证核心,又可用应用认证核心的认证,实现单点登录。基于Java平台开发,采纳Spring、MySQL、Tomcat、Apache Kafka、Redis等开源技术,反对微服务,扩展性强。许可证 Apache License, Version 2.0,开源收费。*界面MaxKey认证 登录界面   主界面   MaxKey治理 拜访报表   用户治理   利用治理   下载以后版本百度网盘下载, 历史版本 版本日期下载地址提取码v 2.1.0 GA2020/08/01链接下载9umvRoadmapSCIM 2 Support-System for Cross-domain Identity Management 动静用户组实现(基于用户属性或机构) 版本阐明MaxKey v 2.1.0 GA 2020/08/01 (MAXKEY-200701) 官方网站降级,清晰简洁的界面,全新的页面导航   (MAXKEY-200702) Mysql降级到8.0.21GA ...

August 6, 2020 · 1 min · jiezi

spring security集成cas

spring security集成cas源码地址在文章末尾,转载请注明出处,谢谢。0.配置本地ssl连接操作记录如下:=====================1.创建证书文件thekeystore ,并导出为thekeystore.crtcd C:\Users\23570\keystoreC:\Users\23570\keystore>keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore输入密钥库口令:changeit再次输入新口令:changeit您的名字与姓氏是什么? [Unknown]: localhost您的组织单位名称是什么? [Unknown]: localhost您的组织名称是什么? [Unknown]:您所在的城市或区域名称是什么? [Unknown]:您所在的省/市/自治区名称是什么? [Unknown]:该单位的双字母国家/地区代码是什么? [Unknown]:CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: y输入 <thekeystore> 的密钥口令 (如果和密钥库口令相同, 按回车):Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。C:\Users\23570\keystore>keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore输入密钥库口令:存储在文件 <thekeystore.crt> 中的证书Warning:JKS 密钥库使用专用格式。建议使用 “keytool -importkeystore -srckeystore thekeystore -destkeystore thekeystore -deststoretype pkcs12” 迁移到行业标准格式 PKCS12。======================2.把证书文件导入到本地证书库中,注意切换JRE相应目录切换为【管理员身份】运行以下命令:C:\Users\23570\keystore>keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"所有者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown发布者: CN=localhost, OU=localhost, O=Unknown, L=Unknown, ST=Unknown, C=Unknown序列号: 657eb9ce有效期为 Fri Mar 29 11:50:08 CST 2019 至 Thu Jun 27 11:50:08 CST 2019证书指纹: MD5: 8D:3C:78:E9:8A:44:77:3F:C2:8B:20:95:C7:6C:91:8F SHA1: 69:F3:46:C4:03:95:E1:D0:E6:9D:8B:72:F4:EB:ED:13:8B:9A:6A:38 SHA256: 79:D1:F8:B2:1B:E3:AF:D4:4F:35:CB:6B:C8:84:3F:85:21:13:0F:96:4A:B5:E5:4C:47:11:44:21:8F:F3:2D:83签名算法名称: SHA256withRSA主体公共密钥算法: 2048 位 RSA 密钥版本: 3扩展:#1: ObjectId: 2.5.29.14 Criticality=falseSubjectKeyIdentifier [KeyIdentifier [0000: B0 38 1D 00 56 65 EE 98 7C 35 58 04 B5 2E C0 A0 .8..Ve…5X…..0010: D5 C2 C5 B5 ….]]是否信任此证书? [否]: y证书已添加到密钥库中=========================3.配置tomcat/conf/server.xml中的ssl连接<Connector port=“8443” protocol=“org.apache.coyote.http11.Http11NioProtocol” maxThreads=“200” SSLEnabled=“true” scheme=“https” secure=“true” clientAuth=“false” sslProtocol=“TLS” keystoreFile=“C:\Users\23570\keystore\thekeystore” keystorePass=“changeit”/> ==========================4.其他命令参考删除JRE中指定别名的证书keytool -delete -alias cas.server.com -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts"查看JRE中指定别名的证书keytool -list -v -keystore “C:\Program Files\Java\jdk1.8.0_191\jre\lib\security\cacerts” -alias cas.server.com 1.cas服务搭建git clone –branch 5.3 https://github.com/apereo/cas-overlay-template.git cas-server注意:这里选用cas server 5.3版本,使用maven构建1.使用数据库账号密码登录cas导入依赖<dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-jdbc</artifactId> <version>${cas.version}</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version></dependency>配置查询#这里是配置用户表单登录时用户名字段为usernamecas.authn.jdbc.query[0].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.username=?;cas.authn.jdbc.query[0].fieldPassword=passwordcas.authn.jdbc.query[0].fieldExpired=expiredcas.authn.jdbc.query[0].fieldDisabled=disabledcas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[0].user=rootcas.authn.jdbc.query[0].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5#配置用户表单登录时用户名字段为phonecas.authn.jdbc.query[1].sql=select password from oauth_account left join oauth_user on oauth_account.user_id=oauth_user.user_id where oauth_user.phone=?;cas.authn.jdbc.query[1].fieldPassword=passwordcas.authn.jdbc.query[1].fieldExpired=expiredcas.authn.jdbc.query[1].fieldDisabled=disabledcas.authn.jdbc.query[1].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[1].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[1].url=jdbc:mysql://127.0.0.1:3306/srm-aurora2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[1].user=rootcas.authn.jdbc.query[1].password=root#默认不加密#cas.authn.jdbc.query[0].passwordEncoder.type=NONE#默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密cas.authn.jdbc.query[1].passwordEncoder.type=DEFAULTcas.authn.jdbc.query[1].passwordEncoder.characterEncoding=UTF-8cas.authn.jdbc.query[1].passwordEncoder.encodingAlgorithm=MD5数据库脚本/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50722 Source Host : localhost:3306 Source Schema : srm-aurora2 Target Server Type : MySQL Target Server Version : 50722 File Encoding : 65001 Date: 19/04/2019 14:40:52*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;– —————————— Table structure for oauth_account– —————————-DROP TABLE IF EXISTS oauth_account;CREATE TABLE oauth_account ( account_id int(255) NOT NULL AUTO_INCREMENT, tenant_id int(255) NULL DEFAULT NULL, user_id int(255) NULL DEFAULT NULL, password varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (account_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_account– —————————-INSERT INTO oauth_account VALUES (1, 1, 1, ’e10adc3949ba59abbe56e057f20f883e’);INSERT INTO oauth_account VALUES (2, 2, 2, ’e10adc3949ba59abbe56e057f20f883e’);– —————————— Table structure for oauth_cas_info– —————————-DROP TABLE IF EXISTS oauth_cas_info;CREATE TABLE oauth_cas_info ( cas_id int(255) NOT NULL, tenant_id int(255) NULL DEFAULT NULL, cas_server varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_login varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_server_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, cas_service_logout varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (cas_id) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_cas_info– —————————-INSERT INTO oauth_cas_info VALUES (1, 2, ‘https://localhost:8443/cas’, ‘https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:8443/cas/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success’);INSERT INTO oauth_cas_info VALUES (2, 3, ‘https://localhost:9443/sso’, ‘https://localhost:9443/sso/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas’, ‘https://localhost:9443/sso/logout’, ‘http://localhost:8083/login/cas’, ‘https://localhost:9443/sso/logout?service=http://localhost:8083/logout/success’);– —————————— Table structure for oauth_tenant– —————————-DROP TABLE IF EXISTS oauth_tenant;CREATE TABLE oauth_tenant ( tenant_id int(255) NOT NULL AUTO_INCREMENT, domain varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_provider varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, login_type varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (tenant_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_tenant– —————————-INSERT INTO oauth_tenant VALUES (1, ‘http://localhost:8084/’, ‘a租户’, ‘oauth’, ‘form’);INSERT INTO oauth_tenant VALUES (2, ‘http://localhost:8085/’, ‘b租户’, ‘cas’, ‘wechat’);INSERT INTO oauth_tenant VALUES (3, ‘http://localhost:8086/’, ‘c租户’, ‘cas’, ‘form’);– —————————— Table structure for oauth_user– —————————-DROP TABLE IF EXISTS oauth_user;CREATE TABLE oauth_user ( user_id int(255) NOT NULL AUTO_INCREMENT, username varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, phone varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, email varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (user_id) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;– —————————— Records of oauth_user– —————————-INSERT INTO oauth_user VALUES (1, ‘22304’, ‘15797656200’, ‘donglin.ling@hand-china.com’);INSERT INTO oauth_user VALUES (2, ‘admin’, ‘15797656201’, ’ericling666@gmail.com’);SET FOREIGN_KEY_CHECKS = 1;发布cas server,访问:https://localhost:8443/cas/login测试账号和密码,admin:1234562.CAS客户端服务注册这里演示通过json文件注册服务,实际项目中,可以配置成从数据库中注册添加json支持依赖<!–json服务注册–><dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-json-service-registry</artifactId> <version>${cas.version}</version></dependency>添加json服务注册文件{ “@class” : “org.apereo.cas.services.RegexRegisteredService”, “serviceId” : “^(https|http|imaps)://.”, “name” : “HTTPS and HTTP and IMAPS”, “id” : 10000001, “description” : “This service definition authorizes all application urls that support HTTPS and HTTP and IMAPS protocols.”, “evaluationOrder” : 10000, “attributeReleasePolicy”: { “@class”: “org.apereo.cas.services.ReturnAllAttributeReleasePolicy” }, “proxyPolicy”: { “@class”: “org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy”, “pattern”: “^(https|http)?://.” }}注意文件目录和文件名格式:目录:resources/services/{xxx}-{id}.jsonxxx表示可以随意配置,后面-{id},这里的id需要和文件中的id一致。作为演示,这个json注册文件,没有限制域名,也就是说所有的服务都可以注册成功。开启json服务注册### 开启json服务注册#cas.serviceRegistry.initFromJson=true以上就是配置json服务注册的过程。3.其它常用配置### 登出后允许跳转到指定页面#cas.logout.followServiceRedirects=true# 设置service ticket的行为# cas.ticket.st.maxLength=20# cas.ticket.st.numberOfUses=1cas.ticket.st.timeToKillInSeconds=120# 设置proxy ticket的行为cas.ticket.pt.timeToKillInSeconds=120# cas.ticket.pt.numberOfUses=1配置说明:配置cas服务登出时,是否跳转到各个子服务的登出页面,默认false【即默认情况下,子服务点击登出,用户统一跳转到cas的登出页面】,子服务登出时访问cas登出端点,并带上service。示例:https://localhost:8443/cas/logout?service=http://localhost:8083/logout/success这样配置,cas注销session之后,会重定向到service。这个字段可以配置,默认是service。配置如下:cas.logout.redirectParameter=service配置service ticket的失效时间,我这里配置这个选项,是为了方便后面debug调试,实际生产中,不必配置这个选项。更多常用配置项,请查看官网链接:https://apereo.github.io/cas/…2.spring security和cas集成1.依赖和其他配置核心依赖<!–security-cas集成–><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>application.yml配置# 我这里是为了方便调试logging.level.org.springframework.security: debuglogging.level.web: debug2.配置登录端点spring security开启表单登陆@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin().loginPage("/login”); }这个配置,会开启用户表单登录,并且配置登录端点为/login配置登录端点响应逻辑@Controllerpublic class LoginEndpointConfig { @Autowired private TenantService tenantService; @Autowired private CasInfoService casInfoService; @GetMapping("/login”) public String loginJump(HttpSession session) { final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST); if (attribute == null) { //默认跳转到登陆页面 return “login”; } if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: return “login”; default: return “login”; } } } else { return “login”; } } return “login”; }}我这里的登陆逻辑实现了:用户从第三方网站【平台的租户】跳转到这个网站时,根据跳转过来的请求头【referer】获取这个租户的域名,再从数据库中查找这个域名对应的租户信息和登录逻辑。这里的租户信息有一个关键字段是:loginProvider,有两种情况cas,oauthcas:租户有自己的cas单点登录系统,平台需要和租户的cas集成oauth:租户没有cas,使用平台统一的表单登陆具体的登录流程分析,在最后详细介绍,这里不过多讲解。3.配置CAS的ticket校验以及登录响应自定义AuthenticationFilter因为我的需求是,每个租户有自己的cas系统,所以每个cas地址不一样,不可能使用官方的CasAuthenticationFilter 。具体原因是,官方的CasAuthenticationFilter在应用程序启动时,资源匹配器就已经初始化好了,它只会对特定的cas地址发送ticket校验请求。而要做到可配置,就只能自己实现这个逻辑,并且可配置的对相应cas server地址发出ticket校验请求。public class CustomCasAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private final static String endpoint = “/login/cas”; private UserDetailsService userDetailsService; public CustomCasAuthenticationFilter(String defaultFilterProcessesUrl, UserDetailsService userDetailsService) { super(defaultFilterProcessesUrl); this.userDetailsService = userDetailsService; } private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); public CustomCasAuthenticationFilter() { super(new AntPathRequestMatcher(endpoint)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response); } /** / public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { return null; } /* * 从HttpServletRequest请求中获取ticket / private String obtainArtifact(HttpServletRequest request) { String artifactParameter = “ticket”; return request.getParameter(artifactParameter); } /* * 获取Cas30ServiceTicketValidator,暂时没有实现代理凭据 / private TicketValidator getTicketValidator(String casServerUrlPrefix) { return new Cas30ServiceTicketValidator(casServerUrlPrefix); } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed); }}2. 把自定义的CustomCasAuthenticationFilter添加到spring security的过滤器链中@Qualifier(“userDetailsServiceImpl”) @Autowired private UserDetailsService userDetailsService;private final static String endpoint = “/login/cas”;@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomCasAuthenticationFilter(endpoint, userDetailsService), UsernamePasswordAuthenticationFilter.class);}### 4.配置单点登出1. 自定义实现LogoutFilterpublic class CustomLogoutFilter extends GenericFilterBean { private RequestMatcher logoutRequestMatcher; private SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler; private LogoutHandler logoutHandler = new SecurityContextLogoutHandler(); //获取casInfo信息,依此来判断当前认证用户的cas地址 private CasInfoService casInfoService; public CustomLogoutFilter(String filterProcessesUrl, String logoutSuccessUrl,CasInfoService casInfoService) { this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl); this.urlLogoutSuccessHandler=new SimpleUrlLogoutSuccessHandler(); this.urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); this.casInfoService = casInfoService; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug(“Logging out user ‘” + auth + “’ and transferring to logout destination”); } //本地登出 logoutHandler.logout(request,response,auth); if (auth == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response, null); }else{ //判断是否通过cas认证,获取cas信息 Object details = auth.getDetails(); if (details == null) { urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } if (details instanceof UserDetails) { Integer tenantId = ((UserDetailsVO) details).getTenant().getTenantId(); CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenantId); response.sendRedirect(casInfoByTenantId.getCasServiceLogout()); }else{ urlLogoutSuccessHandler.onLogoutSuccess(request,response,auth); } } return; } filterChain.doFilter(request, response); } /* * 当前请求是否为登出请求 */ private boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) { return logoutRequestMatcher.matches(request); }}2. 把CustomLogoutFilter添加到spring security的过滤器链中@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(new CustomLogoutFilter("/logout”, “/logout/success”, casInfoService), LogoutFilter.class);}### 5.流程分析#### 1.表单登陆流程分析目前有5个服务cas server,tenant-a,tenant-b,tenant-c,a2-oauth租户a,b,c就是一个超链接而已,为了模拟三个租户的域名,所以弄了三个租户。这三个域名分别是:&lt;http://localhost:8084/&gt; , &lt;http://localhost:8085/&gt; , &lt;http://localhost:8086/&gt;数据库中,对这3个租户的配置如下:其中b和c租户是配置了cas登录的。cas server发布了两个,都开了SSL链接,分别是:https://localhost:8443/cas ,https://localhost:9443/sso我们先测试表单登录。启动租户a,访问链接http://localhost:8084 ,这个页面只有一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=youku&amp;response_type=token&amp;redirect_uri=http://localhost:8081/youku/qq/redirect查看日志://前面经过spring security的一堆过滤器链,都没有匹配到FrameworkEndpointHandlerMapping : Mapped to public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)//用户未认证,无法授权,抛出异常,ExceptionTranslationFilter对异常处理,跳转到配置的authentication //entry point,这里的authentication entry point,就是我之前配置的/login端点2019-04-19 16:01:14.608 DEBUG 21568 — [nio-8083-exec-1] o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.2019-04-19 16:01:14.611 DEBUG 21568 — [nio-8083-exec-1] o.s.s.w.a.ExceptionTranslationFilter : Authentication exception occurred; redirecting to authentication entry pointorg.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.可以看到,已经进入到了controller里面。final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”; Object attribute = session.getAttribute(SAVED_REQUEST);这段代码的作用是为了拿到,之前发起的请求。那么这个请求是什么时候被保存的呢?我们知道抛出异常之后,ExceptionTranslationFilter对异常进行处理,检测到用户没有登录,所以才跳转到authentication entry point,所以,猜想应该是这里保存了最开始的请求信息。以下是ExceptionTranslationFilter的核心代码:public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);}private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { this.logger.debug(“Authentication exception occurred; redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!this.authenticationTrustResolver.isAnonymous(authentication) && !this.authenticationTrustResolver.isRememberMe(authentication)) { this.logger.debug(“Access is denied (user is not anonymous); delegating to AccessDeniedHandler”, exception); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { this.logger.debug(“Access is denied (user is " + (this.authenticationTrustResolver.isAnonymous(authentication) ? “anonymous” : “not fully authenticated”) + “); redirecting to authentication entry point”, exception); this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage(“ExceptionTranslationFilter.insufficientAuthentication”, “Full authentication is required to access this resource”))); } }}这里对异常的处理,其实,核心就只有两个方法:1. this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); ,这种情况下,用户已经登陆了,但是权限不够,所以交给accessDeniedHandler进行处理,一般来讲,如果没有进行特殊的配置,会返回一个403错误和异常信息【不再跳转到authentication entry point,因为用户已经登陆了】,这里不深究。2. this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception); ,这个方法核心代码如下:protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { SecurityContextHolder.getContext().setAuthentication((Authentication)null); //就是在这里保存的这次请求的所有信息,包括请求头,请求路径,参数,cookie等详细信息。所以,后面跳转到/login端点时,我在controller里面可以拿出来。 this.requestCache.saveRequest(request, response); this.logger.debug(“Calling Authentication entry point.”); //这里就是发起用户认证了,根据我的配置,它就会跳转到/login this.authenticationEntryPoint.commence(request, response, reason); }再回到前面的controller登录逻辑,往下走:@GetMapping("/login”)public String loginJump(HttpSession session) {final String SAVED_REQUEST = “SPRING_SECURITY_SAVED_REQUEST”;Object attribute = session.getAttribute(SAVED_REQUEST);// 默认情况下,用户直接访问/login时,没有SAVED_REQUESTif (attribute == null) { //默认跳转到登陆页面 return “login”;}if (attribute instanceof DefaultSavedRequest) { DefaultSavedRequest savedRequest = (DefaultSavedRequest) attribute; List<String> referer = savedRequest.getHeaderValues(“referer”); if (referer.size() == 1) { //有referer请求头 String domain = referer.get(0); //获取到数据库中配置的租户信息 Tenant tenant = tenantService.selectByDomain(domain); if (tenant == null) { return “login”; } else { String loginProvider = tenant.getLoginProvider(); switch (loginProvider) { case “cas”: //获取cas地址 CasInfo casInfoByTenantId = casInfoService.getCasInfoByTenantId(tenant.getTenantId()); String casServerLogin = casInfoByTenantId.getCasServerLogin(); session.setAttribute(“casInfoByTenantId”,casInfoByTenantId); return “redirect:” + casServerLogin; case “oauth”: //因为我在数据库中配置的是oauth,所以,最后响应login视图 return “login”; default: return “login”; } } } else { return “login”; }}return “login”;}用户跳转到登陆页面输入用户名密码,点击登陆,进入UsernamePasswordAuthenticationFilter ,开始尝试认证用户public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = “”; } if (password == null) { password = “”; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the “details” property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);}最终会调用AuthenticationManager接口的authenticate方法,而AuthenticationManager委托一堆的AuthenticationProvider来进行认证。后面的流程,不再赘述,不在本篇文章的讨论范畴。用户认证成功后,调用successfulAuthentication(request, response, chain, authResult); 其实,这个方法里面核心代码就是successHandler.onAuthenticationSuccess(request, response, authResult);AuthenticationSuccessHandler有很多实现类,我们也可以自定义实现AuthenticationSuccessHandler。最常用的实现是,SavedRequestAwareAuthenticationSuccessHandler ,看一下它里面的核心代码:@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request .getParameter(targetUrlParameter)))) { requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); logger.debug(“Redirecting to DefaultSavedRequest Url: " + targetUrl); getRedirectStrategy().sendRedirect(request, response, targetUrl);}其实,这个方法,就是获取到之前保存的请求信息,然后再重定向到之前的请求。#### 2.CAS登录流程分析这次,我们访问租户b,这个租户,配置了cas登录。访问租户b:<http://localhost:8085/> ,这个页面里,也就是一个超链接,点击超链接,访问http://localhost:8083/oauth/authorize?client_id=iqiyi&response_type=token&redirect_uri=http://localhost:8081/iqiyi/qq/redirect前面的流程还是一样的,经过spring security的过滤器链,都没有匹配到,在最后DispatcherServlet抛出异常,然后ExceptionTranslationFilter对异常处理,跳转到/login端点,然后拿出配置在数据库中的casInfo,跳转到https://localhost:8443/cas/login?service=http%3A%2F%2Flocalhost%3A8083%2Flogin%2Fcas输入用户名密码,cas成功认证用户之后,生成TGT=============================================================WHO: adminWHAT: Supplied credentials: [admin]ACTION: AUTHENTICATION_SUCCESSAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,300 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: TGT-GHfz0lUJQE-8fkKJgyv8WXNE5FYLBqb7zfWGfNoKwDZ0AjqA-DESKTOP-GDU9JIIACTION: TICKET_GRANTING_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:12019-04-19 16:51:01,307 INFO [org.apereo.cas.DefaultCentralAuthenticationService] - <Granted ticket [ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII] for service [http://localhost:8083/login/cas] and principal [admin]>2019-04-19 16:51:01,308 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGINWHO: adminWHAT: ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII for http://localhost:8083/login/casACTION: SERVICE_TICKET_CREATEDAPPLICATION: CASWHEN: Fri Apr 19 16:51:01 CST 2019CLIENT IP ADDRESS: 0:0:0:0:0:0:0:1SERVER IP ADDRESS: 0:0:0:0:0:0:0:1然后跳转到service地址,也就是localhost:8083/login/cas ,并带上为这个service生成的service ticket,所以最后的请求地址为:http://localhost:8083/login/cas?ticket=ST-35-Mf1v9Z2qVVVKlWeTgyc-Hlzh2xY-DESKTOP-GDU9JII而这个端点/login/cas会被我配置的自定义CustomCasAuthenticationFilter拦截@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!requiresAuthentication(req, res)) { chain.doFilter(request, response); return; } String ticket = obtainArtifact(req); //开始校验ticket try { CasInfo casInfo = (CasInfo) req.getSession().getAttribute(“casInfoByTenantId”); if (StringUtils.hasText(casInfo.getCasServer())) { //获取当前项目地址 String service; int port = request.getServerPort(); if (port != 80) { service = request.getScheme() + “://” + request.getServerName() + “:” + request.getServerPort() + endpoint; } else { service = request.getScheme() + “://” + request.getServerName() + endpoint; } //开始校验ticket Assertion validateResult = getTicketValidator(casInfo.getCasServer()).validate(ticket, service); //根据校验结果,获取用户详细信息 UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName()); if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName()); } } catch (UsernameNotFoundException e) { unsuccessfulAuthentication(req, res, e); } //手动封装authentication对象 assert userDetails != null; UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities()); authentication.setDetails(userDetails); successfulAuthentication(req,res,chain,authentication); } else { unsuccessfulAuthentication(req, res, new BadCredentialsException(“bad credential:ticket校验失败”)); } } catch (TicketValidationException e) { //ticket校验失败 unsuccessfulAuthentication(req, res, new BadCredentialsException(e.getMessage())); }// chain.doFilter(request, response);}校验成功之后,我的逻辑是,手动加载用户信息,然后把当前认证信息Authentication放到SecurityContextHolder中。protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult);}protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug(“Authentication request failed: " + failed.toString(), failed); this.logger.debug(“Updated SecurityContextHolder to contain null Authentication”); this.logger.debug(“Delegating to authentication failure handler " + this.failureHandler); } this.failureHandler.onAuthenticationFailure(request, response, failed);}#### 3.单点登出流程分析用户发送/logout请求,被我自定义的CustomLogoutFilter拦截之后的逻辑是,先从本地登出,然后判断之前是否是从cas认证的,如果是,再获取cas信息,然后把cas也登出了。这里判断登陆用户的认证方式,我想了很久,最后的实现思路如下:之前通过cas登录时,我手动的添加登陆用户的认证方式到Authentication中。代码如下://根据校验结果,获取用户详细信息UserDetails userDetails = null;try {userDetails = userDetailsService.loadUserByUsername(validateResult.getPrincipal().getName());if (this.logger.isDebugEnabled()) { logger.debug(“userDetailsServiceImpl is loading username:"+validateResult.getPrincipal().getName());}} catch (UsernameNotFoundException e) {unsuccessfulAuthentication(req, res, e);}//手动封装authentication对象assert userDetails != null;UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(validateResult.getPrincipal(), ticket, userDetails.getAuthorities());//就是这里做了文章authentication.setDetails(userDetails);successfulAuthentication(req,res,chain,authentication);然后,登出时,拿到这个信息,进行登出操作。因为,我在userdetails中封装了这个信息,所以可以拿到。public class UserDetailsVO implements UserDetails {//userprivate Integer userId;private String username;private String phone;private String email;//tenantprivate Tenant tenant;//accountprivate Integer accountId;private String password;//省略setter和getter} ...

April 19, 2019 · 11 min · jiezi

nextcloud与oauth2集成教程

基础环境Nextcloud 15.0.5oauth2安装 sociallogin在官方app应用商店下载 sociallogin解压并拷贝 sociallogin 至 app 目录打开 Nextcloud 并在 应用中启用配置oauth2使用管理员账号打开 Nextcloud 找到 sociallogin设置,选择 Custom OAuth2 并添加oauth2信息,如下设置只使用oauth2登录,取消系统登录 编辑 /var/www/html/config/config.php 添加以下配置:‘social_login_auto_redirect’ => true使用Oauth2注销 Nextcloud使用中发现点击登出后,Nextcloud只是注销了本身的session,并没有注销oauth2的session,因此会登出失败. 打开 core/Controller/LoginController.php 找到 logout 方法,修改 $response 值如下:$response = new RedirectResponse(‘http://10.0.4.3/logout?redirect_uri=http://10.0.4.33:8088’);http://10.0.4.3/logout 为oauth2认证系统登出地址使用用户信息创建组在使用oauth2登录成功后,为了与原有Nextcloud用户区分,Nextcloud会在数据库中创建一个用户名为 oauth2 Internal name + 登录名称的用户,这样使用起来及其不方便,我们可以通过修改以下代码,保证用户名正常不带前缀:根据返回用户信息中其他信息创建组sociallogin/lib/Controller/LoginController.phpprivate function login($uid, Profile $profile){ $user = $this->userManager->get($uid); if (null === $user) { $connectedUid = $this->socialConnect->findUID($uid); $user = $this->userManager->get($connectedUid); } if ($this->userSession->isLoggedIn()) { if (!$this->config->getAppValue($this->appName, ‘allow_login_connect’)) { throw new LoginException($this->l->t(‘Social login connect is disabled’)); } if (null !== $user) { throw new LoginException($this->l->t(‘This account already connected’)); } $currentUid = $this->userSession->getUser()->getUID(); $this->socialConnect->connectLogin($currentUid, $uid); return new RedirectResponse($this->urlGenerator->linkToRoute(‘settings.PersonalSettings.index’, [‘section’ => ‘additional’])); } if (null === $user) { if ($this->config->getAppValue($this->appName, ‘disable_registration’)) { throw new LoginException($this->l->t(‘Auto creating new users is disabled’)); } if ( $profile->email && $this->config->getAppValue($this->appName, ‘prevent_create_email_exists’) && count($this->userManager->getByEmail($profile->email)) !== 0 ) { throw new LoginException($this->l->t(‘Email already registered’)); } $password = substr(base64_encode(random_bytes(64)), 0, 30); $user = $this->userManager->createUser($uid, $password); $user->setDisplayName($profile->displayName ?: $profile->identifier); $user->setEMailAddress((string)$profile->email); $newUserGroup = $this->config->getAppValue($this->appName, ’new_user_group’); if ($newUserGroup) { try { $group = $this->groupManager->get($newUserGroup); $group->addUser($user); } catch (\Exception $e) { } } if ($profile->photoURL) { $curl = new Curl(); try { $photo = $curl->request($profile->photoURL); $avatar = $this->avatarManager->getAvatar($uid); $avatar->set($photo); } catch (\Exception $e) { } } $this->config->setUserValue($uid, $this->appName, ‘disable_password_confirmation’, 1); if ($profile->data[‘departmentName’] !== null) { $existGroup = $this->groupManager->get($profile->data[‘departmentName’]); if ($existGroup === null) { $newGroup = $this->groupManager->createGroup($profile->data[‘departmentName’]); $newGroup->addUser($user); } else { $existGroup->addUser($user); } } } $this->userSession->completeLogin($user, [’loginName’ => $user->getUID(), ‘password’ => null]); $this->userSession->createSessionToken($this->request, $user->getUID(), $user->getUID()); if ($redirectUrl = $this->session->get(’login_redirect_url’)) { return new RedirectResponse($redirectUrl); } $this->session->set(’last-password-confirm’, time()); return new RedirectResponse($this->urlGenerator->getAbsoluteURL(’/’));}sociallogin/lib/Provider/CustomOAuth2.phppublic function getUserProfile() { $profileFields = array_filter( array_map(’trim’, explode(’,’, $this->config->get(‘profile_fields’))), function ($val) { return !empty($val); } ); $profileUrl = $this->config->get(’endpoints’)->get(‘profile_url’); if (count($profileFields) > 0) { $profileUrl .= (strpos($profileUrl, ‘?’) !== false ? ‘&’ : ‘?’) . ‘fields=’ . implode(’,’, $profileFields); } $response = $this->apiRequest($profileUrl); if (!isset($response->identifier) && isset($response->id)) { $response->identifier = $response->id; } if (!isset($response->identifier) && isset($response->data->id)) { $response->identifier = $response->data->id; } if (!isset($response->identifier) && isset($response->user_id)) { $response->identifier = $response->user_id; } $data = new Data\Collection($response); if (!$data->exists(‘identifier’)) { throw new UnexpectedApiResponseException(‘Provider API returned an unexpected response.’); } $userProfile = new User\Profile(); foreach ($data->toArray() as $key => $value) { if (property_exists($userProfile, $key)) { $userProfile->$key = $value; } } if (!empty($userProfile->email)) { $userProfile->emailVerified = $userProfile->email; } $attributes = new Data\Collection($data->get(‘attributes’)); $userProfile->data = [ “organizationName” => $attributes->get(‘organizationName’), “departmentName” => $attributes->get(‘departmentName’), ]; if ($attributes->get(’name’) !== null) { $userProfile->displayName = $attributes->get(’name’); } return $userProfile; } ...

April 2, 2019 · 2 min · jiezi

统一认证 - Apereo CAS 客户端的集成以及小结

前两篇介绍了Apereo CAS以及服务器端的安装,但还不够完整,服务端还没有Application真正用起来呢!这篇文章将介绍怎么用起来集成的目的客户端我们想要与Apereo CAS做什么集成呢?回顾一下Apereo CAS是做什么的?Apereo CAS的一个功能就是单点登录,统一的登录登出接口与页面,让系统中的模块只需要关注在业务点,而把安全认证的功能交给统一认证来做。所以客户端的集成主要是单点登录的集成,客户端指定需要做安全认证的页面,然后Apereo CAS的安全包检测校验用户登录情况,并自动与CAS登录页面进行跳转交互。客户端的配置Apereo CAS提供了Springboot的包,可以让我们的集成些微方便了那么一丢丢!首先我们创建一个Springboot的application,里面带了Apereo CAS start的依赖<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency>同时在application.properties文件里面指定启动的端口 server.port = 9000有了Apereo CAS的包之后,我们就可以进行代码的配置。客户端的配置按照SpringSecurity的安全检验流程进行的:用户尝试打开一个受保护的url,比如/admin/userAuthenticationEntryPoint被触发了,把用户重定向到配置好的CAS登录页面https://localhost:6443/cas用户输入用户名密码,登录成功后, CAS会跳转回application指定的回调url http://localhost:9000/login/cas, 并带上ticket作为查询参数CasAuthenticationFilter一直在监听/login/cas这个路径,当发现有请求后,它会触发CasTicketValidator,由CasTickerValidator检验ticket的有效性当ticket也验证成功后,用户将会被跳转回原来请求的受保护url下面代码大致描述了这个过程:@Beanpublic ServiceProperties serviceProperties() { ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService(“http://localhost:9000/login/cas”); serviceProperties.setSendRenew(false); return serviceProperties;} @Bean@Primarypublic AuthenticationEntryPoint authenticationEntryPoint( ServiceProperties sP) { CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint(); entryPoint.setLoginUrl(“https://localhost:6443/cas/login”); entryPoint.setServiceProperties(sP); return entryPoint;} @Beanpublic TicketValidator ticketValidator() { return new Cas30ServiceTicketValidator( “https://localhost:6443/cas”);} @Beanpublic CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties()); provider.setTicketValidator(ticketValidator()); provider.setUserDetailsService( s -> new User(“casuser”, “Mellon”, true, true, true, true, AuthorityUtils.createAuthorityList(“ROLE_ADMIN”))); provider.setKey(“CAS_PROVIDER_LOCALHOST_9000”); return provider;}@EnableWebSecurity@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { private AuthenticationProvider authenticationProvider; private AuthenticationEntryPoint authenticationEntryPoint; private SingleSignOutFilter singleSignOutFilter; private LogoutFilter logoutFilter; @Autowired public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP, LogoutFilter lF , SingleSignOutFilter ssF ) { this.authenticationProvider = casAuthenticationProvider; this.authenticationEntryPoint = eP; this.logoutFilter = lF; this.singleSignOutFilter = ssF; } // … @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } @Override protected AuthenticationManager authenticationManager() throws Exception { return new ProviderManager(Arrays.asList(authenticationProvider)); } @Bean public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sP) throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setServiceProperties(sP); filter.setAuthenticationManager(authenticationManager()); return filter; }}下面这个文件配置了application中所有/secured/,login的URL都是受保护资源,都要经过CAS认证过才可以访问:@EnableWebSecurity@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .regexMatchers("/secured.", “/login”) .authenticated() .and() .authorizeRequests() .regexMatchers("/") .permitAll() .and() .httpBasic() .authenticationEntryPoint(authenticationEntryPoint); } // …}服务端Apereo CAS的配置跟所有统一认证平台一样,所有application想要跟CAS做集成的,都需要在CAS配置相应的参数才可以使用。Apereo CAS提供了很多配置的方式,有YML,JSON, MongoDB以及其他(可查官网)。但高度自由的CAS一如既往的,没有提供可视化操作的界面。比如我们采用JSON的方式。首先我们需要通知Apereo CAS我们采用的是JSON的方式,并通知JSON文件的路径在哪里cas.serviceRegistry.initFromJson=truecas.serviceRegistry.config.location=classpath:/services然后我们在这个目录里面,创建一个对应的JSON文件,保存我们的客户端信息,为了方面管理,建议文件名为 application_id.json, 比如"secureApp_9991.json", 内容如下:{ “@class” : “org.apereo.cas.services.RegexRegisteredService”, “serviceId” : “^http://localhost:9000/login/cas”, “name” : “CAS Spring Secured App”, “description”: “This is a Spring App that usses the CAS Server for it’s authentication”, “id” : 19991, “evaluationOrder” : 1}第一次配置从JSON加载客户端配置的话,需要重启Apereo CAS。之后再加新的客户端的话就不用再重启,Apereo CAS会自动监测这个文件夹的变动小结至此我们对于Apereo CAS就有了一个稍微完整一点点的了解,从服务端安装部署,到配置,以及客户端如何集成等。但从这个短时间的学习来看,如果企业已经重度使用了Apereo CAS,那相信它可以很好地服务支撑企业的应用。但如果是新的项目,特别是项目周期比较紧张的项目,并且团队之前没有对统一认证有技术积累的话,不是很建议采用Apereo CAS,这些细微的配置以及无所不在的隐藏功能,会让你给项目经理催死的! 后面我会介绍另外一个统一认证的框架,个人感觉能弥补Apereo CAS的短板的 ...

February 23, 2019 · 2 min · jiezi

统一认证 - Apereo CAS 小试

上一篇文章我们对Apereo CAS有了简要的了解,这篇文章我们将动手练习Apereo CAS。主要是CAS单机版的搭设,用户信息存储到数据库,以及dashboard的使用做这些尝试的时候,Apereo CAS比较稳定的版本是5.3.x,使用如果想按照这个文章搭设的话,最好采用相同的版本Apereo CAS单机版的搭设Apereo CAS秉承耶鲁的自由文化传统,整个产品高度自由化,哪哪都提供了极其灵活的使用方式。比如单机版的部署,一般的软件提供的单机版都是下载一来,运行某个文件就直接开跑的。Apereo就不同,即使是单机版,也要配置一些内容才可以运行的。不单单是配置,单机版的代码实现也是可以改的,而且还可以很优雅地改,就是可以在不修改原来代码的前提下进行修改。Apereo CAS采用了Maven的overlayer 特性,提供了一份CAS的overlayer或者叫template,我们可以从下载一份layer ,然后在里面按照约定的方式,实现功能覆盖Apereo CAS提供的类,或者配置文件。git clone https://github.com/apereo/cas-overlay-template这是Apereo CAS官方提供的一个overlay,大家也可以下载使用其他组织提供的overlay。该项目的目录结构如下:C:\githome\github\cas\cas-server>ls -ltotal 1220-rw-r–r– 1 NOTECH 1049089 11560 Jan 25 14:25 LICENSE.txt-rw-r–r– 1 NOTECH 1049089 2768 Jan 25 14:28 README.md-rw-r–r– 1 NOTECH 1049089 4353 Jan 25 14:28 build.cmd-rwxr-xr-x 1 NOTECH 1049089 5608 Jan 25 14:28 build.shdrwxr-xr-x 1 NOTECH 1049089 0 Jan 25 14:25 etcdrwxr-xr-x 1 NOTECH 1049089 0 Jan 25 14:28 maven-rwxr-xr-x 1 NOTECH 1049089 7332 Jan 25 14:28 mvnw-rw-r–r– 1 NOTECH 1049089 5839 Jan 25 14:28 mvnw.bat-rw-r–r– 1 NOTECH 1049089 9458 Jan 28 10:15 pom.xmldrwxr-xr-x 1 NOTECH 1049089 0 Jan 25 14:31 src其实就是一个简单的maven项目,多了一个etc的目录,然后pom文件里面有一个cas-server-webapp的overlayer依赖。这时我们可以直接跑mvn package, 一样会生成相应的cas包,只是这个包跑不起来,因为cas需要一些配置才能起来的。前面说了overlayer会按照目录路径进行覆盖,也就是如果overlayer的项目里面有文件路径相同,那么打包的时候就会进行覆盖。而上一篇blog说了,Apereo CAS是基于springboot的开发的,那么我们要覆盖对应的配置文件,那就新建src\main\resources目录。安全证书首先,Apereo CAS作为一个安全的统一认证中心,那么本身也要安全的吧。所有它提供了HTTPS的链接方式,也就意味着我们需要提供一个keystore。命令行打开目录cas-server/src/main/resources/etc/cas,执行以下命令生成对应的keystorekeytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048changeit 是这个keystore的密码,最好改成你自己的密码,当然,作为demo用这个也是可以的接着我们要把这个keystore导成证书给客户端用:keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore现在我们要把这个导出来的证书导进去JVM里面keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore “C:\Program Files\Java\jdk1.8.0_101\jre\lib\security\cacerts"keytool -import -alias thekeystore -storepass changeit -file thekeystore.crt -keystore “C:\Program Files\Java\jre1.8.0_101\lib\security\cacerts"CAS配置接着我们把证书的路径,CAS启动端口等信息配置到springboot标准的配置文件application.properties里面。server.context-path=/casserver.port=6443server.ssl.key-store=classpath:/etc/cas/thekeystoreserver.ssl.key-store-password=changeitserver.ssl.key-password=changeit好了,打包build package,然后build run跑一下看看。应该可以看到CAS的默认登录页面 https://localhost:6443/cas 验证信息存储好了,到这时可能发现:天啦噜!用哪个用户可以登录啊? 从头到尾都没有用户信息的配置,而按照我们所了解的Apereo CAS的尿性,它可不会有什么默认值的。为简单试玩一下,我们可以直接在上面的application.properties文件里面,直接hardcode一个用户在里面,如下:cas.authn.accept.users=casuser::Mellon重新build package,build run,打开登录页面 https://localhost:6443/cas, 输入用户名casuser,密码Mellon,应该就可以登录成功了把用户信息hardcode在配置文件显然是很helloworld的做法,CAS提供了很多用户信息存储方式,有各种DB,LDAP等。具体可以参考官网的配置文件,有很详细的说明 https://apereo.github.io/cas/… 这次我们采用的是MYSQL的链接方式。首先我们要在overlayers的pom文件里面添加JDBC的support,如下: <dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-jdbc</artifactId> <version>${cas.version}</version> </dependency> <dependency> <groupId>org.apereo.cas</groupId> <artifactId>cas-server-support-jdbc-drivers</artifactId> <version>${cas.version}</version> </dependency>然后还是在原来的application.properties文件里面,把authentication的相关配置写上:cas.authn.jdbc.query[0].sql=select * from cms_auth_user where user_name=?cas.authn.jdbc.query[0].healthQuery=cas.authn.jdbc.query[0].isolateInternalQueries=falsecas.authn.jdbc.query[0].url=jdbc:mysql://127.0.0.1:3306/CASTEST?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=falsecas.authn.jdbc.query[0].failFast=truecas.authn.jdbc.query[0].isolationLevelName=ISOLATION_READ_COMMITTEDcas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialectcas.authn.jdbc.query[0].leakThreshold=10cas.authn.jdbc.query[0].propagationBehaviorName=PROPAGATION_REQUIREDcas.authn.jdbc.query[0].batchSize=1cas.authn.jdbc.query[0].user=root#cas.authn.jdbc.query[0].ddlAuto=create-dropcas.authn.jdbc.query[0].maxAgeDays=180cas.authn.jdbc.query[0].password=123456cas.authn.jdbc.query[0].autocommit=falsecas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Drivercas.authn.jdbc.query[0].idleTimeout=5000这里只是列出了一些必要的配置,比如driver.class是什么,query的语句是什么,连接哪个数据库等等。配置成功后,我们可以在数据库里面插入一两个用户试试CAS监控中心Apereo CAS提供了一个监控中心, 当大家登录CAS成功后,满怀希望地点击dashboard的链接时,展示给大家的是冷冷的 “Access Denied"页面! 无良啊无良,高度地自由是有代价的!!! CAS认为大家默认是不需要这个功能的,所以默认是关闭的! 我们需要通过配置文件打开这个功能! 你以为只要打开endpoints.enabled=true就可以了吗? 我们把CAS想简单的,这家伙龟毛到每一个监控的功能都需要独立打开的!所以就有了下面长长的一个配置文件!endpoints.enabled=trueendpoints.sensitive=falseendpoints.restart.enabled=falseendpoints.shutdown.enabled=falsemanagement.security.enabled=truemanagement.security.roles=ACTUATOR,ADMINmanagement.security.sessions=if_requiredmanagement.context-path=/statusmanagement.add-application-context-header=falsesecurity.basic.authorize-mode=rolesecurity.basic.enabled=falsesecurity.basic.path=/cas/status/**cas.adminPagesSecurity.ip=.+cas.monitor.endpoints.dashboard.enabled=true cas.monitor.endpoints.dashboard.sensitive=falsecas.monitor.endpoints.discovery.enabled=true cas.monitor.endpoints.discovery.sensitive=falsecas.monitor.endpoints.auditEvents.enabled=true cas.monitor.endpoints.auditEvents.sensitive=falsecas.monitor.endpoints.authenticationEvents.enabled=true cas.monitor.endpoints.authenticationEvents.sensitive=falsecas.monitor.endpoints.configurationState.enabled=true cas.monitor.endpoints.configurationState.sensitive=falsecas.monitor.endpoints.healthCheck.enabled=true cas.monitor.endpoints.healthCheck.sensitive=falsecas.monitor.endpoints.loggingConfig.enabled=true cas.monitor.endpoints.loggingConfig.sensitive=falsecas.monitor.endpoints.metrics.enabled=true cas.monitor.endpoints.metrics.sensitive=falsecas.monitor.endpoints.attributeResolution.enabled=true cas.monitor.endpoints.attributeResolution.sensitive=falsecas.monitor.endpoints.singleSignOnReport.enabled=true cas.monitor.endpoints.singleSignOnReport.sensitive=falsecas.monitor.endpoints.statistics.enabled=true cas.monitor.endpoints.statistics.sensitive=falsecas.monitor.endpoints.trustedDevices.enabled=true cas.monitor.endpoints.trustedDevices.sensitive=falsecas.monitor.endpoints.status.enabled=true cas.monitor.endpoints.status.sensitive=falsecas.monitor.endpoints.singleSignOnStatus.enabled=true cas.monitor.endpoints.singleSignOnStatus.sensitive=falsecas.monitor.endpoints.springWebflowReport.enabled=true cas.monitor.endpoints.springWebflowReport.sensitive=falsecas.monitor.endpoints.registeredServicesReport.enabled=true cas.monitor.endpoints.registeredServicesReport.sensitive=falsecas.monitor.endpoints.configurationMetadata.enabled=true cas.monitor.endpoints.configurationMetadata.sensitive=false这里是实在受不了CAS的配置粒度细微到怀疑人生,所以adminSercurity没有打开,放开给所有的IP所有的用户,只要登录成功后都可以访问 ...

February 17, 2019 · 1 min · jiezi

统一认证 - Apereo CAS 简介

为什么要做这个尝试?微服之道,方兴未艾;农之来学者,盖已千者! 这句是从《陶山集·太学案问》瞎改出来的。意思就是微服务的架构理念还在不断地发展,现在整个啥都 言必出微服务,差点都到了 没学过微服务的码农不是一个好码农。搞到微服务这个词都快跟区块链差不多臭了。在将臭未臭之前,我们赶紧把其中的统一认证这块过一下。在微服务的概念中,恨不得每一个API都起一个独立的微服务,所有一个系统有几十个,甚至成百上千个独立的微服务也不见怪。 而安全又是每一个服务都必须面对的问题!如果让每一个微服务都独立处理的话,那Martin Fowler估计早就被码农拉去祭天了。所以统一认证大势所趋,将安全有关的认证与授权集中到一个服务中进行处理,各个微服务只需简单校验即可,无需另起炉灶!统一认证的开源实现有很多,目前比较出名的有Apereo CAS (发音为 /kæ’s/),Keycloak等,我们尽量都介绍到,今天先看一下Apereo CAS什么是Apereo CAS首先CAS是Central Authentication Service的首字母缩写,Apereo CAS 是由耶鲁大学实验室2002年出的一个开源的统一认证服务。据官网介绍,Apereo CAS是一个开源的企业级单点登录系统,包括了如下特性:基于SpringBoot开发的Java系统一个开放且各种手册齐全的协议以可插拔的形式支持各种认证协议(LDAP, database, X.509, 2-factor)支持各种认证协议(CAS, SAML, OAuth, OpenID)各种客户端应有尽有(Java, .Net, PHP, Perl, Apache, uPortal)跟各种 不大出名 的系统集成 (uPortal, BlueSocket, TikiWiki, Mule, Liferay, Moodle)社区文档大把,有问题就吹街Apereo CAS的架构与组成码农上手一个新东西,第一时间还不赶紧把它的架构搂两眼从这个图可以看到,Apereo CAS主要组成就两大组件,一个服务器端,还有各种语言的客户端。应用程序通过CAS的客户端,拦截校验用户请求是否通过认证,如果尚未认证,则重定向到CAS服务端的用户登录页面进行登录,登录成功后,会生成一个ticket给回应用程序,下次用户请求带着这个ticket就所向无阻。Apereo CAS的历史前面说了Apereo CAS是耶鲁大学Technology and Planning实验室的Shawn Bayern 在2002年出的一个开源系统。刚开始名字叫Yale CAS。 Yale CAS 1.0的目标只是一个单点登录的系统,随着慢慢用开,功能就越来越多了,2.0就提供了多种认证的方式。目前版本为6.02004年12月,CAS转成JASIG(Java Administration Special Interesting Group)的一个项目,项目也随着改名为 JASIG CAS,这就是为什么现在有些CAS的链接还是有jasig的字样。2012年,JASIG跟Sakai基金会合并,改名为Apereo基金会,所有CAS也随着改名为Apereo CAS. 看起来这娃也不容易,嫁鸡随鸡,名字都改了3次了。结论?关于Apereo CAS能不能用的结论这里先不下,等到后面介绍安装部署集成的文章写完再一起看看。 这次我们先看看Apereo CAS官网出的一幅图,这张图片介绍了Apereo的组成以及支持的各种协议,各种特性,不烦看看

February 3, 2019 · 1 min · jiezi

CAS 算法 —— Compare and Swap

本文翻译和原创各占一半,所以还是厚颜无耻归类到原创好了…https://howtodoinjava.com/jav...java 5 其中一个令人振奋的改进是新增了支持原子操作的类型,例如 AtomicInteger, AtomicLong 等。在多线程环境中进行简单的自增自减操作时,这些原子类能帮助你减少很多用于多线程同步的复杂代码。这些原子类依赖于 CAS (compare and swap) 算法,接下来我们会讨论 CAS 这个概念。乐观锁和悲观锁传统的锁机制,例如 java 的 synchronized 关键字,他代表了 java 中悲观锁技术,保证了某一时刻仅有一个线程能访问同步代码/方法。synchronized 能够很好地工作,却有着 (相对) 比较大的性能开销。乐观锁 (相对悲观锁) 对性能会有很大的帮助。他的核心思想是:你寄希望于在没有冲突的情况下完成一次更新操作,使用乐观锁技术更新时会进行 “冲突检测” 来判断是否有其他的线程干扰,若是 (有其他线程干扰) 则视本次更新操作失败,一般会进行重试 (可以了解一下CAS自旋)。Compare and Swap 就是典型的乐观锁技术。CAS 算法CAS 算法会先对一个内存变量(位置) V 和一个给定的值进行比较 A ,如果相等,则用一个新值 B 去修改这个内存变量(位置)。上述过程会作为一个原子操作完成 (intel处理器通过 cmpxchg 指令系列实现)。CAS 原子性保证了新值的计算是基于上一个有效值,期间如果内存变量(位置) V 被其他线程更新了,本线程的 CAS 更新操作将会失败。CAS 操作必须告诉调用者成功与否,可以返回一个 boolean 值来表示,或者返回一个从内存变量读到的值 (应该是上一次有效值)CAS 操作数有三个:内存变量(位置) V,表示被更新的变量线程上一次读到的旧值 A用来覆盖 V 的新值 BCAS 表示:“我认为现在 V 的值还是之前我读到的旧值 A,若是则用新值 B 覆盖内存变量 V,否则不做任何动作并告诉调用者操作失败”。CAS 是一项乐观锁技术,他在更新的时候总是希望能成功 (没有冲突),但也能检测出来自其他线程的冲突和干扰Java 中的 Compare and Swap这里我们关注一下ReentrantLock锁定和解锁那部分的源码//ReentrantLock.lock()public void lock() { sync.lock();}他依赖了其内部类Sync的 lock(),以下是内部类 Sync (继承了队列同步器 AQS)abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; abstract void lock(); …………….Sync还是个抽象类,一般 new ReentrantLock() 时创建的是 NonfairSync// ReentrantLock的构造方法public ReentrantLock() { sync = new NonfairSync();}下面就是NonfairSync的 lock() 方法了final void lock() { if (compareAndSetState(0, 1)) // 1 setExclusiveOwnerThread(Thread.currentThread()); // 2 else acquire(1); // 3}1 中的 compareAndSetState() 承继自队列同步器 AQS,封装了 CAS 指令。因为是 NonfairSync 非公平锁,所以一上来就尝试抢占锁:给定旧值 0 并希望用新值 1 去更新内存变量 State。若更新成功则视为获取锁成功,并执行 22 成功完成了 CAS 操作 (没错,当你使用 CAS 指令成功把 State 从 0 更新成 1 便视为获取锁,就是这么简单粗暴 ╮(╯▽╰)╭ ),把当前线程设为独占线程3 操作失败 (被人抢先获取锁(╯`□′)╯╧╧),进行 acquire 操作再次尝试获取锁,若还是不行,则把当前线程加入 AQS 等待队列,由 AQS 来管理队列中等待线程的阻塞和唤醒,具体代码就不贴出来了,AQS 的源码多处使用到 CAS 指令,有兴趣的同学可以查看锁用完了要释放,下面贴出 unlock() 方法// ReentrantLock.unlock()public void unlock() { sync.release(1);}这里还是依赖了 sync,release() 是 AQS 的通用方法,其内部调用了 tryRelease() (由 Sync 类实现),这里直接贴出 Sync 的 tryRelease()protected final boolean tryRelease(int releases) { // releases 参数的值是上面传进来的 1 int c = getState() - releases; // 1 if (Thread.currentThread() != getExclusiveOwnerThread()) // 1.5 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 2 free = true; setExclusiveOwnerThread(null); } setState(c); // 3 return free;}1 处的c 是内存变量 State 即将要被更新的值,因为 ReentrantLock 是可重入锁 (当前线程可多次获取锁),所以 State 的值是可以大于 1 的。2 判断若新值为 0,则视为锁被释放并设置当前独占线程为 null3 把 State 的值更新为 c,思考一下这里的更新操作为什么没用到 CAS 指令?1.5 解释了上面的疑问,只有当前独占线程有能力对 State 变量进行修改,不需要进行同步或使用 CASSummaryAQS 队列同步器以及 java.util.concurrent 下各种锁和原子类都运用到的 CAS 算法,有时间的同学建议阅读加深印象。 ...

January 18, 2019 · 2 min · jiezi