授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据等)
- 主体:访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
- 资源:在应用中用户可以访问的URL,比如访问JSP页面,查看/编辑某些数据等等。
- 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权利;Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)。
- 角色:权限的集合,一般情况下会赋予用户角色而不是权限。
授权方式
Shiro支持三种方式的授权:
- 编程式: Subject#hasRole()
- 注解式:通过在执行的Java方法上放置相应的直接完成,没有权限将抛出相应的异常,如:@RequiresRoles()
- JSP/GSP标签:<shiro:hasRole name=""></shiro:hasRole>
默认拦截器
Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的,默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter.class中的枚举拦截器:- 身份验证相关的:
默认拦截器名 | 拦截器类 | 说明(括号内为默认值) |
---|---|---|
authc | FormAuthenticationFilter | 基于表单的拦截;如 "/**=authc",如果没有登录会跳转到响应的登录页面登录; 主要属性: usernameParam:表单提交的用户名参数名(username); password:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp); successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后操作信息存储key(shiroLoginFailure); |
authcBasic | BasicHttpAuthenticationFilter | Basic HTTP身份验证拦截器,主要属性: applicationName: 弹出登录提示框的信息(application) |
logout | LogoutFilter | 退出拦截器,主要属性: redirectUrl:退出成功后重定向的地址(/); |
user | UserFilter | 用户拦截器,用户已经身份验证/记住我登录的都可; |
anon | AnonymousFilter | 匿名拦截器,即不通过登录即可访问;一般用于静态资源过滤 |
- 授权相关的:
默认拦截器名 | 拦截器类 | 说明(括号内为默认值) |
---|---|---|
roles | RolesAuthorizationFilter | 角色授权拦截器,验证用户是否拥有该角色; 主要属性: loginUrl:登录页面地址(/login.jsp); unauthorizedUrl:未授权后重定向的地址; |
perms | PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样 |
port | PortFilter | 端口拦截器,主要属性: port:可以通过的端口(80) 实例:"/pay/1=port[80]",如果用户访问该请求时非80, 将自动将请求端口修改为80并重定向到该80端口,其他路径/参数一致 |
rest | HttpMethodPermissionFilter | rest风格拦截器,自动根据请求方法构造权限字符串 (GET=read,POST=cratea,PUT=update,DELETE=delete,HEAD=read, TRACE=read,OPTIONS=read,MKCOL=create) |
ssl | SslFilter | SSL拦截器,只有请求协议是https才能通过;否则自动跳转到https端口;其他和port拦截器一样 |
- 其他:
默认拦截器名 | 拦截器类 | 说明(括号内为默认值) |
---|---|---|
noSessionCreation | NoSessionCreationFilter | 不创建会话拦截器 |
Permissions
- 规则:
资源标识符:操作:对象实例ID即对哪个资源的哪个实例进行什么操作.其默认支持通配符权限字符串,冒号(:)表示资源/操作/实例的分隔;逗号(,)表示操作的分隔,星号(*)表示任意资源/操作/实例。 - 多层次管理:
- 例如:user:query、user.edit
- 冒号是一个特殊字符,它用来分隔权限字符串的下一部件:第一部分是权限被操作的领域(打印机),第二部分是被执行的操作。
- 多个值:每个部件能够保护多个值。因此,出了授予用户 user:query 和 user:edit 权限外,也可以简单的授予他们一个:user:quert,edit
- 还可以用 * 号代替所有的值,如:user:*,也可以写:*:query,表示某个用户在所有的领域都有query的权限。
Shiro 的 Permissions
- 实例级访问控制
- 这种情况通常会使用三个部件:域、操作、被付诸实施的实例。如:user:edit:manager
- 也可以使用通配符来定义,如:user:edit:*、user:*:*、user:*:manager
- 部分省略通配符:缺少的部位意味着用户可以访问所有与之匹配的值,比如:user:edit 等价于user:edit:*、user 等价于 user:*:*
- 注意:通配符只能从字符串的结尾处省略部位,也就是说 user:edit 并不等价于 user:*:edit
授权流程
- AuthorizingRealm
- 授权需要继承 AuthenticatingRealm 类,并实现其 doGetAuthenticationInfo 方法
- AuthorizingRealm 类继承自 AuthenticatingRealm 类,但没有实现 AuthenticatingRealm 中的 doGetAuthenticationInfo 方法,所以认真和授权只需要继承 AuthorizingRealm 就可以了,同时实现它的两个抽象方法 doGetAuthorizationInfo 和 doGetAuthenticationInfo
- 流程如下
- 首先调用 Subject.isPermitted/hasRole接口,其会委托给 SecurityManage,而 SecurityManage 接着会委托给 Authorizer;
- Authorizer 是真正的授权者,如果调用如 isPermitted("user:view"),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
- 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用户匹配传入的角色/权限;
- Authorizer 会判断 Realm 的角色/权限是否和传入的匹配, 如果有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted/hasRole 会返回true,否则返回false表示授权失败;
- ModularRealmAuthorizer :多Realm匹配流程
- 首先检查相应的 Realm是否实现了Authorizer;
- 如果实现了 Authorizer,那么接着调用其对应的 isPermitted/hasRole 接口进行匹配;
- 如果有一个Realm匹配那么将返回true,否则返回false。
修改 ShiroRealm 类继承 AuthorizingRealm,并实现其对应的授权方法
package org.keyhua.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.HashSet;
import java.util.Set;
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("[FirstRealm] doGetAuthenticationInfo");
//1.把AuthenticationToken转换为UserNamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.从UserNamePasswordToken中获取username
String username = upToken.getUsername();
//3.调用dao层方法,从数据库中查询username对应的用户记录
System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
//4.若用户不存在,则可以抛出 UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5.根据用户信息的情况,觉得是否需要抛出其他的异常.
if ("monster".equals(username)) {
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
//principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
Object principal = username;
//credentials:密码
Object credentials = null;
if ("admin".equals(username)){
credentials = "9aa75c4d70930277f59d117ce19188b0";
} else if ("user".equals(username)) {
credentials = "dd957e81b004227af3e0aa4bde869b25";
}
//realmName:当前realm对象的name.调用父类的getName()
String realmName = getName();
//盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null;
//info = new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object source = "123456";
Object salt = ByteSource.Util.bytes("user");
int hashIterations = 3;
SimpleHash hash = new SimpleHash(hashAlgorithmName, source, salt, hashIterations);
System.out.println(hash.toString());
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1.从PrincipalCollection 中获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2.利用登录用户的信息来获取当前用户的角色或权限(可能需要查数据库)
Set<String> roles = new HashSet<>();
roles.add("user");
if ("admin".equals(principal)){
roles.add("admin");
}
//3.创建SimpleAuthorizationInfo ,并设置其reles属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4.返回 SimpleAuthorizationInfo 对象
return info;
}
}
启动项目测试:当使用admin用户登录时,所有页面均可访问,当使用user用户登录时,admin.jsp页面无法访问。
权限注解
- @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true
- @RequiresUser:表示当前 Subject 已经身份验证或者通过记住登录的
- @RequiresGuest:表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份
- @RequiresRoles(value={"admin","user"},logical=Logical.AND):表示当前Subject需要角色admin和user
- @RequiresPermissions(value={"user:a","user:b"},logical=Logical.OR):表示当前Subject需要权限user:a 或 user:b
如何从数据表中初始化资源和权限
applicationContext.xml修改内容:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login.jsp"/>
<property name="successUrl" value="/list.jsp"/>
<property name="unauthorizedUrl" value="/unauthorized.jsp" />
<property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>
<!--配置哪些页面需要受保护,以及访问这些页面需要的权限
a. anon 可以被匿名访问
b. authc 必须认证(登录)后才可以访问的页面
c. logout 登出b
d. roles 角色过滤器
-->
<!--
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/shiroLogin = anon
/shiro/logout = logout
/user.jsp = roles[user]
/admin.jsp = roles[admin]
/** = authc
</value>
</property>
-->
</bean>
<!-- 配置一个bean,该bean实际上是一个Map。通过实例工厂方法的方式 -->
<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildfilterChainDefinitionMap"/>
<bean id="filterChainDefinitionMapBuilder" class="org.keyhua.shiro.FilterChainDefinitionMapBuilder"></bean>
FilterChainDefinitionMapBuilder:
public class FilterChainDefinitionMapBuilder {
public LinkedHashMap<String,String> buildfilterChainDefinitionMap(){
LinkedHashMap<String,String> map = new LinkedHashMap<>();
//可以从数据表中初始化资源和权限
map.put("/login.jsp", "anon");
map.put("/shiro/shiroLogin", "anon");
map.put("/shiro/logout", "logout");
map.put("/**", "authc");
return map;
}
}