前言
前面两篇文章主要讲了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
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); Setroles = new HashSet<>(); //角色集合 Set permissions = new HashSet<>(); //权限集合 //根据userId查出所拥有的roleId List roleIds = userRoleMapper.selectByUserId(user.getUserId()); Iterator iterator = roleIds.iterator(); while (iterator.hasNext()) { Long roleId = iterator.next(); Role role = roleMapper.selectByPrimaryKey(roleId); //将角色放入角色集合 roles.add(role.getRoleName()); //根据角色查出权限Id List permissionIds = rolePermissionMapper.selectByRoleId(roleId); Iterator 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) { Collectionperms = 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; }复制代码