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精讲》。
原文:https://blog.51cto.com/xtayfjpk/2662835