Shiro安全框架学习心得(源码分析)
概述
shiro(http://shiro.apache.org/)是apache公司推出的一款轻量级开源安全框架。它支持身份验证,授权,加密和会话管理。Shiro提供易于理解的API,在项目整合时较为轻松。目前Shiro在国内的软件公司中使用较为广泛,是一个值得学习的框架。
一、基础概念
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。
上图就是Shiro的基本结构图,可以看到从Application程序段是通过一个Subject和Shiro的SecurityManager相连接的。学过英语的人都知道SecurtiyManager是指的“安全管理”的意思。理论上说这个模块应该是shiro的核心。继续往下的话可以看到Realm和SecurityManager相连。并且途中标出了Realm是用来访问安全数据的。那么也就是说在Realm中我们需要在这里访问账号密码等可以鉴权的数据从而实现认证的效果。
总结来看:
- 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
- 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。
二、核心源码解析
这里我使用SpringBoot作为项目的脚手架,Shiro官方也提供了Shiro整合SpringBoot相对应的starter。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
1、Login 流程
既然是用的SpringBoot项目,那么我们一开始就应该找Shiro相关的AutoConfigration类。可以看到,shiro的spring starter提供了如图所示如下几个自动配置类。我们先看到ShiroAutoConfiguration。
@Configuration
@SuppressWarnings("SpringFacetCodeInspection")
@ConditionalOnProperty(name = "shiro.enabled", matchIfMissing = true)
public class ShiroAutoConfiguration extends AbstractShiroConfiguration {
...
@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
return super.securityManager(realms);
}
...
}
可以看到这个类有继承AbstractShiroConfiguration,我们往父类去看。终于我们找到了主角,我们的SecurityManager。从源码中可以看到这里返回的是其子类SessionsSecurityManager。并且设置了几个属性。
protected SessionsSecurityManager securityManager(List<Realm> realms) {
SessionsSecurityManager securityManager = createSecurityManager();
securityManager.setAuthenticator(authenticator());
securityManager.setAuthorizer(authorizer());
securityManager.setRealms(realms);
securityManager.setSessionManager(sessionManager());
securityManager.setEventBus(eventBus);
if (cacheManager != null) {
securityManager.setCacheManager(cacheManager);
}
return securityManager;
}
继续深挖,可以看到真正创建出来的是DefaultSecurityManager这个对象。
protected SessionsSecurityManager createSecurityManager() {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setSubjectDAO(subjectDAO());
securityManager.setSubjectFactory(subjectFactory());
RememberMeManager rememberMeManager = rememberMeManager();
if (rememberMeManager != null) {
securityManager.setRememberMeManager(rememberMeManager);
}
return securityManager;
}
我们打开DefaultSecurityManager这个类,可以看到有很多方法。我们先重点看到 login() & logout() 这两个方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hWyh5gqO-1600916753918)(https://s1.ax1x.com/2020/09/23/wjxRXj.png)]
可以看到,login()方法就是调用了authenticate(token); 这个方法实现了认证。我们继续看authenticate(token);这个方法。
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
authenticate(Token) 这个方法仅仅是调用了“认证器”authenticator的 authenticate(token)方法进行认证。这个authenticator认证器是在初始化SecurityManager时就已经注入进来了。我们可以看一下具体是哪一个类。
/**
* Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
*/
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
可以看到使用的是ModularRealmAuthenticator这个类。并且这个类还设置了一个AuthenticationStrategy (认证策略)的东西。先不看认证策略是什么。我们先打开ModularRealmAuthenticator这个类。
protected Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(authenticationStrategy());
return authenticator;
}
看到ModularRealmAuthenticator这个类继承了AbstractAuthenticator,并且这个类中没有重写authenticate() 这个方法,那么说明重写authenticate()方法是在其父类完成的。我们再网上找一层AbstractAuthenticator。
public class ModularRealmAuthenticator extends AbstractAuthenticator {
...
}
果然在AbstractAuthenticator中找到了authenticate()方法,我们可以看到,这个方法又调用了doAuthenticate(token)这个方法,我们发现doAuthenticate(token)是一个抽象方法,那么可以确认doAuthenticate(token)这个方法是在ModularRealmAuthenticator这个类中实现的。那么我们转过头来看到ModularRealmAuthenticator。
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {
notifyFailure(token, ae);
} catch (Throwable t2) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. " +
"Please check your AuthenticationListener implementation(s). Logging sending exception " +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
....
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
throws AuthenticationException;
可以看到这里会判断realm的个数,如果仅有一个realm,那么就会执行doSingleRealmAuthentication, 否则就执行doMultiRealmAuthentication。到这里我们可以从源码中看出,SecurityManager是需要配置一个或者多个Realm的。紧接着,可以发现Realm需要support传入的token,并且realm需要获取到Authenticationinfo。也就是说Realm中需要重写support()和AuthenticationInfo()这两个方法。我们打开Realm这个类发现这也是一个接口,那么这两个方法也必定是由其子类实现的。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
...
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
在AuthenticatingRealm这个子类中实现了getAuthenticationInfo(token)这个方法。简单阅读一下代码,发现在一开始程序会读取缓存中的认证信息。如果找不到就执行doGetAuthenticationInfo(token)这个方法来获取。想想也知道doGetAuthenticationInfo(token)这个方法肯定也是抽象的,需要留给子类实现。获取到认证信息以后执行assertCredentialsMatch(token, info)来验证认证信息是否匹配。这样一来调理就很清晰了。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
2、Logout流程
相对于Login流程,Logout流程就显得较为简单了。我们回到DefaultSecurityManager这个类中,看到如下这一段代码。首先从subject中获取到认证器,然后执行认证器的onLogout(principals)方法。principals就是指用户登录时填写的账号。
public void logout(Subject subject) {
if (subject == null) {
throw new IllegalArgumentException("Subject method argument cannot be null.");
}
beforeLogout(subject);
PrincipalCollection principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Logging out subject with primary principal {}", principals.getPrimaryPrincipal());
}
Authenticator authc = getAuthenticator();
if (authc instanceof LogoutAware) {
((LogoutAware) authc).onLogout(principals);
}
}
try {
delete(subject);
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly unbind Subject. Ignoring (logging out).";
log.debug(msg, e);
}
} finally {
try {
stopSession(subject);
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
"Ignoring (logging out).";
log.debug(msg, e);
}
}
}
}
最终会找到所有设置好的Realm,并执行Realm中的OnLogout方法。
public void onLogout(PrincipalCollection principals) {
super.onLogout(principals);
Collection<Realm> realms = getRealms();
if (!CollectionUtils.isEmpty(realms)) {
for (Realm realm : realms) {
if (realm instanceof LogoutAware) {
((LogoutAware) realm).onLogout(principals);
}
}
}
}
3、Spring Web app执行流程
首先打开官方文档 http://shiro.apache.org/spring.html 。我们首先需要配置DelegatingFilterProxy这个Filter。关于DelegatingFilterProxy这个类,简单来说就是可以使用Spring来管理servlet filter的生命周期。详细的可以看这一篇博客 https://blog.csdn.net/fly910905/article/details/95062258
紧接着需要定义securityManager,ShiroFilterFactoryBean以及Realm等等。
对于如何整合为一个Spring Web app官网说的并不是很详细。所以我们还是从源码入手。首先打开ShiroWebFilterConfiguration这个类。可以看到,框架中已经给我们自动向IOC容器中注册了ShiroFilterFactoryBean这一个Filter,并且拦截了所有请求。
@Configuration
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebFilterConfiguration extends AbstractShiroWebFilterConfiguration {
@Bean
@ConditionalOnMissingBean
@Override
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
return super.shiroFilterFactoryBean();
}
@Bean(name = "filterShiroFilterRegistrationBean")
@ConditionalOnMissingBean
protected FilterRegistrationBean filterShiroFilterRegistrationBean() throws Exception {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR);
filterRegistrationBean.setFilter((AbstractShiroFilter) shiroFilterFactoryBean().getObject());
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
}
可以看到ShiroFilterFactoryBean这个类实现了,FactoryBean和BeanPostProcessor这两个接口。FactoryBean时Spring提供的用于创建Bean工厂的接口。会将要注入IOC容器的对象通过这个FactoryBean进行描述最终通过getObject()方法生成Bean对象。而BeanPostProcessor则是Spring提供的对于IOC 容器中的bean对象的生命周期的管理。提供postProcessBeforeInitialization() & postProcessAfterInitialization()两个方法,分别用来处理IOC容器加载bean对象时之前做什么,之后做什么。
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
...
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
filterFactoryBean.setFilters(filterMap);
return filterFactoryBean;
}
...
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
}
在ShiroFilterFactoryBean注入Spring IOC容器时会调用AbstractShiroFilter的createInstance()方法。其中执行了createFilterChainManager()方法。
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);
}
此时会创建DefaultFilterChainManager对象。先看一下这个类的构造器会发现会执行 addDefaultFilters(false)方法。这个方法会将DefaultFilter中定义的Filter加载到DefaultFilterChainManager对象的filters中。然后将这些默认的filter添加到global properties中。当把这些默认的filter加载完成后,ShiroFilterFactoryBean还会加载额外配置的Filter并添加global properties中。
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;
}
public class DefaultFilterChainManager implements FilterChainManager {
....
public DefaultFilterChainManager() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
addDefaultFilters(false);
}
public DefaultFilterChainManager(FilterConfig filterConfig) {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
setFilterConfig(filterConfig);
addDefaultFilters(true);
}
...
protected void addDefaultFilters(boolean init) {
for (DefaultFilter defaultFilter : DefaultFilter.values()) {
addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
}
这里存在一个DefaultFilter枚举类,列举出来了Shiro默认提供的12种Filter。
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);
...
}
通过addFilter方法将12种default filter添加DefaultFilterChainManager的filters中。
protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
Filter existing = getFilter(name);
if (existing == null || overwrite) {
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
if (init) {
initFilter(filter);
}
this.filters.put(name, filter);
}
}
当完成这一切关于filter的操作都完成后,最终返回了一个SpringShiroFilter,注入到IOC容器中,并注册成为一个Web Filter。
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);
}
}
}
默认的路由拦截规则,具体的意思暂时按下不表。
protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
当然别忘了ShiroFilterFactoryBean还实现了BeanPostProcessor这个Spring生命周期接口。看如下方法,当Spring IOC容器启动时会把所有实现了Filter接口的bean对象ShiroFilterFactoryBean的filters中。那么派生一下,开发者也可以通过自定义Filter的方式实现一些Shiro官方没有提供的特性,已满足你具体的业务需求。
/**
* 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;
}
web app启动完成后,当有请求访问进来后,按照J2EE 的web组件访问顺序, Filter -> Servlet。于是会先找此webapp中注册好的Filter,先进入Filter 执行doFilter()方法。这部分属于J2EE基础知识,如果不清楚可先看这一篇博客。https://blog.csdn.net/yi_lang/article/details/78710910
在上文中,我们已经研究出来,shiroFilterFactoryBean最终注入到IOC容器中的Filter对象的SpringShiroFilter。现在我们继续通过源码来研究它的工作流程。
我们往它的父类中寻找,最终在OncePerRequestFilter这个类中找到了doFilter()方法的实现。很简单的代码,判断是否已经执行过filter操作,或者是否可用,如果都满足条件则会执行doFilterInternal()方法。这个方法又是一个抽象方法,所以得由其子类来实现。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
...
/**
* Same contract as for
* {@link #doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)},
* but guaranteed to be invoked only once per request.
*
* @param request incoming {@code ServletRequest}
* @param response outgoing {@code ServletResponse}
* @param chain the {@code FilterChain} to execute
* @throws ServletException if there is a problem processing the request
* @throws IOException if there is an I/O problem processing the request
*/
protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException;
}
我们在其子类AbstractShiroFilter中找到了doFilterInternal()方法得实现。代码也很简单,构建好request、response和subject对象,最终执行subject.execute()方法。这个方法需要传入一个callable参数,也就是说这一步是一个异步操作。在这个异步线程中,最终执行了executeChain()方法。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
看这一连串得代码操作,是不是和之前得对应上了,getFilterChainResolver()就是在初始化ShiroFilterFactoryBean时创建并设置好得。不明白得话仔细回头看看,这里不再赘述了。直接贴源代码了。
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}
return chain;
}
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
// in spring web, the requestURI "/resource/menus" ---- "resource/menus/" bose can access the resource
// but the pathPattern match "/resource/menus" can not match "resource/menus/"
// user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
if(requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
&& requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
requestURI = requestURI.substring(0, requestURI.length() - 1);
}
//the 'chain names' in this implementation are actually path patterns defined by the user. We just use them
//as the chain name for the FilterChainManager's requirements
for (String pathPattern : filterChainManager.getChainNames()) {
if (pathPattern != null && !DEFAULT_PATH_SEPARATOR.equals(pathPattern)
&& pathPattern.endsWith(DEFAULT_PATH_SEPARATOR)) {
pathPattern = pathPattern.substring(0, pathPattern.length() - 1);
}
// If the path does match, then pass on to the subclass implementation for specific checks:
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + Encode.forHtml(requestURI) + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
public FilterChain proxy(FilterChain original, String chainName) {
NamedFilterList configured = getChain(chainName);
if (configured == null) {
String msg = "There is no configured chain under the name/key [" + chainName + "].";
throw new IllegalArgumentException(msg);
}
return configured.proxy(original);
}
public FilterChain proxy(FilterChain orig) {
return new ProxiedFilterChain(orig, this);
}
最终返回了一个ProxiedFilterChain这个类实现了FilterChain接口。接下来就很基础了,一个简单的过滤器链。最终执行的会是这个链中所有filter的doFilter() 方法。哪些Filter呢?还记得我们之前按下不表的ShiroFilterChainDefinition对象吗?就是通过它来实现的。
public class ProxiedFilterChain implements FilterChain {
//TODO - complete JavaDoc
private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);
private FilterChain orig;
private List<Filter> filters;
private int index = 0;
public ProxiedFilterChain(FilterChain orig, List<Filter> filters) {
if (orig == null) {
throw new NullPointerException("original FilterChain cannot be null.");
}
this.orig = orig;
this.filters = filters;
this.index = 0;
}
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
//we've reached the end of the wrapped chain, so invoke the original one:
if (log.isTraceEnabled()) {
log.trace("Invoking original filter chain.");
}
this.orig.doFilter(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Invoking wrapped filter at index [" + this.index + "]");
}
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}
在shiroFilterFactoryBean中有这么一段代码,上文中也有提及。这次我们把所有源码都贴出来。是不是很好理解了。Shiro会把ShiroFilterChainDefinition定义好的路由拦截规则所对应的Filter加载进这个FilterChain对象中。构建成一个完整的过滤器链。
//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);
}
}
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]);
}
}
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);
}
到此已经搞清楚了Shiro是如何通过Web Filter&Filter Chain原理工作的了。现在我选择一个最重要的Defualt Filter进行源码解析。也就是FormAuthenticationFilter。
一直向上找它的父类,可以看到最终在OncePerRequestFilter这个类中找到了doFilter方法的实现。它最终也会执行doFilterInternal()方法。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
在其子类AdviceFilter中找到了doFilterInternal()方法的实现。逻辑很简单,执行preHandle() 判断是否还需要执行FilterChain,之后执行postHandle().
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
Exception exception = null;
try {
boolean continueChain = preHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
}
if (continueChain) {
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);
}
}
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
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()) {
// If the path does match, then pass on to the subclass implementation for specific checks
//(first match 'wins'):
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 {
if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
if (log.isTraceEnabled()) {
log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}]. " +
"Delegating to subclass implementation for 'onPreHandle' check.",
new Object[]{getName(), path, pathConfig});
}
//The filter is enabled for this specific request, so delegate to subclass implementations
//so they can decide if the request should continue through the chain or not:
return onPreHandle(request, response, pathConfig);
}
if (log.isTraceEnabled()) {
log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}]. " +
"The next element in the FilterChain will be called immediately.",
new Object[]{getName(), path, pathConfig});
}
//This filter is disabled for this specific request,
//return 'true' immediately to indicate that the filter will not process the request
//and let the request/response to continue through the filter chain:
return true;
}
顺着代码往下走可以看到最终执行的是AccessControlFilter中的onPreHandle()方法。
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
继续往下走终于,我们看到了这一段代码。当系统已经完成认证,则直接放行,否则判断是否是登录的地址,如果是则执行登录操作。通过这一段代码,我们可以了解到真正执行登录操作的代码时:subject.login();这个方法又做了些什么呢?继续往下看
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated() && subject.getPrincipal() != null;
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
哈哈,是不是更加眼熟了。最终执行了securityManager.login()方法。这就回到了我们一开始在登录流程中解析的SecurityManager login原理。到现在为止,总算时弄清楚了整个框架的执行流程。
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}