spring boot 2.0 集成 shiro 和 pac4j cas單點登陸

新開的項目,果斷使用  spring boot  最新版本  2.0.3 ,省得後期升級坑太多,前期把雷先排了。java

因爲對 shiro 比較熟,故使用 shiro 來作權限控制。同時已經存在了 cas 認證中心, shiro 官方在 1.2 中就代表已經棄用了 CasFilter ,建議使用 buji-pac4j ,故使用 pac4j 來作單點登陸的控制。git

廢話不說,代碼以下:github

2018-08-29更新:因爲pac4j 3.1 版本未支持單點登出,故升級到 4.0.0 版本,pac4j-cas 升級到 3.0.2版本,能夠實現單點登出。web

首先是 maven 配置。spring

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

        <dependency>
            <groupId>org.pac4j</groupId>
            <artifactId>pac4j-cas</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>io.buji</groupId>
            <artifactId>buji-pac4j</artifactId>
            <version>4.0.0</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-web</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
            </exclusions>
        </dependency>

 

import io.buji.pac4j.filter.LogoutFilter;
import io.buji.pac4j.filter.SecurityFilter;
import io.buji.pac4j.subject.Pac4jSubjectFactory;
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.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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.jasig.cas.client.session.SingleSignOutFilter;
import javax.servlet.DispatcherType; import javax.servlet.Filter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * @author gongtao * @version 2018-03-30 10:49 * @update 2018-08-29 升級 pac4j 版本到 4.0.0 **/ @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; @Bean("securityManager") public DefaultWebSecurityManager securityManager(Pac4jSubjectFactory subjectFactory, SessionManager sessionManager, CasRealm casRealm){ DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(casRealm); manager.setSubjectFactory(subjectFactory); manager.setSessionManager(sessionManager); return manager; } @Bean public CasRealm casRealm(){ CasRealm realm = new CasRealm(); // 使用自定義的realm realm.setClientName(clientName); realm.setCachingEnabled(false); //暫時不使用緩存 realm.setAuthenticationCachingEnabled(false); realm.setAuthorizationCachingEnabled(false); //realm.setAuthenticationCacheName("authenticationCache"); //realm.setAuthorizationCacheName("authorizationCache"); return realm; } /** * 使用 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; } /** * 加載shiroFilter權限控制規則(從數據庫讀取而後配置) * @param shiroFilterFactoryBean */ private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){ /*下面這些規則配置最好配置到配置文件中 */ Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/", "securityFilter"); filterChainDefinitionMap.put("/application/**", "securityFilter"); filterChainDefinitionMap.put("/index", "securityFilter"); filterChainDefinitionMap.put("/callback", "callbackFilter"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**","anon"); // filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); } /** * shiroFilter * @param securityManager * @param config * @return */ @Bean("shiroFilter") public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager, Config config) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 添加casFilter到shiroFilter中 loadShiroFilterChain(shiroFilterFactoryBean); Map<String, Filter> filters = new HashMap<>(3); //cas 資源認證攔截器 SecurityFilter securityFilter = new SecurityFilter(); securityFilter.setConfig(config); securityFilter.setClients(clientName); filters.put("securityFilter", securityFilter); //cas 認證後回調攔截器 CallbackFilter callbackFilter = new CallbackFilter(); callbackFilter.setConfig(config); callbackFilter.setDefaultUrl(projectUrl); filters.put("callbackFilter", callbackFilter); // 註銷 攔截器 LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setConfig(config); logoutFilter.setCentralLogout(true); logoutFilter.setLocalLogout(true); logoutFilter.setDefaultUrl(projectUrl + "/callback?client_name=" + clientName); filters.put("logout",logoutFilter); shiroFilterFactoryBean.setFilters(filters); return shiroFilterFactoryBean; } @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; } /** * 下面的代碼是添加註解支持 */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // 強制使用cglib,防止重複代理和可能引發代理出錯的問題 // https://zhuanlan.zhihu.com/p/29161098 defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @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);
    bean.setOrder(Ordered.HIGHEST_PERCEDENCE);
return bean;
}
}

 

上面是  shiro 的配置。數據庫

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;

/**
 * @author gongtao
 * @version 2018-07-06 9:35
 * @update 2018-08-29 升級 pac4j 版本到 4.0.0
 **/
@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("authcConfig")
    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服務端配置
     * @param casLogoutHandler
     */
    @Bean
    public CasConfiguration casConfig(){
        final CasConfiguration configuration = new CasConfiguration();
        //CAS server登陸地址
        configuration.setLoginUrl(casServerUrl + "/login");
        //CAS 版本,默認爲 CAS30,咱們使用的是 CAS20
        configuration.setProtocol(CasProtocol.CAS20);
        configuration.setAcceptAnyProxy(true);
        configuration.setPrefixUrl(casServerUrl + "/");
        return configuration;
    }


}

以上爲pac4j 配置apache

 

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.redirect.RedirectAction; import org.pac4j.core.util.CommonHelper; /** * @author gongtao * @version 2018-07-06 9:41 * @update 2018-08-29 升級 pac4j 版本到 4.0.0 **/ public class CasClient extends org.pac4j.cas.client.CasClient { public CasClient() { super(); } public CasClient(CasConfiguration configuration) { super(configuration); } /* * (non-Javadoc) * @see org.pac4j.core.client.IndirectClient#getRedirectAction(org.pac4j.core.context.WebContext) */ @Override public RedirectAction getRedirectAction(WebContext context) { this.init(); if (getAjaxRequestResolver().isAjax(context)) { this.logger.info("AJAX request detected -> returning the appropriate action"); RedirectAction action = getRedirectActionBuilder().redirect(context); this.cleanRequestedUrl(context); return getAjaxRequestResolver().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); //這裏按本身需求處理,默認是返回了401,我在這邊改成跳轉到cas登陸頁面 //throw HttpAction.unauthorized(context); return this.getRedirectActionBuilder().redirect(context); } else { return this.getRedirectActionBuilder().redirect(context); } } } 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, ""); } } }
/**
 * @author gongtao
 * @version 2018-07-05 15:30
 **/
public class CallbackFilter extends io.buji.pac4j.filter.CallbackFilter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        super.doFilter(servletRequest, servletResponse, filterChain);
    }
}

 CallbackFilter 是單點登陸後回調使用的過濾器。緩存

/**
 * 認證與受權
 * @author gongtao
 * @version 2018-03-30 13:55
 **/
public class CasRealm extends Pac4jRealm {

    private String clientName;
    

    /**
     * 認證
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        final Pac4jToken pac4jToken = (Pac4jToken) authenticationToken;
        final List<CommonProfile> commonProfileList = pac4jToken.getProfiles();
     final CommonProfile commonProfile = commonProfileList.get(0); 
        System.out.println("單點登陸返回的信息" + commonProfile.toString());
        //todo 
        final Pac4jPrincipal principal = new Pac4jPrincipal(commonProfileList, getPrincipalNameAttribute());
        final PrincipalCollection principalCollection = new SimplePrincipalCollection(principal, getName());
        return new SimpleAuthenticationInfo(principalCollection, commonProfileList.hashCode());
    }

    /**
     * 受權/驗權(todo 後續有權限在此增長)
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
        authInfo.addStringPermission("user");
        return authInfo;
    }
}

 

 CasRealm 這個就是和以前  shiro  的 CasRealm  同樣了。cookie

最後就是  application.yml 的配置了。session

#cas配置
cas:
  client-name: mfgClient
  server:
    url: http://127.0.0.1:8080/cas
  project:
    url: http://127.0.0.1:8081

參考: https://blog.csdn.net/hxm_code/article/details/79226456

參考: https://github.com/bujiio/buji-pac4j

參考:https://github.com/pac4j/pac4j

相關文章
相關標籤/搜索