在一个用户登陆后,即身份认证经过,只能证实该登陆身份是合法的,至于具体能访问系统中的什么资源,须要经过受权来控制。通常系统中都是经过用户关联角色、角色再关联权限来实现判断一个用户是否有某资源的使用权限,Shiro也提供了相应的实现权限控制。java
Shiro中的权限控制也是经过Filter来实现的,在前面认证流程中讲到,Shiro的DefaultFilterChainManager
类会建立Filter链,链中包含了Shiro一些默认Filter,也能够添加自定义Filter,并且这些Filter都有名字,Shiro会根据Filter配置为每个配置的URL匹配符建立一个Filter链。web
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时是否有相应角色。apache
因为Shiro中Filter继承体系比较复杂,要想理解Shiro权限是如何控制的就必须先理解Filter的继承体系,以及理解继承体系中父类Filter的特色及做用。因为Filter继承体系庞大,下面只列出PermissionsAuthorizationFilter
与RolesAuthorizationFilter
的继承关系。数组
下面对继承关系中一些重要的Filter做简要说明,具体的Filter详细分析容后续再讲。app
NameableFilter
:为Filter添加名称jsp
OncePerRequestFilter
:保证Filter在链中只被执行一次ide
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); } }
PathMatchingFilter
:基于路径匹配的Filter,重写preHandle
方法post
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; }
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
方法。
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; }
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; }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
是基于用户角色进行判断的。
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链,反之则重定向至配置的未受权页面。
至此,Shiro受权流程分析完毕,若有错误之处,敬请指正。
-------------------------------- END -------------------------------
及时获取更多精彩文章,请关注公众号《Java精讲》。