• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

Spring MVC 中集成 Apache Shiro 安全框架

我们在这里将对一个集成了 Spring MVC+Hibernate+Apache Shiro的项目进行了一个简单说明。这个项目将展示如何在 Spring MVC 中使用 Apache Shiro来构建我们的安全框架。
阅读文章前,您需要做以下准备:
  • Maven 3 环境
  • Mysql-5.6+
  • JDK1.7+
  • git 环境
  • git.oschina.net 帐号
  • Apache Tomcat 7+
  • 您熟练掌握的编辑工具,推荐使用 InterlliJ IDEA 14+
开始
安全管理框架数据结构
首先,我们在 mysql 数据库中创建 schema,命名为 shirodemo。我们在创建两个用户 shiroDemo@localhost 和 shiroDemo@%,这里我们将用户的密码简单设置成 123456。
然后,我们将项目从 git 服务器上 clone 到本地后,我们可以在项目根目录下的 resources 中发现 db.sql 文件。这个文件是项目的数据库结构文件,你可以将 db.sql 导入到数据库 shirodemo 中。
我们这里的权限结构设计比较简单,我们以表格的形式说明主要数据库结构:
t_user 和 t_role 表就不用详细介绍了,就是系统的用户表和角色表。它与 t_role 角色表的关系是多对多的关系,即一个用户可以有多个角色,一个角色可以包含多个用户。
那么我们介绍一下 t_permission 表,这个表存放的数据是角色拥有的 permission(这里我们就用 shiro 的 permission 概念,不翻译了。因为翻译过来是许可,但是许可二字还不能完全阐释 permission 的概念)。每一个 role 会对应一个 permission,即一对一的关系。
表 t_authc_map 存储的是Shiro filter 需要的配置数据,这些数据组合起来,定义了访问控制(Access Controll)的规则,即定义了哪些 url 可以被拥有哪些 permission 或者拥有哪些 role 的用户访问。在我们这个例子中,其实这张表用处不大。当初设计这样一张表的目的是,能够动态管理访问控制的规则,但是并不能。
提示: 访问控制规则的数据是在 Spring bean 初始化时就加载给了访问控制的 filter。我们试想一下,在你的 webapp 运行时(runtime),我们可以通过一些手段来修改系统的访问控制规则,那么势必会造成用户提交事务时的处理变得非常复杂。例如,用户正在访问一个 url 连接,我们通过后台修改了 url 的访问控制权限,这时这个用户已经提交了一次事务操作,那么怎么判断这次提交是否合法呢?要把这个问题处理清楚就很复杂。那么你可能会问,如果我为系统增加了一个模块,模块中有一些新建的 url 需要提供给用户访问,但是我不想重启我的应用,直接在数据库中配置完成,怎么办?我想,既然增加了模块,当然需要重新部署,那么仅通过配置数据完成部署,我感觉在现在 Spring MVC 下,很难实现。所以个人看法,访问控制规则数据使用配置文件还是持久化到数据库,没有什么区别。但是本文中还是会介绍如何将访问控制规则持久化到数据库中。
shiroTest 模块
我们可以看见项目中有一个 shiroTest 模块,这个模块中主要实现在单元测试时,使用的通用程序。在本例中,我们在 shiroTest 模块中实现一个 proxool 数据源,为其他模块在单元测试时提供数据库连接。
请注意,我们这里配置的数据源,仅提供给单元测试使用。而我们的 webapp 中将使用 Spring 的 JNDI 数据源。为什么这么做呢?主要原因是:本例中我们使用的是 Tomcat 做为中间件,但是实际项目的生产环境,可能使用商业中间件,例如 Weblogic 等等。那么我们在迁移过程中,就不用考虑中间件使用的是什么数据源,只去调用中间件 JNDI 上绑定的数据源名称就可以了。而且这些商业中间件一般都有很好的数据源管理功能。如果我们使用独立的数据源,那么数据源就脱离的中间件的管理,岂不是功能浪费?
我们在 test 中,实现一个测试用例,这个测试用例主要测试数据源的连接:
public void testApp() throws SQLException {
        ApplicationContext cxt = new ClassPathXmlApplicationContext(
                “classpath*:conf/*-beans.xml”);
        DataSource ds= (DataSource) cxt.getBean(“ds-default”);
        Connection con=ds.getConnection();
        con.close();
        assertTrue(true);
}
我们在 shiroTest 项目根目录下运行 mvn test,测试一下。
base 模块
base 模块主要实现的是整个项目中,各个模块公用的程序。其中包含了:
  • Hibernate Session Factory
  • Ehcache
  • POJO Class
  • BaseDao 所有 dao 的父类
  • Hibernte 事务管理
authmgr 模块
authmgr 模块实现了如下功能
  • 登录
  • 登出
  • 查询访问控制规则数据
  • 实现自定义 Realm
  • 实现Shiro的 SecurityManager
authmgr 模块业务接口
我们来看一下接口
com.ultimatech.shirodemo.authmgr.service.IAuthService
public interface IAuthService {
    /**
     * 用户登录接口
     * @param userName 登录用户名
     * @param password 密码
     * @throws AuthenticationException
     */
    void logIn(String userName, String password) throws AuthenticationException;
 
    /**
     * 用户登出系统
     */
    void logOut();
 
    /**
     * 获得数据库中存储的访问控制数据
     * @return
     */
    List<AuthcMap> getFilterChainDefinitions();
}
自定义实现 Realm
我们来看一下我们的 Realm 是如何实现:
com.ultimatech.shirodemo.authmgr.realm.MyRealm
……
@Component(“myRealm”)
public class MyRealm extends AuthorizingRealm {
 
    @Autowired
    public MyRealm(@Qualifier(“shiroEncacheManager”) CacheManager cacheManager) {
        super(cacheManager);
    }
 
    @Autowired
    private IAuthDao dao;
 
   ……
 
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取登录时输入的用户名
        String loginName = (String) principalCollection.fromRealm(getName()).iterator().next();
        //到数据库查是否有此对象
        User user = this.getDao().findByName(loginName);
        if (user != null) {
            //权限信息对象 info,用来存放查出的用户的所有的角色(role)及权限(permission)
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            //用户的角色集合
            info.setRoles(user.getRolesName());
            //用户的角色对应的所有权限,如果只使用角色定义访问权限,下面的四行可以不要
            List<Role> roleList = user.getRoleList();
            for (Role role : roleList) {
                info.addStringPermissions(role.getPermissionsString());
            }
            return info;
        }
        return null;
    }
 
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //UsernamePasswordToken 对象用来存放提交的登录信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //查出是否有此用户
        User user = this.getDao().findByName(token.getUsername());
        if (user != null) {
            //若存在,将此用户存放到登录认证 info 中
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
        }
        return null;
    }
}
Shiro的 SecurityManager
我们在 Spring 容器中声明一个名叫 securityManager 的 bean。在 resources/conf/authmgr-beans.xml 中,我们看见如下代码:
<bean id=”securityManager” class=”org.apache.shiro.web.mgt.DefaultWebSecurityManager”>
    <!– ref 对应我们写的 realm  myRealm –>
    <property name=”realm” ref=”myRealm”/>
    <!– 使用下面配置的缓存管理器 –>
    <property name=”cacheManager” ref=”shiroEncacheManager”/>
</bean>
<bean id=”lifecycleBeanPostProcessor” class=”org.apache.shiro.spring.LifecycleBeanPostProcessor”/>
由于我们很多模块都会用到共享缓存,所以以上<property name=”cacheManager” ref=”shiroEncacheManager”/>中的 shiroEncacheManager 被定义在 base 模块中。
我们可以去 base 模块的目录下找到 resources/conf/base-beans.xml,找到如下代码:
<bean id=”shiroEncacheManager” class=”org.apache.shiro.cache.ehcache.EhCacheManager”>
    <property name=”cacheManager” ref=”ehCacheManager”/>
</bean>
 
<bean id=”ehCacheManager” class=”org.springframework.cache.ehcache.EhCacheManagerFactoryBean”>
    <property name=”configLocation” value=”classpath:ehcache.xml”></property>
    <property name=”shared” value=”true”/>
</bean>
shiroWebapp 模块
shiroWebapp 模块是本例中的 web 应用。主要集成了 Spring MVC 框架、Hibernate 框架,以及我们的安全框架 Apache Shiro。
我们使用 Shiro Filter 来进行访问控制,那么在 web.xml 文件中进行了如下配置:
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
我们使用 Spring 的 DelegatingFilterProxy 来创建 Shiro Filter。<filter-name>shiroFilter</filter-name>这个参数要与 Spring 中 Shiro Filter Bean 的名字保持一致。在 shiroWebapp 下的 resources/conf 下的 web-beans.xml 文件中,我们可以看见 Shiro Filter 的配置:
<bean id=”filterChainDefinitions” class=”com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions”>
    <property name=”filterChainDefinitions”>
        <value>
            /html/**=anon
            /js/**=anon
            /css/**=anon
            /images/**=anon
            /authc/login=anon
            /login=anon
            <!–/user=perms[user:del]–>
            /user/add=roles[manager]
            /user/del/**=roles[admin]
            /user/edit/**=roles[manager]
            <!–/** = authc–>
        </value>
    </property>
</bean>
 
<!– 配置 shiro 的过滤器工厂类,id- shiroFilter 要和我们在 web.xml 中配置的过滤器一致 –>
<bean id=”shiroFilter” class=”org.apache.shiro.spring.web.ShiroFilterFactoryBean”>
    <!– 调用我们配置的权限管理器 –>
    <property name=”securityManager” ref=”securityManager”/>
    <!– 配置我们的登录请求地址 –>
    <property name=”loginUrl” value=”/”/>
    <!– 配置我们在登录页登录成功后的跳转地址,如果你访问的是非/login 地址,则跳到您访问的地址 –>
    <property name=”successUrl” value=”/user”/>
    <!– 如果您请求的资源不再您的权限范围,则跳转到/403 请求地址 –>
    <property name=”unauthorizedUrl” value=”/html/403.html”/>
    <!– 权限配置 –>
    <property name=”filterChainDefinitionMap” ref=”filterChainDefinitions” />
</bean>
访问控制数据
我们看见上面的 filterChainDefinitions 中,我们自定义了一个 FacotryBean,这个 bean 主要实现将配置文件中的访问控制数据和数据库中的访问控制数据整合在一起。(虽然我们之前已经说了,这两种方式没什么区别。)
*com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions*
public class ShiroFilterChainDefinitions implements FactoryBean<Ini.Section> {
 
    @Autowired
    private IAuthService authService;
 
    ……
 
    public static final String PREMISSION_STRING = “perms[{0}]”;
 
    public static final String ROLE_STRING = “roles[{0}]”;
 
    public Ini.Section getObject() throws Exception {
        List<AuthcMap> list = this.getAuthService().getFilterChainDefinitions();
        Ini ini = new Ini();
        ini.load(this.getFilterChainDefinitions());
        Ini.Section section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        for (AuthcMap map : list) {
            String s = null;
            switch (AuthcType.valueOf(map.getAuthcType())) {
                case roles:
                    s = MessageFormat.format(ROLE_STRING, map.getVal());
                    break;
                case perms:
                    s = MessageFormat.format(PREMISSION_STRING, map.getVal());
                    break;
                case authc:
                    s = AuthcType.authc.name();
                case anon:
                    s = AuthcType.anon.name();
                default:
                    s = AuthcType.authc.name();
            }
            section.put(map.getUrl(), s);
        }
        return section;
    }
 
   ……
}
关于访问控制数据,我们要注意 Shiro Filter 在执行访问控制时,是按访问控制数据的顺序来逐个验证的,而我们将数据库中的访问控制数据追加到配置文件的后面。例如上面的配置:
<bean id=”filterChainDefinitions” class=”com.ultimatech.shirodemo.web.filter.ShiroFilterChainDefinitions”>
    <property name=”filterChainDefinitions”>
        <value>
            /html/**=anon
            /js/**=anon
            /css/**=anon
            /images/**=anon
            /authc/login=anon
            /login=anon
            /user/add=roles[manager]
            /user/del/**=roles[admin]
            /user/edit/**=roles[manager]
        </value>
    </property>
</bean>
追加上我们在数据库中的访问配置数据:
那么全部访问控制数据应该是:
/html/**=anon
/js/**=anon
/css/**=anon
/images/**=anon
/authc/login=anon
/login=anon
/user/add=roles[manager]
/user/del/**=roles[admin]
/user/edit/**=roles[manager]
/user=perms[user:query]
/**=authc
这里我们要强调一下,如果把基于角色和基于 permission 的控制放在/**=authc 之后的控制是不会起作用的。例如:
/html/**=anon
/js/**=anon
/css/**=anon
/images/**=anon
/authc/login=anon
/login=anon
/user/add=roles[manager]
/user/del/**=roles[admin]
/user/edit/**=roles[manager]
/**=authc
/user=perms[user:query]
这时如果用户 rose 拥有 user:del 的 permission,在他登录系统以后也是可以访问/user 路径的。其实我们想实现只有拥有 user:query 的用户才能访问/user,而 rose 拥有的是 user:del。我们设置了/user=perms[user:query],而 rose 并没有 user:query 这样的 permission,为什么 rose 还能访问/user 呢?这是因为,Shior Filter 先使用访问控制规则/**=authc 对 rose 的权限进行了验证,那么 rose 是一个已知身份的用户,所以他可以访问所有 url,除了/**=authc 之前设置的规则限制不能访问的 url。
是不是很混乱,一部分访问控制规则在配置文件中,一部分又在数据库中,而且访问控制还有顺序要求,一旦我们忽略任意一部分访问控制数据,我们的设置就很难达到我们预期的效果。所以,将访问控制数据分开并不是一个好的实践。
我们这里实现了使用数据库配置访问控制数据,仅仅是为了开阔一下思路,并不推荐同时使用数据库配置和配置文件配置。
关于 permission 语法
我们可以参考 Understanding Permissions in Apache Shiro。你可能会发现好长的一篇文章啊!
那么下面我就我个人的理解,简单对 permission 语法说明一下。
在我们设计系统时,我们经过一系列的分析过程,会得到我们要实现的系统中存在哪些实体。例如,系统中存在用户(user),工作流(workflow)等实体。我们对这些实体进行抽象化,构成我们系统的基础模型。那么实体除了数据属性,例如,用户名(username),流程名称(flowname)等,还具备一些功能(function)或者叫做方法(method)的特征。我们使用 OOP 的思想来设计系统,那么我们抽象出来的实体就是我们所说的实体类,这些实体类代表了很多实体对象,例如,user 类中,实际包含了 tom,jack 和 rose,这些用户的一个子集就组成了一个数据域。那么我们的 permission 就是由系统实体和功能,以及一个数据域组成,格式就像这样:实体:功能:数据域。
例如: 
  • perms[user:query:*]——表示允许查询(query)用户(user 实体类)所有(*)对象的许可。 
  • perms[user:query,add,del,update]——表示允许查询(query)、添加(add)、删除(del)和更新(update)用户(user)所有对象的许可。 
  • perms[user:query:jack,rose]——表示允许查询(query)用户(user)中 jack 和 rose 数据的许可。 
  • perms[workflow:approve:order]——表示允许操作工作流程(workflow)中一个名叫 order 的流程的审批许可。
试验
我们掌握了项目中集成 Apache Shiro 的方法后,将项目在 IDEA 中 run 或者 debug 起来。使用 tom、jack 和 rose 用户登录系统中,看看会有什么现象。当然用户、角色和 permission 数据都在 db.sql 文件中,导入数据库时已经一起导入了。你可以修改这些数据,以及他们之间的关系来体验 Shiro 的安全框架。
总结
希望通过这个说明,能够让你了解在本项目中是如何集成 Apache Shiro 安全框架的。
在接下来的计划中,我们将实现在集群环境中使用 Apache Shiro。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:Spring MVC 中集成 Apache Shiro 安全框架
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00