前端时间需要实现公司内网证书自动登录CAS.
由于对CAS的底层还不是特别了解所以学习了下,看了下源码.
这里我由上而下的讲解实现的过程.
我们都知道CAS目前使用了Spring Web Flow,
在CAS中Spring Web Flow的配置文件为login-webflow.xml
里面主要配置了登录的流程.这个如果用图来表示的话那应该是一个状态图,
一些节点会有一些判断然后会有不同的分支.
这里增加了startX509Authenticate这个节点,当需要登录的时候首先进入这个节点来验证,如果这里验证不成功的话才会进入普通的登录界面.负责直接登录成功,或者登录失败.
修改后的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-1.0.xsd"> <start-state idref="initialFlowSetup"/> <action-state id="initialFlowSetup"> <action bean="initialFlowSetupAction" /> <transition on="success" to="ticketGrantingTicketExistsCheck" /> </action-state> <decision-state id="ticketGrantingTicketExistsCheck"> <if test="${flowScope.ticketGrantingTicketId != null}" then="hasServiceCheck" else="gatewayRequestCheck" /> </decision-state> <decision-state id="gatewayRequestCheck"> <if test="${externalContext.requestParameterMap['gateway'] != '' && externalContext.requestParameterMap['gateway'] != null && flowScope.service != null}" then="redirect" else="startX509Authenticate" /> </decision-state> <decision-state id="hasServiceCheck"> <if test="${flowScope.service != null}" then="renewRequestCheck" else="viewGenericLoginSuccess" /> </decision-state> <decision-state id="renewRequestCheck"> <if test="${externalContext.requestParameterMap['renew'] != '' && externalContext.requestParameterMap['renew'] != null}" then="startX509Authenticate" else="generateServiceTicket" /> </decision-state> <decision-state id="warn"> <if test="${flowScope.warnCookieValue}" then="showWarningView" else="redirect" /> </decision-state> <action-state id="startX509Authenticate"> <action bean="x509Check" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state> <view-state id="viewLoginForm" view="casLoginView"> <render-actions> <action bean="authenticationViaFormAction" method="setupForm"/> <action bean="authenticationViaFormAction" method="referenceData"/> </render-actions> <transition on="submit" to="bindAndValidate" /> </view-state> <action-state id="bindAndValidate"> <action bean="authenticationViaFormAction" /> <transition on="success" to="submit" /> <transition on="error" to="viewLoginForm" /> </action-state> <action-state id="submit"> <action bean="authenticationViaFormAction" method="submit" /> <transition on="warn" to="warn" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state> <action-state id="sendTicketGrantingTicket"> <action bean="sendTicketGrantingTicketAction" /> <transition on="success" to="serviceCheck" /> </action-state> <decision-state id="serviceCheck"> <if test="${flowScope.service != null}" then="generateServiceTicket" else="viewGenericLoginSuccess" /> </decision-state> <action-state id="generateServiceTicket"> <action bean="generateServiceTicketAction" /> <transition on="success" to ="warn" /> <transition on="error" to="viewLoginForm" /> <transition on="gateway" to="redirect" /> </action-state> <end-state id="viewGenericLoginSuccess" view="casLoginGenericSuccessView" /> <end-state id="showWarningView" view="casLoginConfirmView" /> <end-state id="redirect" view="bean:alibabaDynamicRedirectViewSelector" /> <end-state id="viewServiceErrorView" view="viewServiceErrorView" /> <end-state id="viewServiceSsoErrorView" view="viewServiceSsoErrorView" /> <global-transitions> <transition to="viewServiceErrorView" on-exception="org.springframework.webflow.execution.repository.NoSuchFlowExecutionException" /> <transition to="viewServiceSsoErrorView" on-exception="org.jasig.cas.services.UnauthorizedSsoServiceException" /> <transition to="viewServiceErrorView" on-exception="org.jasig.cas.services.UnauthorizedServiceException" /> </global-transitions> </flow>
从上面的配置文件我们可看出startX509Authenticate这个节点对应的Bean是x509Check
那么我们需要增加这样的一个Bean
我们在cas-servlet.xml这个配置文件中增加这个类的配置,因为CAS自带了证书认证的Action
<bean id="x509Check" class="org.jasig.cas.adaptors.x509.web.flow.X509CertificateCredentialsNonInteractiveAction" p:centralAuthenticationService-ref="centralAuthenticationService"/>
这里不得不插进来说一下,在执行的时候是怎么个机制了.
当有一个请求过来之后,web flow安排对应节点的Action 来处理,
Action便通过它的centralAuthenticationService 来进行createTicketGrantingTicket,传递的参数是credentials.
centralAuthenticationService ,即org.jasig.cas.CentralAuthenticationServiceImpl这个类.
在颁发TGT之前先通过自己的authenticationManager来验证当前传递过来的credentials是否合法.
来到authenticationManager 的家中之后,使用什么来验证credentials呢,对了各种authenticationHandlers该上场了,这些authenticationHandlers 们也都是在配置authenticationManager的时候xml配置进去的.这里会选择一个能够处理当前credential的authenticationHandler来进行验证工作(authenticationHandler.supports(credentials)).也就是这些处理器都需要实现supports这个方法.
如果验证成功了,我们需要将一些标示信息带到cas的client端阿,那边需要这个信息.
所以验证之后,credentialsToPrincipalResolvers 们上场了,他们能够很好的带出需要传回给cas client的信息.
经过上面的分析,我们需要将我们自己的Handler和credentialsToPrincipalResolvers添加到authenticationManager的配置中
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl"> <property name="credentialsToPrincipalResolvers"> <list> <bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" /> <bean class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" /> <bean class="org.jasig.cas.adaptors.x509.authentication.principal.X509CertificateCredentialsToDNEmailPrincipalResolver"/> </list> </property> <property name="authenticationHandlers"> <list> <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" /> <ref bean="dynamicAuthenticationHandler" /> <ref bean="x509CredentialsAuthenticationHandler"/> </list> </property> </bean>
x509CredentialsAuthenticationHandler这个CAS自带了,但是我们需要返回到CAS Client的为证书的邮箱,而CAS没有提供这样的Resolver,所以我自己写了一个从证书中取得email的X509CertificateCredentialsToDNEmailPrincipalResolver
当然这里引用到的类也需要在Spring中进行配置
<bean id="x509CredentialsAuthenticationHandler" class="org.jasig.cas.adaptors.x509.authentication.handler.support.X509CredentialsAuthenticationHandler" > <property name="trustedIssuerDnPattern" value="CN=intranet.+"/> </bean>
这样便可以了.
接下来我们便需要将CA的证书加入到servlet容器的TrustStore中.并开启客户端认证.
<Connector port="8447" maxHttpHeaderSize="8192" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" disableUploadTimeout="true" acceptCount="100" scheme="https" secure="true" clientAuth="want" sslProtocol="TLS" keystoreFile="conf/ssl/keystore.jks" keystorePass="changeit" truststoreFile="conf/ssl/keystore.jks" truststorePass="changeit" />
clientAuth="want"配置成want表示不强求,如果有则使用,配置成true的话那就必须通过证书,否则直接返回404
我们这里为了如果没有证书还可以进去普通登录页面,所以采取了want这个参数.
启动CAS ,一切OK了.
---------------------------------------------------------------------------------------------
PS:附上从证书中获取Email的代码
package org.jasig.cas.adaptors.x509.authentication.principal; import java.security.cert.X509Certificate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bouncycastle.asn1.x509.X509NameTokenizer; public class X509CertificateCredentialsToDNEmailPrincipalResolver extends AbstractX509CertificateCredentialsToPrincipalResolver { public static final String EMAIL = "rfc822name"; public static final String EMAIL1 = "email"; public static final String EMAIL2 = "EmailAddress"; public static final String EMAIL3 = "E"; private static final String[] EMAILIDS = { EMAIL, EMAIL1, EMAIL2, EMAIL3 }; protected final Log log = LogFactory.getLog(this.getClass()); @Override protected String resolvePrincipalInternal(X509Certificate certificate) { String email = getEmailFromDN(certificate.getSubjectDN().getName()); dynamicAuthenticationHandler.reload(); if(email!=null){ return email; } return null; } /** * Convenience method for getting an email address from a DN. * @param dn the DN * @return the found email address, or <code>null</code> if none is found */ public String getEmailFromDN(String dn) { log.info(">getEmailFromDN(" + dn + ")"); String email = null; for (int i = 0; (i < EMAILIDS.length) && (email == null); i++) { email = getPartFromDN(dn, EMAILIDS[i]); } log.error("<getEmailFromDN(" + dn + "): " + email); return email; } /** * Gets a specified part of a DN. Specifically the first occurrence it the DN contains several * instances of a part (i.e. cn=x, cn=y returns x). * * @param dn String containing DN, The DN string has the format "C=SE, O=xx, OU=yy, CN=zz". * @param dnpart String specifying which part of the DN to get, should be "CN" or "OU" etc. * * @return String containing dnpart or null if dnpart is not present */ public String getPartFromDN(String dn, String dnpart) { log.debug(">getPartFromDN: dn:'" + dn + "', dnpart=" + dnpart); String part = null; if ((dn != null) && (dnpart != null)) { String o; dnpart += "="; // we search for 'CN=' etc. X509NameTokenizer xt = new X509NameTokenizer(dn); while (xt.hasMoreTokens()) { o = xt.nextToken(); //log.debug("checking: "+o.substring(0,dnpart.length())); if ((o.length() > dnpart.length()) && o.substring(0, dnpart.length()).equalsIgnoreCase(dnpart)) { part = o.substring(dnpart.length()); break; } } } log.debug("<getpartFromDN: resulting DN part=" + part); return part; } //getPartFromDN }