Apache Shiro学习笔记
Shiro是一个支持身份验证、授权、加密、会话管理的Java安全框架。Shiro的API很简单,比Sprng Security简单的多。它的特性如下:
- 在身份验证方面,Shiro支持LDAP、JDBC、AD等数据源
- 在授权(访问控制)方面,Shiro支持基于角色的权限控制,或者细粒度的Permissions控制。授权信息也存放在可拔插的数据源中
- 内置的企业级会话管理功能,支持Web/非Web应用,支持需要SSO或者分布式会话的应用场景
- 支持SSO,登陆到共享会话的多个应用中的任意一个,其它应用自动获得会话信息
- 内置简单好用的密码算法,包括哈希
- 支持跨越线程传递Subject
- 支持RunAs,也就是以另外一个身份进行访问
概念 | 说明 |
Subject | 主体,和应用程序交互的任何实体,可以是人、网络爬虫,等等 |
Principal | 主体的一个属性,具有识别主体的能力,可能是用户账号、身份证号等 |
SecurityManager | 安全管理器,所有和安全相关的操作都要通过该对象,它管理所有Subject |
Realm |
领域,Shiro从Realm获取用户、角色、权限等数据,它是LDAP、RDBMS、AD等任意形式的数据源 可以配置多个Realm,Shiro的默认行为是依次调用Realm进行身份验证 |
Authenticator |
认证器,一个扩展点,可以自定义身份验证逻辑 |
Authrizer | 授权器,一个扩展点,可以自定义访问控制逻辑 |
Session | 和Subject关联的有状态的数据上下文,你可以对其进行CRUD操作。当Subject登出,或者因为长期不活动而超时,会话终结 |
SessionManager | 会话管理器,可以用它实现分布式会话支持 |
Resource | 应用程序中任何可以供用户访问的东西,例如URL、页面、数据、方法、摄像头,等等 |
Permission | 权限,原子的访问控制单元,代表对某种资源的某种操作许可 |
Role | 权限的集合,通常为用户赋予角色而非权限 |
本节贴出一个简单的、基于Shiro的Web应用。
1 2 3 4 5 |
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.2</version> </dependency> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <!-- 在Web容器启动时初始化Shiro需要的WebEnvironment对象 --> <context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>org.apache.shiro.web.env.IniWebEnvironment</param-value> </context-param> <context-param> <param-name>shiroConfigLocations</param-name> <!-- 默认从/WEB-INF/shiro.ini加载配置文件 --> <param-value>classpath:shiro.ini</param-value> </context-param> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <filter> <filter-name>ShiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> |
位于Classpath下的配置文件,可以包含硬编码的用户信息,配合隐含声明的Realm使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[main] ; 登陆URL authc.loginUrl = /login ; 基于角色授权失败时重定向到的URL roles.unauthorizedUrl = /unauthorized ; 基于权限授权失败时重定向到的URL perms.unauthorizedUrl = /unauthorized [users] ; 硬编码的用户信息,格式:username = password, roleName1, roleName2, …, roleNameN alex = passw0rd,admin meng = passw0rd [roles] ; 硬编码的角色信息,格式:rolename = permissionDefinition1, permissionDefinition2, …, permissionDefinitionN admin = user:*,menu:* [urls] ; 硬编码的,基于URL的ACL ; 允许匿名访问 /login = anon /unauthorized = anon ; 支持多种通配符: ; ? 匹配一个字符 ; * 匹配0-N个字符,但是不能跨越 / ; ** 匹配0-N个路径(路径即/分隔的片断) /static/** = anon ; 需要身份验证 /authenticated = authc ; 需要获得授权:要求admin角色 /role = authc,roles[admin] ; 需要获得授权:要求user:create权限 /permission = authc,perms["user:create"] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package cc.gmem.study.shiro.servlet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet( "/login" ) public class LoginServlet extends HttpServlet { private static final Logger LOGGER = LogManager.getLogger( LoginServlet.class ); @Override protected void doGet( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException { /** * 获得当前主体,通常就是类似于用户的概念: * 1、对于Web应用,从当前线程或者请求中获取用户信息 * 2、对于独立运行的应用,从自定义位置获取用户信息 */ Subject user = SecurityUtils.getSubject(); if ( !user.isAuthenticated() ) { // 从外部获取用户凭证,例如密码、X509证书、OpenID,产生一个令牌对象 UsernamePasswordToken token = new UsernamePasswordToken( req.getParameter( "name" ), req.getParameter( "pswd" ) ); // 是否跨越会话记住用户 token.setRememberMe( true ); try { // 登陆 user.login( token ); // 可以获得一个会话对象,类似HttpSession,在Web应用中,该对象就是基于HttpSession的 Session session = user.getSession(); session.setAttribute( "loginTime", System.currentTimeMillis() ); LOGGER.debug( "Logged in as {}", user.getPrincipal().toString() ); // 判断是否具有特定的角色、权限 assert user.hasRole( "admin" ); assert user.isPermitted( "user:create" ); resp.sendRedirect( "/authenticated" ); } catch ( UnknownAccountException uae ) { LOGGER.error( "账号未知" ); } catch ( IncorrectCredentialsException ice ) { LOGGER.error( "密码错误" ); } catch ( LockedAccountException lae ) { LOGGER.error( "账户锁定" ); } } } } |
启动Web容器后,访问 http://host:port/login?name=alex&pswd=passw0rd即可登陆并通过身份验证。
在非Web环境下使用Shiro类似,但是需要显式初始化安全管理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 示例一 Realm realm = new DatabaseRealm(); SecurityManager securityManager = new DefaultSecurityManager(realm); SecurityUtils.setSecurityManager(securityManager); // 示例二 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject user = SecurityUtils.getSubject(); // ... |
你可以使用INI格式为Shiro提供必要的配置信息,INI格式类似于属性文件,使用 #或者 ;作为注释前导符号。INI配置文件包含四个段:
段 | 说明 | ||
[main] |
在此声明安全管理器的各属性及其依赖的对象,支持简单的DI 示例配置:
|
||
[users] | 在简单的应用中,静态化的配置用户、角色、权限信息 | ||
[roles] | |||
[urls] | 在Web应用中,实现基于URL的访问控制 |
Realm表示一个安全域,它持有用户、角色、权限等安全数据,有能力对Subject进行身份验证。如果不进行配置,Shiro默认使用org.apache.shiro.realm.text.IniRealm这个Realm,安全数据静态的配置在ini文件中。
Realm接口包括以下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * 大部分情况下不需要直接实现此接口: * 1、如果仅需要身份验证功能,可以继承AuthenticatingRealm * 2、如果还需要访问控制功能,可以继承AuthorizingRealm(AuthenticatingRealm的子类) */ public interface Realm { /** * 返回Realm的名称,此名称必须唯一 */ String getName(); /** * 判断此Realm是否支持对token所代表的Subject进行身份验证 */ boolean supports( AuthenticationToken token ); /** * 执行身份验证,或者说基于token进行登陆 * * @param token 包含了Subject的标识(Principal)和凭证(Credential)信息的令牌 * @return 和令牌相关的账户的身份验证信息,如果相关账户不存在则返回null * @throws org.apache.shiro.authc.AuthenticationException 如果身份验证失败 */ AuthenticationInfo getAuthenticationInfo( AuthenticationToken token ) throws AuthenticationException; } |
Shiro内置的Realm实现如下图所示:
其中常用实现说明如下:
实现 | 说明 | ||
IniRealm | 使用INI文件中的配置信息,Web环境下默认使用shiro.ini,格式在上文已有说明 | ||
PropertiesRealm | 默认使用类路径下的属性文件shiro-users.properties中的配置信息,格式如下:
|
||
JdbcRealm |
从JDBC数据源中查找配置信息,默认使用的SQL如下:
|
Realm的配置信息放在INI的main段,你可以配置多个Realm:
1 2 3 4 |
[main] dbRealm = cc.gmem.security.shiro.DatabaseRealm ldapRealm = cc.gmem.security.shiro.LDAPRealm securityManager.realms=$dbRealm, $ldapRealm |
如果不配置securityManager.realms属性,则Shiro根据声明顺序,依次调用各Realm进行身份验证。
- 调用Subject.login(token)进行身份验证
- Subject委托给SecurityManager进行处理,SecurityManager需要事先通过SecurityUtils. setSecurityManager()设置
- SecurityManager委托Authenticator进行实际的身份验证。Shiro缺省的安全管理器本身实现了Authenticator接口,但是它自己不负责身份验证,而是委托ModularRealmAuthenticator。后者也是Authenticator的实现
- ModularRealmAuthenticator持有一个Realm的集合,也就是说它可以从多个安全数据源获取信息,对Subject进行身份验证
- ModularRealmAuthenticator持有AuthenticationStrategy即身份验证策略,后者能够决定是每个Realm都需要对Subject进行成功的身份验证,还是仅仅需要一个就可以了
牵涉到的类如下:
此接口和ModularRealmAuthenticator配合,指定存在多个Realm时的身份验证策略:
1 2 3 4 5 6 7 8 9 10 11 12 |
public interface AuthenticationStrategy { // 尝试基于任何Realm进行身份验证之前调用 AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException; // 尝试基于某个Realm进行身份验证之前调用 AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException; // 基于某个Realm进行身份验证之后调用 AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException; // 基于全部Realm进行身份验证之后调用 AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException; } |
实现 | 说明 |
FirstSuccessfulStrategy | 只需要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的AuthenticationInfo |
AtLeastOneSuccessfulStrategy | 只需要有一个Realm验证成功即可,返回所有Realm的AuthenticationInfo |
AllSuccessfulStrategy | 必须所有Realm验证成功,返回所有Realm的AuthenticationInfo |
此接口用于收集主体的身份验证信息,例如主体标识、密码:
1 2 3 4 5 6 |
public interface AuthenticationToken extends Serializable { // 返回主体唯一标识,可能是用户名、X.509证书,等等 Object getPrincipal(); // 返回主体的身份凭证,可能是密码、X.509证书,等等 Object getCredentials(); } |
此接口表示主体已经存储到安全域中的、和身份验证(登陆)相关的账户信息。
需要注意AuthenticationInfo和AuthenticationToken的不同,前者是经过验证的、存储在安全域(Realm)中的账户信息。后者则是登陆请求所提交的数据,不一定能和AuthenticationInfo匹配。
每个应用中可以有多个Realm,因此同一Subject就可以对应多个Principal,此接口用于聚合多个Realm提供的、关联目标Subject的Principal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public interface PrincipalCollection extends Iterable, Serializable { /** * 返回主要的Principal,如果只有一个Realm则只能有一个Principal * 如果有多个Realm,Shiro默认返回迭代得到的第一个Principal */ Object getPrimaryPrincipal(); /** * 得到第一个指定类型的Principal,如果找不到返回null */ <T> T oneByType(Class<T> type); /** * 得到所有指定类型的Principal,如果找不到返回空集合 */ <T> Collection<T> byType(Class<T> type); /** * 返回所有Principal构成的列表 */ List asList(); /** * 返回所有Principal构成的集合 */ Set asSet(); /** * 返回从指定Realm取得的Subject的Principal的集合 */ Collection fromRealm(String realmName); /** * 返回此集合中Principal的来源Realm */ Set<String> getRealmNames(); /** * 判断集合中是否存在至少一个Principal */ boolean isEmpty(); } |
Shiro原生支持三种方式,来指定访问某种资源所需的角色或权限。
1 2 3 4 |
Subject user = SecurityUtils.getSubject(); if( user.hasRole( "admin" ) ) { //有权限 } |
相关方法都定义在Subject上,例如:
方法 | 说明 |
isPermitted | 判断Subject是否有权访问参数所指定的资源 |
isPermittedAll | 判断Subject是否有权访问参数所指定的所有资源 |
checkPermission | 检查Subject是否有权访问资源,如果无权则抛出异常 |
checkPermissions | 检查Subject是否有权访问参数指定的所有资源,如果无权则抛出异常 |
hasRole | 判断Subject是否具有指定的角色 |
hasAllRoles | 判断Subject是否具有所有指定的角色 |
checkRole | 检查Subject是否具有指定的角色,如果没有则抛出异常 |
checkRoles | 检查Subject是否具有所有指定的角色,如果没有则抛出异常 |
这些方法会委托安全管理器来检查Subject(准确的说是其Principal)具有资源的访问资格。
1 2 3 |
@RequiresRoles("admin") public void saveUser( User u ) { } |
相关注解如下表:
注解 | 说明 |
RequiresAuthentication | Subject的当前会话,必须已经通过身份验证,才能访问注解了此类型的类、实例、方法 |
RequiresGuest | Subject必须没有经过身份验证,才能访问 |
RequiresUser |
Subject必须是应用程序的一个用户,才能访问。所谓用户,可以是:
|
RequiresPermissions | Subject必须(隐含的)具有指定的权限,才能访问 |
RequiresRoles | Subject必须具有指定的角色,才能访问 |
使用JSP时,可以通过特殊标签进行访问控制:
1 2 3 |
<shiro:hasRole name="admin"> <button>Create User</button> </shiro:hasRole> |
- 当Subject.isPermitted/hasRole等方法被调用后,Subject委托SecurityManager进行访问控制
- SecurityManager委托Authorizer进行处理,尽管默认的安全管理全就是Authorizer的实现,它还是创建了一个ModularRealmAuthorizer,委托后者进行处理
- ModularRealmAuthorizer持有所有实现了Authorizer的Realm。它会循环调用Realm的isPermitted/hasRole方法,如果返回true则表示授权成功
- 只要有一个Realm授权成功即可
如果你的Realm支持授权功能,请让它继承AuthorizingRealm,内置的Realm都继承自该类。
牵涉到的类如下:
权限使用字符串来表示,格式为 资源类型:操作类型:资源实例标识符。 多个资源类型、操作类型使用逗号分隔,字符*可以用于通配。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# 对资源system:user的update操作权 system:user:update # 对system:user的update权,以及对system:user的delete权 system:user:update,system:user:delete # 等价写法 system:user:update,delete # 对system:user的任何操作权限 system:user:* # 等价写法 system:user # 针对任何资源的view权限 *:*:view # 对用户1的update和delete权 user:update,delete:1 # 对用户1的任何权限 user:*:1 # 对任何用户的任何权限 user:*:* # 前缀匹配规则 user # 隐含了user:*或者user:*:* |
如何从上面的字符串形式的权限语法中解析出Permission对象,依赖于以下接口:
接口 | 说明 |
PermissionResolver | 从权限字符串中解析出Permission对象 |
RolePermissionResolver | 从角色解析出其对应的一系列Permission对象 |
你可以自定义权限解析器,并按照如下方式配置、启用:
1 2 3 4 5 6 7 8 9 |
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer permissionResolver=cc.gmem.study.shiro.CustomPermissionResolver authorizer.permissionResolver=$permissionResolver rolePermissionResolver=cc.gmem.study.shiro.CustomRolePermissionResolver authorizer.rolePermissionResolver=$rolePermissionResolver securityManager.authorizer=$authorizer |
此接口代表一个Subject已经存储到安全域中的授权数据(角色、权限),仅仅在进行授权(访问控制)时使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public interface AuthorizationInfo extends Serializable { /** * 此授权信息关联的主体所拥有的角色 */ Collection<String> getRoles(); /** * 此授权信关联的主体所拥有的权限,以字符串形式表示 */ Collection<String> getStringPermissions(); /** * 此授权信关联的主体所拥有的权限,以类型安全方式表示 */ Collection<Permission> getObjectPermissions(); } |
AuthorizingRealm会持有Principal到AuthorizationInfo的映射。在进行访问控制时,先得到Subject的PrincipalCollection,然后获取和这些Principal对应的AuthorizationInfo,与入参提供的角色、权限信息进行比对。
1 2 3 4 |
// 编码 Base64.encodeToString(str.getBytes()); // 解码 Base64.decodeToString(base64Encoded); |
1 2 3 4 |
// 编码 Hex.encodeToString(str.getBytes()); // 解码 new String(Hex.decode(base64Encoded.getBytes())); |
1 2 3 4 5 6 7 8 9 10 |
String pswd = "passw0rd"; String salt = "userid"; // 加盐散列,增强安全性 // MD5算法 String md5 = new Md5Hash(pswd, salt).toString(); // SHA256算法 String sha1 = new Sha256Hash(pswd, salt).toString(); // 其它散列算法 new SimpleHash("SHA-1", str, salt).toString(); |
此接口提供更加复杂的哈希计算逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
DefaultHashService hashService = new DefaultHashService(); // 默认哈希算法 hashService.setHashAlgorithmName( "SHA-512" ); // 私盐,服务内部使用,和公盐配对生成加盐哈希 hashService.setPrivateSalt( new SimpleByteSource( "privatesalt" ) ); // 是否生成公盐,如果HashRequest没有提供盐,则可以使用随机生成的公盐 hashService.setGeneratePublicSalt( true ); // 公盐生成器 hashService.setRandomNumberGenerator( new SecureRandomNumberGenerator() ); // 生成哈希时,默认的迭代计算次数 hashService.setHashIterations( 1 ); // 哈希生成请求 HashRequest request = new HashRequest.Builder() .setAlgorithmName( "MD5" ) // 原文 .setSource( ByteSource.Util.bytes( "secret" ) ) // 请求可以指定盐 .setSalt( ByteSource.Util.bytes( "userid" ) ) // 迭代计算次数 .setIterations( 2 ).build(); // 计算哈希 String hex = hashService.computeHash( request ).toHex(); |
Shiro内置了对多种对称密钥算法的支持,例如AES:
1 2 3 4 5 6 7 8 9 |
AesCipherService aesCipherService = new AesCipherService(); // 密钥长度 aesCipherService.setKeySize( 128 ); // 生成密钥 Key key = aesCipherService.generateNewKey(); // 加密 String encrptText = aesCipherService.encrypt( "secret".getBytes(), key.getEncoded() ).toHex(); // 解密 new String( aesCipherService.decrypt( Hex.decode( encrptText ), key.getEncoded() ).getBytes() ); |
该接口提供密码加密服务,Shiro提供默认实现DefaultPasswordService。
该接口提供密码验证服务,Shiro提供实现PasswordMatcher、HashedCredentialsMatcher。
在Servlet环境下,Shiro支持基于Servlet Filter的拦截器机制:
Shiro的过滤器类层次从javax.servlet.Filter扩展。NameableFilter允许设置过滤器的名字,可以基于名字来引用过滤器。OncePerRequestFilter可以确保过滤器针对单个请求只能执行一次(不会因为forward导致重复执行),还支持禁用过滤器。ShiroFilter则是Shiro应用逻辑的入口点,需要在web.xml中注册。
拦截器逻辑主要在AdviceFilter类层次中实现,AdviceFilter引入了AOP风格的请求拦截机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public abstract class AdviceFilter extends OncePerRequestFilter { /** * 执行预处理操作,例如身份验证、授权 * 如果应当继续向前处理拦截器链,则返回true,否则返回false */ protected boolean preHandle( ServletRequest request, ServletResponse response ) throws Exception { return true; } /** * 执行后处理操作 * 如果当前拦截器执行过程中没有抛出异常,则执行此方法 */ protected void postHandle( ServletRequest request, ServletResponse response ) throws Exception { } /** * 执行清理操作,例如解除Subject和线程的绑定关系 * 即使preHandle方法返回false,也会执行此方法 */ public void afterCompletion( ServletRequest request, ServletResponse response, Exception exception ) throws Exception { } /** * 此默认实现仅仅是继续执行拦截器链 */ protected void executeChain( ServletRequest request, ServletResponse response, FilterChain chain ) throws Exception { chain.doFilter( request, response ); } /** * 此方法和Filter.doFilter类似,但是由父类保证了一次性处理。默认逻辑如下: * 1、调用preHandle,如果结果为true转到2,否则转到3 * 2、通过executeChain调用下一个拦截器。如果没有出现异常转到3,否则转到4 * 3、调用postHandle,转到4 * 4、调用afterCompletion * */ public void doFilterInternal( ServletRequest request, ServletResponse response, FilterChain chain ) throws ServletException, IOException { } } |
AdviceFilter的主要子类型包括:
拦截器 | 说明 |
PathMatchingFilter |
下面一个拦截器的基类 支持Ant风格的URL匹配,识别URL需要的访问权限 |
AccessControlFilter |
下面两个拦截器的基类 |
AuthenticationFilter | 用于确保当前Subject已经通过身份验证 |
AuthorizationFilter | 用于确保当前Subject被授权访问当前URL |
Shiro的拦截器链,代理了Servlet的过滤器链。在Servlet过滤器链执行之前,首先执行Shiro的拦截器链。代理工作由ProxiedFilterChain负责。
ProxiedFilterChain利用FilterChainResolver为当前请求装配拦截器链:
1 2 3 4 |
public interface FilterChainResolver { // 为当前请求装配拦截器链,originalChain为原始的Servlet过滤器链 FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain); } |
FilterChainResolver的实现类PathMatchingFilterChainResolver,根据INI配置文件中的urls段来决定如何装配拦截器链。
PathMatchingFilterChainResolver利用DefaultFilterChainManager来管理拦截器链,后者维护url pattern和拦截器链的映射关系。
DefaultFilterChainManager默认会添加枚举DefaultFilter中定义的拦截器。枚举成员的名称在urls段中引用,表示某个url pattern需要启用某个拦截器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); } |
IniWebEnvironment的默认行为是从INI文件的urls或filters段读取信息,来配置拦url pattern的拦截器链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
protected FilterChainResolver createFilterChainResolver() { FilterChainResolver resolver = null; Ini ini = getIni(); if (!CollectionUtils.isEmpty(ini)) { Ini.Section urls = ini.getSection( IniFilterChainResolverFactory.URLS); Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS); if (!CollectionUtils.isEmpty(urls) || !CollectionUtils.isEmpty(filters)) { IniFilterChainResolverFactory factory = new IniFilterChainResolverFactory(ini, this.objects); resolver = factory.getInstance(); } } return resolver; } |
要编程式的配置url pattern对应的拦截器,而不是静态的编写在INI文件中,可以扩展IniWebEnvironment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class IniWebEnvironment extends org.apache.shiro.web.env.IniWebEnvironment { @Override protected FilterChainResolver createFilterChainResolver() { // 创建拦截器链解析器 PathMatchingFilterChainResolver fcr = new PathMatchingFilterChainResolver(); // 添加默认拦截器实现 FilterChainManager fcm = new DefaultFilterChainManager(); for ( DefaultFilter filter : DefaultFilter.values() ) { fcm.addFilter( filter.name(), (Filter) ClassUtils.newInstance( filter.getFilterClass() ) ); } // 动态配置URL和拦截器的对应关系 fcm.addToChain( "/authenticated", "authc" ); fcm.addToChain( "/role", "authc" ); fcm.addToChain( "/role", "roles", "admin" ); return fcr; } } |
然后让Shiro使用此IniWebEnviroment:
1 2 3 4 |
<context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>cc.gmem.study.shiro.IniWebEnvironment</param-value> </context-param> |
名字 | 说明 |
身份验证 | |
authc |
org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单验证的登陆拦截器,如果当前Subject没有登陆,则跳转到预设的URL,主要属性: usernameParam 登陆名请求参数 |
authcBasic |
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter 基于HTTP基本验证的登陆拦截器,主要属性: applicationName 登陆弹框提示的应用程序名称 |
logout |
org.apache.shiro.web.filter.authc.LogoutFilter 退出登陆拦截器,主要属性: redirectUrl 退出登陆后的重定向地址 |
user |
org.apache.shiro.web.filter.authc.UserFilter 允许通过身份验证的或者记住我的用户访问 |
anon |
org.apache.shiro.web.filter.authc.AnonymousFilter 不需要登陆即可访问 |
访问控制 | |
roles |
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 基于角色的访问控制,用于验证Subject是否有当前URL的访问权限。主要属性: loginUrl 登陆URL |
perms |
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 基于权限的访问控制,用于验证Subject是否有当前URL的访问权限。主要属性: loginUrl 登陆URL |
port |
org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,自动改写URL的端口(默认80)并重定向 |
rest |
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter REST风格的拦截器,根据HTTP方法自动构建期望Subject拥有的权限字符串 例如配置/users=rest[user],会自动为不同请求方法构建: POST = user:create |
ssl |
org.apache.shiro.web.filter.authz.SslFilter 强制HTTPS拦截器,如果请求访问不是基于HTTPS协议,强制重定向 |
内置拦截器会默认注册,不需要在INI中手工配置。你可以直接修改这些拦截器的属性:
1 2 3 4 |
; 设置属性 perms.unauthorizedUrl=/unauthorized ; 禁用拦截器 perms.enabled=false |
Leave a Reply