Springboot 集成 Shiro 和 CAS 實(shí)現(xiàn)單點(diǎn)登錄(客戶端)
前言
這里我先要說(shuō)明一下,我們的項(xiàng)目架構(gòu)是Springboot+Shiro+Ehcache+ThymeLeaf+Mybaits,在這個(gè)基礎(chǔ)上,我們?cè)偌尤肓薈AS單點(diǎn)登錄,雖然前面的框架看著很長(zhǎng),但是和單點(diǎn)登錄相關(guān)的核心架構(gòu)其實(shí)就是Springboot和Shiro而已,所以在看這篇文章之前,需要你掌握的知識(shí)有Springboot的基礎(chǔ)框架搭建以及集成Shiro后的一些操作,因?yàn)橹蟮募蒀AS其實(shí)也是在這個(gè)基礎(chǔ)上進(jìn)行的修改。
引入Shiro-cas包
需要集成CAS那么肯定要引入CAS相關(guān)的組件包,在POM.xml中引入:
1
2<dependency>
3????<groupId>org.apache.shirogroupId>
4????<artifactId>shiro-springartifactId>
5????<version>1.2.6version>
6dependency>
7
8
9<dependency>
10????<groupId>org.apache.shirogroupId>
11????<artifactId>shiro-ehcacheartifactId>
12????<version>1.2.6version>
13dependency>
14
15<dependency>
16????<groupId>org.apache.shirogroupId>
17????<artifactId>shiro-casartifactId>
18????<version>1.2.6version>
19dependency>
前兩個(gè)一個(gè)是Spring和Shiro結(jié)合的shiro-spring包和與ehcache結(jié)合的shiro-ehcache包,這兩個(gè)包應(yīng)該是之前就有的,之所以也把他們寫(xiě)進(jìn)來(lái)是因?yàn)槿绻隒AS的組件包,需要保證這三個(gè)包的版本號(hào)一致,筆者之前引入的前兩個(gè)包的版本號(hào)是1.2.4,結(jié)果單獨(dú)引入1.2.6的shiro-cas包后,一些cas關(guān)鍵的類是找不到的,所以這里盡量保持這三個(gè)引入包的版本號(hào)一致。
小插曲
我在升級(jí)1.2.4的shiro-spring和shiro-ehcache這連個(gè)組件包的時(shí)候,是直接修改的1.2.4為1.2.6,但是引入一直報(bào)錯(cuò),嘗試了各種辦法都不行,后來(lái)發(fā)現(xiàn),你需要剪切該引入包的dependency再黏貼到pom中去,不能直接修改版本號(hào),否則會(huì)出現(xiàn)引入不成功的問(wèn)題,這個(gè)問(wèn)題卡了我一下午,坑啊!
加入單點(diǎn)登錄的配置
如果你在你的Springboot項(xiàng)目中集成過(guò)shiro框架,應(yīng)該對(duì)兩個(gè)自定義的類不陌生,一個(gè)是myShiroConfig另一個(gè)是myShiroRealm,這兩個(gè)類其實(shí)就是用戶自定義的Shiro的設(shè)置類和登錄驗(yàn)證獲取權(quán)限的管理類,在這里我將不再贅述該類如何使用,直接上集成了CAS的這兩個(gè)類:
首先是設(shè)置類:
1import?com.dhcc.pa.domain.SPermission;
2import?com.dhcc.pa.other.shiro.MyShiroCasRealm;
3import?com.dhcc.pa.service.SystemService;
4import?com.dhcc.pa.util.PublicMsg;
5import?org.apache.shiro.cache.ehcache.EhCacheManager;
6import?org.apache.shiro.cas.CasFilter;
7import?org.apache.shiro.cas.CasSubjectFactory;
8import?org.apache.shiro.spring.LifecycleBeanPostProcessor;
9import?org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
10import?org.apache.shiro.spring.web.ShiroFilterFactoryBean;
11import?org.apache.shiro.web.mgt.DefaultWebSecurityManager;
12import?org.slf4j.Logger;
13import?org.slf4j.LoggerFactory;
14import?org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
15import?org.springframework.boot.web.servlet.FilterRegistrationBean;
16import?org.springframework.context.annotation.Bean;
17import?org.springframework.context.annotation.Configuration;
18import?org.springframework.util.StringUtils;
19import?org.springframework.web.filter.DelegatingFilterProxy;
20
21import?javax.servlet.Filter;
22import?java.util.HashMap;
23import?java.util.LinkedHashMap;
24import?java.util.List;
25import?java.util.Map;
26
27@Configuration
28public?class?ShiroConfig?{
29
30????private?static?final?Logger?logger?=?LoggerFactory.getLogger(ShiroConfig.class);
31
32????//?Cas登錄頁(yè)面地址
33????public?static?final?String?casLoginUrl?=?PublicMsg.CASServerUrlPrefix?+?"/login";
34????//?Cas登出頁(yè)面地址
35????public?static?final?String?casLogoutUrl?=?PublicMsg.CASServerUrlPrefix??+?"/logout";
36
37????//?casFilter?UrlPattern
38????public?static?final?String?casFilterUrlPattern?=?"/";
39????//?登錄地址
40????public?static?final?String?loginUrl?=?casLoginUrl?+?"?service="?+?PublicMsg.SHIROServerUrlPrefix?+?casFilterUrlPattern;
41????//?登出地址(casserver啟用service跳轉(zhuǎn)功能,需在webapps\cas\WEB-INF\cas.properties文件中啟用cas.logout.followServiceRedirects=true)
42????public?static?final?String?logoutUrl?=?casLogoutUrl+"?service="+loginUrl;
43
44????@Bean
45????public?EhCacheManager?getEhCacheManager()?{
46????????EhCacheManager?em?=?new?EhCacheManager();
47????????em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
48????????return?em;
49????}
50
51????@Bean(name?=?"myShiroCasRealm")
52????public?MyShiroCasRealm?myShiroCasRealm(EhCacheManager?cacheManager)?{
53????????MyShiroCasRealm?realm?=?new?MyShiroCasRealm();
54????????realm.setCacheManager(cacheManager);
55????????return?realm;
56????}
57
58????/**
59?????*?注冊(cè)DelegatingFilterProxy(Shiro)
60?????*
61?????*?@param
62?????*?@return
65?????*/
66????@Bean
67????public?FilterRegistrationBean?filterRegistrationBean()?{
68????????FilterRegistrationBean?filterRegistration?=?new?FilterRegistrationBean();
69????????filterRegistration.setFilter(new?DelegatingFilterProxy("shiroFilter"));
70????????//??該值缺省為false,表示生命周期由SpringApplicationContext管理,設(shè)置為true則表示由ServletContainer管理
71????????filterRegistration.addInitParameter("targetFilterLifecycle",?"true");
72????????filterRegistration.setEnabled(true);
73????????filterRegistration.addUrlPatterns("/*");
74????????return?filterRegistration;
75????}
76
77????@Bean(name?=?"lifecycleBeanPostProcessor")
78????public?LifecycleBeanPostProcessor?getLifecycleBeanPostProcessor()?{
79????????return?new?LifecycleBeanPostProcessor();
80????}
81
82????@Bean
83????public?DefaultAdvisorAutoProxyCreator?getDefaultAdvisorAutoProxyCreator()?{
84????????DefaultAdvisorAutoProxyCreator?daap?=?new?DefaultAdvisorAutoProxyCreator();
85????????daap.setProxyTargetClass(true);
86????????return?daap;
87????}
88
89????@Bean(name?=?"securityManager")
90????public?DefaultWebSecurityManager?getDefaultWebSecurityManager(MyShiroCasRealm?myShiroCasRealm)?{
91????????DefaultWebSecurityManager?dwsm?=?new?DefaultWebSecurityManager();
92????????dwsm.setRealm(myShiroCasRealm);
93//??????
94????????dwsm.setCacheManager(getEhCacheManager());
95????????//?指定?SubjectFactory
96????????dwsm.setSubjectFactory(new?CasSubjectFactory());
97????????return?dwsm;
98????}
99
100????@Bean
101????public?AuthorizationAttributeSourceAdvisor?getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager?securityManager)?{
102????????AuthorizationAttributeSourceAdvisor?aasa?=?new?AuthorizationAttributeSourceAdvisor();
103????????aasa.setSecurityManager(securityManager);
104????????return?aasa;
105????}
106
107
108????/**
109?????*?CAS過(guò)濾器
110?????*
111?????*?@return
114?????*/
115????@Bean(name?=?"casFilter")
116????public?CasFilter?getCasFilter()?{
117????????CasFilter?casFilter?=?new?CasFilter();
118????????casFilter.setName("casFilter");
119????????casFilter.setEnabled(true);
120????????//?登錄失敗后跳轉(zhuǎn)的URL,也就是?Shiro?執(zhí)行?CasRealm?的?doGetAuthenticationInfo?方法向CasServer驗(yàn)證tiket
121????????casFilter.setFailureUrl(loginUrl);//?我們選擇認(rèn)證失敗后再打開(kāi)登錄頁(yè)面
122????????return?casFilter;
123????}
124
125????/**
126?????*?ShiroFilter
127?????*?注意這里參數(shù)中的?StudentService?和?IScoreDao?只是一個(gè)例子,因?yàn)槲覀冊(cè)谶@里可以用這樣的方式獲取到相關(guān)訪問(wèn)數(shù)據(jù)庫(kù)的對(duì)象,
128?????*?然后讀取數(shù)據(jù)庫(kù)相關(guān)配置,配置到 shiroFilterFactoryBean 的訪問(wèn)規(guī)則中。實(shí)際項(xiàng)目中,請(qǐng)使用自己的Service來(lái)處理業(yè)務(wù)邏輯。
129?????*
130?????*?@param
131?????*?@param
132?????*?@param
133?????*?@return
136?????*/
137????@Bean
138????public?ShiroFilterFactoryBean?shiroFilter(DefaultWebSecurityManager?securityManager,?CasFilter?casFilter,SystemService?sysPermissionInitService)?{
139????????ShiroFilterFactoryBean?shiroFilterFactoryBean?=?new?ShiroFilterFactoryBean();
140????????//?必須設(shè)置?SecurityManager
141????????shiroFilterFactoryBean.setSecurityManager(securityManager);
142????????//?如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁(yè)面
143????????shiroFilterFactoryBean.setLoginUrl(loginUrl);
144????????//?登錄成功后要跳轉(zhuǎn)的連接
145????????shiroFilterFactoryBean.setSuccessUrl("/templete");
146????????shiroFilterFactoryBean.setUnauthorizedUrl("/403");
147
148????????//?添加casFilter到shiroFilter中
149????????Map?filters?=?new?HashMap<>();
150????????filters.put("casFilter",?casFilter);
151????????shiroFilterFactoryBean.setFilters(filters);
152????????///////////////////////?下面這些規(guī)則配置最好配置到配置文件中?///////////////////////
153????????Map?filterChainDefinitionMap?=?new?LinkedHashMap<>();
154
155????????filterChainDefinitionMap.put(casFilterUrlPattern,?"casFilter");//?shiro集成cas后,首先添加該規(guī)則
156
157????????// authc:該過(guò)濾器下的頁(yè)面必須驗(yàn)證后才能訪問(wèn),它是Shiro內(nèi)置的一個(gè)攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
158????????// anon:它對(duì)應(yīng)的過(guò)濾器里面是空的,什么都沒(méi)做
159????????logger.info("##################從數(shù)據(jù)庫(kù)讀取權(quán)限規(guī)則,加載到shiroFilter中##################");
160????????filterChainDefinitionMap.put("/js/**",?"anon");
161????????filterChainDefinitionMap.put("/css/**",?"anon");
162????????filterChainDefinitionMap.put("/bootstrapDatePicker/**",?"anon");
163????????//阻止登錄成功后下載favicon
164????????filterChainDefinitionMap.put("/favicon.ico",?"anon");
165
166????????//從數(shù)據(jù)庫(kù)獲取
167????????List?list?=?sysPermissionInitService.menuGetAll();
168
169????????for?(SPermission?sysPermissionInit?:?list)?{
170????????????if(!StringUtils.isEmpty(sysPermissionInit.getUrl())){
171????????????????filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
172????????????????????????"perms["+sysPermissionInit.getPermission()+"]");
173????????????}
174????????}
175????????//配置退出過(guò)濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
176????????filterChainDefinitionMap.put(logoutUrl,?"logout");
177????????filterChainDefinitionMap.put("/**",?"authc");
178????????shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
179????????return?shiroFilterFactoryBean;
180????}
181
182}
注釋寫(xiě)的都比較清楚了, 我這里將不再贅述,這里只有一個(gè)知識(shí)點(diǎn)需要強(qiáng)調(diào)一下:
在這個(gè)設(shè)置類中如果需要從數(shù)據(jù)庫(kù)獲取用戶的權(quán)限列表,一定要將對(duì)應(yīng)的Service寫(xiě)在shiroFilter這個(gè)方法里當(dāng)作一個(gè)參數(shù)來(lái)使用,而不能直接用@AutoWired將該類引入,否則使用時(shí)會(huì)報(bào)該Service空指針的異常,至于原因我也不是很清楚….待查
之后是登錄驗(yàn)證和權(quán)限獲取類:
1import?com.dhcc.pa.domain.Role;
2import?com.dhcc.pa.domain.SUser;
3import?com.dhcc.pa.other.config.ShiroConfig;
4import?com.dhcc.pa.service.UserService;
5import?com.dhcc.pa.util.PublicMsg;
6import?org.apache.shiro.authz.AuthorizationInfo;
7import?org.apache.shiro.authz.SimpleAuthorizationInfo;
8import?org.apache.shiro.cas.CasRealm;
9import?org.apache.shiro.subject.PrincipalCollection;
10import?org.slf4j.Logger;
11import?org.slf4j.LoggerFactory;
12import?org.springframework.beans.factory.annotation.Autowired;
13import?org.springframework.util.StringUtils;
14
15import?javax.annotation.PostConstruct;
16import?java.util.List;
17
18
24public?class?MyShiroCasRealm?extends?CasRealm?{
25
26????private?static?final?Logger?logger?=?LoggerFactory.getLogger(MyShiroCasRealm.class);
27
28????@Autowired
29????private?UserService?userService;
30
31????@PostConstruct
32????public?void?initProperty(){
33//??????setDefaultRoles("ROLE_USER");
34????????setCasServerUrlPrefix(PublicMsg.CASServerUrlPrefix);
35????????//?客戶端回調(diào)地址
36????????setCasService(PublicMsg.SHIROServerUrlPrefix?+?ShiroConfig.casFilterUrlPattern);
37????}
38
39????/**
40?????*?權(quán)限認(rèn)證,為當(dāng)前登錄的Subject授予角色和權(quán)限
41?????*?@see?:本例中該方法的調(diào)用時(shí)機(jī)為需授權(quán)資源被訪問(wèn)時(shí)
42?????*?@see?:并且每次訪問(wèn)需授權(quán)資源時(shí)都會(huì)執(zhí)行該方法中的邏輯,這表明本例中默認(rèn)并未啟用AuthorizationCache
43?????*?@see?:如果連續(xù)訪問(wèn)同一個(gè)URL(比如刷新),該方法不會(huì)被重復(fù)調(diào)用,Shiro有一個(gè)時(shí)間間隔(也就是cache時(shí)間,在ehcache-shiro.xml中配置),超過(guò)這個(gè)時(shí)間間隔再刷新頁(yè)面,該方法會(huì)被執(zhí)行
44?????*/
45????@Override
46????protected?AuthorizationInfo?doGetAuthorizationInfo(PrincipalCollection?principalCollection)?{
47????????logger.info("##################執(zhí)行Shiro權(quán)限認(rèn)證##################");
48????????//獲取用戶的輸入的賬號(hào).
49????????//獲取當(dāng)前登錄輸入的用戶名,等價(jià)于(String)?principalCollection.fromRealm(getName()).iterator().next();
50????????String?username?=?(String)super.getAvailablePrincipal(principalCollection);
51????????//到數(shù)據(jù)庫(kù)查是否有此對(duì)象
52????????List?userList?=?userService.findByUsername(username);
53????????System.out.println("----->>userInfo="?+?userList.size());
54????????if?(userList.size()==0)?{
55????????????return?null;
56????????}
57
58????????//賬號(hào)判斷;
59????????//凌海天2017?-11-14?修改
60????????SUser?user=?userList.get(0);
61????????if(user!=null){
62????????????//權(quán)限信息對(duì)象info,用來(lái)存放查出的用戶的所有的角色(role)及權(quán)限(permission)
63????????????SimpleAuthorizationInfo?info=new?SimpleAuthorizationInfo();
64????????????int?id??=?user.getId().intValue();
65????????????//凌海天2017?-11-14?修改
66????????????List?role?=?userService.findByUserid(id);
67????????????for?(Role?r?:role){
68????????????????//用戶的角色集合
69????????????????if(!StringUtils.isEmpty(r.getRole())){
70????????????????????info.addRole(r.getRole());
71????????????????}
72????????????????//用戶的角色對(duì)應(yīng)的所有權(quán)限,如果只使用角色定義訪問(wèn)權(quán)限
73????????????????if(!StringUtils.isEmpty(r.getPermission())){
74????????????????????info.addStringPermission(r.getPermission());
75????????????????}
76????????????}
77????????????//?或者按下面這樣添加
78????????????//添加一個(gè)角色,不是配置意義上的添加,而是證明該用戶擁有admin角色
79//????????????simpleAuthorInfo.addRole("admin");
80????????????//添加權(quán)限
81//????????????simpleAuthorInfo.addStringPermission("admin:manage");
82//????????????logger.info("已為用戶[mike]賦予了[admin]角色和[admin:manage]權(quán)限");
83????????????return?info;
84????????}
85????????//?返回null的話,就會(huì)導(dǎo)致任何用戶訪問(wèn)被攔截的請(qǐng)求時(shí),都會(huì)自動(dòng)跳轉(zhuǎn)到unauthorizedUrl指定的地址
86????????return?null;
87????}
88}
這兩個(gè)類中都用到了PublicMsg類,這個(gè)類里主要設(shè)置的是CAS的服務(wù)端路徑和本項(xiàng)目的對(duì)外路徑,其實(shí)就兩個(gè)參數(shù):
1//CAS服務(wù)器地址
2public?static?final?String?CASServerUrlPrefix?=?"http://xxx.xx.xx.xxx:9092/cas";
3//?當(dāng)前工程對(duì)外提供的服務(wù)地址
4public?static?final?String?SHIROServerUrlPrefix?=?"http://127.0.0.1:9091";讀者可以直接放置到設(shè)置類中,我這里單獨(dú)提出來(lái)是因?yàn)槲业捻?xiàng)目專門(mén)有一個(gè)類管理這些參數(shù)而已。
查看效果
在啟動(dòng)CAS服務(wù)端的情況下,啟動(dòng)本項(xiàng)目,然后再瀏覽器中輸入:
http://localhost:9091
瀏覽器的url路徑會(huì)自動(dòng)轉(zhuǎn)化為:
http://172.18.18.25:9092/cas/login?service=http://127.0.0.1:9091/
這是一個(gè)CAS特有的URL路徑,它的界面如下:

之后在這個(gè)界面登錄正確的用戶名和密碼后,系統(tǒng)會(huì)自動(dòng)跳轉(zhuǎn)到項(xiàng)目的主頁(yè)中去。
獲取用戶信息
在你不在服務(wù)端做任何設(shè)置的默認(rèn)情況下,CAS服務(wù)端只會(huì)給客戶端返回一個(gè)用戶名,比如你的服務(wù)端的用戶名是admin,只要你登錄成功,就會(huì)把服務(wù)端的用戶名傳遞給客戶端,客戶端通過(guò):
1Subject?currentUser?=?SecurityUtils.getSubject();
2String?username?=?currentUser.getPrincipal().toString();
這兩行代碼就可以獲取到登錄用戶的用戶名,然后再通過(guò)自己寫(xiě)的通過(guò)用戶名獲取用戶信息的Service就可以獲取到相關(guān)的用戶信息了,這里應(yīng)該不難理解。
至于獲取用戶的多屬性,就要結(jié)合到之前的服務(wù)端的設(shè)置了,首先你要在服務(wù)端設(shè)置如下參數(shù):
1#多屬性
2cas.authn.attributeRepository.jdbc[0].singleRow=true
3cas.authn.attributeRepository.jdbc[0].order=0
4cas.authn.attributeRepository.jdbc[0].url=jdbc:mysql://172.18.18.25:3306/pa_db?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false
5cas.authn.attributeRepository.jdbc[0].username=username
6cas.authn.attributeRepository.jdbc[0].user=root
7cas.authn.attributeRepository.jdbc[0].password=1234
8cas.authn.attributeRepository.jdbc[0].sql=select?*?from?s_user?where?{0}
9cas.authn.attributeRepository.jdbc[0].dialect=org.hibernate.dialect.MySQLDialect
10cas.authn.attributeRepository.jdbc[0].ddlAuto=none
11cas.authn.attributeRepository.jdbc[0].driverClass=com.mysql.jdbc.Driver
12cas.authn.attributeRepository.jdbc[0].leakThreshold=10
13cas.authn.attributeRepository.jdbc[0].propagationBehaviorName=PROPAGATION_REQUIRED
14cas.authn.attributeRepository.jdbc[0].batchSize=1
15cas.authn.attributeRepository.jdbc[0].healthQuery=SELECT?1
16cas.authn.attributeRepository.jdbc[0].failFast=trueyeshi
以上代碼就允許用戶返回服務(wù)端的s_user 數(shù)據(jù)庫(kù)表中的所有字段,當(dāng)然你再客戶端的寫(xiě)法也要跟著改變:
1AttributePrincipal?principal?=?(AttributePrincipal)?request.getUserPrincipal();
2final?Map?attributes?=?principal.getAttributes();
后記
CAS客戶端的配置差不多就是這樣了,注釋寫(xiě)的都比較明白了,需要注意的坑有以下兩點(diǎn):
設(shè)置類中的Service引入方式
POM.xml中更改組件版本號(hào)一定要剪切黏貼,不要直接修改版本號(hào)
剩下的大家看著文章一步一步的走出來(lái)應(yīng)該問(wèn)題就不大了,下一篇我們講兩個(gè)小的內(nèi)容:
修改CAS服務(wù)端的默認(rèn)登錄頁(yè)
如何登出CAS客戶端
1source:jasoncool.github.io/2017/12/04/Springboot集成Shiro和Cas實(shí)現(xiàn)單點(diǎn)登錄-客戶端篇

喜歡,在看
