一、cas单点登录的应用场景:
很简单的道理,一个大型的网站或者系统,比如Tencent、AliBaBa、BaiDu这些系统,你登录QQ以后再去玩腾讯游戏、腾讯视频、QQ音乐,又或者支付宝、淘宝、再或者百度文库,百度外卖等等这些用户认证系统,再比如一些集团公司的用户OA系统,肯定不会是你登陆完一个应用,再去认证下一个,这样繁琐的逻辑和认证,就应用到了SSO单点登录这种场景。
二、cas单点登录的原理:
盗用一张图,简单描述了CAS原理和协议,直观易懂。说白了就是cas服务端充当公共认证中心,负责颁发票据和检验身份。CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。用户在第 3 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted Cookie(TGC),CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5,6 步中与 CAS Server 进行身份合适,以确保 Service Ticket 的合法性。
在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保,ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向的过程,但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的。
三、直接进入主题,笔者本次是在Windows环境下,开启不同的Tomcat模拟两个客户端和一个服务端的情况,Linux是一样的道理,待下次进行,附上Linux的操作步骤
1.简单介绍下keytool
Keytool是一个Java数据证书的管理工具。Keytool将密钥(key)和证书(certificates)存在一个称为keystore的文件中在keystore里,包含两种数据:
a、 密钥实体(Key entity)——密钥(secret key)又或者是私钥和配对公钥(采用非对称加密)
b、 可信任的证书实体(trusted certificate entries)——只包含公钥
Alias(别名):每个keystore都关联这一个独一无二的alias,这个alias通常不区分大小写
keystore的存储位置
在没有制定生成位置的情况下,keystore会存在与用户的系统默认目录, 如:对于window xp系统,会生成在系统的C:/Documents and Settings/UserName/ 文件名为“.keystore”
2. 服务器生成证书
(注:生成证书时,CN要和服务器的域名相同,如果在本地测试,则使用localhost)
Window : keytool -genkey -alias tomcat -keyalg RSA -keystore d:/my.keystore -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN" -keypass changeit -storepass changeit
Linux : keytool -genkey -alias tomcat -keyalg RSA -keystore ~/my.keystore -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN" -keypass changeit -storepass changeit
3.导出证书,客户端安装
window : keytool -export -alias tomcat -keystore d:/my.keystore -file d:/mycerts.cer -storepass changeit
Linux : keytool -export -alias tomcat -keystore ~/my.keystore -file ~/mycerts.cer -storepass changeit
4.客户端的配置:为JVM导入秘钥
window : keytool -import -trustcacerts -alias tomcat -keystore "%JAVA_HOME%/jre/lib/security/cacerts" -file d:/mycerts.cer -storepass changeit
Linux : keytool -import -trustcacerts -alias tomcat -keystore "$JAVA_HOME/jre/lib/security/cacerts" -file ~/mycerts.cer -storepass changeit
5.配置服务端Tomcat/server.xml配置
去掉8443端口的注释
<Connector port="8453" protocol="org.apache.coyote.http11.Http11Protocol" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="d:/my.keystore" keystorePass="changeit"
/>
由于我的Tomcat8443端口一直被占用,便改成8453,没什么影响,Linux下的Tomcat配置和上面一样。
6.到此cas服务端配置完成,以下用到的所有jar包或者项目,笔者会提供。现在需要将cas-server-4.0.0-release.zip解压后module里的cas-server-webapp-4.0.0.war部署到你的服务端Tomcat下
进行测试.提供两种访问方式http://localhost:9997/cas/login 或者https://localhost:8453/cas/login都可以,
出现如下页面
初始用户名用默认casuser/Mellon,登陆成功后我们开始改用连接mysql自定义查询登录
需要添加 jar包cas-server-support-jdbc-4.1.4.jar和mysql-connector-java-5.0.8-bin.jar第一个在下载的cas-server-4.0.0-release.zip解压文件有,第二个很常见。
找到下面这个bean注释掉
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
</map>
</property>
</bean>
添加如下配置
配置数据库
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/txjh?characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="1234" />
</bean>
登录入口,密码MD5加密
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource"
p:passwordEncoder-ref="MD5PasswordEncoder"
p:sql="select password from t_user where username=?" />
数据库存的是MD5加密后的密码,没有盐和散列算法
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg index="0">
<value>MD5</value>
</constructor-arg>
</bean>
再启动,输入用户名和密码,登录成功
7.接下来准备客户端(子应用项目)
其实随便建个web项目,添加如下jar包和几张页面,
先说web.xml,笔者在这准备三个项目,两个放在同一个服务器下(Tomcat)就是端口一致,另一个单独放
<
listener
>
<
listener-class
>
org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</
listener-class
>
</
listener
>
<!-- 登出filter -->
<
filter
>
<
filter-name
>
CAS Single Sign Out Filter
</
filter-name
>
<
filter-class
>
org.jasig.cas.client.session.SingleSignOutFilter
</
filter-class
>
<
init-param
>
<
param-name
>
casServerUrlPrefix
</
param-name
>
</
init-param
>
</
filter
>
<!-- 拦截单点登录验证 -->
<
filter
>
<
filter-name
>
CAS Authentication Filter
</
filter-name
>
<!--<filter-class>org.jasig.cas.client.authentication.Saml11AuthenticationFilter</filter-class>-->
<
filter-class
>
org.jasig.cas.client.authentication.AuthenticationFilter
</
filter-class
>
<
init-param
>
<
param-name
>
casServerLoginUrl
</
param-name
>
</
init-param
>
<
init-param
>
<!-- 把子系统注册给认证中心 这里的ip是各个子应用所部署的服务器地址-->
<
param-name
>
serverName
</
param-name
>
</
init-param
>
</
filter
>
<!-- 请求参数ticket验证(ticket即子系统与CAS系统进行交互的凭证) -->
<
filter
>
<
filter-name
>
CAS Validation Filter
</
filter-name
>
<!--<filter-class>org.jasig.cas.client.validation.Saml11TicketValidationFilter</filter-class>-->
<
filter-class
>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</
filter-class
>
<
init-param
>
cas服务端的地址
<
param-name
>
casServerUrlPrefix
</
param-name
>
</
init-param
>
<
init-param
>
<
param-name
>
serverName
</
param-name
>
</
init-param
>
</
filter
>
<!-- 该过滤器负责实现HttpServletRequest请求的包裹,
比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<
filter
>
<
filter-name
>
CAS HttpServletRequest Wrapper Filter
</
filter-name
>
<
filter-class
>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</
filter-class
>
</
filter
>
<!--
该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
比如AssertionHolder.getAssertion().getPrincipal().getName()。
-->
<
filter
>
<
filter-name
>
CAS Assertion Thread Local Filter
</
filter-name
>
<
filter-class
>
org.jasig.cas.client.util.AssertionThreadLocalFilter
</
filter-class
>
</
filter
>
<
filter-mapping
>
<
filter-name
>
CAS Single Sign Out Filter
</
filter-name
>
<
url-pattern
>
/*
</
url-pattern
>
</
filter-mapping
>
<
filter-mapping
>
<
filter-name
>
CAS Validation Filter
</
filter-name
>
<
url-pattern
>
/*
</
url-pattern
>
</
filter-mapping
>
<
filter-mapping
>
<
filter-name
>
CAS Authentication Filter
</
filter-name
>
<
url-pattern
>
/*
</
url-pattern
>
</
filter-mapping
>
<
filter-mapping
>
<
filter-name
>
CAS HttpServletRequest Wrapper Filter
</
filter-name
>
<
url-pattern
>
/*
</
url-pattern
>
</
filter-mapping
>
<
filter-mapping
>
<
filter-name
>
CAS Assertion Thread Local Filter
</
filter-name
>
<
url-pattern
>
/*
</
url-pattern
>
</
filter-mapping
>
<
welcome-file-list
>
<
welcome-file
>
index.jsp
</
welcome-file
>
</
welcome-file-list
>
注意:不同的子应用在配置wen.xml时候,ip要写对应的端口。这个xml是我的第一个子系统web配置,其余两个就是改了ticket验证、拦截单点登录、登出的子应用ip
再配置index.jsp,加上几个超链接,超链分别是其他子应用的jsp页面,这样就实现了登陆完这个应用再去访问其他的应用,而不用再去登录接口
index页面如下
<%@page contentType="text/html" %>
<%@page pageEncoding="UTF-8" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.List" %>
<%@ page import="org.jasig.cas.client.authentication.AttributePrincipal" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>CAS Example Java Web App</title>
</head>
<body>
<h1>CAS Example Java Web App</h1>
<p>A sample web application that exercises the CAS protocol features via the Java CAS Client.</p>
<hr>
<a href="http://localhost:9998/hello1/index.jsp">9998hello1</a>
<a href="http://localhost:9996/hello1/index.jsp">9996hello1</a>
<p><b>Authenticated User Id:</b> <a href="logout.jsp" title="Click here to log out"><%= request.getRemoteUser() %>
</a></p>
<%
if (request.getUserPrincipal() != null) {
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
final Map attributes = principal.getAttributes();
if (attributes != null) {
Iterator attributeNames = attributes.keySet().iterator();
out.println("<b>Attributes:</b>");
if (attributeNames.hasNext()) {
out.println("<hr><table border='3pt' width='100%'>");
out.println("<th colspan='2'>Attributes</th>");
out.println("<tr><td><b>Key</b></td><td><b>Value</b></td></tr>");
for (; attributeNames.hasNext(); ) {
out.println("<tr><td>");
String attributeName = (String) attributeNames.next();
out.println(attributeName);
out.println("</td><td>");
final Object attributeValue = attributes.get(attributeName);
if (attributeValue instanceof List) {
final List values = (List) attributeValue;
out.println("<strong>Multi-valued attribute: " + values.size() + "</strong>");
out.println("<ul>");
for (Object value : values) {
out.println("<li>" + value + "</li>");
}
out.println("</ul>");
} else {
out.println(attributeValue);
}
out.println("</td></tr>");
}
out.println("</table>");
} else {
out.print("No attributes are supplied by the CAS server.</p>");
}
} else {
out.println("<pre>The attribute map is empty. Review your CAS filter configurations.</pre>");
}
} else {
out.println("<pre>The user principal is empty from the request object. Review the wrapper filter configuration.</pre>");
}
%>
</body>
</html>
<%@page pageEncoding="UTF-8" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.List" %>
<%@ page import="org.jasig.cas.client.authentication.AttributePrincipal" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>CAS Example Java Web App</title>
</head>
<body>
<h1>CAS Example Java Web App</h1>
<p>A sample web application that exercises the CAS protocol features via the Java CAS Client.</p>
<hr>
<a href="http://localhost:9998/hello1/index.jsp">9998hello1</a>
<a href="http://localhost:9996/hello1/index.jsp">9996hello1</a>
<p><b>Authenticated User Id:</b> <a href="logout.jsp" title="Click here to log out"><%= request.getRemoteUser() %>
</a></p>
<%
if (request.getUserPrincipal() != null) {
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
final Map attributes = principal.getAttributes();
if (attributes != null) {
Iterator attributeNames = attributes.keySet().iterator();
out.println("<b>Attributes:</b>");
if (attributeNames.hasNext()) {
out.println("<hr><table border='3pt' width='100%'>");
out.println("<th colspan='2'>Attributes</th>");
out.println("<tr><td><b>Key</b></td><td><b>Value</b></td></tr>");
for (; attributeNames.hasNext(); ) {
out.println("<tr><td>");
String attributeName = (String) attributeNames.next();
out.println(attributeName);
out.println("</td><td>");
final Object attributeValue = attributes.get(attributeName);
if (attributeValue instanceof List) {
final List values = (List) attributeValue;
out.println("<strong>Multi-valued attribute: " + values.size() + "</strong>");
out.println("<ul>");
for (Object value : values) {
out.println("<li>" + value + "</li>");
}
out.println("</ul>");
} else {
out.println(attributeValue);
}
out.println("</td></tr>");
}
out.println("</table>");
} else {
out.print("No attributes are supplied by the CAS server.</p>");
}
} else {
out.println("<pre>The attribute map is empty. Review your CAS filter configurations.</pre>");
}
} else {
out.println("<pre>The user principal is empty from the request object. Review the wrapper filter configuration.</pre>");
}
%>
</body>
</html>
我解释下,我在本地9998的Tomcat部署了hello和hello1两个项目,在9996部署了hello1,cas服务端是9997,同时启动三个服务器,
地址栏:http://localhost:9998/hello/index.jsp,回去服务端认证并且带着地址,认证成功会有如下页面,然后点击另外两个超链,不用登陆,实现了不同系统间的单点登录。
写这篇文章本来是因为做shiro的项目,遇到要结合单点登录,就随机记录下来,因为很不容易。用到的项目和jar包、下载文件,会整理下放在下载地址里。