创建项目
下载cas4.1.10的源代码,里面有非常多的module,我们使用cas-server-webapp来作为模块进行二次开发。
自己创建一个项目,将cas-server-webapp拷贝相关文件过来,注意整理pom的依赖。
数据源的修改
我们这里数据源使用的是jndi的方式,所以修改 deployerConfigContext.xml 增加如下配置
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value>java:comp/env/jdbc/OracleXXXX</value></property>
</bean>
同时,在webapp/META-INF/context.xml中配置对应的数据源信息
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource name="jdbc/OracleXXXX"
auth="Container"
type="javax.sql.DataSource"
maxActive="10"
maxIdle="5"
maxWait="1000"
logAbandoned="true"
username="XXXX"
password="XXXX"
driverClassName="oracle.jdbc.driver.OracleDriver"
url="jdbc:oracle:thin:@192.168.0.1:1521:xxxx"/>
</Context>
考虑到数据的操作,我封装了一个DAO方法,顺便定义一下
<bean id="casDao"
class="com.xxx.xxx.sso.CasDao"
p:dataSource-ref="dataSource" />
接入系统的配置
哪些系统能够使用cas,默认的定义在
<bean id="serviceRegistryDao" class="org.jasig.cas.services.JsonServiceRegistryDao"
c:configDirectory="${service.registry.config.location:classpath:services}" />
cas支持4中接入方式的定义:
1、InMemoryServiceRegistryDaoImpl
2、JsonServiceRegistryDao
3、JpaServiceRegistryDaoImpl:
4、MongoServiceRegistryDao
考虑到实际的业务场景,这部分的数据,肯定是需要配置到数据库中的,而且能够自动刷新,支持新增新的service时,能够做到不停机。
所以我们需要自己去实现,定义如下
<bean id="serviceRegistryDao" class="com.xxxx.xxxx.sso.DbServiceRegistryDaoImpl"
p:casDao-ref="casDao" />
public class DbServiceRegistryDaoImpl implements ServiceRegistryDao {
@NotNull
private CasDao casDao;
public DbServiceRegistryDaoImpl() {
}
public void setCasDao(CasDao casDao) {
this.casDao = casDao;
}
@Override
public RegisteredService save(RegisteredService registeredService) {
return null;
}
@Override
public boolean delete(RegisteredService registeredService) {
return false;
}
@Override
public List<RegisteredService> load() {
return casDao.findRegisteredService();
}
@Override
public RegisteredService findServiceById(long l) {
return casDao.findRegisteredServiceByServiceId(l);
}
}
我们定义一个类去继承ServiceRegistryDao 主要去实现load和findServiceById的方法,暂时不实现delete和save。
主要是从数据库获取数据 返回对应的对象
public List<RegisteredService> findRegisteredService() {
List<RegisteredService> registeredServices = jdbcTemplate.query("select * from T_SYSTEM ",
new Object[] {}, new int[] {}, new RowMapper<RegisteredService>() {
@Override
public RegisteredService mapRow(ResultSet rs, int rowNum) throws SQLException {
RegexRegisteredService registeredServices = new RegexRegisteredService();
registeredServices.setServiceId("^"+rs.getString("redirect_uri")+".*");
registeredServices.setName(rs.getString("systemname"));
registeredServices.setDescription(rs.getString("systemdes"));
registeredServices.setId(rs.getLong("SYSTEMID"));
List<String> allowedAttributes = new ArrayList<String>();
allowedAttributes.add(rs.getString("systemcode"));
registeredServices
.setAttributeReleasePolicy(new ReturnAllowedAttributeReleasePolicy(allowedAttributes));
return registeredServices;
}
});
return registeredServices;
}
RegisteredService是一个接口,有两个实现类,我们使用 RegexRegisteredService ,该实现类支持RegisteredService的正则匹配。
allowedAttributes 很重要,后面我们将用它来过滤返回给接入服务的attribute。
修改登录方法和根据业务系统返回不同数据
登录的方法是定义在 deployerConfigContext.xml 的
<bean id="authenticationManager"
class="org.jasig.cas.authentication.PolicyBasedAuthenticationManager">
<constructor-arg>
<map>
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</map>
</constructor-arg>
<property name="authenticationPolicy">
<bean class="org.jasig.cas.authentication.AnyAuthenticationPolicy" />
</property>
</bean>
其中primaryAuthenticationHandler控制的是登录认证 ,primaryPrincipalResolver是返回的属性
primaryAuthenticationHandler 修改如下
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource" p:passwordEncoder-ref="MD5PasswordEncoder"
p:sql="select PWD from T_USER where USERACCOUNT = ?" />
<bean id="MD5PasswordEncoder"
class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg index="0">
<value>MD5</value>
</constructor-arg>
</bean>
因为我们认证相对比较简单,这里使用QueryDatabaseAuthenticationHandler去处理登录时候的认证
主要是primaryPrincipalResolver的处理,这里是处理的认证成功后,返回业务系统的用户的数据,默认只返回了userId,我们需要返回更多的数据,这里自己定义一个类来处理
<bean id="primaryPrincipalResolver"
class="com.xxx.xxx.sso.MyPrincipalResolver"
p:casDao-ref="casDao" />
public class MyPrincipalResolver implements PrincipalResolver {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@NotNull
private CasDao casDao;
@Override
public Principal resolve(Credential credential) {
final MyUsernamePasswordCredentials usernamePasswordCredentials = (MyUsernamePasswordCredentials) credential;
String userStr = "";
Map<String,Object> attr = new HashMap<String,Object>();
try {
String username = usernamePasswordCredentials.getUsername();
userStr = username;
User user = casDao.queryUser(username);
for(Role role : user.getRoles()){
Map<String,Object> systemAttr = new HashMap<String,Object>();
//TODO 设置要返回给接入服务的属性
attr.put(role.getSystemCode(), systemAttr);
}
return new SimplePrincipal(userStr, attr);
} catch (Exception e) {
logger.error("获取用户属性异常{}",e);
}
return null;
}
@Override
public boolean supports(Credential credential) {
return true;
}
public void setCasDao(CasDao casDao) {
this.casDao = casDao;
}
}
这里描述一下业务场景
我们经常遇到如下需求,N个系统接入CAS;
对于同一个用户,N个系统可能有不同的属性信息(比如针对每个系统用户的权限信息),每个系统之间,这类信息应该做到隔离。
所以我们在用户登录后,构造一个Map, 使用接入系统的代码去做key,想想前面我们在构造registeredServices是,设置的
setAttributeReleasePolicy(new ReturnAllowedAttributeReleasePolicy(allowedAttributes));
cas会根据这个去过滤相关的属性。
有人问 ,为什么要全取出来 ,而不是根据登录系统的情况,每次动态获取呢?
可以测试一下,登录完A系统后,如果用户直接点击B系统,CAS是不会进入这个方法的。
修改对应的protocol模板,返回属性,由于我们接入系统,使用的是 shiro cas那个包,支持的是2.0的协议,所以修改view/protocol/2.0/casServiceValidationSuccess.jsp
<%@ page session="false" contentType="application/xml; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${fn:escapeXml(principal.id)}</cas:user>
<!-- 这段 -->
<c:if test="${fn:length(assertion.primaryAuthentication.principal.attributes) > 0}">
<cas:attributes>
<c:forEach var="attr" items="${assertion.primaryAuthentication.principal.attributes}">
<c:forEach var="systemAttr" items="${attr.value}">
<cas:${fn:escapeXml(systemAttr.key)}>${fn:escapeXml(systemAttr.value)}</cas:${fn:escapeXml(systemAttr.key)}>
</c:forEach>
</c:forEach>
</cas:attributes>
</c:if>
<!-- 这段 end-->
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(chainedAuthentications) > 0}">
<cas:proxies>
<c:forEach var="proxy" items="${chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(chainedAuthentications)}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>