springboot+shiro+cas+pac4j客戶端集成單點登錄

前言:接上一篇,已經搭建好cas服務器,現在開始集成springboot客戶端,由於使用的框架是開源熱門springboot項目bootdo,這裏直接用bootdo作演示,另外提供一個簡單的springboot的demo用作參考。

1.導入所需maven包

        <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>

2.修改shiro配置文件,shiroConfig.java:

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.cecjx.system.filter.LoginFilter;
import com.cecjx.system.shiro.UserRealm;
import io.buji.pac4j.filter.CallbackFilter;
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 javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author hqg
 * @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;

    @Value("${server.session-timeout}")
    private int tomcatTimeout;

    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(Pac4jSubjectFactory subjectFactory, SessionManager sessionManager, UserRealm casRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(casRealm);
        manager.setSubjectFactory(subjectFactory);
        manager.setSessionManager(sessionManager);
        return manager;
    }

    @Bean
    public UserRealm casRealm(){
        UserRealm realm = new UserRealm();
        // 使用自定義的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("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        filterChainDefinitionMap.put("/upload/**", "anon");
        filterChainDefinitionMap.put("/files/**", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/kaptcha", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");

        filterChainDefinitionMap.put("/callback", "callbackFilter");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/index","securityFilter");
        filterChainDefinitionMap.put("/**","securityFilter");
//        filterChainDefinitionMap.put("/**","loginFilter");
        // 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<>(4);

        //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(casServerUrl + "/logout?service=" + projectUrl);
        filters.put("logout",logoutFilter);
        shiroFilterFactoryBean.setFilters(filters);

        LoginFilter loginFilter=new LoginFilter();
        filters.put("loginFilter",loginFilter);
        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);
        sessionManager.setGlobalSessionTimeout(tomcatTimeout * 1000);
        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;
    }
}

3.添加pac4j的配置文件,Pac4JConfig.java:

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 hqg
 * @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
     */
    @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;
    }

}

4.cas客戶端CasClient.java:

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 hqg
 * @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, "");
        }

    }


}

5.修改原來的UserRealm爲繼承Pac4jRealm,修改裏面的doGetAuthorizationInfo方法:

import com.cecjx.common.config.ApplicationContextRegister;
import com.cecjx.common.utils.ShiroUtils;
import com.cecjx.system.domain.UserDO;
import com.cecjx.system.service.MenuService;
import com.cecjx.system.service.UserService;
import io.buji.pac4j.realm.Pac4jRealm;
import io.buji.pac4j.subject.Pac4jPrincipal;
import io.buji.pac4j.token.Pac4jToken;
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.pac4j.core.profile.CommonProfile;

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

public class UserRealm extends Pac4jRealm {

    private String clientName;

    public String getClientName() {
        return clientName;
    }

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection authenticationToken) {
        Long userId = ShiroUtils.getUserId();
        MenuService menuService = ApplicationContextRegister.getBean(MenuService.class);
        Set<String> perms = menuService.listPerms(userId);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(perms);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        final Pac4jToken token = (Pac4jToken) authenticationToken;
        final List<CommonProfile> profiles = token.getProfiles();
        final Pac4jPrincipal principal = new Pac4jPrincipal(profiles);
        //登錄用戶名
        String userName = principal.getProfile().getId();
        //String password = new String((char[]) token.getCredentials());
        UserService userService = ApplicationContextRegister.getBean(UserService.class);
        // 查詢用戶信息
        UserDO user = userService.get(userName);
        // 查詢用戶是否管理員
        boolean isAdmin = userService.isAdmin(user.getUserId());
        user.setAdmin(isAdmin);
        // 賬號不存在
        if (user == null) {
            throw new UnknownAccountException("賬號不存在");
        }
        // 密碼錯誤
        // if (!password.equals(user.getPassword())) {
        // 	throw new IncorrectCredentialsException("賬號或密碼不正確");
        // }
        // 賬號鎖定
        // if (user.getStatus() == 0) {
        // 	throw new LockedAccountException("賬號已被鎖定,請聯繫管理員");
        // }
        // SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, "123", getName());
        // return info;
        PrincipalCollection principalCollection = new SimplePrincipalCollection(user, this.getName());
        return new SimpleAuthenticationInfo(principalCollection, profiles.hashCode());
    }
}

6.cas服務器地址及客戶端回調地址的配置application.yml:

#cas配置
cas:
  client-name: ywjk
  #測試工作機地址
  server:
    url: http://127.0.0.1:8443/cas
  #本機ip地址
  project:
    url: http://127.0.0.1:8081/ywjk/

7.至此,bootdo客戶端集成cas單點登錄完成。項目demo下載https://download.csdn.net/download/qq_29467891/12548449

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章