CAS单点登录使用

CAS简介

CAS架构图

CAS server 和CAS client 是构成CAS系统架构的两个物理组件,他们之间通过多种协议来通信

CAS server

CAS server 的主要作用是通过分发ticket并使之生效来对用户进行认证并授权用户访问CAS认可的服务,通常这些服务就是指CAS client,当用户成功登录 server发放一个已授权的ticket 给用户(TGT),此时一个sso session就被创建了。

CAS client

通常CAS client有两层含义,一,CAS client 可以是任何被CAS认可的应用,二 ,CAS client也可以是能与各种软件平台或者应用集成的软件包

准备工作

到官网下载CAS源码 https://www.apereo.org/projects/cas/download-cas
本文使用v4.2.1版本 现在比较新的版本需要自己用gradle打包

下载tomcat 本文使用tomcat8版本

jdk使用1.8版本

部署 CAS Server

配置Tomcat使用Https协议

创建证书

证书是单点登录认证系统中很重要的一把钥匙,客户端于服务器的交互安全靠的就是证书;本文使用jdk自带的keytool生成,真正在产品环境中使用肯定要去证书提供商去购买,证书认证一般都是由VeriSign认证,中文官方网站:http://www.verisign.com/cn/

$ keytool -genkey -alias jason -keyalg RSA -keystore /Users/lilixin/programming/jasonssokey    

添加hosts

在/etc/hosts(linux系统)中加入127.0.0.1 sso.jason-li.cn

导出证书

keytool -export -file /Users/lilixin/programming/jasonssokey.crt -alias jason -keystore /Users/lilixin/programming/jasonssokey

至此导出证书完成,可以分发给应用的JDK使用了,接下来讲解客户端的JVM怎么导入证书

为客户端JVM导入证书

keytool -import -keystore /Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/jre/lib/security/cacerts -file /Users/lilixin/programming/jasonssokey  -alias jason

过程中可能遇到如下错误,请重命名/…./security/cacerts

keytool 错误: java.io.IOException: Keystore was tampered with, or password was incorrect

应用证书到Web服务器

启用Web服务器(Tomcat)的SSL,也就是HTTPS加密协议
打开tomcat目录的conf/server.xml文件,开启83和87行的注释代码,并设置keystoreFile、keystorePass,修改后结果如下,请注意属性值区分大小写

1
2
3
4
5
6
7
8
9
10
11
12
<Connector
port="8443"
protocol="org.apache.coyote.http11.Http11Protocol"
SSLEnabled="true"
maxThreads="150"
scheme="https"
secure="true"
clientAuth="false"
sslProtocol="TLS"
keystoreFile="/Users/lilixin/programming/jasonssokey"
keystorePass="adwars"
/>

测试

浏览器打开 https://sso.jason-li.cn:8443/ 可访问到tomcat页面既成功

启动CAS server

把下栽来的CAS源码中 modules目录中的cas-server-webapp-3.5.2.war 重命名后放入tomcat的webapps目录中 启动tomcat
访问https://sso.jason-li.cn:8443/cas 如果出现正常CAS登录页面则部署成功

扩展认证接口

由于缺省的实现仅能用于测试,我们还需要自己扩展认证接口

扩展 AuthenticationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface AuthenticationHandler {
/**
* Method to determine if the credentials supplied are valid.
* @param credentials The credentials to validate.
* @return true if valid, return false otherwise.
* @throws AuthenticationException An AuthenticationException can contain
* details about why a particular authentication request failed.
*/
boolean authenticate(Credentials credentials) throws AuthenticationException;
/**
* Method to check if the handler knows how to handle the credentials
* provided. It may be a simple check of the Credentials class or something
* more complicated such as scanning the information contained in the
* Credentials object.
* @param credentials The credentials to check.
* @return true if the handler supports the Credentials, false othewrise.
*/
boolean supports(Credentials credentials);
}

通过源码可以看到AuthenticationHandler主要有两个方法 supports ()方法用于检查所给的包含认证信息的Credentials 是否受当前 AuthenticationHandler 支持;而 authenticate() 方法则担当验证认证信息的任务,这也是需要扩展的主要方法,根据情况与存储合法认证信息的介质进行交互,返回 boolean 类型的值,true 表示验证通过,false 表示验证失败。

JDBC 认证方法

通常我们会将用户信息保存在数据库中,所以采用jdbc认证方式

配置DataStore

打开 /webapps/cas/WEB-INF/deployerConfigContext.xml添加一个新的 bean 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/ucenter?useUnicode=true"
p:user="root"
p:password="root"
p:initialPoolSize="1"
p:minPoolSize="1"
p:maxPoolSize="3"
p:maxIdleTimeExcessConnections="120"
p:checkoutTimeout="10000"
p:acquireIncrement="6"
p:acquireRetryAttempts="5"
p:acquireRetryDelay="2000"
p:idleConnectionTestPeriod="30"
p:preferredTestQuery="select 1" />

记得添加名称空间 xmlns:p=”http://www.springframework.org/schema/p

配置 AuthenticationHandler

在 cas-server-support-jdbc-3.1.1.jar 包中,提供了 3 个基于 JDBC 的 AuthenticationHandler,分别为 BindModeSearchDatabaseAuthenticationHandler, QueryDatabaseAuthenticationHandler, SearchModeSearchDatabaseAuthenticationHandler。其中 BindModeSearchDatabaseAuthenticationHandler 是用所给的用户名和密码去建立数据库连接,根据连接建立是否成功来判断验证成功与否;QueryDatabaseAuthenticationHandler 通过配置一个 SQL 语句查出密码,与所给密码匹配;SearchModeSearchDatabaseAuthenticationHandler 通过配置存放用户验证信息的表、用户名字段和密码字段,构造查询语句来验证。

我们这里选用QueryDatabaseAuthenticationHandler

CAS4.2.1默认使用的是一个简单的acceptUsersAuthenticationHandler,找到他然后注释掉他

使用我们自己定义的AuthenticationHandler

1
2
3
4
5
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource"
p:passwordEncoder-ref="MD5PasswordEncoder"
p:sql="SELECT password FROM uc_members WHERE username=?" />

这里我们采用MD5加密 MD5PasswordEncoder是对具体实现的引用

1
2
3
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" >
<constructor-arg value="MD5"/>
</bean>

定制返回属性

注释掉原来的attributeRepository和attrRepoBackingMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<bean id="attributeRepository" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">
<constructor-arg index="0" ref="dataSource" />
<constructor-arg index="1">
<value>
select username,email,myid,EmployeeID,mobile,EmployType,Department,Gender from uc_members where username=?
</value>
</constructor-arg>
<property name="queryAttributeMapping">
<map>
<entry key="username" value="username"/>
</map>
</property>
<property name="resultAttributeMapping">
<map>
<entry key="email" value="email" />
<entry key="myid" value="myid" />
<entry key="EmployeeID" value="EmployeeID" />
<entry key="EmployType" value="EmployType" />
<entry key="Department" value="Department" />
</map>
</property>
</bean>

添加相应jar包

  • cas-server-support-jdbc.4.2.1.jar
  • mchange-commons-java-0.2.10.jar
  • c3p0-0.9.5.1.jar
  • commons-collections-3.2.jar
  • commons-dbcp-1.2.1.jar
  • commons-pool-1.3.jar

把相应jar包放入/webapps/cas/WEB-INF/lib下面

最后的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?xml version="1.0" encoding="UTF-8"?>
<!--
| deployerConfigContext.xml centralizes into one file some of the declarative configuration that
| all CAS deployers will need to modify.
|
| This file declares some of the Spring-managed JavaBeans that make up a CAS deployment.
| The beans declared in this file are instantiated at context initialization time by the Spring
| ContextLoaderListener declared in web.xml. It finds this file because this
| file is among those declared in the context parameter "contextConfigLocation".
|
| By far the most common change you will need to make in this file is to change the last bean
| declaration to replace the default authentication handler with
| one implementing your approach for authenticating usernames and passwords.
+-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<util:map id="authenticationHandlersResolvers">
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</util:map>
<util:list id="authenticationMetadataPopulators">
<ref bean="successfulHandlerMetaDataPopulator" />
<ref bean="rememberMeAuthenticationMetaDataPopulator" />
</util:list>
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" >
<constructor-arg value="MD5"/>
</bean>
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource"
p:passwordEncoder-ref="MD5PasswordEncoder"
p:sql="SELECT password FROM uc_members WHERE username=?" />
<alias name="dataSource" alias="queryEncodeDatabaseDataSource" />
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/ucenter?useUnicode=true"
p:user="root"
p:password="root"
p:initialPoolSize="1"
p:minPoolSize="1"
p:maxPoolSize="3"
p:maxIdleTimeExcessConnections="120"
p:checkoutTimeout="10000"
p:acquireIncrement="6"
p:acquireRetryAttempts="5"
p:acquireRetryDelay="2000"
p:idleConnectionTestPeriod="30"
p:preferredTestQuery="select 1" />
<!--<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" /> -->
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />
<!-- 定制返回属性 -->
<!--
<bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao"
p:backingMap-ref="attrRepoBackingMap" />
<util:map id="attrRepoBackingMap">
<entry key="uid" value="uid" />
<entry key="eduPersonAffiliation" value="eduPersonAffiliation" />
<entry key="groupMembership" value="groupMembership" />
<entry>
<key><value>memberOf</value></key>
<list>
<value>faculty</value>
<value>staff</value>
<value>org</value>
</list>
</entry>
</util:map>-->
<bean id="attributeRepository" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">
<constructor-arg index="0" ref="dataSource" />
<constructor-arg index="1">
<value>
select username,email,myid,EmployeeID,mobile,EmployType,Department,Gender from uc_members where username=?
</value>
</constructor-arg>
<property name="queryAttributeMapping">
<map>
<entry key="username" value="username"/>
</map>
</property>
<property name="resultAttributeMapping">
<map>
<entry key="email" value="email" />
<entry key="myid" value="myid" />
<entry key="EmployeeID" value="EmployeeID" />
<entry key="EmployType" value="EmployType" />
<entry key="Department" value="Department" />
</map>
</property>
</bean>
<!-- 定制返回属性 end -->
<alias name="serviceThemeResolver" alias="themeResolver" />
<alias name="jsonServiceRegistryDao" alias="serviceRegistryDao" />
<alias name="defaultTicketRegistry" alias="ticketRegistry" />
<alias name="ticketGrantingTicketExpirationPolicy" alias="grantingTicketExpirationPolicy" />
<alias name="multiTimeUseOrTimeoutExpirationPolicy" alias="serviceTicketExpirationPolicy" />
<alias name="anyAuthenticationPolicy" alias="authenticationPolicy" />
<alias name="acceptAnyAuthenticationPolicyFactory" alias="authenticationPolicyFactory" />
<bean id="auditTrailManager"
class="org.jasig.inspektr.audit.support.Slf4jLoggingAuditTrailManager"
p:entrySeparator="${cas.audit.singleline.separator:|}"
p:useSingleLine="${cas.audit.singleline:false}"/>
<alias name="neverThrottle" alias="authenticationThrottle" />
<util:list id="monitorsList">
<ref bean="memoryMonitor" />
<ref bean="sessionMonitor" />
</util:list>
<alias name="defaultPrincipalFactory" alias="principalFactory" />
<alias name="defaultAuthenticationTransactionManager" alias="authenticationTransactionManager" />
<alias name="defaultPrincipalElectionStrategy" alias="principalElectionStrategy" />
<alias name="tgcCipherExecutor" alias="defaultCookieCipherExecutor" />
</beans>

替换CAS Server 界面

页面在目录 cas/WEB-INF/view/jsp/default下

有4个页面是必须的:

  • casConfirmView.jsp: 当用户选择了“ warn ”时会看到的确认界面
  • casGenericSuccess.jsp: 在用户成功通过认证而没有目的Service时会看到的界面
  • casLoginView.jsp: 当需要用户提供认证信息时会出现的界面
  • casLogoutView.jsp: 当用户结束 CAS 单点登录系统会话时出现的界面

重新启动tomcat 浏览器打开http://sso.jason-li.cn:8080/cas/login 从数据库中找到一个账号 登录 OK 成功 进行下一步

部署客户端应用

与CAS Server建立信任关系

引入CAS client jar包

我们这里使用shiro做权限控制并与CAS集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-cas</artifactId>
<version>${shiro-cas.version}</version>
</dependency>

shiro.properties

1
2
cas.url=http://passport.jason-li.cn:8080
shiro.service=http://client1.jason-li.cn

applicationContext-shiro.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"
>
<context:property-placeholder
location="classpath:conf/shiro.properties"
ignore-unresolvable="true" />
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean>
<!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="subjectFactory" ref="casSubjectFactory"></property>
<property name="sessionManager" ref="sessionManager"/>
<property name="realm" ref="casRealm" />
<property name="cacheManager" ref="shiroRedisCacheManager"/>
</bean>
<bean id="casRealm" class="com.jason-li.coupon.web.auth.realm.MyCasRealm">
<!-- 应用服务地址,用来接收cas服务端票据 -->
<!--<property name="roleService" ref="roleService"/>-->
<!--<property name="resourceService" ref="resourceService"/>-->
<property name="casService" value="${shiro.service}/cas" />
<property name="cachingEnabled" value="true"/>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorizationCache"/>
<property name="casServerUrlPrefix" value="${cas.url}" />
</bean>
<!--<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">-->
<bean id="casFilter" class="com.jason-li.coupon.web.auth.shiro.MyCasFilter">
<!-- 配置验证错误时的失败页面 -->
<property name="failureUrl" value="/casFailure.jsp"/>
</bean>
<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<!-- 配置验证错误时的失败页面 -->
<property name="redirectUrl" value="${cas.url}/logout?service=${shiro.service}" />
</bean>
<!-- Shiro Filter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- 设定用户的登录链接,这里为cas登录页面的链接地址可配置回调地址 -->
<property name="loginUrl" value="${cas.url}/login?service=${shiro.service}/cas" />
<property name="successUrl" value="/html/couponManager.html" />
<property name="filters">
<map>
<!-- 添加casFilter到shiroFilter -->
<entry key="cas" value-ref="casFilter" />
<entry key="logout" value-ref="logout" />
</map>
</property>
<property name="filterChainDefinitions">
<value>
/casFailure.jsp = anon
/cas/** = cas
/logout = logout
/** = authc
</value>
</property>
</bean>
<!-- 会话ID生成器 -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
</bean>
<!-- redisManager -->
<bean id = "redisManager" class = "com.jason-li.coupon.web.auth.manager.RedisManager" init-method="init">
<property name = "host" value = "${redis.host}"/>
<property name = "port" value = "${redis.port}"/>
<property name = "expire" value = "${redis.timeout}"/>
<property name = "password" value="${redis.password}"/>
</bean>
<!-- 用户授权信息Cache, -->
<bean id = "shiroRedisCacheManager" class = "com.jason-li.coupon.web.auth.manager.ShiroRedisCacheManager">
<property name = "redisManager" ref = "redisManager"/>
</bean>
<bean id = "sessionDAO" class = "com.jason-li.coupon.web.dao.ShiroRedisSessionDao">
<property name = "sessionIdGenerator" ref = "sessionIdGenerator"/>
<property name = "redisManager" ref = "redisManager"/>
<property name="cacheManager" ref="shiroRedisCacheManager" />
</bean>
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<property name="sessionIdCookieEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionFactory" ref="shiroSessionFactory" />
<property name="globalSessionTimeout" value="1800000"/>
</bean>
<bean id="shiroSessionFactory" class="com.jason-li.coupon.web.auth.shiro.ShiroSessionFactory" />
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>
</beans>

UserRealm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**@description 用户授权信息域
* @author lilixin
* @description
* @create 2017-08-25 10:26 AM
**/
public class UserRealm extends CasRealm {
// @Resource
// private RoleService roleService;
//
// @Resource
// private UserService userService;
protected final Map<String, SimpleAuthorizationInfo> roles = new ConcurrentHashMap<String, SimpleAuthorizationInfo>();
/**
* 设置角色和权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String account = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = null;
if (authorizationInfo == null) {
// authorizationInfo = new SimpleAuthorizationInfo();
// authorizationInfo.addStringPermissions(roleService.getPermissions(account));
// authorizationInfo.addRoles(roleService.getRoles(account));
roles.put(account, authorizationInfo);
}
return authorizationInfo;
}
/**
* 1、CAS认证 ,验证用户身份
* 2、将用户基本信息设置到会话中
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
String account = null;
if(authc != null){
account = (String) authc.getPrincipals().getPrimaryPrincipal();
}
User user = null;
if(account != null){
user = null;//userService.getUserByAccount(account);
}
SecurityUtils.getSubject().getSession().setAttribute("user", user);
return authc;
}
}

filterChainDefinitions过滤器配置

Shiro内置有FilterChain ,Shiro验证URL时,URL匹配成功便不再继续匹配查找,详细见官网 ,通常可将这些过滤器分为两组

认证过滤器

anon,authc,authcBasic,user

授权过滤器

perms,port,rest,roles,ssl

注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的,user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe,说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc

举几个例子

/admin=authc,roles[admin] 表示用户必需已通过认证,并拥有admin角色才可以正常发起’/admin’请求

/home=user 表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起’/home’请求

Web.xml中配置filter

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

注意到 DelegatingFilterProxy 它会自动的把filter请求交给相应名称的bean处理,也就是我们在shiro-xml中配置的shiroFilter

web.xml单点登录相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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://passport.jason-li.cn:8080/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

多系统单点退出功能改造

以上实现不能实现多个系统退出状态的同步,所以需要改造一下,因为session是由各个client端的shiro来维护的,CAS退出的时候并不会清理掉session

重写CasFilter

主要是把CasFilter和logoutFilter合并 这样就可以在logout的时候拿到shiro中的subject,并进行注销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package com.jason-li.coupon.web.auth.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.jasig.cas.client.session.SingleSignOutHandler;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
import org.slf4j.Logger;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.LoggerFactory;
/**
* @author lilixin
* @description
* @create 2017-09-07 10:13 AM
**/
public class MyCasFilter extends AuthenticatingFilter {
private static Logger logger = LoggerFactory.getLogger(MyCasFilter.class);
private static final String TICKET_PARAMETER = "ticket";
private String logoutParameterName = "logoutRequest";
private static final SingleSignOutHandler handler = new SingleSignOutHandler();
private String failureUrl;
public MyCasFilter() {
}
public static SingleSignOutHandler getHandler() {
return handler;
}
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest)request;
String ticket = httpRequest.getParameter("ticket");
return new CasToken(ticket);
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return this.executeLogin(request, response);
}
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
this.issueSuccessRedirect(request, response);
return false;
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
logger.debug("执行到了MyCasFilter的doFilterInternal方法");
final HttpServletRequest req = (HttpServletRequest) request;
if (handler.isTokenRequest(req)) {
logger.debug("执行到了MyCasFilter的登录方法");
handler.recordSession(req); // 登录,记录session SingleSignOutFilter做的事情
super.doFilterInternal(request, response, chain); // 记录完了之后,就调用CasFilter自己的doFilterInternal
return;
} else if (handler.isLogoutRequest(req)) {
logger.debug("执行到了登出--------------的方法");// 如果是登出
// 一堆的代码,就是为了获取SimplePrincipalCollection,设置到Subject里边去,并在最后调用subject.logout()
final String logoutMessage = CommonUtils.safeGetParameter(req, "logoutRequest");
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
logger.debug("执行到了登出logoutMessage={},token={}",logoutMessage,token);
if (CommonUtils.isNotBlank(token)) {
HttpSession session = null;
try {
Field msField = handler.getSessionMappingStorage().getClass().getDeclaredField("MANAGED_SESSIONS");
msField.setAccessible(true);
Map<String,HttpSession> MANAGED_SESSIONS = (Map)msField.get(handler.getSessionMappingStorage());
logger.debug("MANAGED_SESSIONS"+MANAGED_SESSIONS.toString());
session = MANAGED_SESSIONS.get(token);
} catch (Exception e) {
logger.error("什么东西出错了",e);
}
if (session != null) {
logger.debug("session=="+session);
Subject subject = getSubject(request, response);
logger.debug("subject=="+subject);
//SimplePrincipalCollection shiroUser = (SimplePrincipalCollection) (((SimplePrincipalCollection)(session.getAttribute("org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY"))).getPrimaryPrincipal());
//logger.error("shiroUser=="+shiroUser);
//SimplePrincipalCollection pc = new SimplePrincipalCollection(shiroUser, "国金平");
//logger.error("SimplePrincipalCollection=="+pc);
SimplePrincipalCollection pc = new SimplePrincipalCollection();
try {
logger.debug("执行到SimplePrincipalCollection="+pc.toString());
Field principalsField = subject.getClass().getSuperclass().getDeclaredField("principals");
principalsField.setAccessible(true);
principalsField.set(subject, pc);
} catch (Exception e) {
logger.error("这里发生了什么",e);
}
try {
logger.debug("执行了subject.logout()");
subject.logout();
} catch (SessionException ise) {
logger.error("ise这里发生了什么ise",ise);
}
}
}
// logout之后,还要销毁session SingleSignOutFilter做的事情
handler.destroySession(req);
return;
} else {
logger.trace("Ignoring URI " + req.getRequestURI());
}
chain.doFilter(request, response);
}
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, ServletResponse response) {
Subject subject = this.getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
try {
WebUtils.issueRedirect(request, response, this.failureUrl);
} catch (IOException var7) {
logger.error("Cannot redirect to failure url : {}", this.failureUrl, var7);
}
} else {
try {
this.issueSuccessRedirect(request, response);
} catch (Exception var8) {
logger.error("Cannot redirect to the default success url", var8);
}
}
return false;
}
public void setFailureUrl(String failureUrl) {
this.failureUrl = failureUrl;
}
}

重写SingleSignOutHttpSessionListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.jason-li.coupon.web.auth.shiro;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* @author lilixin
* @description
* @create 2017-09-07 10:44 AM
**/
public class MySingleSignOutHttpSessionListener implements HttpSessionListener {
private static Logger logger = LoggerFactory.getLogger(MySingleSignOutHttpSessionListener.class);
private SessionMappingStorage sessionMappingStorage;
public MySingleSignOutHttpSessionListener() {
}
public void sessionCreated(HttpSessionEvent event) {
logger.error("执行到了MySingleSignOutHttpSessionListener的sessionCreated方法");
}
public void sessionDestroyed(HttpSessionEvent event) {
logger.error("执行到了MySingleSignOutHttpSessionListener的sessionDestroyed方法");
if(this.sessionMappingStorage == null) {
this.sessionMappingStorage = getSessionMappingStorage();
}
HttpSession session = event.getSession();
this.sessionMappingStorage.removeBySessionById(session.getId());
}
protected static SessionMappingStorage getSessionMappingStorage() {
//主要是重写了这里,用我们自己写的
return
MyCasFilter.getHandler().getSessionMappingStorage();
}
}

web.xml更改

  • 删除掉web.xml中单点登录过滤器配置
  • listener改成我们自己的

shiro.xml更改

  • 把casFilter 配置成我们自己写的MyCasFilter

需要每个客户端都进行这样的改造才能保证多系统单点退出时是真正的退出,单点退出改造部份内容参考 http://edolphin.site/2016/07/01/cas-single-sign-out/

关注我的微信,共同分享与讨论!