springboot 整合shiro + cas 實(shí)現(xiàn)單點(diǎn)登錄權(quán)限管理(一)(sso)

簡(jiǎn)介

  • 本項(xiàng)目將單點(diǎn)登錄和權(quán)限認(rèn)證結(jié)合在一起挂捅,實(shí)現(xiàn)了用戶登錄與授權(quán)的基礎(chǔ)框架包竹,后續(xù)可以很好的在此框架上進(jìn)行二次開(kāi)發(fā)
  • 在原理和配置上也有很多不清楚的地方,希望大家留言討論~
  • 后續(xù)將會(huì)進(jìn)行spring cloud系列工程的搭建籍凝,并將shiro-cas嘗試接入spring cloud中
  • spring cloud系列直達(dá)地址 spring cloud

源碼地址


準(zhǔn)備:cas服務(wù)器搭建


單點(diǎn)登錄流程

  1. 客戶端請(qǐng)求目標(biāo)服務(wù)器
  2. 目標(biāo)服務(wù)器重定向到cas服務(wù)器
  3. cas服務(wù)器進(jìn)行驗(yàn)證,通過(guò)則請(qǐng)求目標(biāo)服務(wù)器苗缩,將ticket傳給目標(biāo)服務(wù)器
  4. 目標(biāo)服務(wù)器根據(jù)ticket饵蒂,請(qǐng)求cas服務(wù)器,獲取用戶登錄信息
  5. cas服務(wù)器返回驗(yàn)證消息給目標(biāo)服務(wù)器

項(xiàng)目實(shí)現(xiàn)

1酱讶、導(dǎo)入依賴包

        <!--Apache Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>${shiro.version}</version>
        </dependency>

2退盯、 編輯配置文件

  • 以下為shiro-cas相關(guān)部分
# shiro - cas 配置
shiro:
  # 在訪問(wèn)cas服務(wù)器登錄之后,會(huì)返回一個(gè)ticket泻肯。由該地址接收
  casFilterUrlPattern: /shiro-cas
  # cas服務(wù)前綴
  casServerUrlPrefix: http://127.0.0.1:8181/cas
  # shiro服務(wù)前綴
  shiroServerUrlPrefix: http://127.0.0.1:${server.port}${server.servlet.context-path}
  # 登錄地址
  loginUrl: ${shiro.casServerUrlPrefix}/login?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
  # 登出地址
  logoutUrl: ${shiro.casServerUrlPrefix}/logout?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
  • 以下為全量配置
# Tomcat
server:
  tomcat:
    uri-encoding: UTF-8
    max-threads: 1000
    min-spare-threads: 30
  port: 40301
  connection-timeout: 5000ms
  servlet:
    context-path: /shiro
    session:
      cookie:
        http-only: true

spring:
  application:
    name: service-auth

# shiro - cas 配置
shiro:
  # 在訪問(wèn)cas服務(wù)器登錄之后渊迁,會(huì)返回一個(gè)ticket。由該地址接收
  casFilterUrlPattern: /shiro-cas
  # cas服務(wù)前綴
  casServerUrlPrefix: http://127.0.0.1:8181/cas
  # shiro服務(wù)前綴
  shiroServerUrlPrefix: http://127.0.0.1:${server.port}${server.servlet.context-path}
  # 登錄地址
  loginUrl: ${shiro.casServerUrlPrefix}/login?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}
  # 登出地址
  logoutUrl: ${shiro.casServerUrlPrefix}/logout?service=${shiro.shiroServerUrlPrefix}${shiro.casFilterUrlPattern}

3灶挟、自定義配置casFileter

import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;

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

/**
 * @author : xingsongtan@qq.com
 * @date : 14:17 2019/7/17
 */
public class MyCasFilter extends CasFilter {
    private static       Logger logger           = LoggerFactory.getLogger(MyCasFilter.class);
    private static final String TICKET_PARAMETER = "ticket";

    public MyCasFilter() {
    }

    @Override
    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        // 獲取請(qǐng)求的ticket
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ticket = getRequestTicket(httpRequest);
        if (StringUtils.isEmpty(ticket)) {
            logger.debug("票證獲取失敗,票證為空琉朽!");
            return null;
        }
        return new CasToken(ticket);
    }

    /**
     * 拒絕除了option以外的所有請(qǐng)求
     **/
    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    @Override
    public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 獲取ticket,如果不存在稚铣,直接返回false
        String ticket = getRequestTicket((HttpServletRequest) request);
        if (StringUtils.isEmpty(ticket)) {
            return false;
        }
        return this.executeLogin(request, response);
    }

    /**
     * 獲取請(qǐng)求的ticket
     */
    private String getRequestTicket(HttpServletRequest httpRequest) {
        // 從參數(shù)中獲取ticket
        String ticket = httpRequest.getParameter(TICKET_PARAMETER);
        if (StringUtils.isEmpty(ticket)) {
            // 如果為空的話箱叁,則從header中獲取參數(shù)
            ticket = httpRequest.getHeader(TICKET_PARAMETER);
        }
        return ticket;
    }

}

4、自定義casRealm

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.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Set;

/**
 * @author : xingsongtan@qq.com
 * @date : 12:00 2019/7/17
 */
public class MyCasRealm extends CasRealm {
    private static Logger log = LoggerFactory.getLogger(MyCasRealm.class);

    /**
     * 在調(diào)用subject.login()時(shí)惕医,首先調(diào)用此接口
     */
    @Override
    public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 調(diào)用父類的方法耕漱,然后授權(quán)用戶
        AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
        // 獲得用戶名
        String username = (String) authc.getPrincipals().getPrimaryPrincipal();
        // TODO:這里應(yīng)該從數(shù)據(jù)庫(kù)中獲取用戶信息

        return authc;
    }

    /**
     * 進(jìn)行權(quán)限驗(yàn)證的時(shí)候,調(diào)用方法抬伺,將用戶的權(quán)限信息寫進(jìn)SimpleAuthorizationInfo
     */
    @Override
    public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 用戶名稱
        log.info("進(jìn)入了權(quán)限認(rèn)證");
        Object username = principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // TODO: 這里應(yīng)該從數(shù)據(jù)庫(kù)獲取用戶權(quán)限
        Set<String> permission = new HashSet<>();
        permission.add("sys:dept:list");
        info.setStringPermissions(permission);
        return info;
    }
}

6螟够、進(jìn)行shiroconfig配置

import com.ttcode.shiro.security.MyCasFilter;
import com.ttcode.shiro.security.MyCasRealm;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.cas.CasSubjectFactory;
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.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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.web.filter.DelegatingFilterProxy;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro的配置文件
 *
 * @author : xingsongtan@qq.com
 * @date : 20:51 2019/7/18
 */

@Configuration
public class ShiroCasConfiguration {
    /**
     * 添加shiro的filter
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }

    /**
     * 保證了shiro內(nèi)部lifecycle函數(shù)bean的執(zhí)行
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 配置授權(quán)策略
     */
    @Bean(name = "authenticator")
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return authenticator;
    }

    @Bean(name = "casRealm")
    public MyCasRealm casRealm(@Value("${shiro.casServerUrlPrefix}") String casServerUrlPrefix,
                               @Value("${shiro.shiroServerUrlPrefix}") String shiroServerUrlPrefix,
                               @Value("${shiro.casFilterUrlPattern}") String casFilterUrlPattern) {
        MyCasRealm casRealm = new MyCasRealm();
        // 認(rèn)證通過(guò)后的默認(rèn)角色
        casRealm.setDefaultRoles("ROLE_USER");
        // cas服務(wù)端地址前綴
        casRealm.setCasServerUrlPrefix(casServerUrlPrefix);
        // 應(yīng)用服務(wù)地址,用來(lái)接收cas服務(wù)端票證
        casRealm.setCasService(shiroServerUrlPrefix + casFilterUrlPattern);
        return casRealm;
    }

    /**
     * 配置安全管理器
     **/
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(ModularRealmAuthenticator authenticator,
                                                               MyCasRealm casRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設(shè)置授權(quán)策略,此步驟必須在設(shè)置realm的前面,不然會(huì)報(bào)錯(cuò)realm未配置
        securityManager.setAuthenticator(authenticator);
        securityManager.setSubjectFactory(new CasSubjectFactory());
        // 緩存管理器
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        // 設(shè)置自定義驗(yàn)證策略
        securityManager.setRealm(casRealm);
        return securityManager;
    }


    /**
     * 配置登錄過(guò)濾器
     */
    @Bean(name = "casFilter")
    public MyCasFilter casFilter(@Value("${shiro.loginUrl}") String loginUrl) {
        MyCasFilter casFilter = new MyCasFilter();
        casFilter.setName("casFilter");
        casFilter.setEnabled(true);
        casFilter.setFailureUrl(loginUrl);
        return casFilter;
    }

    /**
     * shiro 過(guò)濾器
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
                                                            MyCasFilter casFilter,
                                                            @Value("${shiro.logoutUrl}") String logoutUrl,
                                                            @Value("${shiro.loginUrl}") String loginUrl,
                                                            @Value("${shiro.casFilterUrlPattern}") String casFilterUrlPattern) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 設(shè)置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 設(shè)置登錄地址
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 設(shè)置登錄成功地址
        shiroFilterFactoryBean.setSuccessUrl("/");
        // 配置攔截地址
        Map<String, Filter> filters = new HashMap<>();
        filters.put("casFilter", casFilter);
        LogoutFilter logoutFilter = new LogoutFilter();
        // 配置登出地址
        logoutFilter.setRedirectUrl(logoutUrl);
        filters.put("logout", logoutFilter);
        shiroFilterFactoryBean.setFilters(filters);
        // 設(shè)置訪問(wèn)用戶頁(yè)面需要授權(quán)的操作
        loadShiroFilterChain(shiroFilterFactoryBean, casFilterUrlPattern);
        // 將設(shè)置的權(quán)限設(shè)置到shiroFilterFactoryBean
        return shiroFilterFactoryBean;
    }

    /**
     * 1妓笙、當(dāng)我們第一次訪問(wèn)客戶端時(shí)若河,先去cas進(jìn)行認(rèn)證,成功后會(huì)返回一個(gè)ticket
     * 2给郊、返回的ticket地址在casRealm已經(jīng)進(jìn)行了配置牡肉,shiroServerUrlPrefix + casFilterUrlPattern
     * 3、即地址為/shiro-cas淆九,對(duì)該地址進(jìn)行casFilter攔截
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean, String casFilterUrlPattern) {
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put(casFilterUrlPattern, "casFilter");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }

    /**
     * 開(kāi)啟Shiro的注解(如@RequiresPermissions)
     * 需借助SpringAOP掃描使用Shiro注解的類
     * 配置以下兩個(gè)bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實(shí)現(xiàn)此功能
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 開(kāi)啟aop注解支持
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}

結(jié)尾:以上為核心配置

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末统锤,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炭庙,更是在濱河造成了極大的恐慌饲窿,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焕蹄,死亡現(xiàn)場(chǎng)離奇詭異逾雄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)腻脏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門鸦泳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人永品,你說(shuō)我怎么就攤上這事做鹰。” “怎么了鼎姐?”我有些...
    開(kāi)封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵钾麸,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我炕桨,道長(zhǎng)饭尝,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任献宫,我火速辦了婚禮钥平,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘姊途。我一直安慰自己帖池,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布吭净。 她就那樣靜靜地躺著睡汹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寂殉。 梳的紋絲不亂的頭發(fā)上囚巴,一...
    開(kāi)封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼彤叉。 笑死庶柿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秽浇。 我是一名探鬼主播浮庐,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柬焕!你這毒婦竟也來(lái)了审残?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤斑举,失蹤者是張志新(化名)和其女友劉穎搅轿,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體富玷,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璧坟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赎懦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雀鹃。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖励两,靈堂內(nèi)的尸體忽然破棺而出黎茎,到底是詐尸還是另有隱情,我是刑警寧澤伐蒋,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站迁酸,受9級(jí)特大地震影響先鱼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奸鬓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一焙畔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧串远,春花似錦宏多、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至留搔,卻和暖如春更胖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工却妨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饵逐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓彪标,卻偏偏與公主長(zhǎng)得像倍权,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捞烟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354