SpringBoot集成shiro+cas单点登录

1、引入需要的依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

2、创建cas_shiro.xml配置

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd  http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd  ">

    <!-- CasFilter为自定义的单点登录过滤拦截Fileter   -->
    <bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
        <!-- ${cas.server.url}是直接从yml中取的值 -->
        <property name="failureUrl" value="${cas.server.url}/login?service=${cas.client.url}/shiro-cas"/>
    </bean>

    <!-- 	(非单点登录)自定义登录认证relm -->
    <!-- 	<bean id="loginAuthCasRealm" class="com.cetc.data.frame.authority.realm.LoginAuthRealm"></bean> -->
    <!--     单点登录下的权限认证 -->
    <bean id="loginAuthCasRealm" class="cn.toroot.bj.config.shiro.LoginAuthCasRealm">
        <!--     cas服务端地址前缀 -->
        <property name="casServerUrlPrefix" value="${cas.server.url}"></property>
        <!--     	应用服务地址,用来接收cas服务端票据,客户端的cas入口   客户端的回调地址设置,必须和上面的shiro-cas过滤器casFilter拦截的地址一致 -->
        <property name="casService" value="${cas.client.url}/shiro-cas"></property>
    </bean>
    <!--     登出过滤器 使用了shiro session无法实现多系统单点退出-->
    <bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter">
        <!--  	配置验证错误时的失败页面 -->
        <property name="redirectUrl" value="${cas.server.url}/logout?service=${cas.client.url}/shiro-cas"></property>
    </bean>

    <!--  	cas服务器登出过滤器  拦截从cas服务器过来的各个应用客户端的logout请求,自行销毁自己的shiro session-->
    <bean id="casLogoutFilter" class="cn.toroot.bj.config.shiro.CasLogoutFilter">
        <property name="sessionManager" ref="sessionManager"></property>
    </bean>

    <!--  	Shiro Filter拦截器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--  	securityManager -->
        <property name="securityManager" ref="securityManager"></property>
        <!--  		登录路径 -->
        <property name="loginUrl" value="${cas.server.url}/login?service=${cas.client.url}/shiro-cas"></property>
        <!--  		成功地址 -->
        <property name="successUrl" value="portal/page/main.jsp"></property>
        <!--  		用户访问无权限时跳转链接 -->
        <property name="unauthorizedUrl" value="/portal/page/noAuthority"></property>
        <!--  		过滤连定义 -->
        <property name="filterChainDefinitions">
            <value>
                /shiro-cas=casLogout,casFilter
                /loginAuth/user/loginout=logout
                /query/**=anon
                /authority/organize/**=anon
                /authority/resource/menuscross=anon
                /api/subOrg/*=anon
                /api/superiorService/*=anon
                /api/external/*=anon
                /swagger-ui/*=anon
                /**=anon <!-- 开发时放开 -->
                <!--/**=casLogout,authc  --> <!-- 开发时注释 -->
                <!-- 	 			/loginAuth/user/login=anon -->
                <!-- 	 			/portal/page/login.jsp=anon -->
                <!-- 	 			/portal/script/**=anon -->
                <!-- 	 			/portal/style/**=anon -->
                <!-- 	 			/portal/page/main.jsp=authc -->
                <!--,userPagePerm-->
            </value>
        </property>
        <property name="filters">
            <map>
                <!-- 				添加登出过滤 -->
                <entry key="logout" value-ref="logoutFilter"></entry>
                <!-- 				添加casFilter到shiroFilter整合 -->
                <entry key="casFilter" value-ref="casFilter"></entry>
                <entry key="casLogout" value-ref="casLogoutFilter"></entry>
            </map>
        </property>
    </bean>
    <bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory"></bean>

    <!-- 	securityManager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="loginAuthCasRealm"/>
        <property name="sessionManager" ref="sessionManager"></property>
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="subjectFactory" ref="casSubjectFactory"></property>
    </bean>
    <!-- 	shiro的默认缓存(本地缓存) -->
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean>


    <!--保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>

    <!-- 	Shiro会话管理器 -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 	Shiro会话30分钟失效 -->
        <property name="globalSessionTimeout" value="1800000"></property>
        <property name="deleteInvalidSessions" value="true"></property>
        <property name="sessionValidationSchedulerEnabled" value="true"></property>
    </bean>

    <!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->
    <!-- 	当使用注解时如果没有该权限或角色的处理 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor">
        <property name="proxyTargetClass" value="true"></property>
    </bean>

    <!-- 用于开启 Shiro Spring AOP 权限注解的支持 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager" />
    </bean>

</beans>

3、创建权限认证类继承CasRealm

package cn.toroot.bj.config.shiro;


import cn.toroot.bj.config.shiro.service.BaseService;
import cn.toroot.bj.config.shiro.entity.Resource;
import cn.toroot.bj.config.shiro.entity.Role;
import org.springframework.util.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;

/**
 * Mr peng
 * 2020-6-10 11:55:34
 */
@SuppressWarnings("deprecation")
public class LoginAuthCasRealm extends CasRealm {

    @Autowired
    private BaseService baseService;

    /**
     * 获取权限
     *
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
        //得到用户名
        String username=(String) principals.fromRealm(getName()).iterator().next();
        //添加权限和角色
        if(!StringUtils.isEmpty(username)){
            SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
            //添加角色
            List<Role> roles = baseService.getRoleByUserName(username);
            if(roles!=null&&roles.size()>0){
                for (Role role : roles) {
                    info.addRole(role.getRoleName());
                }
            }
            //添加权限
            List<Resource> resources = baseService.getResourceByUserName(username);
            if(resources!=null&&resources.size()>0){
                for (Resource resource : resources) {
                    info.addStringPermission(resource.getFullName());
                }
            }
            System.out.println("当前用户拥有角色和权限分别为:");
            System.out.println(info.getRoles());
            System.out.println(info.getStringPermissions());
            return info;
        }
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo authc = super.doGetAuthenticationInfo(token);

        String name = (String) authc.getPrincipals().getPrimaryPrincipal();
        Map<String, Object> userInfo = baseService.getUserInfo(name);
        SecurityUtils.getSubject().getSession().setAttribute("userInfo", userInfo);
        System.out.println("登录SESSIONID:"+SecurityUtils.getSubject().getSession().getId());
        return authc;
    }

}

4、直接上其他相关配置,这里就不一一介绍了

package cn.toroot.bj.config.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.servlet.AdviceFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 * Mr peng
 * 控制登出
 * 2020-6-10 11:55:34
 */
public class CasLogoutFilter extends AdviceFilter{

    private static final Logger log = LoggerFactory.getLogger(CasLogoutFilter.class);

    private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();

    private SessionManager sessionManager;

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /**
     * 如果请求中包含了ticket参数,记录ticket和sessionID的映射
     * 如果请求中包含logoutRequest参数,标记session为无效(不能在此时进行清理,因为subject和线程绑定在一起,此时不能获取正确的subject)
     * 如果session不为空,且被标记为无效,则登出
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response)
            throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        if(HANDLER.isTokenRequest(req)){
            //通过浏览器发送的请求,连接中含有token参数,记录token和sessionID
            HANDLER.recordSession(req);
        }else if (HANDLER.isLogoutRequest(req)) {
            //CAS服务器发送的请求,链接中含有logoutRequest参数,在之前记录的session中设置logoutRequest参数为true
            //因为Subject和线程是绑定的,所以无法获取登录的Subject直接logout
            HANDLER.invalidateSession(req, sessionManager);
            return false;
        }else {
            log.trace("Ignoring URI "+req.getRequestURI());
        }
        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession(false);
        if(session!=null&&session.getAttribute(HANDLER.getLogoutParameterName())!=null){
            try {
                subject.logout();
            } catch (Exception e) {
                log.debug("Encountered session exception during logout. This can generally safely be ignored.",e);
            }
        }
        return true;
    }
}

package cn.toroot.bj.config.shiro;

import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;

/**
 * Mr peng
 * shiro配置类
 * 2020-6-10 11:55:34
 */
@Configuration
public class FilterConfig2 {

    public FilterConfig2() {
    }

    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new DelegatingFilterProxy("shiroFilter"));
        filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns(new String[]{"/*"});
        return filterRegistrationBean;
    }

    @Bean
    public ServletListenerRegistrationBean singleSignOutHttpSessionListener() {
        ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
        bean.setListener(new SingleSignOutHttpSessionListener());
        bean.setEnabled(true);
        return bean;
    }

    @Bean
    public FilterRegistrationBean singleSignOutFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setName("singleSignOutFilter");
        filterRegistrationBean.setFilter(new SingleSignOutFilter());
        filterRegistrationBean.addUrlPatterns(new String[]{"/*"});
        filterRegistrationBean.setEnabled(true);
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean registLogoutFilter(LogoutFilter logoutFilter) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(logoutFilter, new ServletRegistrationBean[0]);
        filterRegistrationBean.setEnabled(false);
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean registCasFilter(CasFilter casFilter) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(casFilter, new ServletRegistrationBean[0]);
        filterRegistrationBean.setEnabled(false);
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean registCasLogoutFilter(CasLogoutFilter casLogoutFilter) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(casLogoutFilter, new ServletRegistrationBean[0]);
        filterRegistrationBean.setEnabled(false);
        return filterRegistrationBean;
    }
}

package cn.toroot.bj.config.shiro;

import org.apache.shiro.session.Session;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * 用于存储ticket到sessionID的引射
 * @author Mr peng
 * 2020-6-10 11:55:57
 */
public final class HashMapBackedSessionMappingStorage {


    private final Map<String,Serializable> MANAGED_SESSIONS_ID = new HashMap<>();

    public synchronized void addSessionById(String mappingId,Session session){
        MANAGED_SESSIONS_ID.put(mappingId, session.getId());
    }

    public synchronized Serializable getSessionIDByMappingId(String mappingId){
        return MANAGED_SESSIONS_ID.get(mappingId);
    }
}

package cn.toroot.bj.config.shiro;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;

/**
 * Mr peng
 * 2020-6-10 11:55:34
 */
public class SingleSignOutHandler {

    private final Log log = LogFactory.getLog(getClass());

    /**
     * 用于捕获 session identifier
     */
    private String artifactParameterName = "ticket";

    private String logoutParameterName = "logoutRequest";

    private HashMapBackedSessionMappingStorage storage = new HashMapBackedSessionMappingStorage();

    protected SingleSignOutHandler(){
        init();
    }

    public void setArtifactParameterName(final String name) {
        this.artifactParameterName = name;
    }

    public String getLogoutParameterName() {
        return logoutParameterName;
    }

    public void setLogoutParameterName(final String name) {
        this.logoutParameterName = name;
    }

    public void init(){
        CommonUtils.assertNotNull(this.artifactParameterName,"artifactParameterName cannot be null.");
        CommonUtils.assertNotNull(this.logoutParameterName,"logoutParameterName cannot be null.");
    }

    public boolean isTokenRequest(final HttpServletRequest request){
        return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName));
    }

    /**
     * 是否request是否含有一个 token
     * @param request
     * @return
     */
    public boolean isLogoutRequest(final HttpServletRequest request){
        return "POST".equals(request.getMethod()) && !isMultipartRequest(request) &&
                CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName));
    }

    /**
     * 记录请求中的token和sessionId的映射对
     * @param request
     */
    public void recordSession(final HttpServletRequest request){
        Session session = SecurityUtils.getSubject().getSession();

        final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName);
        if(log.isDebugEnabled()){
            log.debug("Recording session for token" + token);
        }

        storage.addSessionById(token, session);
    }

    /**
     * 从logoutRequest 参数中解析出token,根据token获取到sessionID,在根据sessionID获取到session,设置logoutRequest参数为true
     * 从而标记此session已经失效
     * @param request
     * @param sessionManager
     */
    public void invalidateSession(final HttpServletRequest request,final SessionManager sessionManager){
        final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName);
        if(log.isTraceEnabled()){
            log.trace("Logout request:\n" + logoutMessage);
        }

        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        if(CommonUtils.isNotBlank(token)){
            Serializable sessionId = storage.getSessionIDByMappingId(token);
            if(sessionId != null){
                try {
                    Session session = sessionManager.getSession(new DefaultSessionKey(sessionId));
                    if(session != null){
                        session.setAttribute(logoutParameterName, true);
                        if(log.isDebugEnabled()){
                            log.debug("Invalidating session["+sessionId+"] for token ["+token +"]");
                        }
                    }
                } catch (Exception e) {
                }
            }
        }
    }

    private boolean isMultipartRequest(final HttpServletRequest request){
        return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");
    }
}

5、上yml配置内容,最后springBoot启动类读取cas_shiro.xml即可

cas:
  server:
    url: http://192.168.91.103:8083/sso-cas
  client:
    url: http://28.9.150.68:8081/das