在pac4j探索的上一篇文章大致讲述了一下buji-pac4j+CAS的认证流程。这里记录一下本人实现的最简单的单点登录,仅作为笔记、学习交流之用,戳这里获取本文源码。
一、项目框架
1、 buji-pac4j(v.3.0.0)
2、shiro (v.1.4.0)
3、springmvc (v.4.3.2)
4、CAS (v.4.2.6)
5、pac4j-cas(v.2.2.1)
在maven项目的pom.xml里配置以上相关依赖,具体依赖配置可以查看我的项目,这里不再赘述。
二、目录结构
客户端项目(pac4jtest1)目录结构如下:
1、java文件目录中,Redirect2CasLoginFilter是测试用的,可以不管,MyCasClient类是继承自CasClient的自定义客户端,ShiroCasLogoutHandler类是单点登出时对shiro的一些操作,Controller类是请求控制器,util包里的是单点登出相关的类;
2、配置文件目录中,log4j.properties是日志管理文件,url.properties配置了项目中用到的各种url,spring-comm.xml配置了shiro集成pac4j的配置,spring-mvc.xml是springmvc的相关配置;
3、另外还有个index.jsp,就是受保护的页面,请求访问前需要先认证。
二、springMvc配置
这里springmvc作最简单的配置:
<context:component-scan base-package="com.pac4j.rest"/>
<mvc:annotation-driven />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
bean>
<mvc:resources mapping="/images/**" location="/WEB-INF/images/"/>
<mvc:resources mapping="/js/**" location="/WEB-INF/js/" />
<mvc:resources mapping="/css/**" location="/WEB-INF/css/"/>
三、pac4j配置
spring-comm.xml是shiro整合pac4j的配置,具体配置如下:
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/classes/url.propertiesvalue>
list>
property>
bean>
<bean id="casConfig" class="org.pac4j.cas.config.CasConfiguration">
<property name="loginUrl" value="${sso.cas.server.loginUrl}">property>
<property name="prefixUrl" value="${sso.cas.server.prefixUrl}">property>
<property name="logoutHandler" ref="casLogoutHandler">property>
bean>
<bean id="casClient" class="com.pac4j.client.MyCasClient">
<constructor-arg ref="casConfig" />
<property name="includeClientNameInCallbackUrl" value="false">property>
<property name="callbackUrl" value="${sso.cas.client.callbackUrl}">property>
bean>
<bean id="casLogoutHandler" class="com.pac4j.handler.ShiroCasLogoutHandler">
<property name="destroySession" value="true">property>
bean>
<bean id="sessionStore" class="com.pac4j.util.MyShiroSessionStore">bean>
<bean id="authcConfig" class="org.pac4j.core.config.Config">
<constructor-arg ref="casClient">constructor-arg>
<property name="sessionStore" ref="sessionStore">property>
bean>
<bean id="sessionIdGenerator"
class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="false" />
<property name="maxAge" value="180000" />
<property name="path" value="/" />
bean>
<bean id="sessionDAO"
class="org.apache.shiro.session.mgt.eis.MemorySessionDAO">
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
bean>
<bean id="sessionValidationScheduler"
class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<property name="sessionValidationInterval" value="1800000" />
<property name="sessionManager" ref="sessionManager" />
bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000" />
<property name="deleteInvalidSessions" value="true" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionDAO" ref="sessionDAO" />
<property name="sessionIdCookieEnabled" value="true" />
<property name="sessionIdCookie" ref="sessionIdCookie" />
bean>
<bean id="pac4jSubjectFactory" class="io.buji.pac4j.subject.Pac4jSubjectFactory">bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm">
<bean class="io.buji.pac4j.realm.Pac4jRealm">
<property name="cachingEnabled" value="false" />
<property name="authenticationCachingEnabled" value="false" />
<property name="authenticationCacheName" value="authenticationCache" />
<property name="authorizationCachingEnabled" value="false" />
<property name="authorizationCacheName" value="authorizationCache" />
bean>
property>
<property name="subjectFactory" ref="pac4jSubjectFactory">property>
bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager">property>
<property name="filters">
<util:map>
<entry key="casSecurityFilter">
<bean class="io.buji.pac4j.filter.SecurityFilter">
<property name="config" ref="authcConfig">property>
<property name="clients" value="MyCasClient">property>
bean>
entry>
<entry key="callback">
<bean class="io.buji.pac4j.filter.CallbackFilter">
<property name="config" ref="authcConfig">property>
<property name="defaultUrl" value="${sso.cas.client.successUrl}">property>
bean>
entry>
<entry key="logout">
<bean id="logout" class="io.buji.pac4j.filter.LogoutFilter">
<property name="defaultUrl" value="${sso.cas.client.callbackUrl}">property>
<property name="config" ref="authcConfig">property>
<property name="centralLogout" value="true">property>
<property name="localLogout" value="false">property>
bean>
entry>
<entry key="login">
<bean class="com.pac4j.filter.Redirect2CasLoginFilter">bean>
entry>
util:map>
property>
<property name="filterChainDefinitions">
<value>
/index = casSecurityFilter
/logout = logout
/callback = callback
/login** = login
/login/** = login
value>
property>
bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean id="annotationProxy"
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
bean>
关于shiro的相关知识,在这里不再赘述,因为不属于本文的讨论范围。需要留意的是,以上配置在原来一般的shiro配置基础上,除了增加pac4j的配置,还修改了一下原来shiro的配置,具体变更如下:
四、url.properties
在spring-comm.xml会用到的一些路径:
##cas服务前缀
sso.cas.server.prefixUrl=http://localhost:8080/hgretail.authc/
##cas服务登录url
sso.cas.server.loginUrl=http://localhost:8080/hgretail.authc/login
##cas客户端回调地址
sso.cas.client.callbackUrl=http://localhost:8080/pac4jtest1/callback?client_name=MyCasClient
##cas服务端成功跳转地址
sso.cas.client.successUrl=http://localhost:8080/pac4jtest1/index
五、MyCasClient
这个类是继承自CasClient的,因为其超类IndirectClient中的getRedirectAction方法用起来有问题,报401异常,所以写了这个类,覆盖getRedirectAction方法,屏蔽掉异常代码。以后思考一下是否有更好的解决办法。
public class MyCasClient extends CasClient {
public MyCasClient(final CasConfiguration configuration) {
super(configuration);
}
/*
* (non-Javadoc)
* @see org.pac4j.core.client.IndirectClient#getRedirectAction(org.pac4j.core.context.WebContext)
*/
@Override
public RedirectAction getRedirectAction(WebContext context) throws HttpAction {
init(context);
// it's an AJAX request -> unauthorized (with redirection url in header)
if (getAjaxRequestResolver().isAjax(context)) {
logger.info("AJAX request detected -> returning 401");
RedirectAction action = getRedirectActionBuilder().redirect(context);
cleanRequestedUrl(context);
throw HttpAction.unauthorized("AJAX request -> 401", context, null, action.getLocation());
}
// authentication has already been tried -> unauthorized
//FIXME 以下这段代码在org.pac4j.cas.client.CasClient中会出现401错误,所以在这里屏蔽掉。以后寻求更好的解决办法。
// final String attemptedAuth = (String) context.getSessionAttribute(getName() + ATTEMPTED_AUTHENTICATION_SUFFIX);
// if (CommonHelper.isNotBlank(attemptedAuth)) {
// cleanAttemptedAuthentication(context);
// cleanRequestedUrl(context);
// throw HttpAction.unauthorized("authentication already tried -> forbidden", context, null, null);
// }
return getRedirectActionBuilder().redirect(context);
}
private void cleanRequestedUrl(final WebContext context) {
context.setSessionAttribute(Pac4jConstants.REQUESTED_URL, "");
}
}
六、ShiroCasLogoutHandler
ShiroCasLogoutHandler继承于DefaultCasLogoutHandler。用于单点登出的时候shiro的一系列登出操作:
public class ShiroCasLogoutHandler<C extends WebContext> extends DefaultCasLogoutHandler<C> {
public ShiroCasLogoutHandler() {
}
public ShiroCasLogoutHandler(final Store store) {
super(store);
}
protected void destroy(final C context, final SessionStore sessionStore, final String channel) {
// remove profiles
final ShiroProfileManager manager = new ShiroProfileManager(context);
manager.logout();//shiro登出操作
logger.debug("destroy the user profiles");
// and optionally the web session
if (isDestroySession()) {
logger.debug("destroy the whole session");
final boolean invalidated = sessionStore.destroySession(context);
if (!invalidated) {
logger.error("The session has not been invalidated for {} channel logout", channel);
}
}
}
}
七、MyShiroSessionStore类和MyShiroProvidedSessionStore类
1、SessionStore是用于暂时缓存session以供后续的操作,MyShiroSessionStore类重写了ShiroSessionStore,因为pac4j本身的ShiroSessionStore不能满足单点登出,会有问题,所以改写了该类:
public class MyShiroSessionStore implements SessionStore<J2EContext> {
private final static Logger logger = LoggerFactory.getLogger(MyShiroSessionStore.class);
//获取shiro session
protected Session getSession(final boolean createSession) {
return SecurityUtils.getSubject().getSession(createSession);
}
//获取shiro的sessionid
@Override
public String getOrCreateSessionId(final J2EContext context) {
final Session session = getSession(false);
if (session != null) {
return session.getId().toString();
}
return null;
}
/**
* 获取shiro session中的属性
*/
@Override
public Object get(final J2EContext context, final String key) {
final Session session = getSession(false);
if (session != null) {
return session.getAttribute(key);
}
return null;
}
/**
* 设置session属性
*/
@Override
public void set(final J2EContext context, final String key, final Object value) {
final Session session = getSession(true);
if (session != null) {
try {
session.setAttribute(key, value);
} catch (final UnavailableSecurityManagerException e) {
logger.warn("Should happen just once at startup in some specific case of Shiro Spring configuration", e);
}
}
}
/**
* 销毁session
*/
@Override
public boolean destroySession(final J2EContext context) {
getSession(true).stop();
return true;
}
/**
* 获取shiro session并缓存用于单点登出
*/
@Override
public Object getTrackableSession(final J2EContext context) {
return getSession(true);
}
/**
* 从getTrackableSession中获取的session来构建SessionStore
*/
@Override
public SessionStore buildFromTrackableSession(final J2EContext context, final Object trackableSession) {
if(trackableSession != null) {
return new MyShiroProvidedSessionStore((Session) trackableSession);
}
return null;
}
/**
* 刷新session属性,这里暂返回false,实际应用中需实现
*/
@Override
public boolean renewSession(final J2EContext context) {
return false;
}
}
2、MyShiroProvidedSessionStore是由MyShiroSessionStore构建来临时存储TrackableSession的:
public class MyShiroProvidedSessionStore extends MyShiroSessionStore{
/**存储的TrackableSession,往后要操作时用这个session操作*/
private Session session;
public MyShiroProvidedSessionStore(Session session) {
this.session = session;
}
protected Session getSession(final boolean createSession) {
return session;
}
}
八、Controller
这是个控制器,负责请求处理
public class Controller {
@RequestMapping(value="/index",method=RequestMethod.GET)
public String index(ModelMap map) {
//获取用户身份
Pac4jPrincipal p = SecurityUtils.getSubject().getPrincipals().oneByType(Pac4jPrincipal.class);
if(p != null) {
CommonProfile profile = p.getProfile();
map.put("profile", profile);
}
return "index";
}
}
九、index.jsp
这里是个简单的jsp页面,上面将会打印出用户的信息
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Hello pac4j1!title>
head>
<body>
hello~~~ <b>I'm pac4jtest1b>
profile:${profile}
body>
html>
十、web.xml
主要配置了springmvc和shiroFilter
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath*:spring-comm.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListenerlistener-class>
listener>
<filter>
<filter-name>shiroFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
<init-param>
<param-name>targetFilterLifecycleparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>shiroFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
<servlet>
<servlet-name>springDispatcherservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring-mvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>springDispatcherservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
十一、运行
CAS服务的项目在这里就不贴出来了,因为改变不大,就是注意要在CAS服务里注册Cas Client,也就是上面的项目,否则CAS服务会报错。
为了认证单点登录,要再弄一个Cas Client,所以把上面的项目pac4jtest1复制一份命名为pac4jtest2,记得把里面对应的配置要改过来。
部署CAS Server、pac4jtest1、pac4jtest2三个项目并启动tomcat。根据以下步骤进行测试:
2、会看到去到了登录页,地址栏也发生了改变:
3、输入用户名密码登录,去到了pac4jtest1的index:
4、此时在地址栏输入 http://localhost:8080/pac4jtest2/index,按道理如果是还没认证,会跳转到第一步中的登录页面;但是我们已经在第三步已经登录认证通过了,所以无须再进行登录,直接进入pac4jtest2的index
5、单点登出,地址栏输入 http://localhost:8080/pac4jtest2/logout,会退出到登录界面:
6、此时再输入 http://localhost:8080/pac4jtest1/index 访问pac4jtest1,则发现还是重定向回第二步的登录页。
至此,一个简单的整合pac4j+CAS+shiro+springmvc的单点登录完成
十二、后记
这是pac4j最迷你版的单点登录,实际应用中比这个复杂很多,其实主要的部分弄清楚了,扩展也就很容易了。本人才疏学浅,项目中存在一些不足,欢迎大家批评指出!
源代码位置:pac4jtest1