前面学习实践了CAS服务端的配置和登陆验证等内容,下面要进行单点登录的应用子系统接入的实践。单点登录最大的使用场景就是解决多个子系统的多次登陆问题,使用CAS框架可以将多次登陆化简为统一一次登陆认证。
1、服务端配置service
客户端接入 CAS 首先需要在服务端进行注册,否则客户端访问将提示“未认证授权的服务”警告:
我们可以参考原来的service配置进行修改,原来的配置文件在war解压后的如下位置:
将这个文件拷贝到工程中的resources/services下,打开这个文件,如下图
分析一下里面的内容
{
//这是注册类,除非自己实现了一个注册类,否则不用动
"@class" : "org.apereo.cas.services.RegexRegisteredService",
//匹配url
"serviceId" : "^(https|imaps)://.*",
//服务的全局名称
"name" : "HTTPS and IMAPS",
//服务的id
"id" : 10000001,
//描述:描述客户端的服务
"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
//定义多个服务的执行顺序
"evaluationOrder" : 10000
}
}
Json文件的命名必须满足如下规则:
${name}-${id}.json 其中 id 与文件中的id一致,名称
下面我们参考上面的内容建立一个要加入到单点登录的子应用的配置文件
名字就设定为APP1-10000003.json
然后在配置文件application.properties下添加配置:
##
# Service Registry(服务注册)
#
# 开启识别Json文件,默认false
cas.serviceRegistry.initFromJson=true
#自动扫描服务配置,默认开启
#cas.serviceRegistry.watcherEnabled=true
#120秒扫描一遍
cas.serviceRegistry.schedule.repeatInterval=120000
#延迟15秒开启
# cas.serviceRegistry.schedule.startDelay=15000
##
# Json配置
cas.serviceRegistry.json.location=classpath:/services
添加完成后,启动CAS服务端,可以看到有2个service已经启动。本例是因为删除了原来的配置json,否则的话,会看到4个services
启动日志看到,有两个service,因为默认的war包下面还有一个service。看到app1
已经注册成功
2、 客户端1(业务系统1)接入
2.1 准备一个客户端工程
首先我们准备一个待接入的客户端1,我们命名为app1,下面我们先将app1工程调起来,本例子中使用的是一个基于maven的简单SSM工程。这个工程,通过从数据库中获取账号和密码进行登录验证,这个工程的详细内容,已经放到github上,可以从github的下面url 获取这个初始工程:https://github.com/cwqsolo/StudySsm.git
这个工程,没有集成cas客户端时,运行起来的登陆界面如下图所示:
完成后,初始工作完成后,我们开始客户端应用1的接入。注意应用1的运行端口配置如下
启动应用1以后,打开url:http://localhost:8380/login.jsp 用账号admin 密码 123456
下面我们需要在此工程的基础上添加基于cas的单点登录。好,让我们开始吧。
2.2 导入证书
客户端证书和服务端证书是同一个证书,不然就会报错,我因为是在同一台机器,所以就没有进行以下操作。
sudo keytool -import -file E:/tmp/tomcat-key/tomcat.cer -alias tomcat -keys
2.3 修改pom.xml 文件
首先修改pom.xml 文件,添加cas客户端依赖
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.0</version>
</dependency>
如下图:
添加完成后,重新导入依赖。可以采用下面的方式用右键方式进行重新导入
2.4 修改web.xml
1)增加displayname
2)添加单点登录的相关内容
注意这里的单点登录的内容中涉及到的服务端url和客户端url需要和实际的一致
<!-- ========================单点登录开始 ======================== -->
<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<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>
<param-value>http://server.cas.com:8080/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器用于实现单点登录功能 -->
<filter>
<filter-name>CAS Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://server.cas.com:8080/cas/login</param-value>
<!-- 使用的CAS-Server的登录地址,一定是到登录的action -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.cas.com:8380/node1/login.jsp</param-value>
<!-- 当前Client系统的地址 -->
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责对Ticket的校验工作 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://server.cas.com:8080/cas</param-value>
<!-- 使用的CAS-Server的地址,一定是在浏览器输入该地址能正常打开CAS-Server的根地址 -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.cas.com:8380/node1/login.jsp</param-value>
<!-- 当前Client系统的地址 -->
</init-param>
<!--<init-param>-->
<!--<param-name>redirectAfterValidation</param-name>-->
<!--<param-value>true</param-value>-->
<!--</init-param>-->
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
比如AssertionHolder.getAssertion().getPrincipal().getName()
或者request.getUserPrincipal().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 Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ========================单点登录结束 ======================== -->
2.5 单点退出
原来客户端的登出是在controller(本例是userController.java)中,在该文件中,将退出重新指向login.jsp。 现在使用cas单点登出后,需要到服务端的退出,这样就实现了单点登出。
2.6 启动客户端1:
客户端的url是http://app1.cas.com:8380/login.jsp, 账号和密码为 admin, 123456。账号和密码登陆后,展示的url是:http://app1.cas.com:8380/index.jsp。
我们预期的结果是:
1)当输入http://app1.cas.com:8380/index.jsp页面的时候,如果当前系统没有登录,则跳转到cas进行登录,登录后,打开index页面。
2)登陆成功后,在app1中进行业务操作跳转时,和原来app1应用一样。
3)单点登出:执行完成后,退出系统,会退出cas服务端,这样下次再访问客户端1的时候,会要求继续输入账号和密码
下面我们分别在cas服务端没有开启和开启的情况下分别查看一下单点登录SSO的情况是否符合我们的预期。
2.6.1 CAS服务未启动
首先测试一下如果cas服务端没有开启的情况下,启动工程会如何。启动完成后,我打开上面的应用1的url,弹出下面的网页,说明应用1去连接cas服务端,但是没有连接上。
2.6.2 开启CAS服务端
在确保CAS服务端正确开启的情况下,我们输入客户端1(app1)的url,http://app1.cas.com:8380/index.jsp (这个页面是正常登陆后的页面),这个时候,客户端1(app1)没有弹出自己的登陆页面,而是弹出cas的登陆界面
在登录界面的右上角,显示出当前登陆的是app1.
我们输入app1的账号 admin 和密码 123456,这个时候,会跳出来下面的界面
看到这个界面说明客户端应用1已经通过单点登录登录到应用。之后,在系统中的测试都和使用原系统一致。
我们现在测试一下退出登录,点击退出登录-退出后显示下面的界面:
说明退出客户端,并且在服务端也注销了。这个时候访问客户端的应用http://app1.cas.com:8380/index.jsp 界面,又会弹出单点登录的界面。说明客户端1的单点登录和单点登出已经被cas服务端(统一登录)全面管理了。
2.6.4 重要说明
如果客户端的账号,因为某种原因删除后,而没有同步删除统一登录服务端的账号,则该账号还是会可以进入到客户端系统,因此账号的同步非常重要,另外客户端应用界面要对用户进行二次检查。
修改后的工程也可以在github上获取:
https://github.com/cwqsolo/StudySsmCas.git
3、客户端2(采用shiro)接入
下面我们在另外一个客户端2(app2)上实现cas单点登录和单点登出。客户端2和客户端1的区别在于客户端2集成了shiro进行管理。客户端2的工程也可以在github上找到:
https://github.com/cwqsolo/StudySsmShiro.git
集成了cas后的工程再github上链接如下
https://github.com/cwqsolo/StudySsmShiroCas.git
3.1 修改pom.xml 文件
首先修改pom.xml 文件,由于shiro集成了cas,所以可以添加如下依赖
<!-- shiro-cas集成依赖包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>1.4.0</version>
</dependency>
添加完成后,重新导入依赖。可以采用右键方式进行重新导入
3.2 修改web.xml
由于基础工程已经是一个shiro工程,所以在web.xml中已经含有shiro相关配置,无需修改
3.3 修改spring-shiro.xml
修改spring-shiro.xml 文件,使得其适用于cas应用。修改的地方主要是一些地址的跳转,完整的文件信息如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- shiro的核心配置: 配置shiroFileter id名必须与web.xml中的filtername保持一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.html"页面 -->
<property name="loginUrl" value="http://server.cas.com:8080/cas/login?service=http://app2.cas.com:8580/node2/shiro-cas" />
<property name="filters">
<map>
<!-- 添加casFilter到shiroFilter, 这里的key cas 需要和下面的/shiro-cas = cas 一致 -->
<entry key="cas" value-ref="casFilter" />
<entry key="logout" value-ref="logoutFilter" />
</map>
</property>
<!--/shiro-cas 是回调地址,不需要实现,指向了casFilter /logout = logout-->
<property name="filterChainDefinitions">
<value>
/shiro-cas = cas
/unauthorized.jsp = anon
/index.jsp = authc
/user/** = user
</value>
</property>
</bean>
<!-- CasFilter为自定义的单点登录Fileter -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<!-- 配置验证错误时的失败页面 -->
<property name="failureUrl" value="/unauthorized.jsp"/>
<property name="successUrl" value="/user/loginSuccess" />
</bean>
<bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!-- 配置验证错误时的失败页面 -->
<property name="redirectUrl" value="http://server.cas.com:8080/cas/logout" />
</bean>
<!-- 单点登录下的配置 -->
<bean id="casRealm" class="com.studyssm.shiro.MyRealm">
<property name="defaultRoles" value="ROLE_USER"/>
<!-- cas服务端地址前缀 -->
<property name="casServerUrlPrefix" value="http://server.cas.com:8080/cas" />
<!-- 应用服务地址,用来接收cas服务端票据 -->
<!-- 客户端的回调地址(函数),必须和上面的shiro-cas过滤器casFilter拦截的地址一致 -->
<property name="casService" value="http://app2.cas.com:8580/node2/shiro-cas" />
</bean>
<!-- 配置安全管理器securityManager, 缓存技术: 缓存管理 realm:负责获取处理数据 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="subjectFactory" ref="casSubjectFactory"></property>
<property name="realm" ref="casRealm" />
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!-- 配置缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" />
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager"></property>
<property name="arguments" ref="securityManager"></property>
</bean>
</beans>
<
3.5 修改MyRealm.java
修改shiro的自定义realm,添加cas认证方面的内容,具体realm类MyRealm.java内容如下:
package com.studyssm.shiro;
import com.studyssm.entity.User;
import com.studyssm.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasAuthenticationException;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.StringUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
* Describe:
*
* @author cwqsolo MyRealm ,继承自 CasRealm ,来完成对 CAS Server 返回数据的验证
* @date 2019/07/22
*/
public class MyRealm extends CasRealm {
@Autowired
private UserService userService;
private User us;
/**
* 授权,在配有缓存的情况下,只加载一次。
* @param principal
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("Myrealm doGetAuthenticationInfo 1++++");
//当前登录用户,账号
us= (User)principal.getPrimaryPrincipal();
String username = us.getUserName();
System.out.println("当前登录用户:"+username);
//获取角色信息
Set<String> roles = userService.findRoles(username);
if(roles.size()==0){
System.out.println("当前用户没有角色!");
}else
{
}
SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
simpleAuthenticationInfo.setRoles(userService.findRoles(username));
simpleAuthenticationInfo.setStringPermissions(userService.findPermissions(username));
return simpleAuthenticationInfo ;
}
/**
* 认证登录,查询数据库,如果该用户名正确,得到正确的数据,并返回正确的数据
* AuthenticationInfo的实现类SimpleAuthenticationInfo保存正确的用户信息
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("Myrealm 认证 ++++1");
CasToken casToken = (CasToken) token;
// token为空直接返回,页面会重定向到 Cas Server 登录页,并且携带本项目回调页
if (token == null) {
System.out.println("Myrealm 认证 token 为空 ++++");
return null;
}
System.out.println("Myrealm 认证 ++++2");
// 获取服务端范围的票根
String ticket = (String) casToken.getCredentials();
// 票根为空直接返回,页面会重定向到 Cas Server 登录页,并且携带本项目回调页
if (!StringUtils.hasText(ticket)) {
System.out.println("Myrealm 认证 ticket 为空 ++++");
return null;
}
TicketValidator ticketValidator = ensureTicketValidator();
try {
System.out.println("Myrealm 认证 ++++4");
// 票根验证
Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
// 获取服务端返回的用户数据
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
System.out.println("Myrealm 认证 ++++5");
// 拿到用户唯一标识
String username = casPrincipal.getName();
// 通过唯一标识查询数据库用户表
// 如果查询到对应用户则直接返回用户数据
us = userService.findUserByUsername(username);
System.out.println("Myrealm 认证 us info="+us.toString());
//如果没有查询到,抛出异常
if( us == null ) {
System.out.println("Myrealm::账户"+username+"不存在!");
throw new UnknownAccountException("账户"+username+"不存在!");
}else{
//如果查询到了,封装查询结果,
Object principal = us.getUserName();
Object credentials = us.getPassword();
String realmName = this.getName();
// 将获取到的本项目数据库用户包装为 shiro 自身的 principal 存于当前 session 中
// 之后在整个项目中都可以通过 SecurityUtils.getSubject().getPrincipal() 直接获取到当前用户信息
List<Object> principals = CollectionUtils.asList(us, casPrincipal.getAttributes());
PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
System.out.println("Myrealm 认证 return sucessfully!!!");
return new SimpleAuthenticationInfo(principalCollection, ticket);
}
} catch (TicketValidationException e) {
System.out.println("Myrealm 认证 ++++++");
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
}
}
3.6 单点退出
原来客户端的登出是在controller(本例是userController.java)中,在该文件中,将退出重新指向login.jsp。 现在使用cas单点登出后,需要到服务端的退出,这样就实现了单点登出。userController.java类可以从github上获取,这里贴出退出函数:
/**
* 退出
*
* @param session
* @return
*/
@RequestMapping("/logout")
public String logout(HttpSession session) {
System.out.println("run logout....");
//session.invalidate();
Subject subject = SecurityUtils.getSubject();
//判断当前用户是否已登录
if (subject.isAuthenticated()) {
//退出登录
subject.logout();
System.out.println("subject.logout!!!");
}
return "redirect:http://server.cas.com:8080/cas/logout?service=http://app2.cas.com:8580/node2/shiro-cas";
}
4、跨系统单点登录验证
4.1 启动客户端2:
客户端的url是http://app2.cas.com:8580/login.jsp, 账号和密码为 admin, 123456。账号和密码登陆后,展示的url是:http://app2.cas.com:8580/index.jsp。
我们预期的结果是:
1)当输入http://app2.cas.com:8580/index.jsp页面的时候,如果当前系统没有登录,则跳转到cas进行登录,登录后,打开index页面。
2)登陆成功后,在app1中进行业务操作跳转时,和原来应用一样。
3)单点登出:执行完成后,退出系统,会退出cas服务端,这样下次再访问客户端1的时候,会要求继续输入账号和密码
4.2 应用1和应用2都没有登录
在确保CAS服务端正确开启的情况下,我们分别在两个浏览器界面两个客户端应用的url
app1.cas.com:8380/node1/index.jsp
app2.cas.com:8580/node2/index.jsp
这个时候,浏览器会跳转到单点登录服务端,显示登陆界面,两个登陆界面的区别是分别有app1和app2提示说明是哪个客户端应用接入
应用2的登陆界面
4.3 应用1登陆,应用2无需登陆
这个时候,我们在应用1登陆,再打开应用2,就无需登陆了。
使用admin账号登陆应用1(app1)
这个时候,刷新一下应用2界面,发现也进入系统了
4.4 应用1登出,应用2也无法访问
接下来,我们将应用1登出,如下图
然后我们再访问应用2时,会出现重新登录界面。
4.5 应用1和应用2的账号差异
当某账号在应用1中存在,在应用2中不存在的时候,进行登录访问,则应用1可以正常访问,应用2会提示
5 重要说明
如果客户端的账号,因为某种原因删除后,而没有同步删除统一登录服务端的账号,则该账号还是会可以进入到客户端系统,因此账号的同步非常重要,另外客户端应用界面要对用户进行二次检查。