1.前言
本文不再赘述单点登录SSO原理,主要针对CAS认证服务方式集成.NET应用,从实施落地过程回顾期间遇到的坑和解决方案做些心得总结,希望对你有帮忙,如有问题,请留言一起探讨学习
2.核心客户端组件
DotNetCasClient.dll,本项目依赖.NET4.5版本,官方提供用于集成CAS客户端源码地址 GitHub - apereo/dotnet-cas-client: Apereo .NET CAS Client,编译和调试工具需基于vs2017及以上
3.项目回顾
CAS认证集成是公司首次对接项目,结合客户提供的CAS认证协议以及官网相关资料,前期的设计方案很粗略,这为后续的项目落地带来不少麻烦,在此告诫各位爱好编程的朋友们,项目前期设计方案很重要。
整个集成主要包含三个主要环节,CAS认证模拟服务环境如何搭建,应用程序如何集成CAS认证,测试联调以及完善,下面也主要从这三个方面来概述
3.1.CAS认证模拟服务环境搭建
为了在开发环境联调,同步模拟一套CAS认证服务,基于windows server2008R2以上/win10+,中间件Java1.8,Tomcat8,安装配置并系统环境变量即可,不做描述了。
CAS在GitHub源代码地址:https://github.com/apereo/cas/releases?q=v4.2.7&expanded=true,当然可以选择Maven官网release已发布版本https://mvnrepository.com/artifact/org.jasig.cas/cas-server-webapp,目前支持最新的发布版本4.2.7,要求版本必须高于需要集成客户CAS服务的版本,选择对应版本FIles详情war下载到本地,如下图所示位置:
将war解压到本地,将解压包拷贝到Tomcat安装目录webapps下重命名cas(站点跟目录),注意版本v4.2.7该release发布的默认是以https访问,需要本地安装证书支持,网上有相关资料。若想默认支持http方式认证,需要做如下修改:
1)\WEB-INF\cas.properties修改tgc.secure=false,warn.cookie.secure=false
2)\WEB-INF\classes\services\修改增加支持http,"serviceId" : "^(https|imaps|http)://.*",
3)\WEB-INF\view\jsp\default\ui\casLoginView.jsp 注释如下代码:
<!--<c:if test="${not pageContext.request.secure}"> <div id="msg" class="errors"> <h2><spring:message code="screen.nonsecure.title" /></h2> <p><spring:message code="screen.nonsecure.message" /></p> </div> </c:if> -->
启动Tomcat,web浏览器http://localhost:8080/cas/login,如下所示:
默认用户密码是在\WEB-INF\cas.properties下accept.authn.users配置项的值,输入即可完成登录
3.2.CAS客户端集成
DotNetCasClient源码下载到本地,启动VS编译,源码下有三个项目
其中1为核心组件项目,启动VS项目编译成功之后将bin文件引入到引用的程序包当中,以MVC案例项目说明,需要进行如下配置:
1)web.config文件配置
引入DotNetCasClient组件,configSections节点下增加section
<section name="casClientConfig" type="DotNetCasClient.Configuration.CasClientConfiguration, DotNetCasClient"/>
添加cas配置信息节点,官网给出的配置中需要将serviceTicketManager,gatewayStatusCookieName去掉
<!--CAS配置说明:casServerLoginUrl配置cas登录地址; casServerUrlPrefix配置cas服务端访问地址; ServerName配置cas回调当前项目地址。其他不做修改--> <casClientConfig casServerLoginUrl="http://10.60.1.9:8080/cas/login" casServerUrlPrefix="http://10.60.1.9:8080/cas/p3" serverName="http://localhost:9587" notAuthorizedUrl="~/NotAuthorized.aspx" cookiesRequiredUrl="~/CookiesRequired.aspx" redirectAfterValidation="true" gateway="false" renew="false" singleSignOut="true" ticketTimeTolerance="5000" ticketValidatorName="Cas20" serviceTicketManager="CacheServiceTicketManager" />
system.web节点下增加权限认证登录方式,增加httpModules,主要IIS应用程序需要兼容32位
<!--用户登录认证配置说明:loginUrl配置CAS登录地址--> <authentication mode="Forms"> <forms name="CasAuthLogin" loginUrl="http://10.60.1.9:8080/cas/login" timeout="3000" cookieless="UseCookies" defaultUrl="~/Home/Index" slidingExpiration="true" path="/" /> </authentication> <httpModules> <add name="DotNetCasClient" type="DotNetCasClient.CasAuthenticationModule,DotNetCasClient"/> </httpModules>
system.webServer下增加DotNetCasClient模块
<modules> <remove name="DotNetCasClient"/> <add name="DotNetCasClient" type="DotNetCasClient.CasAuthenticationModule,DotNetCasClient"/> <remove name="FormsAuthenticationModule" /> </modules>
最后增加system.diagnostics节点,属性描述可从官网上了解,主要配置initializeData需要开启IUser,IIS_Users的控制权限,否则客户端认证无法写入日志,不方便问题排查
<system.diagnostics> <trace autoflush="true" useGlobalLock="false" /> <sharedListeners> <add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="D:\project\logs\DotNetCasClient.Log" traceOutputOptions="DateTime" /> </sharedListeners> <sources> <source name="DotNetCasClient.Config" switchName="Config" switchType="System.Diagnostics.SourceSwitch" > <listeners> <add name="TraceFile" /> </listeners> </source> <source name="DotNetCasClient.HttpModule" switchName="HttpModule" switchType="System.Diagnostics.SourceSwitch" > <listeners> <add name="TraceFile" /> </listeners> </source> <source name="DotNetCasClient.Protocol" switchName="Protocol" switchType="System.Diagnostics.SourceSwitch" > <listeners> <add name="TraceFile" /> </listeners> </source> <source name="DotNetCasClient.Security" switchName="Security" switchType="System.Diagnostics.SourceSwitch" > <listeners> <add name="TraceFile" /> </listeners> </source> </sources> <switches> <add name="Config" value="Information"/> <add name="HttpModule" value="Information"/> <add name="Protocol" value="Verbose"/> <add name="Security" value="Information"/> </switches> </system.diagnostics>
客户端相关Config配置已经完成,接下来需要增加控制层用户权限过滤器CasAuthorizeAttribute继承AuthorizeAttribute
/// <summary> /// 定义用户登录状态 /// 0未登录 1已登录 2状态错误 3无权限 /// </summary> int _loginStatus; public override void OnAuthorization(AuthorizationContext filterContext) { //获取用户casTicket var casTicket = filterContext.HttpContext.Request.GetCasAuthorizeTicket(); if (CasAuthorizeHelper.IsTokenValid(casTicket)) { Auth_Accounts account = null; var accountId = string.Empty; //根据CAS认证返回信息创建本地用户信息缓存 if (casTicket.Assertion.Attributes.ContainsKey("userId")) { accountId = casTicket.Assertion.Attributes["userId"][0]; account = CasUserContext.GetUserInfo(accountId); } if (account != null) { this._loginStatus = 1; UserContext.GetUserAccount = GetUser; //执行了基类的OnAuthorization才会执行AuthorizeCore base.OnAuthorization(filterContext); } else { //CAS用户登录,系统无该用户回到登录界面 this._loginStatus = 2; } } else { this._loginStatus = 3; } base.OnAuthorization(filterContext); } protected override bool AuthorizeCore(HttpContextBase httpContext) { LogUtil.Info("AuthorizeCore _loginStatus数值:{0}", this._loginStatus.ToString()); if (ConstConfig.CasAuthorizedSwitch) { return this._loginStatus == 1; } else { return base.AuthorizeCore(httpContext); } } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (ConstConfig.CasAuthorizedSwitch) { base.HandleUnauthorizedRequest(filterContext); var redirectUrl = UrlUtil.ConstructLoginRedirectUrl(false, false); filterContext.Result = new RedirectResult(redirectUrl, true); } else { return; } }
创建获取票据验证类CasAuthorizeHelper
public static class CasAuthorizeHelper { public static CasAuthenticationTicket GetCasAuthorizeTicket(this HttpRequestBase request) { CasAuthenticationTicket casTicket = null; var ticketCookie = request.Cookies[FormsAuthentication.FormsCookieName];if (ticketCookie != null && !string.IsNullOrWhiteSpace(ticketCookie.Value)) { var ticket = FormsAuthentication.Decrypt(ticketCookie.Value);if (ticket != null && CasAuthentication.ServiceTicketManager != null) { casTicket = CasAuthentication.ServiceTicketManager.GetTicket(ticket.UserData); //记录登录用户Ticket消息明细信息 LogUtil.Info("ticket:{0},获取CAS用户认证信息:{1}", ticket.UserData, SerializerUtil.ToJson(casTicket)); } } return casTicket; } public static CasAuthenticationTicket GetCasAuthorizeTicket() { CasAuthenticationTicket casTicket = null; var ticketCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; if (ticketCookie != null && !string.IsNullOrWhiteSpace(ticketCookie.Value)) { var ticket = FormsAuthentication.Decrypt(ticketCookie.Value); if (ticket != null && CasAuthentication.ServiceTicketManager != null) { casTicket = CasAuthentication.ServiceTicketManager.GetTicket(ticket.UserData); } } return casTicket; } /// <summary> /// 验证票据是否有效 /// </summary> /// <param name="ticket"></param> /// <returns></returns> public static bool IsTokenValid(CasAuthenticationTicket ticket) { if (ticket == null) return false; return CasAuthentication.ServiceTicketManager.VerifyClientTicket(ticket); } public static void Abandon() { CasAuthentication.ClearAuthCookie(); } }
最后需要控制器Controller上增加过滤器
启动站点此时默认会跳转到统一认证登录界面了。如果需要对每个请求接口都增加用户身份认证,则需要对重写ActionFilterAttribute的OnActionExecuting,来验证票据是否有效,如果失效,则需要回到统一认证登录页面,注意ajax请求时,需要重定向处理。
3.2.CAS集成数据库联调
CAS4.2.7认证服务中心集成mysql说明,包括引入组件,数据库链接,用户返回属性设置等
1)数据库链接配置修改:\WEB-INF\deployerConfigContext.xml,本次案例采用MD5加密
<!-- begin 从数据库中的用户表中读取 --> <bean id="queryDatabaseAuthenticationHandler" name="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <!-- 加密配置,注释之后显示为明文 --> <property name="passwordEncoder" ref="MD5PasswordEncoder"/> </bean> <!-- MD5加密 --> <bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" autowire="byName"> <constructor-arg value="MD5"/> </bean> <alias name="dataSource" alias="queryDatabaseDataSource"/> <!-- mysql-connector-java-5.x.x jar 包对应的数据库连接驱动为"com.mysql.jdbc.Driver", mysql-connector-java-8.x.x jar 包对应的数据库连接驱动为"com.mysql.cj.jdbc.Driver" --> <!-- 数据库连接 127.0.0.1 为数据库地址,3306为 mysql 数据库默认端口,iportalusers 为数据库名--> <!-- 数据库用户名为"root",密码为"supermap" --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" p:driverClass="com.mysql.jdbc.Driver" p:jdbcUrl="jdbc:mysql://10.60.1.189:3306/test_cas?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false" p:user="root" p:password="HAIyi@2022" p:initialPoolSize="6" p:minPoolSize="6" p:maxPoolSize="18" p:maxIdleTimeExcessConnections="120" p:checkoutTimeout="10000" p:acquireIncrement="6" p:acquireRetryAttempts="5" p:acquireRetryDelay="2000" p:idleConnectionTestPeriod="30" p:preferredTestQuery="select 1"/> <!--end 从数据库中的用户表中读取 -->
同时需要注释掉原来的acceptUsersAuthenticationHandler
<!-- <alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" /> -->
2)增加用户信息字段输出,这里是出现问题排查时间最长的点,请求一致无法获取需要的字段,值得注意的是CAS低版本和当前版本配置存在很大差异,目前只在4.2.7版本上完成配置验证
<!--begin 返回属性信息 --> <bean id="dataSourceAttribute" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao"> <constructor-arg index="0" ref="dataSource" /> <constructor-arg index="1" value="select * from t_user where {0}" /> <property name="queryAttributeMapping"> <map>
<entry key="username" value="数据库字段" /> </map> </property> <property name="resultAttributeMapping"> <map> <entry key="数据库字段" value="userId" /> </map> </property> </bean> <alias name="dataSourceAttribute" alias="attributeRepository" /> <!--begin 返回更多信息 --> <!-- <bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao" p:backingMap-ref="attrRepoBackingMap" /> --> <!-- <util:map id="attrRepoBackingMap"> </util:map> -->
注意注意注释默认attributeRepository,attrRepoBackingMap必须注释,否则无法返回用户信息。
修改\WEB-INF\ cas.properties增加数据库链接配置,如下:
cas.jdbc.authn.query.sql=select pass_word from t_user where work_no = ?
增加认证票成功对象定义,修改\WEB-INF\view\jsp\protocol\3.0\casServiceValidationSuccess.jsp,增加如下返回节点,如果你的认证服务协议用的2.0,那么修改目录\WEB-INF\view\jsp\protocol\2.0\下
<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}"> <cas:attributes> <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"> <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}> </c:forEach> </cas:attributes> </c:if>
OK,模拟搭建的CAS票据认证用户信息已返回,接下来客户端封装的组件如何抓取cas:attributess需要的属性,在此感谢lention博主的分享结合实际项目进行改造
3)拓展DotNetCASClient源码,修改Validation\Schema\Cas20\AuthenticationSuccess认证返回信息类型
[Serializable] [DebuggerStepThrough] [DesignerCategory("code")] [XmlType(Namespace = "http://www.yale.edu/tp/cas")] public class AuthenticationSuccess { internal AuthenticationSuccess() { } [XmlElement("user")] public string User { get; set; } [XmlElement("proxyGrantingTicket")] public string ProxyGrantingTicket { get; set; } [XmlArray("proxies")] [XmlArrayItem("proxy", IsNullable = false)] public string[] Proxies { get; set; } [XmlElement("attributes")] public Attributes Attributes { get; set; } } [Serializable] [DebuggerStepThrough] [XmlType(Namespace = "http://www.yale.edu/tp/cas")] public class Attributes { [XmlElement("user_name")] public string user_name { get; set; } [XmlElement("userId")] public string userId{ get; set; } }
修改Validation\TicketValidator\ParseResponseFromServer,处理反序列化的对象
if (authSuccessResponse.Proxies != null && authSuccessResponse.Proxies.Length > 0) { return new CasPrincipal(new Assertion(authSuccessResponse.User), proxyGrantingTicketIou, authSuccessResponse.Proxies); } else { try { var assertion = new Assertion(authSuccessResponse.User); if (authSuccessResponse.Attributes != null) { var dic = new Dictionary<string, IList<string>>(); if (!string.IsNullOrEmpty(authSuccessResponse.Attributes.user_name)) { dic.Add("user_name", new List<string> { authSuccessResponse.Attributes.user_name }); } if (!string.IsNullOrEmpty(authSuccessResponse.Attributes.userId)) { dic.Add("userId", new List<string> { authSuccessResponse.Attributes.userId }); } assertion = new Assertion(authSuccessResponse.User, dic); } return new CasPrincipal(assertion, proxyGrantingTicketIou); } catch (Exception ex) { throw new TicketValidationException(string.Format("CAS Server response parse failure:{0}", ex.Message)); } }
CAS集成项目落地实施完成,中间联调出现的问题很多,1)客户协议给的验证服务地址是http://ip:port/casserver,而实际用的是http://ip:port/casserver/p3,自己排查起来花了很长时间,一致找到返回用户属性,2)返回cas:attributes模拟环境中配置了返回属性一致无法显示,切记\WEB-INF\deployerConfigContext.xml配置关于返回的用户属性attrRepoBackingMap一定要注释掉。
集成客户或第三方系统联调过程的成本要比预期的都难,作为技术的爱好者,方法总比困难多,坚持不放弃,结合周边资源,多角度考虑问题,坚信自己有所突破,至此全篇结束。
标签:CAS,casTicket,认证,cas,NET,null,public From: https://www.cnblogs.com/fqzhong2007/p/17792947.html