首页 新闻 会员 周边

springboot+cas+shiro+pac4j实现单点登录,但是登出有问题,服务端退出了,客户端还是登录状态!

0
[待解决问题]

之前没在shiroConfig里加public FilterRegistrationBean singleSignOutFilter() ;可以实现单点登录,登出有问题,家里这个方法后,启动报错:
java.lang.IllegalArgumentException: casServerUrlPrefix cannot be null.
at org.jasig.cas.client.util.CommonUtils.assertNotNull(CommonUtils.java:87)
at org.jasig.cas.client.session.SingleSignOutHandler.init(SingleSignOutHandler.java:130)
at org.jasig.cas.client.session.SingleSignOutFilter.init(SingleSignOutFilter.java:54)
.......................................

package com.audaque.gm.config;

import com.audaque.gm.modules.sys.shiro.ShiroPermsFilterFactoryBean;
import com.audaque.gm.modules.sys.shiro.UserFilter;
import com.audaque.gm.modules.sys.shiro.UserPermFilter;
import com.audaque.gm.modules.sys.sso.CallbackFilter;
import com.audaque.gm.modules.sys.sso.CasRealm;
import com.audaque.gm.support.shiro.listener.UserSessionListener;
import com.audaque.gm.support.shiro.session.UserSessionDAO;
import com.audaque.gm.support.shiro.session.UserSessionFactory;
import io.buji.pac4j.filter.LogoutFilter;
import io.buji.pac4j.filter.SecurityFilter;
import io.buji.pac4j.subject.Pac4jSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.pac4j.core.config.Config;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.DelegatingFilterProxy;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.*;
import java.util.Map.Entry;

/**

  • shiro配置
  • */
    @DependsOn("springContextUtils")
    @Configuration
    public class ShiroConfig {

    /** 项目工程路径 */
    @Value("${cas.project.url}")
    private String projectUrl;

    /** 项目cas服务路径 */
    @Value("${cas.server.url}")
    private String casServerUrl;

    /** 客户端名称 */
    @Value("${cas.client-name}")
    private String clientName;

    /**
    • 安全管理器
    • @param sessionManager
    • @return
      */
      @Bean
      public SecurityManager securityManager(SessionManager sessionManager) {
      DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      securityManager.setSessionManager(sessionManager);
      securityManager.setSubjectFactory(subjectFactory());
      securityManager.setRealm(this.userRealm());
      return securityManager;
      }
    /**
    • 用户realm
    • @return
      */
      @Bean
      public CasRealm userRealm(){
      CasRealm userRealm = new CasRealm();

      //------新加代码开始-------
      // 使用自定义的realm
      userRealm.setClientName(clientName);
      userRealm.setCachingEnabled(false);
      //暂时不使用缓存
      userRealm.setAuthenticationCachingEnabled(false);
      userRealm.setAuthorizationCachingEnabled(false);
      //----------新加代码结束----------

      return userRealm;
      }

    /**
    • 使用 pac4j 的 subjectFactory
    • @return
      */
      @Bean
      public Pac4jSubjectFactory subjectFactory(){
      return new Pac4jSubjectFactory();
      }

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
    // 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
    filterRegistration.addInitParameter("targetFilterLifecycle", "true");
    filterRegistration.setEnabled(true);
    filterRegistration.addUrlPatterns("/*");
    filterRegistration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
    return filterRegistration;
    }

    private void loadShiroFilterChain(ShiroPermsFilterFactoryBean shiroFilter) {
    shiroFilter.setLoginUrl("/doLogin");
    shiroFilter.setSuccessUrl("/");
    // 跳转cas服务器
    // shiroFilter.setUnauthorizedUrl("/error/403");

      // user过滤器,处理ajax请求超时不跳转情况
      Map<String, Filter> filters = new HashMap<>(2);
      filters.put("user", new UserFilter());
      filters.put("perms", new UserPermFilter());
      shiroFilter.setFilters(filters);
    
      Map<String, String> filterMap = new LinkedHashMap<>();
      filterMap.put("/callback", "callbackFilter");
      filterMap.put("/logout", "logout");
      filterMap.put("/doLogin", "anon");
      filterMap.put("/static/**", "anon");
      filterMap.put("/error/**", "anon");
      filterMap.put("/captcha.jpg", "anon");
      filterMap.put("/rest/**", "anon");
      filterMap.put("/*", "securityFilter");
      shiroFilter.setFilterChainDefinitionMap(filterMap);

    }

    /**
    • shiro过滤器
    • /rest/**,请求采用token验证(com.audaque.gm.support.interceptor.RestApiInterceptor)
    • @param securityManager
    • @return
      */
      @Bean("shiroFilter")
      public ShiroPermsFilterFactoryBean shiroFilter(SecurityManager securityManager, Config config) {
      ShiroPermsFilterFactoryBean shiroFilter = new ShiroPermsFilterFactoryBean();
      shiroFilter.setSecurityManager(securityManager);

      loadShiroFilterChain(shiroFilter);
      HashMap<String, Filter> filterMap = new HashMap<>(3);
      // cas 资源认证拦截器
      SecurityFilter securityFilter = new SecurityFilter();
      securityFilter.setConfig(config);
      securityFilter.setClients(clientName);
      filterMap.put("securityFilter", securityFilter);
      //cas 认证后回调拦截器
      CallbackFilter callbackFilter = new CallbackFilter();
      callbackFilter.setConfig(config);
      callbackFilter.setDefaultUrl(projectUrl);
      filterMap.put("callbackFilter", callbackFilter);
      // 注销 拦截器
      LogoutFilter logoutFilter = new LogoutFilter();
      logoutFilter.setConfig(config);
      logoutFilter.setCentralLogout(true);
      logoutFilter.setLocalLogout(true);
      logoutFilter.setLogoutUrlPattern(".*");
      logoutFilter.setDefaultUrl(projectUrl + "/callback?client_name=" + clientName);
      filterMap.put("logout",logoutFilter);
      shiroFilter.setFilters(filterMap);

      return shiroFilter;
      }

    //------新加代码开始----------
    @Bean
    public SessionDAO sessionDAO(){
    return new MemorySessionDAO();
    }

    /**
    • 自定义cookie名称
    • @return
      */
      @Bean
      public SimpleCookie sessionIdCookie(){
      SimpleCookie cookie = new SimpleCookie("sid");
      cookie.setMaxAge(-1);
      cookie.setPath("/");
      cookie.setHttpOnly(false);
      return cookie;
      }

    @Bean
    public DefaultWebSessionManager sessionManager(SimpleCookie sessionIdCookie, SessionDAO sessionDAO){
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionIdCookie(sessionIdCookie);
    sessionManager.setSessionIdCookieEnabled(true);
    //30分钟
    sessionManager.setGlobalSessionTimeout(180000);
    sessionManager.setSessionDAO(sessionDAO);
    sessionManager.setDeleteInvalidSessions(true);
    sessionManager.setSessionValidationSchedulerEnabled(true);
    return sessionManager;
    }
    //-----------新加代码结束---------------

    /**
    • session管理器
    • @return
      */
      @Bean
      public SessionManager sessionManager(GlobalProperties globalProperties){
      DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
      sessionManager.setSessionValidationSchedulerEnabled(true);
      sessionManager.setSessionIdUrlRewritingEnabled(false);
      sessionManager.setDeleteInvalidSessions(true);
      if (globalProperties.isRedisSessionDao()) {
      // 开启redis会话管理器
      sessionManager.setSessionFactory(new UserSessionFactory());
      sessionManager.setSessionDAO(new UserSessionDAO());
      List<SessionListener> sessionListeners = new ArrayList<>();
      sessionListeners.add(new UserSessionListener());
      sessionManager.setSessionListeners(sessionListeners);
      }
      return sessionManager;
      }
    /**
    • 使用cglib方式创建代理对象
    • @return
      */
      @Bean
      @DependsOn("lifecycleBeanPostProcessor")
      public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
      DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
      proxyCreator.setProxyTargetClass(true);
      return proxyCreator;
      }
    /**
    • shiro生命周期处理器
    • @return
      */
      @Bean
      public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
      return new LifecycleBeanPostProcessor();
      }
    /**
    • 启用注解
    • @param securityManager
    • @return
      */
      @Bean
      public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
      AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
      advisor.setSecurityManager(securityManager);
      return advisor;
      }

    //-------新加代码开始---------
    @Bean
    public FilterRegistrationBean singleSignOutFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setName("singleSignOutFilter");
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setCasServerUrlPrefix(casServerUrl);
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    bean.setFilter(singleSignOutFilter);
    bean.addUrlPatterns("/*");
    bean.setEnabled(true);
    return bean;
    }

    //------新加代码结束----------
    }

package com.audaque.gm.modules.sys.sso;

import com.audaque.gm.common.utils.ShiroUtils;
import com.audaque.gm.modules.sys.entity.SysUserEntity;
import com.audaque.gm.modules.sys.service.SysUserService;
import com.audaque.gm.modules.task.GetOaUserTask;

import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.subject.Pac4jPrincipal;
import io.buji.pac4j.token.Pac4jToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.pac4j.core.profile.CommonProfile;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Set;

@Slf4j
public class CasRealm extends Pac4jRealm {

private String clientName;

@Autowired
private SysUserService sysUserService;

/**
 * 权限验证
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    log.info("---- [CasRealm]   >>>>   doGetAuthorizationInfo");
    Long userId = ShiroUtils.getUserId();
    Set<String> roles = sysUserService.listUserRoles(userId);
    Set<String> perms = sysUserService.listUserPerms(userId);
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.setRoles(roles);
    info.setStringPermissions(perms);
    return info;
}

/**
 * 登录验证
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    log.info("---- [CasRealm]   >>>>   doGetAuthenticationInfo");
    String username;
    String password = null;
    int hashCode = 0;
    SimpleAuthenticationInfo simpleAuthenticationInfo;
    if (token instanceof UsernamePasswordToken) {
        username = (String) token.getPrincipal();
        password = new String((char[])token.getCredentials());
    } else {
        Pac4jToken pac4jToken = (Pac4jToken) token;
        List<CommonProfile> profiles = pac4jToken.getProfiles();
        hashCode = profiles.hashCode();
        username = pac4jToken.getProfiles().get(0).getId();
    }
    SysUserEntity user = sysUserService.getByUserName(username);
    //如果数据库中没有该用户,先把它添加到数据库,再执行操作
    if(user==null) {
        GetOaUserTask  task = new GetOaUserTask();
        task.run();
    }
        //再查询一遍
    user = sysUserService.getByUserName(username);
    
    simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, password == null ? hashCode : password, getName());

    return simpleAuthenticationInfo;
}

@Override
public boolean supports(AuthenticationToken token) {
    Boolean flag = false;
    if (super.supports(token) ||  token instanceof UsernamePasswordToken) {
        flag = true;
    }
    return flag;
}

public String getClientName() {
    return clientName;
}

public void setClientName(String clientName) {
    this.clientName = clientName;
}

}

package com.audaque.gm.modules.sys.sso;

import lombok.extern.slf4j.Slf4j;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.core.context.Pac4jConstants;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.http.ajax.AjaxRequestResolver;
import org.pac4j.core.redirect.RedirectAction;
import org.pac4j.core.redirect.RedirectActionBuilder;
import org.pac4j.core.util.CommonHelper;

@Slf4j
public class CasClient extends org.pac4j.cas.client.CasClient {

public CasClient() {
    super();
}

public CasClient(CasConfiguration configuration) {
    super(configuration);
}

@Override
public RedirectAction getRedirectAction(WebContext context) {
    log.info("---- [CasClient] >>>> getRedirectAction");
    this.init();
    AjaxRequestResolver requestResolver = getAjaxRequestResolver();
    if (requestResolver.isAjax(context)) {
        RedirectAction action = getRedirectActionBuilder().redirect(context);
        this.cleanRequestedUrl(context);
        return requestResolver.buildAjaxResponse(action.getLocation(), context);
    } else {
        final String attemptedAuth = (String)context.getSessionStore().get(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX);
        if (CommonHelper.isNotBlank(attemptedAuth)) {
            this.cleanAttemptedAuthentication(context);
            this.cleanRequestedUrl(context);
            return getRedirectActionBuilder().redirect(context);
        } else {
            RedirectActionBuilder redirectActionBuilder = getRedirectActionBuilder();
            RedirectAction redirect = redirectActionBuilder.redirect(context);
            return redirect;

        }
    }
}

private void cleanRequestedUrl(WebContext context) {
    SessionStore<WebContext> sessionStore = context.getSessionStore();
    if (sessionStore.get(context, Pac4jConstants.REQUESTED_URL) != null) {
        sessionStore.set(context, Pac4jConstants.REQUESTED_URL, "");
    }

}

private void cleanAttemptedAuthentication(WebContext context) {
    SessionStore<WebContext> sessionStore = context.getSessionStore();
    if (sessionStore.get(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX) != null) {
        sessionStore.set(context, this.getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, "");
    }

}

}

package com.audaque.gm.modules.sys.sso;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

@Slf4j
public class CallbackFilter extends io.buji.pac4j.filter.CallbackFilter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    log.info("---- [CallbackFilter] >>>> doFilter");
    super.doFilter(servletRequest, servletResponse, filterChain);
}

}

package com.audaque.gm.config;

import com.audaque.gm.modules.sys.sso.CasClient;
import io.buji.pac4j.context.ShiroSessionStore;
import org.pac4j.cas.config.CasConfiguration;
import org.pac4j.cas.config.CasProtocol;
import org.pac4j.core.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Pac4jConfig {

/** 地址为:cas地址 */
@Value("${cas.server.url}")
private String casServerUrl;

/** 地址为:验证返回后的项目地址:http://localhost:8081 */
@Value("${cas.project.url}")
private String projectUrl;

/** 相当于一个标志,可以随意 */
@Value("${cas.client-name}")
private String clientName;

/**
 * pac4j 配置
 * @param casClient
 * @param shiroSessionStore
 * @return
 */
@Bean
public Config config(CasClient casClient, ShiroSessionStore shiroSessionStore) {
    Config config = new Config(casClient);
    config.setSessionStore(shiroSessionStore);
    return config;
}

/**
 * 自定义存储
 * @return
 */
@Bean
public ShiroSessionStore shiroSessionStore(){
    return new ShiroSessionStore();
}

/**
 * cas 客户端配置
 * @param casConfig
 * @return
 */
@Bean
public CasClient casClient(CasConfiguration casConfig){
    CasClient casClient = new CasClient(casConfig);
    //客户端回调地址
    casClient.setCallbackUrl(projectUrl + "/callback?client_name=" + clientName);
    casClient.setName(clientName);
    return casClient;
}

/**
 * 请求cas服务端配置
 */
@Bean
public CasConfiguration casConfig(){
    final CasConfiguration configuration = new CasConfiguration();
    //CAS server登录地址
    configuration.setLoginUrl(casServerUrl + "/login");
    //CAS 版本,默认为 CAS30
    configuration.setProtocol(CasProtocol.CAS20);
    configuration.setAcceptAnyProxy(true);
    configuration.setPrefixUrl(casServerUrl + "/");
    return configuration;
}

}

package com.audaque.gm.modules.sys.shiro;

import com.audaque.gm.modules.sys.entity.SysMenuEntity;
import com.audaque.gm.common.entity.Query;
import com.audaque.gm.common.utils.SpringContextUtils;
import com.audaque.gm.modules.sys.mapper.SysMenuMapper;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**

  • 产生责任链,确定每个url的访问权限
  • */
    public class ShiroPermsFilterFactoryBean extends ShiroFilterFactoryBean {

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

    private SysMenuMapper sysMenuMapper = SpringContextUtils.getBean("sysMenuMapper", SysMenuMapper.class);

    /**
    • 默认配置权限链
      */
      public static final Map<String, String> DEFAULT_CHAIN_MAP = new HashMap<>(16);
    /**
    • 增加数据库权限
    • @param filterChainDefinitionMap
      */
      @Override
      public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
      log.info("---- [ShiroPermsFilterFactoryBean] >>>> setFilterChainDefinitionMap");
      // 清空默认配置之后保存配置
      DEFAULT_CHAIN_MAP.clear();
      DEFAULT_CHAIN_MAP.putAll(filterChainDefinitionMap);
      // 从菜单表中查询菜单配置
      List<SysMenuEntity> lists = sysMenuMapper.list(new Query());
      for(SysMenuEntity menu : lists) {
      String permKey = menu.getPerms();
      String permUrl = menu.getUrl();
      if(StringUtils.isNotEmpty(permKey) && StringUtils.isNotEmpty(permUrl)) {
      filterChainDefinitionMap.put(permUrl, "perms[" + permKey + "]");
      }
      }
      filterChainDefinitionMap.put("/**", "user");
      filterChainDefinitionMap.put("/*", "securityFilter");
      super.setFilterChainDefinitionMap(filterChainDefinitionMap);
      log.info("[ShiroPermsFilterFactoryBean] >>>>>>> init perms finished.");
      }

}

application.yml 配置:

cas单点登录配置

cas:
project:
url: http://172.16.110.92:8087/Apiweb
server:
url: http://10.143.2.174:8089/audaque-platform
client-name: apiClient
logoutUrl: http://10.143.2.174:8089/audaque-platform/logout

superming168的主页 superming168 | 菜鸟二级 | 园豆:202
提问于:2019-05-07 17:44
< >
分享
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册