shiro添加CAS单点登录
目的
已有一个通过shiro控制的系统,现在希望系统接入CAS单点登录。
- 单点登录使用CAS认证协议,CAS服务端已经存在,该系统作为CAS客户端接入
- 仍然保留原系统登陆页面,只有当通过一个特定单点登录链接访问时,才走单点登录流程
- 这里有点像很多网站都提供的
通过XXX登录
的按钮。所不同的是,我这里不在页面展现,只是一个URL
- 这里有点像很多网站都提供的
- 如果单点登录的用户在系统中不存在,则直接跳转到系统默认的登陆页面。
修改分析
其实shiro有默认的CAS接入包,提供了CAS接入的默认实现,只想用默认的实现,参考之篇文章就够了(地址在这),不过这里我的需求和默认实现有些不同。
主要体现在
- 登录链接,默认的CasFilter只处理CAS ticket回调。我这里即处理ticket回调,同时也判断如果请求不带ticket,则跳转到单点登录页。
- 登录失败梳理,默认的CasFilter会判断该用户之前是否已经登录,我这里只要失败,一律跳转到默认登录页
- 默认CasRealm是通过一些属性生成最终的AuthenticationToken,由于我这里的系统之前对token格式进行了改造,所以我需要自己扩展CasRealm填充Authentication和Authorization信息。
共三个地方进行修改:
- 新增一个CAS的Filter扩展自自带的
CasFilter
,拦截处理单点登录地址的请求 - 新增一个CAS的Realm扩展自自带的
CasRealm
,用来解析返回的ticket,生成自定义的Authentication和Authorization信息 - 修改配置文件,添加新增的Filter和Realm。
修改代码
Filter
public class CustomCasFilter extends CasFilter {
private Logger logger = LoggerFactory
.getLogger(CustomCasFilter.class);
private String casServerLoginUrl;
// the url where the application is redirected if the CAS service ticket validation failed (example : /mycontextpatch/cas_error.jsp)
private String failureUrl;
/**
* Custom session key used for other purpose
*/
public static String CUSTOM_SESSION_KEY = "custom_session_key";
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = httpRequest.getParameter("ticket");
logger.info("CAS login with ticket: {}.", ticket);
if (StringUtils.isEmpty(ticket)) {
logger.info("SSO ticket not provided, redirect to CAS server.");
//redirect to SSO server if ticket not provided
WebUtils.issueRedirect(request, response, casServerLoginUrl);
return false;
}
return super.onPreHandle(request, response, mappedValue);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, ServletResponse response) {
//Go to login page if CAS not verified
Subject subject = getSubject(request, response);
if (subject.isAuthenticated()) {
subject.logout();
}
try {
WebUtils.issueRedirect(request, response, failureUrl);
} catch (IOException e) {
logger.error("Cannot redirect to failure url : {}", failureUrl, e);
}
return false;
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token,
Subject subject, ServletRequest req, ServletResponse resp)
throws Exception {
//use custom key to store user info in session
UserMessage user = UserUtils.getCurrentUserMessage();
Session session = SecurityUtils.getSubject().getSession();
String sessionValue = user.getId()+"_"+user.getUsername();
session.setAttribute(CUSTOM_SESSION_KEY,sessionValue);
logger.info("User [{}}] login success!", sessionValue);
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
WebUtils.issueRedirect(request, response, getSuccessUrl());
return false;
}
public String getCasServerLoginUrl() {
return casServerLoginUrl;
}
public void setCasServerLoginUrl(String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public void setFailureUrl(String failureUrl) {
this.failureUrl = failureUrl;
}
}
Realm
public class CustomCasRealm extends CasRealm {
private Logger log = LoggerFactory.getLogger(CustomCasRealm.class);
@Resource
protected IAccountService accountService;
@Override
protected void onInit() {
super.onInit();
//SSO doesn't need to check password
this.setCredentialsMatcher(new AllowAllCredentialsMatcher());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//UserMessage here is a customized principal object
UserMessage user = (UserMessage) principals.getPrimaryPrincipal();
Integer userId = user.getId();
String userType = user.getTypeOfUser();
SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo();
if (userId != null) {
//get and add authorization infos
//auth.addRole(xxxxxxxxxxxxxx);
//auth.addStringPermission(xxxxxxxx);
}
return auth;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//Call super method to retrieve CAS user
AuthenticationInfo info = super.doGetAuthenticationInfo(token);
log.info("CAS realm login");
if (null == info) {
return null;
}
PrincipalCollection principals = info.getPrincipals();
//CasRealm store username as principal
String username = principals.getPrimaryPrincipal().toString();
log.info("CAS realm login account:{}", username);
//add custom data
CasToken authToken = (CasToken) token;
SysAccount account = accountService.getByUsername(username, typeValue);
if (null == account) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = usersLogin(account);
return simpleAuthenticationInfo;
}
private SimpleAuthenticationInfo usersLogin(Object user) {
UserMessage commonUser = new UserMessage();
//get data from db and add to this `UserMessage` class;
return new SimpleAuthenticationInfo(commonUser, commonUser.getPassword(),
commonUser.getSalt(), getName());
}
}
shiro配置修改
省略掉了一些不重要的配置,主要的修改点做了注释,共有5处。
<?xml version="1.0" encoding="UTF-8"?>
<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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"
default-lazy-init="true">
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/admin/login" />
<property name="successUrl" value="/admin/sys/index" />
<property name="filters">
<util:map>
<entry key="authc" value-ref="authcFilter" />
<entry key="user" value-ref="userFilter" />
<entry key="logout" value-ref="logoutFilter" />
<!-- 修改点1:增加CAS的Filter注册到shiro -->
<entry key="cas" value-ref="casFilter" />
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/ = anon
<!-- 修改点2:增加CAS Filter对应的URL -->
/admin/cas = cas
/admin/login = authc
/statics/* = anon
/admin/logout = logout
/admin/**/* = authc,user
</value>
</property>
</bean>
<!-- Shiro Filter -->
<bean id="authcFilter" class="com.my.portal.web.sys.shiro.SysAuthenticationFilter">
</bean>
<bean id="userFilter" class="com.my.portal.web.sys.shiro.SysUserFilter"/>
<bean id="logoutFilter" class="com.my.portal.web.sys.shiro.SysLogoutFilter"/>
<!-- 修改点3:注册casFilter对应的实现 -->
<bean id="casFilter" class="com.my.portal.web.sys.shiro.cas.CustomCasFilter">
<property name="casServerLoginUrl" value="http://单点登录服务地址/cas/login?service=http://127.0.0.1:8080/portal-web/admin/cas"/>
<property name="failureUrl" value="/admin/login.jsp"/>
</bean>
<!-- 修改点4:注册casRealm对应实现 -->
<bean id="casRealm" class="com.my.portal.web.sys.shiro.cas.CustomCasRealm">
<property name="casServerUrlPrefix" value="http://单点登录服务地址/cas"/>
<property name="casService" value="http://127.0.0.1:8080/portal-web/admin/cas"/>
</bean>
<!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroDbRealm" />
<!-- 修改点5:增加casRealm注册到SecurityManager中,SecurityManager支持单个Realm或者多个Realm,多个需要用realms -->
<property name="realms">
<list>
<ref bean="shiroDbRealm" />
<ref bean="casRealm" />
</list>
</property>
<property name="sessionManager" ref="sessionManager"></property>
<property name="cacheManager" ref="shiroEhcacheManager" />
</bean>
</beans>