注意,該授權流程指的是,在xml中配置filter映射時,指定了某個url需要的角色和權限。如果是用註解授權的話,該流程不合適,後面會有一篇針對註解授權的文章對註解授權流程解讀。
==========================================================
在一個用戶登錄後,即身份認證通過,只能證明該登錄身份是合法的,至於具體能訪問系統中的什麼資源,需要通過授權來控制。一般系統中都是通過用戶關聯角色、角色再關聯權限來實現判斷一個用戶是否有某資源的使用權限,Shiro也提供了相應的實現權限控制。
Shiro中的權限控制也是通過Filter來實現的,在前面認證流程中講到,Shiro的DefaultFilterChainManager類會創建Filter鏈,鏈中包含了Shiro一些默認Filter,也可以添加自定義Filter,而且這些Filter都有名字,Shiro會根據Filter配置爲每一個配置的URL匹配符創建一個Filter鏈。
protected FilterChainManager createFilterChainManager() {
// 創建DefaultFilterChainManager
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// 創建Shiro默認Filter,根據org.apache.shiro.web.filter.mgt.DefaultFilter創建
Map<String, Filter> defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
// 設置相關Filter的loginUrl、successUrl、unauthorizedUrl屬性
applyGlobalPropertiesIfNecessary(filter);
}
// 獲取在Spring配置文件中配置的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);
}
// 將配置的Filter添加至鏈中,如果同名Filter已存在則覆蓋默認Filter
manager.addFilter(name, filter, false);
}
}
//build up the chains:
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
// 爲配置的每一個URL匹配創建FilterChain定義,
// 這樣當訪問一個URL的時候,一旦該URL配置上則就知道該URL需要應用上哪些Filter
// 由於URL匹配符會配置多個,所以以第一個匹配上的爲準,所以越具體的匹配符應該配置在前面,越寬泛的匹配符配置在後面
manager.createChain(url, chainDefinition);
}
}
return manager;
}
下面我們看來都有哪些默認Filter,在DefaultFilterChainManager構造方法中調用addDefaultFilters方法:
protected void addDefaultFilters(boolean init) {
for (DefaultFilter defaultFilter : DefaultFilter.values()) {
addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
// 省略一些代碼...
}
DefaultFilter中的PermissionsAuthorizationFilter與RolesAuthorizationFilter就是用於權限控制的Filter,名稱分別爲perms與roles。PermissionsAuthorizationFilter用於判斷用戶訪問某URL時是否有相應權限,RolesAuthorizationFilter用於判斷用戶訪問某URL時是否有相應角色。
由於Shiro中Filter繼承體系比較複雜,要想理解Shiro權限是如何控制的就必須先理解Filter的繼承體系,以及理解繼承體系中父類Filter的特點及作用。由於Filter繼承體系龐大,下面只列出PermissionsAuthorizationFilter與RolesAuthorizationFilter的繼承關係。
下面對繼承關係中一些重要的Filter作簡要說明,具體的Filter詳細分析容後續再講。
1.NameableFilter:爲Filter添加名稱
2.OncePerRequestFilter:保證Filter在鏈中只被執行一次
3.AdviceFilter:
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
Exception exception = null;
try {
// 前置處理,如果返回false則不再執行鏈中的後續Filter
boolean continueChain = preHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
}
if (continueChain) {
// 繼續執行鏈中的後續Filter
executeChain(request, response, chain);
}
// 後置處理
postHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Successfully invoked postHandle method");
}
} catch (Exception e) {
exception = e;
} finally {
cleanup(request, response, exception);
}
}
4.PathMatchingFilter:基於路徑匹配的Filter,重寫preHandle方法
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
// appliedPaths存儲該Filter需要被應用的URL路徑,例如有這樣一個配置:/user_add.jsp = perms["user:add"]
// 那麼在PermissionsAuthorizationFilter.appliedPaths中就有一條key爲/user_add.jsp, value爲[user:add]數組
// value爲數組而不是字符串的原因是權限可以有多個
// 如果appliedPaths爲空則直接繼續執行Filter鏈
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");
}
return true;
}
for (String path : this.appliedPaths.keySet()) {
// 如果匹配,則根據onPreHandle方法的返回值來確定是否繼續執行Filter鏈
if (pathsMatch(path, request)) {
log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);
Object config = this.appliedPaths.get(path);
return isFilterChainContinued(request, response, path, config);
}
}
//no path matched, allow the request to go through:
return true;
}
@SuppressWarnings({"JavaDoc"})
private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
String path, Object pathConfig) throws Exception {
// 如果該Filter可用
if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
// 省略一些代碼...
return onPreHandle(request, response, pathConfig);
}
// 省略一些代碼...
return true;
}
5.AccessControlFilter:實現onPreHandle方法
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
根據isAccessAllowed方法的返回值來確定是否繼續執行Filter鏈,如果不執行Filter鏈,則還會執行onAccessDenied方法
6.AuthorizationFilter:實現了onAccessDenied方法
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
// 如果沒有登錄則重定向至登錄頁面
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
} else {
// 重定向至未授權頁面
String unauthorizedUrl = getUnauthorizedUrl();
//SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
// 如果未授權頁面未配置則發送401狀態碼
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
7.PermissionsAuthorizationFilter:實現了isAccessAllowed方法
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
// 訪問時需要的權限
String[] perms = (String[]) mappedValue;
// 調用Subject對象的isPermitted或isPermittedAll來判斷是否有權限
boolean isPermitted = true;
if (perms != null && perms.length > 0) {
if (perms.length == 1) {
if (!subject.isPermitted(perms[0])) {
isPermitted = false;
}
} else {
if (!subject.isPermittedAll(perms)) {
isPermitted = false;
}
}
}
return isPermitted;
}
isPermitted和isPermittedAll最終都委託給了ModularRealmAuthorizer.isPermitted與ModularRealmAuthorizer.isPermittedAll方法,至於爲什麼爲會委託給ModularRealmAuthorizer請參看:Shiro源碼分析----登錄流程
下面以ModularRealmAuthorizer.isPermitted爲例,分析一下是如何進行權限判斷的:
public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
// 調用Realm的isPermitted方法
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;
}
}
return false;
}
在使用Shiro時,Realm對象包含了認證與授權信息,在實際應用時,一般都是存儲在數據庫中的,且Realm一般都會自定義實現。實現自定義的Realm時,一般繼承自org.apache.shiro.realm.AuthorizingRealm類,下面是一個例子:
public class UserRealm extends AuthorizingRealm {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
// 從數據庫中獲取權限信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 從數據庫中查詢當前用戶所擁有的角色
authorizationInfo.setRoles(userService.findRoles(username));
// 從數據庫中查詢當前用戶所擁有的權限
authorizationInfo.setStringPermissions(userService.findPermissions(username));
return authorizationInfo;
}
// 從數據庫中獲取認證信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException();//沒找到帳號
}
if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帳號鎖定
}
//交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以自定義實現
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用戶名
user.getPassword(), //密碼
ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
}
AuthorizingRealm.isPermetted方法中就是根據用戶所擁有的權限與訪問時需要的權限進行匹配,如果有權限則繼續執行Filter鏈,反之則重定向至配置的未授權頁面。
理解了PermissionsAuthorizationFilter的判斷邏輯,那麼RolesAuthorizationFilter的判斷邏輯就很容易理解了,因爲其流程是一樣的,只是RolesAuthorizationFilter是基於用戶角色進行判斷的。
8.RolesAuthorizationFilter:實現了isAccessAllowed方法:
@SuppressWarnings({"unchecked"})
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
// 獲取訪問時需要的角色
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0) {
//no roles specified, so nothing to check - allow access.
return true;
}
Set<String> roles = CollectionUtils.asSet(rolesArray);
// 委託給Subject.hasAllRoles方法
return subject.hasAllRoles(roles);
}
同理,hasAllRoles方法,最終都委託給了ModularRealmAuthorizer.hasAllRoles方法
public boolean hasAllRoles(PrincipalCollection principals, Collection<String> roleIdentifiers) {
assertRealmsConfigured();
for (String roleIdentifier : roleIdentifiers) {
if (!hasRole(principals, roleIdentifier)) {
return false;
}
}
return true;
}
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
return true;
}
}
return false;
}
AuthorizingRealm.hasAllRoles方法中就是根據用戶所擁有的角色與訪問時需要的角色進行匹配,如果有角色則繼續執行Filter鏈,反之則重定向至配置的未授權頁面。