前言
前面兩篇文章主要講了SpringShiroFilter的初始化以及doFilter方法。總結一下:初始化的主要操作是根據配置構建所有url對應的過濾鏈,doFilter()方法將url對應的過濾鏈新增到javaEE原生的的過濾器中。
本篇文章的內容
本篇文章主要解析具體的Filter是如何處理鑑權的(即如何判斷某個使用者是否有許可權訪問該url)。本篇文章一PermissionsAuthorizationFilter為例(shiro預設的攔截器有9個,包括roles,anno,perms等等)
正文
首先給出PermissionsAuthorizationFilter的類結構,該類只有一個isAccessAllowed方法
public class PermissionsAuthorizationFilter extends AuthorizationFilter {
//TODO - complete JavaDoc
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
String[] perms = (String[]) mappedValue;
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;
}
}
複製程式碼
第一步,既然是過濾器,我們當然還是一步一步往上尋找doFilter()方法,最終還是在OncePerRequestFilter()方法中找到
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
Exception exception = null;
try {
boolean continueChain = preHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
}
if (continueChain) {
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);
}
}
複製程式碼
這裡鑑權的核心在於doFilterInternal方法中的boolean continueChain = preHandle(request, response),如果鑑權通過則返回true,並且呼叫executeChain()呼叫下一個過濾器。(這裡我找了好一番時間才發現鑑權邏輯躲在preHandle裡面,抓狂,正常理解不怎麼都應該在executeChain裡面嘛。暗暗吐槽)
下面讓我們來一層一層的剝開preHandle的外衣。
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
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()) {
// If the path does match, then pass on to the subclass implementation for specific checks
//(first match 'wins'):
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;
}
複製程式碼
第一步檢查appliedPaths是否為空,這裡可能有小夥伴忘記appliedPaths裡面存的是什麼了,還是得祭出第一篇文章的dubug圖。
第二步開始遍歷appliedPaths的key,與request裡的uri進行匹配。匹配的邏輯與第二篇文章uri匹配過濾鏈的邏輯一致,都是交給了預設的AntPathMatcher。for (String path : this.appliedPaths.keySet()) {
// If the path does match, then pass on to the subclass implementation for specific checks
//(first match 'wins'):
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);
}
}
複製程式碼
這裡得到了許可權配置的config,這裡的config物件通過debug圖可看到是一個String陣列,這都是在第一篇文章Filter初始化時完成的,其實裡面就是我們配置的許可權,例如perms[user:add,user:delete]。第三步呼叫isFilterChainContinued()進行鑑權
private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
String path, Object pathConfig) throws Exception {
if (isEnabled(request, response, path, pathConfig)) {
return onPreHandle(request, response, pathConfig);
}
return true;
}
複製程式碼
點開onPreHandle,注意這裡是個邏輯表示式,如果前面isAccessAllowed()返回true,後面的OnAccessDenied()方法將不會執行。(短路原則)
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
複製程式碼
最終呼叫到我們PermissionsAuthorizationFilter的isAccessAllowed方法。getSubject得到使用者,將鑑權的邏輯委派給Subject。(Subject在SpringShiroFilter中建立了,所以這裡可以直接獲取到,如何建立以及維護的在後面的文章講解)
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
String[] perms = (String[]) mappedValue;
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;
}
複製程式碼
這裡首先判斷有沒有principals(憑證,登入時放進去的,許可權是通過憑證獲取的,也是識別使用者的唯一標識,一般為使用者名稱)。如果有憑證則將鑑權委託給securityManager。(shiro官網也提及到securityManager是shiro的核心,到後面會發現所有的登入,鑑權,session管理都是由它掌控的)
public boolean isPermittedAll(String... permissions) {
return hasPrincipals() && securityManager.isPermittedAll(getPrincipals(), permissions);
}
複製程式碼
這裡稍微看一下securityManager的類圖結構,方便以後章節的講解。authenticator處理登入,authorizer處理鑑權,sessionManager處理sesison管理
本篇文章涉及到的是authorizer,回頭看到我們的spring-shiro.xml配置檔案,我配置的是一個多認證器,併為他設定了認證策略(一個成功為成功)。
點進ModularRealmAuthenticator的isPermitted方法,第一步獲取到我們所有的realms,並逐個呼叫isPermitted()方法,一個通過則鑑權通過 public boolean isPermittedAll(PrincipalCollection principals, String... permissions) {
assertRealmsConfigured();
if (permissions != null && permissions.length > 0) {
for (String perm : permissions) {
if (!isPermitted(principals, perm)) {
return false;
}
}
}
return true;
}
複製程式碼
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;
}
}
return false;
}
複製程式碼
public boolean isPermitted(PrincipalCollection principals, String permission) {
Permission p = getPermissionResolver().resolvePermission(permission);
return isPermitted(principals, p);
}
複製程式碼
這裡將許可權字串構建成了一個permission物件,這個permission物件可以執行萬用字元式的許可權比對。然後開始獲取AuthorizationInfo(授權資訊)
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permission, info);
}
複製程式碼
注意這個getAuthorizationInfo先從快取中獲取(所以shiro是支援授權快取的),如果快取為空才呼叫doGetAuthorizationInfo獲取授權。
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [" + principals + "]");
}
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
if (log.isTraceEnabled()) {
log.trace("Attempting to retrieve the AuthorizationInfo from cache.");
}
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [" + principals + "]");
} else {
log.trace("AuthorizationInfo found in cache for principals [" + principals + "]");
}
}
}
if (info == null) {
// Call template method if the info was not found in a cache
info = doGetAuthorizationInfo(principals);
// If the info is not null and the cache has been created, then cache the authorization info.
if (info != null && cache != null) {
if (log.isTraceEnabled()) {
log.trace("Caching authorization info for principals: [" + principals + "].");
}
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
複製程式碼
doGetAuthorizationInfo最終會呼叫到我們的配置的Realms類的,實現我們自己的授權邏輯
/**
* 授權,會在需要驗證許可權的時候被shiro呼叫,如果開啟了快取則只會第一次驗證的時候被呼叫.
*
* @param principals
* @return AuthorizationInfo
* @author liuruojing
* @since ${PROJECT_NAME} 0.1.0
*/
@Override
protected final AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userMapper.selectByUsername(username);
Set<String> roles = new HashSet<>(); //角色集合
Set<String> permissions = new HashSet<>(); //許可權集合
//根據userId查出所擁有的roleId
List<Long> roleIds = userRoleMapper.selectByUserId(user.getUserId());
Iterator<Long> iterator = roleIds.iterator();
while (iterator.hasNext()) {
Long roleId = iterator.next();
Role role = roleMapper.selectByPrimaryKey(roleId);
//將角色放入角色集合
roles.add(role.getRoleName());
//根據角色查出許可權Id
List<Long> permissionIds = rolePermissionMapper.selectByRoleId(roleId);
Iterator<Long> pIterator = permissionIds.iterator();
while (pIterator.hasNext()) {
Long permissionId = (Long) pIterator.next();
Permission permission = permissionMapper.selectByPrimaryKey(permissionId);
//將所有許可權放入許可權集合
permissions.add(permission.getPermissionName());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles); //放入角色
info.setStringPermissions(permissions); //放入許可權
return info;
}
}
複製程式碼
現在我們知道了訪問url需要的許可權,同時也查詢出了此使用者具有的許可權,我們只需要進行比對兩者便可以知道使用者是否有許可權訪問此url了。將獲取的AuthorizationInfo解析成Permission物件集合,逐個呼叫Permission物件的implies方法進行比對。如果比對成功則返回true執行下一個所需許可權的比對。
//changed visibility from private to protected for SHIRO-332
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
複製程式碼
最後所有許可權比對完後會回到上面的onpreHandle(),如果isAccessAllowed鑑權通過則返回true,否則呼叫onAccessDenied方法設定重定向到unloginUrl或者unAuthorizedUrl,這兩個url是我們在xml中配置的
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
複製程式碼
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
// If the subject isn't identified, redirect to login URL
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
} else {
// If subject is known but not authorized, redirect to the unauthorized URL if there is one
// If no unauthorized URL is specified, just return an unauthorized HTTP status code
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 {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
}
複製程式碼