shiro認(rèn)證的流程(非常重要)
為什么要重寫realm?如何重寫realm?
在執(zhí)行調(diào)用subject.login(token)方法后,會(huì)把subject以及token都傳進(jìn)去
(subject是從環(huán)境中取出的扑媚,也就是說subject是可以代表當(dāng)前用戶所處的上下文環(huán)境扯饶,也即是說可以拿到當(dāng)前環(huán)境的realm的真實(shí)數(shù)據(jù));
程序會(huì)先判斷賬號(hào)藻雪,再判斷密碼砰琢。
為什么要重寫realm?
因?yàn)樵趙eb開發(fā)中胡桨,我們需要realm內(nèi)的真實(shí)數(shù)據(jù)是我們自己查出來的數(shù)據(jù)庫中的數(shù)據(jù)扣孟,
包含我們自己的邏輯多矮,所以我們需要重寫realm以存放我們的數(shù)據(jù)如何去重寫realm?
Realm的繼承體系
使用AuthorizingRealm來繼承
shrio進(jìn)行認(rèn)證的底層的邏輯主要在realm.doGetAuthenticationInfo(token)中,原生的方法里
(在一開始創(chuàng)建securityManager實(shí)例對(duì)象的時(shí)候哈打,會(huì)將用戶指定的realm(ini方式)加載進(jìn)環(huán)境)
有個(gè)getUser->realm.getUser(upToken.getUsername())(realm在初始化securityManager的時(shí)候就加載進(jìn)內(nèi)存塔逃,所以這里的數(shù)據(jù)源是從環(huán)境中來的)方法來獲得account(這就是一個(gè)AuthenticationInfo),如果此時(shí)account為空,那么就不再判斷密碼而是直接報(bào)錯(cuò)出來料仗,
如果這個(gè)account有值湾盗,那么程序會(huì)拿著這個(gè)account繼續(xù)往下判斷密碼
而我們的邏輯主要在于一開始的數(shù)據(jù)從哪來,怎么進(jìn)行第一步的判斷賬號(hào)
所以 我們重寫realm主要就是重寫doGetAuthenticationInfo(token)方法立轧,在該方法中使用service來從數(shù)據(jù)庫獲得數(shù)據(jù)格粪,并進(jìn)行初次判斷躏吊。
因?yàn)樵膔ealm里doGetAuthenticationInfo(token)方法中只進(jìn)行用戶賬號(hào)的判斷,然后將account(info)返回帐萎,交給后續(xù)程序處理(realm.assertCredentialsMatch(token, info))比伏,這個(gè)info是包含數(shù)據(jù)源中的信息的,相當(dāng)于一個(gè)標(biāo)準(zhǔn)疆导,用來被比較赁项。)
所以在我們重寫的只需要把一個(gè)包含我們doGetAuthenticationInfo(token)方法中,只需要把標(biāo)準(zhǔn)的密碼封裝到一個(gè)info對(duì)象即可澈段,這里自己
new SimpleAuthenticationInfo(employee,employee.getPassword(),getName());將其返回就好悠菜,剩下的交給shrio。
@Autowired
private IEmployeeService employeeService;
/*通過注入的方式給realm設(shè)置憑證匹配器*/
@Autowired
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(credentialsMatcher);
}
//認(rèn)證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
Employee employee = employeeService.getByUsername((String)token.getPrincipal());
if(employee!=null){
return new SimpleAuthenticationInfo(employee,employee.getPassword(), ByteSource.Util.bytes(employee.getName()),getName());
}
return null;
}
- 重寫了realm败富,我們就需要讓shiro知道使用我們自定義的數(shù)據(jù)源
JavaSE:通過ini配置文件告知
JavaEE: 通過spring的配置文件悔醋,在安全管理器中的bean配置
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myWebRealm"/>
</bean>
怎么使用Spring和shrio進(jìn)行認(rèn)證
在JavaSE中,我們只是在做簡單的驗(yàn)證模擬兽叮,這時(shí)我們已經(jīng)是在做登錄操作了芬骄!
那么在JavaEE中,我們要怎么判斷用戶的哪些行為是登錄操作?
- 通過過濾器判斷用戶的哪些行為是登錄操作(即什么時(shí)候來進(jìn)行登錄認(rèn)證)
所有的請(qǐng)求都需要過濾器鹦聪,所以shrio使用的是過濾器(不要認(rèn)為也是攔截器账阻,攔截器是SpringMVC的東西)
那么就像我們以前寫原生的servlet一樣,我們要把filter(那么現(xiàn)在我們需要用到這個(gè)shiroFilter已經(jīng)寫好椎麦,我們直接用)交給tomcat來管理,此時(shí)在web.xml來配置filter(DelegatingFilterProxy),那么現(xiàn)在我們需要用到這個(gè)shiroFilter,顯然材彪,有了Spring观挎,我們不可能自己創(chuàng)建,而且這個(gè)filter也就是單例就好
配置Spring配置文件
把這個(gè)filter交給Spring管理段化,通過ShiroFilterFactoryBean來創(chuàng)建這個(gè)ShiroFilter嘁捷。里面可配置(anon,logout,authc)等多個(gè)過濾器。
配置好了過濾器显熏,再不妨想想在JavaSE中我們是怎么進(jìn)行登錄認(rèn)證的?
我們先是進(jìn)行安全管理器的設(shè)置雄嚣,告知當(dāng)前環(huán)境用的是什么管理器,所以我們也需要在spring中配置安全管理器喘蟆。上面講到缓升,通過源碼分析發(fā)現(xiàn)自定義數(shù)據(jù)源是在創(chuàng)建securityManager實(shí)例對(duì)象時(shí)加載的,所以在spring配置
securityManager的時(shí)候蕴轨,我們要告知安全管理器是用哪個(gè)數(shù)據(jù)源港谊。配置property過濾中執(zhí)行認(rèn)證操作
過濾功能有了,認(rèn)證功能也有了此時(shí)我們就是需要在過濾的時(shí)候進(jìn)行認(rèn)證操作橙弱,也就是結(jié)合這個(gè)兩個(gè)功能歧寺,這是只需要將filter和securityManager配置關(guān)聯(lián)即可
<!--注意:名字必須要和web.xml中配置的名字一致-->
<!-- 定義ShiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/>
<property name="filterChainDefinitions">
<value>
/js/**=anon
/bootstrap-3.3.7-dist/**=anon
/jQuery/**=anon
/images/**=anon
/css/**=anon
/style/**=anon
/logout.do=logout
/**=authc
</value>
</property>
<property name="filters">
<map>
<entry key="authc" value-ref="myCRMFormFilter"/>
</map>
</property>
</bean>
<!-- 配置安全管理器SecurityManager 在web環(huán)境下使用默認(rèn)web安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myWebRealm"/>
</bean>
登錄表單提交的用戶名和密碼名字必須是username和passord燥狰,通過源碼發(fā)現(xiàn)其底層就是req.getParameter("username")
授權(quán)操作
授權(quán)操作的整體實(shí)現(xiàn)和認(rèn)證差不多,需要使用到我們自己的業(yè)務(wù)和數(shù)據(jù)源斜筐,在web環(huán)境下開發(fā)就需要使用我們自己定義的數(shù)據(jù)源龙致,同樣還是繼承AuthorizingRealm,重寫其中的doGetAuthorizationInfo方法
在這里我們不需要判斷權(quán)限顷链,只需要將用戶的權(quán)限和角色查出目代,丟進(jìn)info里(new SimpleAuthorizationInfo())即可
//授權(quán)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//創(chuàng)建一個(gè)空的AuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到當(dāng)前用戶
Employee employee = (Employee)principalCollection.getPrimaryPrincipal();
//判斷是否是超級(jí)管理員
if(employee.getAdmin()){
//給管理員設(shè)置用戶信息,并且可以查詢所有權(quán)限
info.addRole("admin");
info.addStringPermission("*:*");
return info;
}else{
//從數(shù)據(jù)庫中查到所對(duì)應(yīng)的角色和權(quán)限
List<String> roles = employeeService.getRolesByEmployeeId(employee.getId());
Set<String> permissions = employeeService.getPermissionsByEmployeeId(employee.getId());
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
}
怎么去判斷權(quán)限
就像我們之前做RBAC的一樣蕴潦,既然要有權(quán)限控制像啼,那么資源所對(duì)應(yīng)的權(quán)限是我們規(guī)定的。
- 加載權(quán)限
加載權(quán)限也是將Controller中貼有注解(@RequiresPermissions)的方法掃描出來存進(jìn)數(shù)據(jù)庫中潭苞,同樣也是通過注入的方式拿到Spring容器對(duì)象
不同點(diǎn)在于:
使用Shiro時(shí)忽冻,使用容器對(duì)象的getBeansWithAnnotation()獲得的controller是可以包括貼有@Controller注解的類的子類的 (這里的意思是說,Shrio會(huì)自動(dòng)將貼有標(biāo)簽的Controller類動(dòng)態(tài)生成相應(yīng)的代理類此疹,而@Controller這個(gè)注解是沒有繼承的僧诚,但是SpringMVC還是能查找到,并且如果有子類只會(huì)找子類而不會(huì)找其父類) 然后我們要判斷這些字節(jié)碼對(duì)象是否是屬于cglib的代理類 (
AopUtils.isCglibProxy(controller)
)再用這些判斷后的字節(jié)碼對(duì)象獲得其父類字節(jié)碼(
controller.getClass().getSuperclass()
)(@RequiresPermissions標(biāo)簽不繼承)找到貼有這些注解的方法,獲取其方法體上的注解字節(jié)碼然后拿到內(nèi)容,存進(jìn)數(shù)據(jù)庫中
- 取出權(quán)限并存入作用域中
使用Shiro可以很方便的做到這一步蝗碎,
就像上面說的湖笨,只需要通過principalCollection.getPrimaryPrincipal()拿到當(dāng)前身份信息,也就是認(rèn)證過的用戶蹦骑,然后把權(quán)限從數(shù)據(jù)庫查到慈省,丟到info即可
與原來RBAC的不同在于
RBAC需要是用RequestContextHolder.getRequestAttributes()
方法拿到session,然后再丟進(jìn)session眠菇,這樣其實(shí)很麻煩边败。
總的來說
Shiro是先進(jìn)行認(rèn)證,認(rèn)證通過后再
進(jìn)行授權(quán)捎废,什么時(shí)候校驗(yàn)權(quán)限笑窜?訪問方法的時(shí)候,代理類增強(qiáng)的方法會(huì)去檢驗(yàn)
一些配置
- 在認(rèn)證的時(shí)候會(huì)用到MD5加密登疗,因?yàn)榇孢M(jìn)數(shù)據(jù)庫的密碼是通過加密的排截,而用戶登錄表單傳過來的密碼也要相同規(guī)則加密才行,這時(shí)我們?cè)跀?shù)據(jù)源中只將鹽丟進(jìn)info里面辐益,讓shiro后續(xù)進(jìn)行加密即可断傲,那么實(shí)行什么方式的加密是我們配置的,在shiro.xml中配置,然后配置給自定義中的
setCredentialsMatcher
方法
<!--配置憑證匹配器智政,并將其設(shè)置給realm(通過注入的方式)-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
</bean>
- 在使用shiro驗(yàn)證權(quán)限時(shí)艳悔,因?yàn)槭莝hiro自動(dòng)生成的動(dòng)態(tài)代理類,使用的AoP織入女仰,所以我們要加上AoP的配置(事務(wù)時(shí)已經(jīng)加過了),又因?yàn)樵诠δ芗訌?qiáng)中要知道當(dāng)前的安全管理器猜年,才能獲取到數(shù)據(jù)源等信息抡锈,所以我們需要指明數(shù)據(jù)源
<!--配置權(quán)限AoP織入增強(qiáng)功能-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
緩存管理器
使用shiro自帶的緩存管理器,這時(shí)權(quán)限和角色信息(shrio只負(fù)責(zé)這兩塊乔外,所以也只緩存這兩塊床三,不需另外指明)緩存到內(nèi)存中,這樣就不會(huì)刷新頁面訪問同樣的資源時(shí)還要執(zhí)行數(shù)據(jù)源中的授權(quán)方法杨幼,這樣就不用再發(fā)SQL了
<!-- 緩存管理器開始 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManager"/>
</bean>
<bean id="ehCacheManager" class ="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:shiro-ehcache.xml" />
<property name="shared" value="true"/>
</bean>
shiro和freemark的兼容配置(在FreeMark中使用Shiro標(biāo)簽)
要增強(qiáng)FreeMark的功能撇簿,而又要按照FreeMark的規(guī)范,這時(shí)我們可以繼承FreeMark的類再重寫自己的方法差购,這里我們需要繼承FreeMark的配置類四瘫,拓展Shiro的便簽類(記得加依賴)
public class MyCRMFreeMarkerConfig extends FreeMarkerConfigurer {
@Override
public void afterPropertiesSet() throws IOException, TemplateException {
super.afterPropertiesSet();
Configuration cfg = this.getConfiguration();
cfg.setSharedVariable("shiro", new ShiroTags());//shiro標(biāo)簽
}
}
此時(shí)配置文件中就要引入我們自己的FreeMark的配置
<!--配置freeMarker的模板路徑 -->
<bean class="cn.kiring.crm.shiro.MyCRMFreeMarkerConfig">
<!-- 配置freemarker的文件編碼 -->
<property name="defaultEncoding" value="UTF-8" />
<!-- 配置freemarker尋找模板的路徑(相當(dāng)于前綴) -->
<property name="templateLoaderPath" value="/WEB-INF/views/" />
</bean>
<!--freemarker視圖解析器 -->
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<!-- 是否在model自動(dòng)把session中的attribute導(dǎo)入進(jìn)去; -->
<property name="exposeSessionAttributes" value="true" />
<!-- 配置邏輯視圖自動(dòng)添加的后綴名 -->
<property name="suffix" value=".ftl" />
<!-- 配置視圖的輸出HTML的contentType -->
<property name="contentType" value="text/html;charset=UTF-8" />
</bean>
關(guān)于登錄
- 在過濾器的配置中配置loginURL就為我們自己的登錄頁面->login.html,而在我們的登錄頁面上的表單的action也是/login.html,那么為什么是這樣欲逃?
通過查看認(rèn)證過濾器中isLoginSubmission()方法就能發(fā)現(xiàn)找蜜,這個(gè)方法內(nèi)部有進(jìn)行判斷這個(gè)請(qǐng)求是什么方式,如果是POST方式稳析,就會(huì)進(jìn)行登錄驗(yàn)證操作洗做,否則就是普通的訪問這個(gè)資源,又因?yàn)槲覀冊(cè)趕hiro.xml中的過濾器的配置上配置了loginUrl屬性值彰居,所以過濾器不會(huì)攔截這個(gè)請(qǐng)求诚纸。
- 我們的登錄頁面的表單的name一定是"username" 和 "password"
通過查看認(rèn)證過濾器FormAuthenticationFilter里的getUsername()和getPassword()可以發(fā)現(xiàn)shiro底層就是使用request.getParameter("username")和request.getParameter("password")來獲取用戶登錄數(shù)據(jù)并裝進(jìn)token的