概要介绍

人人网开源了一款基于Mybatis、Springmvc、Shiro框架的一套权限管理系统,项目的地址在此:【SpringMVC版】,【SpringBoot版】。

既然是web应用,就先从web配置开始学习。 renren-security是基于shiro框架做的权限管理。shiro和spring-security都能实现权限粒度的管控,只不过shiro提供了一套Java api,spring-security需要和springmvc整合在一起。

Shiro和Spring-security的比较

该比较来源此文:安全框架Shiro和Spring Security比较,为了方便阅读,现copy此处。

Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。如下是它所具有的特点:

  • 易于理解的 Java Security API;
  • 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  • 对角色的简单的签权(访问控制),支持细粒度的签权;
  • 支持一级缓存,以提升应用程序的性能;
  • 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
  • 异构客户端会话访问;
  • 非常简单的加密 API;
  • 不跟任何的框架或者容器捆绑,可以独立运行。

Spring Security除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。

注:

OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。

"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。

OpenID 系统的第一部分是身份验证,即如何通过 URI 来认证用户身份。目前的网站都是依靠用户名和密码来登录认证,这就意味着大家在每个网站都需要注册用户名和密码,即便你使用的是同样的密码。如果使用 OpenID ,你的网站地址(URI)就是你的用户名,而你的密码安全的存储在一个 OpenID 服务网站上(你可以自己建立一个 OpenID 服务网站,也可以选择一个可信任的 OpenID 服务网站来完成注册)。

与OpenID同属性的身份识别服务商还有ⅥeID,ClaimID,CardSpace,Rapleaf,Trufina ID Card等,其中ⅥeID通用账户的应用最为广泛。

springmvc整合shiro的第一步

介绍完shiro和spring-security的比较之后,我们来学习shiro跟springmvc的整合之一委托代理过滤器-DelegatingProxyFilter。

renren-security的web.xml中配置了shirofilter,至于shiroFilter是干什么的,我们只需要明白,该ShiroFilter一定是处理安全过滤的即可,其他的在后面讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 配置Shiro过滤器,先让Shiro过滤系统接收到的请求 -->  
<!-- 这里filter-name必须对应applicationContext.xml中定义的<bean id="shiroFilter"/> -->
<!-- 使用[/*]匹配所有请求,保证所有的可控请求都经过Shiro的过滤 -->
<!-- 通常会将此filter-mapping放置到最前面(即其他filter-mapping前面),以保证它是过滤器链中第一个起作用的 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由servlet container管理 -->
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

看到上面配置的注释,说filter-name必须对应上下文中定义的bean的id,这个特性主要是与DelegatingRoxyFilter的实现相关,具体后面讲解。此处,我们大不可将filter-name跟spring上下文的bean的id保持一致,我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<filter>
<filter-name>delegatingProxyFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>delegatingProxyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

此时我们定义了一个叫delegatingProxyFilter的filter,他不与任何其他的bean的id相同,但是我们增加了形影的初始化参数targetBeanName,其值是shiroFilter,这个很有意思,也就是上面说的spring上下文定义的bean的id,至于为什么会是这样的,下文继续分析。

Note: 实际开发中,我们尽量保持filter和bean的id一致。

DelegatingProxyFilter的作用

说明一点,上述配置中,DelegatingProxyFilter给人会引起错觉,以为是Spring-security的入口,其实不是,该类是spring-web下面的包中的一个类,说明此类与spring-security没有半毛钱的关系。

DelegatingProxyFilter继承自GenericFilterBean类,间接实现了javax.servlet.Filter接口。Sevlet容器在启动时,会调用Filter的init(FilterConfig filterConfig)方法,GenericFilterBean的作用是把Filter的初始化参数自动的set到继承于GenericFilterBean类的Filter中去,其在init方法中做了如下的东东:

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
public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
}

this.filterConfig = filterConfig;
PropertyValues pvs = new GenericFilterBean.FilterConfigPropertyValues(filterConfig, this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
Environment env = this.environment;
if (env == null) {
env = new StandardServletEnvironment();
}

bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, (PropertyResolver)env));
this.initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException var6) {
String msg = "Failed to set bean properties on filter '" + filterConfig.getFilterName() + "': " + var6.getMessage();
this.logger.error(msg, var6);
throw new NestedServletException(msg, var6);
}
}

this.initFilterBean();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
}

}

init方法主要加载了上下文的相关资源,并且调用了initFilterBean方法。而initFilterBean方法是GenericFilterBean留给子类扩展的方法,是一个空方法,下面看看DelegatingProxyFtlter子类对该方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}

从上述代码中可以看到,首先检查代理类的Filter对象delegate是否为空,如果为空,则检测代理类是否提供了代理的目标类名,如果没有提供则直接使用filter的name做为beanName,产生了beanName后,通过调用initDelegate(WebApplicationContext wac)方法,从spring的IOC容器中取出beanName对应Bean对象,再判断web.xml配置的参数targetFilterLifecycle,根据其value值决定是否调用代理目标类的init方法。

Note: spring能从IOC容器中取出beanName对应Bean对象,是因为我们在web.xml配置了listen-class为org.springframework.web.context.ContextLoaderListener,然后其对应的contextConfigLocation Spring会把spring context及spring security的XML配置文件装载入Spring Bean容器中(由XmlWebApplicationContent来装载)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Initialize the Filter delegate, defined as bean the given Spring
* application context.
* <p>The default implementation fetches the bean from the application context
* and calls the standard {@code Filter.init} method on it, passing
* in the FilterConfig of this Filter proxy.
* @param wac the root application context
* @return the initialized delegate Filter
* @throws ServletException if thrown by the Filter
* @see #getTargetBeanName()
* @see #isTargetFilterLifecycle()
* @see #getFilterConfig()
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}

我们再看看,DelegatingProxyFilter类重写GenericFilterBean的方法doFilter方法,该方法首先获取当前对象的代理目标对象delegate, 如果该对象时空的情况下,再次从IOC中获取代理的目标对象。

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
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}

// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}

最后一句invokeDelegate(delegateToUse, request, response, filterChain);是一个重点,DelegatingFilterProxy类实际是用其delegate对象doFilter方法,然后通过FilterChain实现类的实例的doFilter方法来响应请求。

Note: 在spring-security中,FilterChain的实现类是:org.springframework.security.FilterChainProxy,在shiro中则为org.apache.shiro.web.servlet.ProxiedFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/**
* Actually invoke the delegate Filter with the given request and response.
* @param delegate the delegate Filter
* @param request the current HTTP request
* @param response the current HTTP response
* @param filterChain the current FilterChain
* @throws ServletException if thrown by the Filter
* @throws IOException if thrown by the Filter
*/
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

delegate.doFilter(request, response, filterChain);
}

嗯啊,上述就是DelegatingFilterProxy类的一些内部运行机制,其实主要作用就是一个代理模式的应用,可以把servlet容器中的filter同spring容器中的bean关联起来。