shiro 原始碼閱讀心得
有些圖片看不清,GitHub裡面有:github.com/nice01qc/sh… GitHub資源已經上傳
shiro介紹(個人理解)
shiro 就是管理使用者許可權的一個框架。就是用來管理和驗證使用者的角色以及每個角色有哪些許可權,功能很簡單,但是shiro把這個搞成了一個擴充套件性很強的框架,就是驗權和認證的一種規範,具體實現就是配置shiro。
下面是我閱讀過程,和一些小總結,且原始碼可以執行。如果把shiro比作一個武士,SecurityManager就是這個武士的刀,shiro Filter 過濾器就是武士這個人,一個是工具,一個是使用方。可見這兩個方面都很重要,本文提供這兩個方面的解釋和一個原始碼閱讀過程,寫的有些粗糙,但是如果你想閱讀shiro原始碼,GitHub裡面有配置好的原始碼,裡面有現成的springboot例子和一個單點登入模擬。
如果你認真學完這個,將獲得技能有:
- 可以輕鬆解決所有有關shiro的配置(如
springmvc
怎麼配置shiro,springboot
怎麼配置shiro,單點登入怎麼配置........) - 對許可權管理系統會有進一步理解
- 加深對框架設定本身的理解
專案整體介紹:
整個專案克隆自shiro官方倉庫,只是在子專案 samples
裡面添加了2個小專案,用於debug。
-
shiro_learn(專案根路徑)
- samples
- shiro_cas_service 使用springboot簡單模擬cas服務(通過debug就可以完全瞭解cas服務請求過程)
- shiro_client 原始碼分析開始的地方,shiro大部分配置已經配置好了
- samples
本文將從三個個方面進行分析:
- shiro配置(很全的)
- Shiro web 過濾器 載入過程
- 完整分析一個shiro cas請求
shiro 配置
上圖便是shiro所有功能展示圖,從上圖可以看出主要分為2大塊,
一塊是Security Manager
,這個模組及授權驗證於一身(對應類為:SecurityManager
的子類),並通過Subject介面對外提供服務,例如登入、驗權等等使用subject就好了(具體使用,後面會詳細分析);
另一塊就是使用方,就是SecurityManager
已經配置好了,第三方應該如何使用呢,本文以Web MVC
這個第三方來敘述,Web MVC
SecurityManager
提供的 Subject
介面 進行相應的操作。
SecurityManager
配置
通過上圖可以看出DefaultSecurityManager可以配置的屬性有哪些都可以看出來,下面通過表格一一說明:
屬性 | 是否存在預設配置 | 預設配置類 | 作用說明 | 常用配置 |
---|---|---|---|---|
subjectFactory | 是 | DefaultSubjectFactory | Subject的具體實現類(對外服務介面,如果是cas服務,建議使用CasSubjectFactory覆蓋) | 否 |
subjectDAO | 是 | DefaultSubjectDAO | 主要用於將subject中最新資訊儲存到session裡面 | 否 |
rememberMeManager | 只存在DefaultWebSecurityManager中 | CookieRememberMeManager | 用於管理rememberMe這個cookie,一般不用 | 否 |
sessionManager | 是 | DefaultSessionManager (DefaultWebSecurityManager 就是DefaultWebSessionManager) | 有關session的操作最終都會委託給他做(他本身還可以配置,見下表) | 是 |
authorizer | 是 | ModularRealmAuthorizer | 授權策略(多個realm時,可以設定自己的策略) | 否 |
authenticator | 是 | ModularRealmAuthenticator | 認證策略(多個realm時可以設定自己的策略) | 是 |
realm | 否 | CasRealm、JdbcRealm ...... | 落實認證和授權操作,需要自己配置(後面會舉個例子細講) | 是 |
cacheManager | 否 | 使用者最好繼承AbstractCacheManager這個抽象類(支援shiro預設週期管理) | 在realm認證和授權的時候會用到(相當於加了一層快取,cas認證就不需要了,如果是使用者名稱密碼,可以使用,增加認證授權速度) | 否(cas就不需要) |
DefaultSecurityManager的sessionManager (DefaultSessionManager)屬性配置:
屬性 | 是否存在預設配置 | 預設配置類 | 作用說明 | 常用配置 |
---|---|---|---|---|
sessionFactory | 是 | SimpleSessionFactory | 用於建立Session的,一般不配置 | 否 |
sessionDAO | 是 | MemorySessionDAO | 用於儲存Session的介面,一般通過繼承AbstractSessionDAO類,並使用Redis重新配置一個,將Session儲存在redis裡面(此抽象類可以配置自己的SessionIdGenerator => 用於產生session id的) | 是 |
casheManager | 否 | 可以跟實現了CashManagerAware 這個類 的sessionDAO 聯合使用,一般就配置sessionDAO就完事 | 否 | |
sessionIdCookie | 是 | new SimpleCookie("JSESSIONID"); | 這個存在於本類的子類DefaultWebSessionManager中,一般都重新配置,這樣cookie名字可以改成自己的(一般有關cookie底層的操作都委託給他來做,例如讀取此cookie的值,配置cookie ......) | 是 |
注:這些屬性通過物件的set方法都可以設定
Web MVC
過濾器配置
DefaultWebSecurityManager 跟DefaultSecurityManager 配置一樣,以下說的securityManager 就是指DefaultWebSecurityManager 。
現在DefaultWebSecurityManager 已經配置好了,這個東西應該放在那裡呢,肯定得應用到shiro過濾器裡面去。
shiro 過濾器相關配置存放在ShiroFilterFactoryBean
這個類裡面,然後使用DelegatingFilterProxy
將ShiroFilterFactoryBean
注入到web容器裡。
- 如果是springmvc,直接在web.xml內配置這個
DelegatingFilterProxy
就好,過濾器名字就是ShiroFilterFactoryBean
這個bean的名字; - 如果是springboot,使用springboot的
FilterRegistrationBean
進行註冊就好(servlet3.0可以使用ServletContext進行註冊,可以註冊servlet、filter......) => 後續會細講
ShiroFilterFactoryBean
,通過名字就可以看出來,他不是真正的Filter,他使用來配置和管理shirofilter的。
從上圖可以看出來,通過FactoryBean 來返回SpringShiroFilter這個物件。ShiroFilterFactoryBean
配置說明如下:
屬性 | 作用說明 |
---|---|
securityManager |
shiro核心 ...... |
filters (Map<String,Filter>) |
將自定義的shiro Filter都放在這個map裡面,一般與下面這個定義聯合使用 |
filterChainDefinitionMap (Map<String,String>) |
Map<請求url表示式,用到的過濾器名字(多個用逗號隔開)> (原文解釋:urlPathExpression_to_comma-delimited-filter-chain-definition) |
loginUrl |
登入URL |
successUrl |
登入成功後跳轉的URL(一般不會使用這個跳轉,而使使用第一次訪問時儲存的url) |
unauthorizedUrl |
授權不成功跳轉的URL |
以上所有配置例子 放在 shiro_learn/samples/shiro_clien/src/main/java/com/nice01qc/config/shiro/ShiroCasConfig.java
這個類裡面,可以對應著看。
Shiro web 過濾器 載入過程
這個載入過程其實就是 ShiroFilterFactoryBean 載入過程
現在securityManager已經手動配置好了,沒什麼好說的。let's coding .......,所有原始碼,只指出比較關鍵的節點,具體細節自己落實。
ShiroFilterFactoryBean.java 載入過程
public class ShiroFilterFactoryBean implements FactoryBean,BeanPostProcessor {
// 就是從這個地方開始(如果不知道,請搜尋 FactoryBean的作用)
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance(); //直接看這個就好
}
return instance;
}
protected AbstractShiroFilter createInstance() throws Exception {
SecurityManager securityManager = getSecurityManager();
// 封裝Filter呼叫管理,繼續深入
FilterChainManager manager = createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//此處建立真正的 總覽全域性的 shiro filter
return new SpringShiroFilter((WebSecurityManager) securityManager,chainResolver);
}
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// 獲取預設Filter ,取自DefaultFilter列舉類(共12個)
Map<String,Filter> defaultFilters = manager.getFilters();
// 將loginUrl、successUrl、unauthorizedUrl填充到符合要求的 filter 內
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
//這是你自己定義的Filter
Map<String,Filter> filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry<String,Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter); // 填充一波
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
//'init' argument is false,since Spring-configured filters should be initialized
//in Spring (i.e. 'init-method=blah') or implement InitializingBean:
manager.addFilter(name,filter,false);
}
}
//build up the chains:
Map<String,String> chains = getFilterChainDefinitionMap(); // filterChainDefinitionMap
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String,String> entry : chains.entrySet()) {
// 例如 filterChainDefinitionMap.put("/index","authc[config1,config2]");
String url = entry.getKey(); // url 就是 "/index"
String chainDefinition = entry.getValue(); // "authc[config1,config2]"
// 會將 chainDefinition的“[config1,config2]” 解析後封裝在 authc對應的filter內部,後續會用到
manager.createChain(url,chainDefinition);
}
}
return manager;
}
// 填充那三個值的地方
private void applyGlobalPropertiesIfNecessary(Filter filter) {
applyLoginUrlIfNecessary(filter);
applySuccessUrlIfNecessary(filter);
applyUnauthorizedUrlIfNecessary(filter);
}
private void applyLoginUrlIfNecessary(Filter filter) {
String loginUrl = getLoginUrl();
if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
AccessControlFilter acFilter = (AccessControlFilter) filter;
String existingLoginUrl = acFilter.getLoginUrl();
if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
acFilter.setLoginUrl(loginUrl);
}
}
}
}
複製程式碼
以上就是ShiroFilterFactoryBean類初始化過程,後續DelegatingFilterProxy
通過getBean("ShiroFilterFactoryBean的bean name") 來獲取這個bean,並將請求委託給他。
shiro Filter
繼承體系詳細介紹
在繼續深入之前,先介紹下shiro filter的特色,就是你想實現不同功能的filter,通過繼承shiro自帶的Filter就好,然後在這基礎之上再做修改。
以上每一個抽象類都有不同的作用,分工明確,下面一個一個來分析(上面的loginUrl在上面初始化的時候就注入進去了):=> 這個很關鍵
AbstractFilter.java
// 實現了Filter的init介面,並對外暴露了onFilterConfigSet 介面
public abstract class AbstractFilter extends ServletContextSupport implements Filter {
// 實現了Filter的init介面,並對外暴露了onFilterConfigSet 介面,這個介面在AbstractShiroFilter中覆蓋了這個方法,其中AbstractShiroFilter 是 SpringShiroFilter的父類喔,SpringShiroFilter的父類喔,SpringShiroFilter的父類喔
public final void init(FilterConfig filterConfig) throws ServletException {
setFilterConfig(filterConfig);
try {
onFilterConfigSet();
} catch (Exception e) {
}
}
public void destroy() {
}
}
複製程式碼
NameableFilter.java
public abstract class NameableFilter extends AbstractFilter implements Nameable {
// 設定filter 名字用的
public void setName(String name) {
this.name = name;
}
}
複製程式碼
OncePerRequestFilter.java
public abstract class OncePerRequestFilter extends NameableFilter {
// 保證每次請求只訪問一次
public final void doFilter(ServletRequest request,ServletResponse response,FilterChain filterChain) throws ServletException,IOException {
// 檢視是否已經訪問過一次
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
// 如果訪問了一次,則跳過這個filter,繼續下一個
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request,response);
} else if (!isEnabled(request,response) || shouldNotFilter(request) ) {
filterChain.doFilter(request,response);
} else {
// 沒有訪問,現在標記
request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);
try {
// 執行本filter 內容
doFilterInternal(request,response,filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
// 子類需要實現的介面,不需要實現doFilter這個方法,通過實現這個方法,可以幹更多事
protected abstract void doFilterInternal(ServletRequest request,FilterChain chain) throws ServletException,IOException;
}
複製程式碼
AdviceFilter.java
public abstract class AdviceFilter extends OncePerRequestFilter {
// 實現了OncePerRequestFilter這個方法,在這個方法有點像aop的風格
public void doFilterInternal(ServletRequest request,FilterChain chain)
throws ServletException,IOException {
try {
// 在執行之前,先執行 preHandle 方法
boolean continueChain = preHandle(request,response);
// 如果 preHandle 沒通過,將不再繼續往下執行
if (continueChain) {
executeChain(request,chain);
}
// 執行之後再執行的方法
postHandle(request,response);
} catch (Exception e) {
exception = e;
} finally {
cleanup(request,exception);
}
}
// 將這個方法暴露出去
protected boolean preHandle(ServletRequest request,ServletResponse response) throws Exception {
return true;
}
// 將這個方法也暴露出去
protected void postHandle(ServletRequest request,ServletResponse response) throws Exception {
}
}
複製程式碼
PathMatchingFilter.java
// 用於判斷請求是否符合本 Filter,只有請求URL 跟本Filter對應的url匹配規則對上
public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
// ShiroFilterFactoryBean 初始化時就填充進去了,
// 裡面value就是那個autho[config1,config2] 中這個[config1,config2]陣列
protected Map<String,Object> appliedPaths = new LinkedHashMap<String,Object>();
// 覆蓋了父類的preHandle方法,首先判斷請求url是否匹配 本Filter的appliedPaths
protected boolean preHandle(ServletRequest request,ServletResponse response) throws Exception {
// 如果Filter本身沒有匹配url,返回true
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
return true;
}
for (String path : this.appliedPaths.keySet()) {
//(first match 'wins'):
if (pathsMatch(path,request)) {
Object config = this.appliedPaths.get(path);
// 如果匹配上了就執行這個方法,它會進一步交給onPreHandle方法,並對外暴露這個方法
return isFilterChainContinued(request,path,config);
}
}
//no path matched,allow the request to go through:
return true;
}
private boolean isFilterChainContinued(ServletRequest request,String path,Object pathConfig) throws Exception {
if (isEnabled(request,pathConfig)) { //isEnabled check added in 1.2
return onPreHandle(request,pathConfig);
}
return true;
}
// 對外暴露此介面
protected boolean onPreHandle(ServletRequest request,Object mappedValue) throws Exception {
return true;
}
}
複製程式碼
AccessControlFilter.java
(如果你要驗證授權什麼的,這個類還是比較關鍵的)
// 此介面用於判斷請求是否可以通過,不通過就跳登入,符合要求就使用subject進行登入 等等
public abstract class AccessControlFilter extends PathMatchingFilter {
// 覆蓋父類PathMatchingFilter 暴露的方法,並在方法內添加了兩種方法
// 一個是isAccessAllowed,用於判斷是否已經驗證過了,例如使用者已經登入過了有session
// 另一個是 onAccessDenied 驗證失敗,失敗後幹嘛,鬼知道,你看他怎麼實現的
public boolean onPreHandle(ServletRequest request,Object mappedValue) throws Exception {
return isAccessAllowed(request,mappedValue) || onAccessDenied(request,mappedValue);
}
// 直接暴露給外界,自己看著實現吧,可別把onAccessDenied的活也幹了就好了
protected abstract boolean isAccessAllowed(ServletRequest request,Object mappedValue) throws Exception;
// 這個老哥,提供了兩個方式供外界覆蓋,也就是方法 過載,就是看你要不要那個引數
protected boolean onAccessDenied(ServletRequest request,Object mappedValue) throws Exception {
return onAccessDenied(request,response);
}
// .......
protected abstract boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception;
// 判斷是不是登入請求
protected boolean isLoginRequest(ServletRequest request,ServletResponse response) {
return pathsMatch(getLoginUrl(),request);
}
// 儲存請求並重定向到登入頁面
protected void saveRequestAndRedirectToLogin(ServletRequest request,ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request,response);
}
protected void saveRequest(ServletRequest request) {
WebUtils.saveRequest(request);
}
protected void redirectToLogin(ServletRequest request,ServletResponse response) throws IOException {
String loginUrl = getLoginUrl();
WebUtils.issueRedirect(request,loginUrl);
}
}
複製程式碼
以上做個小總結:
- 如果你就想寫個簡單的Filter,請直接實現Filter介面
- 如果你想給Filter搞個名字,請繼承NameableFilter抽象類
- 如果你想保證你的filter只被呼叫了一次,請繼承OncePerRequestFilter抽象類
- 如果你想你的filter在dofilter 方法前後(類似這個方法的aop),請繼承AdviceFilter(看著這個Advice是不是特別熟悉-----spring aop也有advice這個概念)
- 如果你想寫個驗證和授權的Filter,請繼續往下看,因為有兩種實現了AccessControlFilter的類(輪子已經建好,上車吧)
一種是 認證類的(Authenticate):
AuthenticationFilter.java
(一般以ion結尾的單詞類,一般提供很基礎的服務)
public abstract class AuthenticationFilter extends AccessControlFilter {
// 提供基礎的驗證
protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
Subject subject = getSubject(request,response);
return subject.isAuthenticated() && subject.getPrincipal() != null;
}
protected void issueSuccessRedirect(ServletRequest request,ServletResponse response) throws Exception {
// 通過驗證後,會被重定向到自己設定好的url,這個可以改
WebUtils.redirectToSavedRequest(request,getSuccessUrl());
}
}
複製程式碼
AuthenticatingFilter.java
(真正幹活的,以後認證什麼的繼承他就好了,稍微修改就好了)
public abstract class AuthenticatingFilter extends AuthenticationFilter {
// 覆蓋了,父類的方法,並在這個方法內部添加了vip功能(可以使用isPermissive走vip通道)
// 如果是 AuthorizationFilter 的這個方法,mappedValue就是用於角色驗證了,預設是所有角色都必須通過,可以覆蓋這個方法
@Override
protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
return super.isAccessAllowed(request,mappedValue) ||
(!isLoginRequest(request,response) && isPermissive(mappedValue));
}
// 這是vip模板,僅僅是vip,普通乘客就別走這個了
protected boolean isPermissive(Object mappedValue) {
if(mappedValue != null) {
String[] values = (String[]) mappedValue;
return Arrays.binarySearch(values,PERMISSIVE) >= 0;
}
return false;
}
// 這個方法一般提供給onAccessDenied 方法的,就是你沒通過認證,應該登入認證一波了
protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request,response);
if (token == null) {
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request,response);
subject.login(token);
return onLoginSuccess(token,subject,request,response);
} catch (AuthenticationException e) {
return onLoginFailure(token,e,response);
}
}
// 對外暴露介面,用於生成token,因為登入必須拿著token去登入,預設提供了兩種生成token的方法,就在這個方法下面喔
protected abstract AuthenticationToken createToken(ServletRequest request,ServletResponse response) throws Exception;
}
複製程式碼
**最後來一個例子: **
一個shiro Filter
示例介紹
CasFilter.java (來看看cas他幹了什麼)
public class CasFilter extends AuthenticatingFilter {
// 直接覆蓋父類這個方法,意思就是,你竟然遇到了我,那你就是沒有認證(需要被安排下)
@Override
protected boolean isAccessAllowed(ServletRequest request,Object mappedValue) {
return false;
}
// 妥妥的實現這個方法,帶我去登入吧,我準備好了
@Override
protected AuthenticationToken createToken(ServletRequest request,ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String ticket = httpRequest.getParameter(TICKET_PARAMETER);
return new CasToken(ticket);
}
// 去吧皮卡丘,送你去登入
@Override
protected boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception {
// 所以casFilter 直接執行這一步,前面那些方法都沒起作用
return executeLogin(request,response);
}
}
複製程式碼
授權類就不說了(同上),下面是上面Filter的一些預設實現(DefaultFilter列舉類中可以看到)(是不是很眼熟)
filter名字 | 對應的Filter |
---|---|
anon |
AnonymousFilter.java |
authc |
FormAuthenticationFilter.java |
authcBasic |
BasicHttpAuthenticationFilter.java |
authcBearer |
BearerHttpAuthenticationFilter.java |
logout |
LogoutFilter.java |
noSessionCreation |
NoSessionCreationFilter.java |
perms |
PermissionsAuthorizationFilter.java |
port |
PortFilter.java |
rest |
HttpMethodPermissionFilter.java |
roles |
RolesAuthorizationFilter.java |
ssl |
SslFilter.java |
user |
UserFilter.java |
通過以上Filter的瞭解,你是否已經瞭解shiro系Filter,如果不理解,請再看一遍(這是死迴圈判斷,除非已經看懂,否則不會break;)容器初始化講完了,是時候來個請求了。
完整分析一個shiro 請求
那就從請求被Filter攔截那個地方說起吧,到底被誰攔截了,是DelegatingFilterProxy這個類,好了,就在這個類的doFilter方法上打個斷點(打斷點前順便看下Filter的init() 方法吧)let's begin ......
DelegatingFilterProxy.java (請求開始的地方)
public class DelegatingFilterProxy extends GenericFilterBean {
// 這個方法是在Filter init時呼叫的,在啟動服務過程就會呼叫,他會初始化delegate,而這個delegate就是ShiroFilterFactoryBean
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified,use filter name.
// 什麼是targetBeanName 見 DelegatingFilterProxy建構函式
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac); // 直接看這個
}
}
}
}
// 看這個就好了
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
// 直接通過ApplicationContex直接getBean了
Filter delegate = wac.getBean(targetBeanName,Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
// targetBeanName的由來
public DelegatingFilterProxy(String targetBeanName,@Nullable WebApplicationContext wac) {
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
// doFilter 在這裡,來吧!!! 在這裡,來吧!!! 在這裡,來吧!!!
@Override
public void doFilter(ServletRequest request,FilterChain filterChain)
throws ServletException,IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
// 直接去這個方法看吧
invokeDelegate(delegateToUse,filterChain);
}
// 啥也不幹,直接就拋給了ShiroFilterFactoryBean的SpringShiroFilter這個內部類
protected void invokeDelegate(
Filter delegate,ServletRequest request,IOException {
delegate.doFilter(request,filterChain);
}
}
複製程式碼
SpringShiroFilter
ShiroFilterFactoryBean$SpringShiroFilter
的父類OncePerRequestFilter.java
(以下便是SpringShiroFilter
使命)
public abstract class OncePerRequestFilter extends NameableFilter {
// 跳到這裡來了
public final void doFilter(ServletRequest request,FilterChain filterChain) {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request,response);
} else {
request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);
try {
// 直接看看覆蓋了這個方法的類吧
doFilterInternal(request,filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
}
複製程式碼
SpringShiroFilter
的父類AbstractShiroFilter.java
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
// 此處是真正的實現
protected void doFilterInternal(ServletRequest servletRequest,ServletResponse servletResponse,final FilterChain chain)throws ServletException,IOException {
try {
final ServletRequest request = prepareServletRequest(servletRequest,servletResponse,chain);
final ServletResponse response = prepareServletResponse(request,chain);
// 在此處建立了 subject喔,記住了喔!!!
final Subject subject = createSubject(request,response);
//noinspection unchecked
// subject建立好,並將下面兩個方法封裝在Callable()裡面再執行,
// 在執行這個call之前,先將subject繫結到當前執行緒,執行完後,清理當前執行緒的繫結
// 為什麼非要搞個Callable,直接在 這兩個方法前後放兩個方法就好了,可能是因為這樣擴充套件性更強
// 以後在外面再封裝一層也方便,說不定還可以搞非同步???
subject.execute(new Callable() {
public Object call() throws Exception {
// 更新session時間
updateSessionLastAccessTime(request,response);
//************************************************
// 執行shiro過濾鏈,(先講subject建立過程吧,後續再講)
//************************************************
executeChain(request,chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
}
}
//*****************************************
// 暫時記這個建立過程為subject建立過程
//*****************************************
// subjcet 建立過程交給了父類Subject$Builder了,並送了他一個securityManager
protected WebSubject createSubject(ServletRequest request,ServletResponse response){
return new WebSubject.Builder(getSecurityManager(),response).buildWebSubject();
}
}
//==============================================================>
// 簡單展示,具體自己debug看看
public class SubjectCallable<V> implements Callable<V> {
public V call() throws Exception {
try {
// 繫結到當前執行緒
threadState.bind();
// 執行自己實現的那個call方法
return doCall(this.callable);
} finally {
// 清除資料
threadState.restore();
}
}
}
複製程式碼
subject建立過程
Subject$Builder
(Subject
內部靜態類Build
)
public interface Subject {
public static class Builder {
// 從這兒可以看出,最終委託給了SecurityManager來幹這個
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
}
}
複製程式碼
DefaultSecurityManager.java
(以下討論的都本類和本類的父類體系內喔)
public class DefaultSecurityManager extends SessionsSecurityManager {
// 在subjcetContext基礎上重新new一個喔,不影響前面的subjectContext
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
// 就是一個Map,裡面儲存了很多驗證過程的東西,例如是否認證,是否是remember等等,
// 在DefaultSubjectContext類裡面可以看到,有:securityManager,sessionId,authenticationToken,authenticationInfo,principals,session,authenticated,host,sessionCreationEnabled,principalsSessionKey,authenticatedSessionKey
// 就是一個臨時狀態和工具集合地,如果是DefaultWebSecurityManager就建立一個
// DefaultWebSubjectContext例項,實際上本文討論的就是web
SubjectContext context = copy(subjectContext);
//確保SecurityManager 已經放到context裡面去了
context = ensureSecurityManager(context);
// 這個最關鍵,也是最複雜一個,也不復雜,就深入了幾個類,這也是下面要講的重點
// step0.........
context = resolveSession(context);
// 這個嘛,等會兒說
context = resolvePrincipals(context);
// 前戲已經準備好了,改開始建立Subject了
Subject subject = doCreateSubject(context);
// 這個會把當前Subject中最新的資訊同步到session裡面,還有其他功能,後續可以深入看看
save(subject);
return subject;
}
// step1,去解決session
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
return context;
}
try {
// 直接看resolveContextSession
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
}
return context;
}
// step2,開始了
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
// 先看看context裡面有沒有,有的話就不用繼續找了,省時間,沒有的話,就將request和response包裝到SessionKey裡面
SessionKey key = getSessionKey(context);
if (key != null) {
// 現在真正開始了,但這事得分工,直接交給專門管理session的父類 SessionsSecurityManager(程式碼裡面的啃老族,把活全交給父類幹,很正常喔)
return getSession(key);
}
return null;
}
}
複製程式碼
SessionsSecurityManager.java
(所有session有關操作,由他管理)
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
// step3. 看到這裡,發現SecurityManager整體是不會幹活的,就管著整個流程,然後分發出去
public Session getSession(SessionKey key) throws SessionException {
// 這個sessionManger,如果你不覆蓋它,預設就是DefaultSessionManager這個類,我們就從這個開始吧
return this.sessionManager.getSession(key);
}
}
複製程式碼
AbstractNativeSessionManager.java
(DefaultSessionManager
父類)
public abstract class AbstractNativeSessionManager extends AbstractSessionManager implements NativeSessionManager,EventBusAware {
// step4. 別急,漸漸開始了
public Session getSession(SessionKey key) throws SessionException {
Session session = lookupSession(key);
return session != null ? createExposedSession(session,key) : null;
}
// step5. 來了
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
}
// 看到do開頭方法,就知道,開始真正幹活了
return doGetSession(key);
}
}
複製程式碼
AbstractValidatingSessionManager.java
(DefaultSessionManager
父類)
public abstract class AbstractValidatingSessionManager extends AbstractNativeSessionManager implements ValidatingSessionManager,Destroyable {
// step6. 開始了
@Override
protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
// 驗證session的有效性,一般不啟動這個分方法(感覺redis可以設定時間控制有效性,可以不啟動驗證)
enableSessionValidationIfNecessary();
// 直接看這個吧,這個直接跳到子類DefaultSessionManager中去了,go
Session s = retrieveSession(key);
if (s != null) {
validate(s,key);
}
return s;
}
}
複製程式碼
DefaultSessionManager.java
public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
// step7. 繼續
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
// 先取sessionId
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
return null;
}
// 通過sessionId來取session
Session s = retrieveSessionFromDataSource(sessionId);
if (s == null) {
throw new UnknownSessionException(msg);
}
return s;
}
// step8. 開始找sessionId
@Override
public Serializable getSessionId(SessionKey key) {
// 檢視key中是否就儲存了這個sessionId(shiro到處搞引用快取,真幾把繞)
Serializable id = super.getSessionId(key);
if (id == null && WebUtils.isWeb(key)) {
ServletRequest request = WebUtils.getRequest(key);
ServletResponse response = WebUtils.getResponse(key);
// 繼續從這兒開始
id = getSessionId(request,response);
}
return id;
}
// 到這裡了,繼續
protected Serializable getSessionId(ServletRequest request,ServletResponse response) {
return getReferencedSessionId(request,response);
}
// step9. 這裡就找完就結束了,沒找到就沒找到了,開始回到step7,假定已經找到了,ok,繼續
private Serializable getReferencedSessionId(ServletRequest request,ServletResponse response) {
// 這個直接從cookie裡面讀,這個過程建議自己debug進去看看,我感覺挺重要的,也很簡單,我就不寫了
String id = getSessionIdCookieValue(request,response);
if (id != null) {
// 儲存到request裡面去
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
} else {
// 如果沒找到就從請求引數裡面找,這個請求規則是這樣的:http:localhost:8001?;ShiroHttpSession.DEFAULT_SESSION_ID_NAME=sessionId(熟稱shiro小尾巴,真心不好看)
//try the URI path segment parameters first:
id = getUriPathSegmentParamValue(request,ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
if (id == null) {
//not a URI path segment parameter,try the query parameters:
String name = getSessionIdName();
id = request.getParameter(name);
if (id == null) {
//try lowercase:
// 還沒找到,那就從request請求引數裡面去找(所以就算瀏覽器存不了cookie,那隻能儲存到請求引數裡面了)
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
}
}
// 把剛獲取到的結果都放在request裡面快取起來
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
}
// always set rewrite flag - SHIRO-361
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED,isSessionIdUrlRewritingEnabled());
return id;
}
// step10. 從你自己設定的儲存來獲取session,如果是redis,就從redis裡面獲取,就到這個了,剩下的自己看吧
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return sessionDAO.readSession(sessionId);
}
}
複製程式碼
到這一步,resolveSession(context)
這個方法已經完成,只剩下doCreateSubject(context)
和save(subject)
public class DefaultSecurityManager extends SessionsSecurityManager {
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext);
context = ensureSecurityManager(context);
context = resolveSession(context);
context = resolvePrincipals(context);
// 來這個很簡單
Subject subject = doCreateSubject(context);
save(subject);
return subject;
}
// 從方法就看出來,最終使用專門的subjectFactory來建立Subject,本文都在講web
// 所以預設是 DefaultWebSubjectFactory這個工廠方法
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
}
複製程式碼
DefaultWebSubjectFactory.java
public class DefaultWebSubjectFactory extends DefaultSubjectFactory {
public Subject createSubject(SubjectContext context) {
boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) {
return super.createSubject(context);
}
// 從context這個儲存裡面取值了
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals(); //
boolean authenticated = wsc.resolveAuthenticated(); // 是否認證了
String host = wsc.resolveHost();
// 還有request和response,是不是subject存了很多,但你卻基本上沒用過,沒事別亂搞事喔
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
// 建立一個真正的Subject
return new WebDelegatingSubject(principals,sessionEnabled,securityManager);
}
}
複製程式碼
以上就是Subject建立過程,如果有session就填充進去,沒有就不填充,但是Subject必須創建出來。好現在讓我們回到AbstractShiroFilter這個類,繼續看doFilterInternal這個方法,前戲已經足夠充分了,改執行shiro 過濾鏈了。來,come on ...... 馬上要講完了
AbstractShiroFilter.java (shiro filter開始分發執行了)
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
protected void doFilterInternal(ServletRequest servletRequest,final FilterChain chain)
throws ServletException,IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest,chain);
// 已經完成
final Subject subject = createSubject(request,response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request,response);
// 來,come on
executeChain(request,chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
}
if (t != null) {
throw new ServletException(msg,t);
}
}
// 在這裡
protected void executeChain(ServletRequest request,FilterChain origChain)
throws IOException,ServletException {
// 這個作用就是根據請求的URL,
// 從 Map<String,String>這個對映中找出第一個匹配的value,以及對應的filtes,對映長這麼樣
// filterChainDefinitionMap.put("/login","casFilter");
// filterChainDefinitionMap.put("/favicon.ico","anon");
// filterChainDefinitionMap.put("/**/*.html","anon");
// filterChainDefinitionMap.put("/**","authc,anon"); 這就有2個喔,也就是chain中有兩個filter
FilterChain chain = getExecutionChain(request,origChain);
// 這個chain就是ProxiedFilterChain(web下就是這個喔),走,去這個類看看,功能很簡單
chain.doFilter(request,response);
}
}
複製程式碼
ProxiedFilterChain.java
(多個filter時,執行策略)
public class ProxiedFilterChain implements FilterChain {
// 執行過濾鏈策略,其實就是把當前chain,當成所有filter的chain,使用本地index變數來確定下一個要執行的filter
public void doFilter(ServletRequest request,ServletResponse response) throws IOException,ServletException {
if (this.filters == null || this.filters.size() == this.index) {
this.orig.doFilter(request,response);
} else {
this.filters.get(this.index++).doFilter(request,this);
}
}
}
複製程式碼
到現在shiro已經講了一大半了,還剩下實際執行一個filter過程,我就拿casFilter來講吧。
shiro cas 請求時序圖
從圖中可以看出來整個認證過程,我就直接將casFilter.java,這個是使用者拿到了那個token,然後向伺服器發起了請求,現在CasFilter.java的doFilter攔截到他了,來是時候做個了斷了(下面用的Filter抽象類,很熟悉吧,前面講過喔):
OncePerRequestFilter.java(CasFilter的父類)
public abstract class OncePerRequestFilter extends NameableFilter {
public final void doFilter(ServletRequest request,FilterChain filterChain){
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request,response);
} else
if (!isEnabled(request,response) || shouldNotFilter(request) ) {
filterChain.doFilter(request,response);
} else {
request.setAttribute(alreadyFilteredAttributeName,Boolean.TRUE);
try {
// 直接看這個吧,我直接跳到CasFilter的onAccessDenied方法吧,
// why => 前面這塊講得真的很清楚喔
// 不知道的請跳到前面講 shiro filter那塊
doFilterInternal(request,filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
}
複製程式碼
CasFilter.java
public class CasFilter extends AuthenticatingFilter {
// 前面那些步驟啥也沒幹,直到這裡開始幹活了
protected boolean onAccessDenied(ServletRequest request,ServletResponse response) throws Exception {
// 這個方法是父類AuthenticatingFilter的,走去這裡
return executeLogin(request,response);
}
}
複製程式碼
AuthenticatingFilter.java
public abstract class AuthenticatingFilter extends AuthenticationFilter {
protected boolean executeLogin(ServletRequest request,ServletResponse response) throws Exception {
// 取出請求引數裡面的token,幷包裝成casToken
AuthenticationToken token = createToken(request,response);
try {
// 看到這裡清楚了吧,使用SecurityManager提供的介面,開始驗證了喔
Subject subject = getSubject(request,response);
// 登入,走起
subject.login(token);
return onLoginSuccess(token,response);
}
}
}
複製程式碼
DelegatingSubject.java
public class DelegatingSubject implements Subject {
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
// 直接看這個吧,登入操作肯定交給securityManager了
// step1. 開始的地方
Subject subject = securityManager.login(this,token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
}
複製程式碼
DefaultSecurityManager.java
public class DefaultSecurityManager extends SessionsSecurityManager {
public Subject login(Subject subject,AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
// step2. 為託給了父類 AuthenticatingSecurityManager
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
// 失敗後,估計就跳轉登入了
onFailedLogin(token,ae,subject);
} catch (Exception e) {
}
throw ae; //propagate
}
// 認證成功,重新封裝subject
Subject loggedIn = createSubject(token,info,subject);
// 這個跟rememberMe有關
onSuccessfulLogin(token,loggedIn);
return loggedIn;
}
}
複製程式碼
AuthenticatingSecurityManager.java
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
// step3. 繼續委託給Authenticator,如果你沒配置,預設就是ModularRealmAuthenticator
// 本文還是重新配置了(建議多個realm時必須重新配置這個,
// 除非你的認證策略跟ModularRealmAuthenticator一樣)
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
// 直接進入
return this.authenticator.authenticate(token);
}
}
複製程式碼
AbstractAuthenticator.java
public abstract class AbstractAuthenticator implements Authenticator,LogoutAware {
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
AuthenticationInfo info;
try {
// step4. do開頭說明真的開始了,
// 本文是自己實現的Authenticator(MyModularRealmAuthenticator),去這裡
info = doAuthenticate(token);
if (info == null) {
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
ae = new AuthenticationException(msg,t);
}
try {
notifyFailure(token,ae);
} catch (Throwable t2) {
}
throw ae;
}
notifySuccess(token,info);
return info;
}
}
複製程式碼
MyModularRealmAuthenticator.java
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
// 當有多個realm時,應該如何使用,本文策略就是:如果是castoken就讓他走casRealm,其他的走單個認真方式
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 所有Realm
Collection<Realm> realms = getRealms();
HashMap<String,Realm> realmHashMap = new HashMap<>(realms.size());
for (Realm realm : realms) {
realmHashMap.put(realm.getName(),realm);
}
if (authenticationToken instanceof CasToken) {
// step5. 直接進入這個方法吧
return doSingleRealmAuthentication(realmHashMap.get("casRealm"),authenticationToken);
} else {
return doSingleRealmAuthentication(realmHashMap.get("tokenRealm"),authenticationToken);
}
}
}
複製程式碼
ModularRealmAuthenticator.java
public class ModularRealmAuthenticator extends AbstractAuthenticator {
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm,AuthenticationToken token) {
if (!realm.supports(token)) {
throw new UnsupportedTokenException(msg);
}
// step6. 直接進入相應realm了,本文是CasRealm,走去casrealm看看
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
throw new UnknownAccountException(msg);
}
return info;
}
}
複製程式碼
AuthenticatingRealm.java
(CasRealm
父類)
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 先檢視這個realm有沒有配置快取,有的話直接從快取裡面取
// 如果你配置CacheManager,casRealm1.setAuthorizationCachingEnabled(true),則會使用快取喔,這個在使用者名稱密碼登入,在這裡加一個快取,可以加快認證速度,cas則不需要(不是不需要,是不能用)
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached,perform the lookup:
// step7. 來這裡吧,一般自己寫個realm,就覆蓋do開頭的方法(因為覆蓋就是為了幹活喔)
info = doGetAuthenticationInfo(token);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token,info);
}
}
if (info != null) {
assertCredentialsMatch(token,info);
}
return info;
}
}
複製程式碼
CasRealm.java
public class CasRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CasToken casToken = (CasToken) token;
if (token == null) {
return null;
}
String ticket = (String)casToken.getCredentials();
if (!StringUtils.hasText(ticket)) {
return null;
}
// 預設使用 Cas20ServiceTicketValidator 來進行通訊,跟前端調後端介面一樣
TicketValidator ticketValidator = ensureTicketValidator();
try {
// step8. 來這裡吧,要開始跟cas伺服器通訊了,驗證下token的正確性
// 這個過程就不說了,建議自己debug進去看看,是怎麼通訊的,我在專案裡寫了這個模擬,可以看看
Assertion casAssertion = ticketValidator.validate(ticket,getCasService());
AttributePrincipal casPrincipal = casAssertion.getPrincipal();
String userId = casPrincipal.getName();
Map<String,Object> attributes = casPrincipal.getAttributes();
// refresh authentication token (user id + remember me)
casToken.setUserId(userId);
String rememberMeAttributeName = getRememberMeAttributeName();
String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
if (isRemembered) {
casToken.setRememberMe(true);
}
List<Object> principals = CollectionUtils.asList(userId,attributes);
PrincipalCollection principalCollection = new SimplePrincipalCollection(principals,getName());
// 上面不多說了設定
return new SimpleAuthenticationInfo(principalCollection,ticket);
} catch (TicketValidationException e) {
throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]",e);
}
}
// doGetAuthorizationInfo 這個方法必須注意,後去驗證角色,角色許可權,都會呼叫到這個方法,
// 因此請務必重寫,注意點為:1為角色roles,2角色的許可權permission
// PS: 你可以把AuthorizationInfo封裝後放在session裡,這樣每次呼叫這個方法就從session裡面取
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// retrieve user information
SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
List<Object> listPrincipals = principalCollection.asList();
Map<String,String> attributes = (Map<String,String>) listPrincipals.get(1);
// create simple authorization info
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// add default roles
addRoles(simpleAuthorizationInfo,split(defaultRoles));
// add default permissions
addPermissions(simpleAuthorizationInfo,split(defaultPermissions));
// get roles from attributes
List<String> attributeNames = split(roleAttributeNames);
for (String attributeName : attributeNames) {
String value = attributes.get(attributeName);
addRoles(simpleAuthorizationInfo,split(value));
}
// get permissions from attributes
attributeNames = split(permissionAttributeNames);
for (String attributeName : attributeNames) {
String value = attributes.get(attributeName);
addPermissions(simpleAuthorizationInfo,split(value));
}
return simpleAuthorizationInfo;
}
}
複製程式碼
到這裡差不多就結束了,具體使用可以參見網上使用方法,你也可以在程式碼裡使用註解,shiro有自己的aop實現,他會把那些打註解的類,方法進行代理。
最後說下可以優化和注意的點:
-
WebSessionManager
在每次獲取session
的時候都會從SessionDAO
裡面讀取,如果快取是redis
,這樣很消耗效能,最好重寫retrieveSession這個方法,將第一次獲取到的Session存放到request裡面去,後面每次從這裡面取。 - 就是使用
Redis
將Session
序列化儲存的時候,SimpleSession
裡面欄位都是transient
修飾的,選擇序列化方案時,請注意。要麼自己重寫SimpleSession
,要麼選一個不會忽略transient
的序列化方式。 - 那些不需要認證的資源跟需要認證的資源一樣都會從
SessionDAO
獲取一次Session
,其實這個完全沒必要,可以,這個也可以在WebSessionManager
裡面進行優化。 - 不管什麼請求傳送到伺服器,伺服器都會先把請求生成一個會話儲存到 會話儲存的地方,如果有人一直請求,會造成 會話儲存跑滿,最終造成拒絕服務攻擊。(解決辦法,將沒有認證的會話和認證過的會話放在不同的地方,也可以不儲存沒有認真的會話,但不儲存會導致使用者第一次登入認證,不會導航到使用者第一次訪問的那個地址,而使原先設定好的地址,這會影響使用者體驗)