pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc实现单点登录

在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)目录结构如下:
pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc实现单点登录_第1张图片
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的配置,具体变更如下:

  • 原来的CasFilter被替换成CallbackFilter
  • CasRealm被替换成Pac4jRealm
  • 当一个url需要被保护时(需要经过认证、鉴权),不仅需要使用shiro默认的过滤器,而且必须使用SecurityFilter。

四、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。根据以下步骤进行测试:

1、在地址栏输入:
这里写图片描述

2、会看到去到了登录页,地址栏也发生了改变:

pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc实现单点登录_第2张图片

3、输入用户名密码登录,去到了pac4jtest1的index:

这里写图片描述

4、此时在地址栏输入 http://localhost:8080/pac4jtest2/index,按道理如果是还没认证,会跳转到第一步中的登录页面;但是我们已经在第三步已经登录认证通过了,所以无须再进行登录,直接进入pac4jtest2的index
这里写图片描述

5、单点登出,地址栏输入 http://localhost:8080/pac4jtest2/logout,会退出到登录界面:

pac4j探索(二): buji-pac4j+Cas+Shiro+SpringMvc实现单点登录_第3张图片

6、此时再输入 http://localhost:8080/pac4jtest1/index 访问pac4jtest1,则发现还是重定向回第二步的登录页。

至此,一个简单的整合pac4j+CAS+shiro+springmvc的单点登录完成

十二、后记
这是pac4j最迷你版的单点登录,实际应用中比这个复杂很多,其实主要的部分弄清楚了,扩展也就很容易了。本人才疏学浅,项目中存在一些不足,欢迎大家批评指出!
源代码位置:pac4jtest1

你可能感兴趣的:(shiro,pac4j,单点登录)