0%

Java:Shiro+SpringMVC的集成实践

摘要

Java:Shiro+SpringMVC的集成实践。

博客

原帖位于IT老兵博客

前言

个人感觉,Shiro的官网有一个问题,讲的不够清楚,尽管看上去好像讲的挺明白,但是我总是感觉很多地方不够清楚,事实上,在阅读了很多帖子之后,发现很多人都对这一点存在疑问,那就不是我一个人的问题了。

Shiro的官网缺乏完整的例子,而且我所处理的项目是Spring的项目,如何清楚地集成在一起,似乎还没有看到,很多地方都需要摸索,看了张开涛的博客,下面一样有很多人存有疑问。

之前研究这个,花了几天的时间研究理论,感觉自己已经明白了(这个感觉在另外一篇帖子《Java:Shiro的架构学习笔记》里面有提到),实际上是,纸上得来终觉浅,绝知此事要躬行。

这篇文章结合着自己的例子,把所理解到的东西做一个总结,以备日后查看,也给需要的朋友们一个参考。

正文

项目用的是XML配置,至于注解如何配置,暂时还没有时间去研究。

项目中定义一个spring-shiro.xml文件,配置在classpath里面,可以被系统读取到,这块涉及Spring读取配置文件的功能,官网是写在了applicationContext.xml文件里面,然后在web.xml里面定义filter,现在做项目,似乎已经很少用到这个web.xml文件,基本都是定义在spring-mvc.xml这个文件里面,这里给shiro单独定义了一个配置文件,原理是一样的。

先定义filter:

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
</bean>

这个将会构造一个shiroFilter,参数是securityManager。

然后需要在web.xml中定义:

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
        <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>

可以参考这里,这里有点复杂,我感觉还没有完全搞明白,大概的意思是要这么做,对SecurityManager进行装配。

DelegatingFilterProxy is a class in Spring's Web module. It provides features for making HTTP calls pass through filters before reaching the actual destination. With the help of DelegatingFilterProxy, a class implementing the javax.Servlet.Filter interface can be wired into the filter chain.

As an example, Spring Security makes use of DelegatingFilterProxy to so it can take advantage of Spring’s dependency injection features and lifecycle interfaces for security filters.

DelegatingFilterProxy also leverages invoking specific or multiple filters as per Request URI paths by providing the configuration in Spring's application context or in web.xml.

原文是这么讲述DelegatingFilterProxy的作用的,最关键的是第二段,这样的话,它就能充分享受Spring的依赖注入的特征和生命周期接口。

再定义securityManager:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="tokenRealm" />
    <property name="cacheManager" ref="cacheManager"></property>
    <property name="sessionManager" ref="sessionManager" />
    <property name="subjectFactory" ref="subjectFactory"/> 
    <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="true"/>
    <!-- By default the servlet container sessions will be used. Uncomment 
        this line to use shiro's native sessions (see the JavaDoc for more): -->
    <property name="sessionMode" value="http"/>
</bean>

这里构造了securityManager,并且传递了6个参数给它,每个参数可以是自己写的继承类,也可以是默认的类,这里涉及一些业务隐私的问题,不能都贴出来了。

第一个tokenRealm是用于进行认证的组件。

<bean id="tokenRealm" class="xx.xx.xx.TokenRealm">
    <property name="credentialsMatcher" ref="credentialsMatcher"/>  
</bean>

参数是自定义的一个凭证匹配器。
这里需要覆写两个方法:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException 返回认证信息。

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 返回授权信息。

这个地方之前一直没有搞明白,是最让我困惑的地方,doGetAuthenticationInfo的第一个参数就是login方法送过来的token,一般这个token带有username和password,这里根据这个用户名去把数据库把用户的密码取出来,然后构造一个SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());返回,然后会交由匹配器去匹配,匹配器主要匹配第二个参数(原型是:SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)),即凭证是否相等。

而自定义的匹配器大体是下面这样,覆写匹配的函数(增加了缓存来保存尝试次数):

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
@Override  
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = loginRetryCache.get(username);
System.out.println("重试次数:" + retryCount);
if (retryCount != null && retryCount.intValue() >= maxRetryCount) {
throw new ExcessiveAttemptsException("username: " + username + " tried to login more than 5 times in period");
}

boolean matches = super.doCredentialsMatch(token, info);
if (matches) {
//clear retry data
System.out.println("清除重试次数缓存");
if (retryCount != null) {
loginRetryCache.remove(username);
}
return true;
} else {
if (null == retryCount) {
retryCount = new AtomicInteger(1);
loginRetryCache.put(username, retryCount);
System.out.println("插入缓存,失败次数:" + retryCount);
} else if (retryCount.incrementAndGet() >= maxRetryCount) {
log.warn("username: " + username + " tried to login more than 5 times in period");
throw new ExcessiveAttemptsException("username: " + username + " tried to login more than 5 times in period");
}

retryCount = loginRetryCache.get(username);
System.out.println("认证失败,失败次数:" + retryCount);
return false;
}

}

在login完成后,Shiro其实会返回给客户端一个JSESSIONID,并且会在缓存中保存关于这个会话的一些信息,这些会话信息会被定期清理(由调度任务15分钟或者是下一次访问时判断是否过期)或者是由logout方法主动注销掉。

总结

初步总结了一下Shiro的用法,实践了一天,总结了一天,终于感觉搞明白了,使用Shiro的难度主要在于牵扯的类比较多,而且文档说的不是太清楚,需要自己反复地实践。这也可能说明它设计得很灵活,一般设计得很灵活的东西,都是不容易掌握,但是,一旦掌握了,就非常得方便。
这篇帖子还会不断更新,直到把这个地方的概念全部梳理清楚。

参考

https://shiro.apache.org/10-minute-tutorial.html
https://shiro.apache.org/static/1.3.0/apidocs/org/apache/shiro/authc/SimpleAuthenticationInfo.html
https://shiro.apache.org/architecture.html
https://stackoverflow.com/questions/6725234/whats-the-point-of-spring-mvcs-delegatingfilterproxy
https://docs.spring.io/spring-security/site/docs/3.0.x/reference/security-filter-chain.html
https://shiro.apache.org/static/1.3.0/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html