Shiro源码分析----授权流程
在一个用户登录后,即身份认证通过,只能证明该登录身份是合法的,至于具体能访问系统中的什么资源,需要通过授权来控制。一般系统中都是通过用户关联角色、角色再关联权限来实现判断一个用户是否有某资源的使用权限,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详细分析容后续再讲。
NameableFilter
:为Filter添加名称OncePerRequestFilter
:保证Filter在链中只被执行一次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
方法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精讲》。
©著作权归作者所有:来自51CTO博客作者Java精讲的原创作品,如需转载,请注明出处,否则将追究法律责任好知识,才能预见未来
赞赏
0人进行了赞赏支持
更多相关文章
- 快速提高技术的方法 & 长期欢迎投稿
- C/C++ 性能优化背后的方法论:TMAM
- 深入理解Java Stream流水线
- 配置 eslint 去掉 no-unused-vars 报错 方法汇总
- 运维助力敏捷交付-我们的运维看板
- 后台六 功能权限(阅读)
- 重要|Spark driver端得到executor返回值的方法
- MongoDB用户权限管理
- 一对多、多对多查询,最简单的方法请拿好