0. 核心概念
Shiro 的架构图如下:
几个核心概念如下:
subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
securityManager:安全管理器,主体进行认证和授权都 是通过securityManager进行。
authenticator:认证器,主体进行认证最终通过authenticator进行的。 Authentication - 认证(也就是验证身份合法性)
authorizer:授权器,主体进行授权最终通过authorizer进行的。Authorization - 鉴权(也就是判断是否有相应的权限)
sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。注意:在realm中存储授权和认证的逻辑。
1. 自动配置
1. pom 引入如下配置:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency>
2. 自定义Realm
import com.zd.bx.bean.user.User; import com.zd.bx.service.user.TokenService; import com.zd.bx.utils.permission.PermissionUtils; import com.zd.bx.utils.shiro.Token; import com.zd.bx.utils.system.SpringBootUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CustomRealm extends AuthorizingRealm { private static final Logger log = LoggerFactory.getLogger(CustomRealm.class); /** * 鉴权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // getPrimaryPrincipal获取到的是doGetAuthenticationInfo方法最后存进去的user对象 Object primaryPrincipal = principalCollection.getPrimaryPrincipal(); if (primaryPrincipal == null) { return null; } SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User currentUser = (User) primaryPrincipal; // 添加角色 authorizationInfo.addRoles(PermissionUtils.listUserRolenames(currentUser)); // 添加权限 authorizationInfo.addStringPermissions(PermissionUtils.listUserPermissionCodes(currentUser)); log.debug("authorizationInfo roles: {}, permissions: {}", authorizationInfo.getRoles(), authorizationInfo.getStringPermissions()); return authorizationInfo; } /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken == null) { throw new IncorrectCredentialsException("token失效,请重新登录"); } Token token = (Token) authenticationToken; TokenService tokenService = SpringBootUtils.getBean(TokenService.class); User user = tokenService.getUser(token); return new SimpleAuthenticationInfo(user, token.getCredentials(), this.getName()); } // 清除缓存 public void clearCache() { PrincipalCollection principalCollection = SecurityUtils.getSubject().getPrincipals(); super.clearCache(principalCollection); } }
3. 编写Configuration 配置文件注入相关bean 到Spring 中
import com.zd.bx.utils.file.PropertiesFileUtils; import org.apache.commons.collections4.CollectionUtils; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; @Configuration public class ShiroConfig { private static final Map<String, String> FILTER_CHAIN_DEFINITION_MAP = new HashMap<>(); static { initFilterChainDefinitionMap(); } private static void initFilterChainDefinitionMap() { // 加载配置在permission.properties文件中的配置 Properties properties = PropertiesFileUtils.getProperties("permission.properties"); if (properties != null && CollectionUtils.isNotEmpty(properties.entrySet())) { Iterator<Entry<Object, Object>> iterator = properties.entrySet().iterator(); while (iterator.hasNext()) { Entry<Object, Object> next = iterator.next(); String key = next.getKey().toString(); String value = next.getValue().toString(); FILTER_CHAIN_DEFINITION_MAP.put(key, value); } } /** * 路径 -> 过滤器名称1[参数1,参数2,参数3...],过滤器名称2[参数1,参数2...]... * 自定义配置(前面是路径, 后面是具体的过滤器名称加参数,多个用逗号进行分割,过滤器参数也多个之间也是用逗号分割)) * 有的过滤器不需要参数,比如anon, authc, shiro 在解析的时候接默认解析一个数组为 [name, null] */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // 测试地址 FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[系统管理员,用户管理员],perms['user:manager:*']"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // 所有资源都需要经过验证 } // 将自己的验证方式加入容器 @Bean public CustomRealm myShiroRealm() { CustomRealm customRealm = new CustomRealm(); return customRealm; } // 权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } // Filter工厂,设置对应的过滤条件和跳转条件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 登录页和成功跳转页面不需要设置(前后端不分离项目可以写) // factoryBean.setLoginUrl("/shiro/login.html"); // factoryBean.setSuccessUrl("/shiro/index.html"); // 自定义需要权限验证的过滤器。对于前后端分离的项目可以自定重写这个filter // Map<String, Filter> filterMaps = new HashMap<>(); // filterMaps.put("authc", new ShiroAuthFilter()); // shiroFilterFactoryBean.setFilters(filterMaps); // 定义处理规则 shiroFilterFactoryBean.setFilterChainDefinitionMap(setFilterChainDefinitionMap()); return shiroFilterFactoryBean; } private Map<String, String> setFilterChainDefinitionMap() { return FILTER_CHAIN_DEFINITION_MAP; } }
4. 编写测试Controller
import com.zd.bx.bean.user.Permission; import com.zd.bx.service.user.PermissionService; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @RequiresPermissions(value = {"test"}) @GetMapping("/test") public String test() { return "test"; } @Autowired private PermissionService permissionService; @GetMapping("/test2") public String test2() { Permission permissions = permissionService.selectByUniqueCode("2aeb7756-62f7-49ba-aa41-31e1bbc87040"); Permission permissions2 = permissionService.selectByUniqueCode("2aeb7756-62f7-49ba-aa41-31e1bbc87040"); int i = permissionService.selectCount(null); System.out.println(permissions); System.out.println(permissions2); System.out.println(i); return "test"; } }
5. 测试
(1) 测试可以匿名访问的连接:
$ curl http://localhost:8081/test2 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 4 100 4 0 0 129 0 --:--:-- --:--:-- --:--:-- 137test
(2) 访问需要鉴权的连接 /test
页面会被重定向到:/login.jsp
如果想设置登录页面、未授权页面、登录成功页面, 可以通过上面 ShiroFilterFactoryBean 进行设置。 到这里基本的功能可以使用。下面研究其执行原理。
2. 源码剖析
我们在Configuration 中注入的对象有三个CustomRealm、SecurityManager、ShiroFilterFactoryBean。 前面两个对象可以说是shiro必须的组件,realm 注入到Securitymanager 中; SecurityManager 注入到了 ShiroFilterFactoryBean 中。
1. SecurityManager 内部包含的相关属性
如下代码:
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; }
1》这里设置了一个自定义的Realm,也可以调用 setRealms 设置多个Realm.
2》DefaultWebSecurityManager 构造查看
其继承关系如下:
1》 从CachingSecurityManager 开始查看其内部重要属性以及方法:
public abstract class CachingSecurityManager implements SecurityManager, Destroyable, CacheManagerAware, EventBusAware { private CacheManager cacheManager; private EventBus eventBus;
这里有缓存管理器相关。
2》RealmSecurityManager
public abstract class RealmSecurityManager extends CachingSecurityManager { private Collection<Realm> realms; public RealmSecurityManager() { } public void setRealm(Realm realm) { if (realm == null) { throw new IllegalArgumentException("Realm argument cannot be null"); } else { Collection<Realm> realms = new ArrayList(1); realms.add(realm); this.setRealms(realms); } } public void setRealms(Collection<Realm> realms) { if (realms == null) { throw new IllegalArgumentException("Realms collection argument cannot be null."); } else if (realms.isEmpty()) { throw new IllegalArgumentException("Realms collection argument cannot be empty."); } else { this.realms = realms; this.afterRealmsSet(); } }
有设置Realm 相关的API
3》 org.apache.shiro.mgt.AuthenticatingSecurityManager
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager { private Authenticator authenticator = new ModularRealmAuthenticator();
包含一个默认的认证器。
4》org.apache.shiro.mgt.AuthorizingSecurityManager
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager { private Authorizer authorizer = new ModularRealmAuthorizer();
包含一个默认的鉴权器
5》org.apache.shiro.mgt.SessionsSecurityManager
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager { private SessionManager sessionManager = new DefaultSessionManager();
包含一个默认的session 管理器
6》 org.apache.shiro.mgt.DefaultSecurityManager
public class DefaultSecurityManager extends SessionsSecurityManager { private static final Logger log = LoggerFactory.getLogger(DefaultSecurityManager.class); protected RememberMeManager rememberMeManager; protected SubjectDAO subjectDAO; protected SubjectFactory subjectFactory; public DefaultSecurityManager() { this.subjectFactory = new DefaultSubjectFactory(); this.subjectDAO = new DefaultSubjectDAO(); } public DefaultSecurityManager(Realm singleRealm) { this(); this.setRealm(singleRealm); } public DefaultSecurityManager(Collection<Realm> realms) { this(); this.setRealms(realms); }
包含两个重要属性:RememberMeManager、 SubjectFactory
7》 org.apache.shiro.web.mgt.DefaultWebSecurityManager#DefaultWebSecurityManager
public DefaultWebSecurityManager() { super(); DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(webEvalutator); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); webEvalutator.setSessionManager(getSessionManager()); }
从上面的继承链可以看到我们的SecurityManager 通过一系列的继承可以具有: cache管理、realm管理器、认证器、鉴权器、session 管理器等重要功能。
2. ShiroFilterFactoryBean 查看
可以看到这个类是一个FactoryBean, 其注入到Spring 内部的是getOabject 返回的对象。
org.apache.shiro.spring.web.ShiroFilterFactoryBean 源码如下:
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.spring.web; import org.apache.shiro.config.Ini; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.util.CollectionUtils; import org.apache.shiro.util.Nameable; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.config.IniFilterChainResolverFactory; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.filter.authc.AuthenticationFilter; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager; import org.apache.shiro.web.filter.mgt.FilterChainManager; import org.apache.shiro.web.filter.mgt.FilterChainResolver; import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; import org.apache.shiro.web.mgt.WebSecurityManager; import org.apache.shiro.web.servlet.AbstractShiroFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanPostProcessor; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * {@link org.springframework.beans.factory.FactoryBean FactoryBean} to be used in Spring-based web applications for * defining the master Shiro Filter. * <h4>Usage</h4> * Declare a DelegatingFilterProxy in {@code web.xml}, matching the filter name to the bean id: * <pre> * <filter> * <filter-name><b>shiroFilter</b></filter-name> * <filter-class>org.springframework.web.filter.DelegatingFilterProxy<filter-class> * <init-param> * <param-name>targetFilterLifecycle</param-name> * <param-value>true</param-value> * </init-param> * </filter> * </pre> * Then, in your spring XML file that defines your web ApplicationContext: * <pre> * <bean id="<b>shiroFilter</b>" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> * <property name="securityManager" ref="securityManager"/> * <!-- other properties as necessary ... --> * </bean> * </pre> * <h4>Filter Auto-Discovery</h4> * While there is a {@link #setFilters(java.util.Map) filters} property that allows you to assign a filter beans * to the 'pool' of filters available when defining {@link #setFilterChainDefinitions(String) filter chains}, it is * optional. * <p/> * This implementation is also a {@link BeanPostProcessor} and will acquire * any {@link javax.servlet.Filter Filter} beans defined independently in your Spring application context. Upon * discovery, they will be automatically added to the {@link #setFilters(java.util.Map) map} keyed by the bean ID. * That ID can then be used in the filter chain definitions, for example: * * <pre> * <bean id="<b>myCustomFilter</b>" class="com.class.that.implements.javax.servlet.Filter"/> * ... * <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> * ... * <property name="filterChainDefinitions"> * <value> * /some/path/** = authc, <b>myCustomFilter</b> * </value> * </property> * </bean> * </pre> * <h4>Global Property Values</h4> * Most Shiro servlet Filter implementations exist for defining custom Filter * {@link #setFilterChainDefinitions(String) chain definitions}. Most implementations subclass one of the * {@link AccessControlFilter}, {@link AuthenticationFilter}, {@link AuthorizationFilter} classes to simplify things, * and each of these 3 classes has configurable properties that are application-specific. * <p/> * A dilemma arises where, if you want to for example set the application's 'loginUrl' for any Filter, you don't want * to have to manually specify that value for <em>each</em> filter instance definied. * <p/> * To prevent configuration duplication, this implementation provides the following properties to allow you * to set relevant values in only one place: * <ul> * <li>{@link #setLoginUrl(String)}</li> * <li>{@link #setSuccessUrl(String)}</li> * <li>{@link #setUnauthorizedUrl(String)}</li> * </ul> * * Then at startup, any values specified via these 3 properties will be applied to all configured * Filter instances so you don't have to specify them individually on each filter instance. To ensure your own custom * filters benefit from this convenience, your filter implementation should subclass one of the 3 mentioned * earlier. * * @see org.springframework.web.filter.DelegatingFilterProxy DelegatingFilterProxy * @since 1.0 */ public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor { private static transient final Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class); private SecurityManager securityManager; private Map<String, Filter> filters; private Map<String, String> filterChainDefinitionMap; //urlPathExpression_to_comma-delimited-filter-chain-definition private String loginUrl; private String successUrl; private String unauthorizedUrl; private AbstractShiroFilter instance; public ShiroFilterFactoryBean() { this.filters = new LinkedHashMap<String, Filter>(); this.filterChainDefinitionMap = new LinkedHashMap<String, String>(); //order matters! } /** * Sets the application {@code SecurityManager} instance to be used by the constructed Shiro Filter. This is a * required property - failure to set it will throw an initialization exception. * * @return the application {@code SecurityManager} instance to be used by the constructed Shiro Filter. */ public SecurityManager getSecurityManager() { return securityManager; } /** * Sets the application {@code SecurityManager} instance to be used by the constructed Shiro Filter. This is a * required property - failure to set it will throw an initialization exception. * * @param securityManager the application {@code SecurityManager} instance to be used by the constructed Shiro Filter. */ public void setSecurityManager(SecurityManager securityManager) { this.securityManager = securityManager; } /** * Returns the application's login URL to be assigned to all acquired Filters that subclass * {@link AccessControlFilter} or {@code null} if no value should be assigned globally. The default value * is {@code null}. * * @return the application's login URL to be assigned to all acquired Filters that subclass * {@link AccessControlFilter} or {@code null} if no value should be assigned globally. * @see #setLoginUrl */ public String getLoginUrl() { return loginUrl; } /** * Sets the application's login URL to be assigned to all acquired Filters that subclass * {@link AccessControlFilter}. This is a convenience mechanism: for all configured {@link #setFilters filters}, * as well for any default ones ({@code authc}, {@code user}, etc), this value will be passed on to each Filter * via the {@link AccessControlFilter#setLoginUrl(String)} method<b>*</b>. This eliminates the need to * configure the 'loginUrl' property manually on each filter instance, and instead that can be configured once * via this attribute. * <p/> * <b>*</b>If a filter already has already been explicitly configured with a value, it will * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property. * * @param loginUrl the application's login URL to apply to as a convenience to all discovered * {@link AccessControlFilter} instances. * @see AccessControlFilter#setLoginUrl(String) */ public void setLoginUrl(String loginUrl) { this.loginUrl = loginUrl; } /** * Returns the application's after-login success URL to be assigned to all acquired Filters that subclass * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. The default value * is {@code null}. * * @return the application's after-login success URL to be assigned to all acquired Filters that subclass * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. * @see #setSuccessUrl */ public String getSuccessUrl() { return successUrl; } /** * Sets the application's after-login success URL to be assigned to all acquired Filters that subclass * {@link AuthenticationFilter}. This is a convenience mechanism: for all configured {@link #setFilters filters}, * as well for any default ones ({@code authc}, {@code user}, etc), this value will be passed on to each Filter * via the {@link AuthenticationFilter#setSuccessUrl(String)} method<b>*</b>. This eliminates the need to * configure the 'successUrl' property manually on each filter instance, and instead that can be configured once * via this attribute. * <p/> * <b>*</b>If a filter already has already been explicitly configured with a value, it will * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property. * * @param successUrl the application's after-login success URL to apply to as a convenience to all discovered * {@link AccessControlFilter} instances. * @see AuthenticationFilter#setSuccessUrl(String) */ public void setSuccessUrl(String successUrl) { this.successUrl = successUrl; } /** * Returns the application's after-login success URL to be assigned to all acquired Filters that subclass * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. The default value * is {@code null}. * * @return the application's after-login success URL to be assigned to all acquired Filters that subclass * {@link AuthenticationFilter} or {@code null} if no value should be assigned globally. * @see #setSuccessUrl */ public String getUnauthorizedUrl() { return unauthorizedUrl; } /** * Sets the application's 'unauthorized' URL to be assigned to all acquired Filters that subclass * {@link AuthorizationFilter}. This is a convenience mechanism: for all configured {@link #setFilters filters}, * as well for any default ones ({@code roles}, {@code perms}, etc), this value will be passed on to each Filter * via the {@link AuthorizationFilter#setUnauthorizedUrl(String)} method<b>*</b>. This eliminates the need to * configure the 'unauthorizedUrl' property manually on each filter instance, and instead that can be configured once * via this attribute. * <p/> * <b>*</b>If a filter already has already been explicitly configured with a value, it will * <em>not</em> receive this value. Individual filter configuration overrides this global convenience property. * * @param unauthorizedUrl the application's 'unauthorized' URL to apply to as a convenience to all discovered * {@link AuthorizationFilter} instances. * @see AuthorizationFilter#setUnauthorizedUrl(String) */ public void setUnauthorizedUrl(String unauthorizedUrl) { this.unauthorizedUrl = unauthorizedUrl; } /** * Returns the filterName-to-Filter map of filters available for reference when defining filter chain definitions. * All filter chain definitions will reference filters by the names in this map (i.e. the keys). * * @return the filterName-to-Filter map of filters available for reference when defining filter chain definitions. */ public Map<String, Filter> getFilters() { return filters; } /** * Sets the filterName-to-Filter map of filters available for reference when creating * {@link #setFilterChainDefinitionMap(java.util.Map) filter chain definitions}. * <p/> * <b>Note:</b> This property is optional: this {@code FactoryBean} implementation will discover all beans in the * web application context that implement the {@link Filter} interface and automatically add them to this filter * map under their bean name. * <p/> * For example, just defining this bean in a web Spring XML application context: * <pre> * <bean id="myFilter" class="com.class.that.implements.javax.servlet.Filter"> * ... * </bean></pre> * Will automatically place that bean into this Filters map under the key '<b>myFilter</b>'. * * @param filters the optional filterName-to-Filter map of filters available for reference when creating * {@link #setFilterChainDefinitionMap (java.util.Map) filter chain definitions}. */ public void setFilters(Map<String, Filter> filters) { this.filters = filters; } /** * Returns the chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted * by the Shiro Filter. Each map entry should conform to the format defined by the * {@link FilterChainManager#createChain(String, String)} JavaDoc, where the map key is the chain name (e.g. URL * path expression) and the map value is the comma-delimited string chain definition. * * @return he chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted * by the Shiro Filter. */ public Map<String, String> getFilterChainDefinitionMap() { return filterChainDefinitionMap; } /** * Sets the chainName-to-chainDefinition map of chain definitions to use for creating filter chains intercepted * by the Shiro Filter. Each map entry should conform to the format defined by the * {@link FilterChainManager#createChain(String, String)} JavaDoc, where the map key is the chain name (e.g. URL * path expression) and the map value is the comma-delimited string chain definition. * * @param filterChainDefinitionMap the chainName-to-chainDefinition map of chain definitions to use for creating * filter chains intercepted by the Shiro Filter. */ public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) { this.filterChainDefinitionMap = filterChainDefinitionMap; } /** * A convenience method that sets the {@link #setFilterChainDefinitionMap(java.util.Map) filterChainDefinitionMap} * property by accepting a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs). * Each key/value pair must conform to the format defined by the * {@link FilterChainManager#createChain(String,String)} JavaDoc - each property key is an ant URL * path expression and the value is the comma-delimited chain definition. * * @param definitions a {@link java.util.Properties Properties}-compatible string (multi-line key/value pairs) * where each key/value pair represents a single urlPathExpression-commaDelimitedChainDefinition. */ public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); //did they explicitly state a 'urls' section? Not necessary, but just in case: Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { //no urls section. Since this _is_ a urls chain definition property, just assume the //default section contains only the definitions: section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } setFilterChainDefinitionMap(section); } /** * Lazily creates and returns a {@link AbstractShiroFilter} concrete instance via the * {@link #createInstance} method. * * @return the application's Shiro Filter instance used to filter incoming web requests. * @throws Exception if there is a problem creating the {@code Filter} instance. */ public Object getObject() throws Exception { if (instance == null) { instance = createInstance(); } return instance; } /** * Returns <code>{@link org.apache.shiro.web.servlet.AbstractShiroFilter}.class</code> * * @return <code>{@link org.apache.shiro.web.servlet.AbstractShiroFilter}.class</code> */ public Class getObjectType() { return SpringShiroFilter.class; } /** * Returns {@code true} always. There is almost always only ever 1 Shiro {@code Filter} per web application. * * @return {@code true} always. There is almost always only ever 1 Shiro {@code Filter} per web application. */ public boolean isSingleton() { return true; } protected FilterChainManager createFilterChainManager() { DefaultFilterChainManager manager = new DefaultFilterChainManager(); Map<String, Filter> defaultFilters = manager.getFilters(); //apply global settings if necessary: for (Filter filter : defaultFilters.values()) { applyGlobalPropertiesIfNecessary(filter); } //Apply the acquired and/or configured filters: 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); } //'init' argument is false, since Spring-configured filters should be initialized //in Spring (i.e. 'init-method=blah') or implement InitializingBean: 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(); manager.createChain(url, chainDefinition); } } return manager; } /** * This implementation: * <ol> * <li>Ensures the required {@link #setSecurityManager(org.apache.shiro.mgt.SecurityManager) securityManager} * property has been set</li> * <li>{@link #createFilterChainManager() Creates} a {@link FilterChainManager} instance that reflects the * configured {@link #setFilters(java.util.Map) filters} and * {@link #setFilterChainDefinitionMap(java.util.Map) filter chain definitions}</li> * <li>Wraps the FilterChainManager with a suitable * {@link org.apache.shiro.web.filter.mgt.FilterChainResolver FilterChainResolver} since the Shiro Filter * implementations do not know of {@code FilterChainManager}s</li> * <li>Sets both the {@code SecurityManager} and {@code FilterChainResolver} instances on a new Shiro Filter * instance and returns that filter instance.</li> * </ol> * * @return a new Shiro Filter reflecting any configured filters and filter chain definitions. * @throws Exception if there is a problem creating the AbstractShiroFilter instance. */ protected AbstractShiroFilter createInstance() throws Exception { log.debug("Creating Shiro Filter instance."); SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } FilterChainManager manager = createFilterChainManager(); //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built //FilterChainResolver. It doesn't matter that the instance is an anonymous inner class //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts //injection of the SecurityManager and FilterChainResolver: return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); } private void applyLoginUrlIfNecessary(Filter filter) { String loginUrl = getLoginUrl(); if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) { AccessControlFilter acFilter = (AccessControlFilter) filter; //only apply the login url if they haven't explicitly configured one already: String existingLoginUrl = acFilter.getLoginUrl(); if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) { acFilter.setLoginUrl(loginUrl); } } } private void applySuccessUrlIfNecessary(Filter filter) { String successUrl = getSuccessUrl(); if (StringUtils.hasText(successUrl) && (filter instanceof AuthenticationFilter)) { AuthenticationFilter authcFilter = (AuthenticationFilter) filter; //only apply the successUrl if they haven't explicitly configured one already: String existingSuccessUrl = authcFilter.getSuccessUrl(); if (AuthenticationFilter.DEFAULT_SUCCESS_URL.equals(existingSuccessUrl)) { authcFilter.setSuccessUrl(successUrl); } } } private void applyUnauthorizedUrlIfNecessary(Filter filter) { String unauthorizedUrl = getUnauthorizedUrl(); if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) { AuthorizationFilter authzFilter = (AuthorizationFilter) filter; //only apply the unauthorizedUrl if they haven't explicitly configured one already: String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl(); if (existingUnauthorizedUrl == null) { authzFilter.setUnauthorizedUrl(unauthorizedUrl); } } } private void applyGlobalPropertiesIfNecessary(Filter filter) { applyLoginUrlIfNecessary(filter); applySuccessUrlIfNecessary(filter); applyUnauthorizedUrlIfNecessary(filter); } /** * Inspects a bean, and if it implements the {@link Filter} interface, automatically adds that filter * instance to the internal {@link #setFilters(java.util.Map) filters map} that will be referenced * later during filter chain construction. */ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Filter) { log.debug("Found filter chain candidate filter '{}'", beanName); Filter filter = (Filter) bean; applyGlobalPropertiesIfNecessary(filter); getFilters().put(beanName, filter); } else { log.trace("Ignoring non-Filter bean '{}'", beanName); } return bean; } /** * Does nothing - only exists to satisfy the BeanPostProcessor interface and immediately returns the * {@code bean} argument. */ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } /** * Ordinarily the {@code AbstractShiroFilter} must be subclassed to additionally perform configuration * and initialization behavior. Because this {@code FactoryBean} implementation manually builds the * {@link AbstractShiroFilter}'s * {@link AbstractShiroFilter#setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager) securityManager} and * {@link AbstractShiroFilter#setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver) filterChainResolver} * properties, the only thing left to do is set those properties explicitly. We do that in a simple * concrete subclass in the constructor. */ private static final class SpringShiroFilter extends AbstractShiroFilter { protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) { super(); if (webSecurityManager == null) { throw new IllegalArgumentException("WebSecurityManager property cannot be null."); } setSecurityManager(webSecurityManager); if (resolver != null) { setFilterChainResolver(resolver); } } } }
1. org.apache.shiro.spring.web.ShiroFilterFactoryBean#getObjectType 决定返回对象的类型:SpringShiroFilter。 SpringShiroFilter的继承关系如下:
可以看到是一个javax.servlet.Filter, 也就是我们常说的filter。 既然是一个filter, 之前在Spring 动态注入filter 的过程中了解过。默认这种方式注入的filter,name 为bean的名称,拦截的url 默认为"/*", 也就是所有请求。
这里也可以看出,shiro 发挥作用是从这个 SpringShiroFilter 开始的。
2. getObject 返回给Spring 的对象 org.apache.shiro.spring.web.ShiroFilterFactoryBean#createInstance 代码解读
(1) 获取到securityManager, 并对其进行验证,类型必须是 WebSecurityManager
(2) 调用 createFilterChainManager 创建 FilterChainManager, 这个对象是shiro 封装的自己内部过滤的责任链条。
protected FilterChainManager createFilterChainManager() { DefaultFilterChainManager manager = new DefaultFilterChainManager(); Map<String, Filter> defaultFilters = manager.getFilters(); //apply global settings if necessary: for (Filter filter : defaultFilters.values()) { applyGlobalPropertiesIfNecessary(filter); } //Apply the acquired and/or configured filters: 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); } //'init' argument is false, since Spring-configured filters should be initialized //in Spring (i.e. 'init-method=blah') or implement InitializingBean: 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(); manager.createChain(url, chainDefinition); } } return manager; }
1》 org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#DefaultFilterChainManager() 构造如下:
public DefaultFilterChainManager() { this.filters = new LinkedHashMap<String, Filter>(); this.filterChains = new LinkedHashMap<String, NamedFilterList>(); addDefaultFilters(false); } protected void addDefaultFilters(boolean init) { for (DefaultFilter defaultFilter : DefaultFilter.values()) { addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false); } }
org.apache.shiro.web.filter.mgt.DefaultFilter 枚举类型是shiro 内置的一些的默认过滤器, 我们也可以模仿其中的过滤器进行替换或者增加自己的filter
import org.apache.shiro.util.ClassUtils; import org.apache.shiro.web.filter.authc.*; import org.apache.shiro.web.filter.authz.*; import org.apache.shiro.web.filter.session.NoSessionCreationFilter; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import java.util.LinkedHashMap; import java.util.Map; /** * Enum representing all of the default Shiro Filter instances available to web applications. Each filter instance is * typically accessible in configuration the {@link #name() name} of the enum constant. * * @since 1.0 */ public enum DefaultFilter { anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), authcBasic(BasicHttpAuthenticationFilter.class), authcBearer(BearerHttpAuthenticationFilter.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); private final Class<? extends Filter> filterClass; private DefaultFilter(Class<? extends Filter> filterClass) { this.filterClass = filterClass; } public Filter newInstance() { return (Filter) ClassUtils.newInstance(this.filterClass); } public Class<? extends Filter> getFilterClass() { return this.filterClass; } public static Map<String, Filter> createInstanceMap(FilterConfig config) { Map<String, Filter> filters = new LinkedHashMap<String, Filter>(values().length); for (DefaultFilter defaultFilter : values()) { Filter filter = defaultFilter.newInstance(); if (config != null) { try { filter.init(config); } catch (ServletException e) { String msg = "Unable to correctly init default filter instance of type " + filter.getClass().getName(); throw new IllegalStateException(msg, e); } } filters.put(defaultFilter.name(), filter); } return filters; } }
这里面每个枚举处理一种,比如 anon 是匿名请求都可以访问; authc 是认证后可以看; perms 是有响应权限可以看; roles 是有相应角色可以看;ssl 是只有https请求可以访问。
以AnonymousFilter 为例子查看:
源码如下:
public class AnonymousFilter extends PathMatchingFilter { /** * Always returns <code>true</code> allowing unchecked access to the underlying path or resource. * * @return <code>true</code> always, allowing unchecked access to the underlying path or resource. */ @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) { // Always return true since we allow access to anyone return true; } }
其继承图如下:(可以看出本质也是一个javax.servlet.Filter, 这种filter 没有添加到servlet 环境,那么是如何发挥作用的?下节研究。)
2》 getFilters() 从org.apache.shiro.spring.web.ShiroFilterFactoryBean#filters 属性中拿自己手动设置的过滤器。 如果是name 和上面默认的一样,会进行覆盖; 如果name 不一样会进行新增。最终新增或者覆盖的还是org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#filters 内部的元素。
将来如果需要新增自己的filter,可以设置到这个属性中; 如果需要替换已有的filter, 也可以设置到这个属性中,name 和原来的一致就回覆盖。
3》 getFilterChainDefinitionMap() 获取到自己设置的url 和 过滤器匹配规则,然后调用 org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#createChain 解析每个url 对应的过滤器链条。
org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#createChain:
public void createChain(String chainName, String chainDefinition) { if (!StringUtils.hasText(chainName)) { throw new NullPointerException("chainName cannot be null or empty."); } if (!StringUtils.hasText(chainDefinition)) { throw new NullPointerException("chainDefinition cannot be null or empty."); } if (log.isDebugEnabled()) { log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]"); } //parse the value by tokenizing it to get the resulting filter-specific config entries // //e.g. for a value of // // "authc, roles[admin,user], perms[file:edit]" // // the resulting token array would equal // // { "authc", "roles[admin,user]", "perms[file:edit]" } // String[] filterTokens = splitChainDefinition(chainDefinition); //each token is specific to each filter. //strip the name and extract any filter-specific config between brackets [ ] for (String token : filterTokens) { String[] nameConfigPair = toNameConfigPair(token); //now we have the filter name, path and (possibly null) path-specific config. Let's apply them: addToChain(chainName, nameConfigPair[0], nameConfigPair[1]); } } protected String[] splitChainDefinition(String chainDefinition) { return StringUtils.split(chainDefinition, StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true); } protected String[] toNameConfigPair(String token) throws ConfigurationException { try { String[] pair = token.split("\[", 2); String name = StringUtils.clean(pair[0]); if (name == null) { throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token); } String config = null; if (pair.length == 2) { config = StringUtils.clean(pair[1]); //if there was an open bracket, it assumed there is a closing bracket, so strip it too: config = config.substring(0, config.length() - 1); config = StringUtils.clean(config); //backwards compatibility prior to implementing SHIRO-205: //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets //if that config required commas. We need to strip those quotes to get to the interior quoted definition //to ensure any existing quoted definitions still function for end users: if (config != null && config.startsWith(""") && config.endsWith(""")) { String stripped = config.substring(1, config.length() - 1); stripped = StringUtils.clean(stripped); //if the stripped value does not have any internal quotes, we can assume that the entire config was //quoted and we can use the stripped value. if (stripped != null && stripped.indexOf('"') == -1) { config = stripped; } //else: //the remaining config does have internal quotes, so we need to assume that each comma delimited //pair might be quoted, in which case we need the leading and trailing quotes that we stripped //So we ignore the stripped value. } } return new String[]{name, config}; } catch (Exception e) { String msg = "Unable to parse filter chain definition token: " + token; throw new ConfigurationException(msg, e); } } public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) { if (!StringUtils.hasText(chainName)) { throw new IllegalArgumentException("chainName cannot be null or empty."); } Filter filter = getFilter(filterName); if (filter == null) { throw new IllegalArgumentException("There is no filter with name '" + filterName + "' to apply to chain [" + chainName + "] in the pool of available Filters. Ensure a " + "filter with that name/path has first been registered with the addFilter method(s)."); } applyChainConfig(chainName, filter, chainSpecificFilterConfig); NamedFilterList chain = ensureChain(chainName); chain.add(filter); } protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) { if (log.isDebugEnabled()) { log.debug("Attempting to apply path [" + chainName + "] to filter [" + filter + "] " + "with config [" + chainSpecificFilterConfig + "]"); } if (filter instanceof PathConfigProcessor) { ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig); } else { if (StringUtils.hasText(chainSpecificFilterConfig)) { //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor //this is an erroneous config: String msg = "chainSpecificFilterConfig was specified, but the underlying " + "Filter instance is not an 'instanceof' " + PathConfigProcessor.class.getName() + ". This is required if the filter is to accept " + "chain-specific configuration."; throw new ConfigurationException(msg); } } } protected NamedFilterList ensureChain(String chainName) { NamedFilterList chain = getChain(chainName); if (chain == null) { chain = new SimpleNamedFilterList(chainName); this.filterChains.put(chainName, chain); } return chain; } private Map<String, NamedFilterList> filterChains;
解析我们为URL配置的过滤器以及相应的参数是在这一步, 也可以看到其解析以及字符串分割规则。
这里代码的核心思想就是根据为每个URL建立一个对应的 NamedFilterList, 并将其需要经过的filter维持到NamedFilterList这个属性内部; 然后将这个 NamedFilterList 添加到 org.apache.shiro.web.filter.mgt.DefaultFilterChainManager#filterChains 属性中。
比如上面的代码解析到的 filterChains如下:
(3) 创建一个PathMatchingFilterChainResolver 对象, 并将上面(2) 解析到的FilterChainManager 对象作为属性维持到 PathMatchingFilterChainResolver 内部
(4) 创建一个SpringShiroFilter, 并且构造方法上面传了 WebSecurityManager 和 PathMatchingFilterChainResolver。接下来后期对请求做处理的也就是这两个重要的属性。
前期的准备以及解析工作已经完成,接下来请求进来就是这个filter 发挥作用。