文档章节

使用 Smart Security 实现安全控制

黄勇
 黄勇
发布于 2014/03/31 23:42
字数 2968
阅读 3624
收藏 42

很多朋友都问过我同样一个问题:“Smart 目前有身份认证与权限管理等安全控制功能吗?”

当听到这样的问题时,我真的非常不好意思,实在是没有这方面的特性。不过当我学习了 Shiro 以后,让我萌发了一个想法:

能否提供一个更加 Smart 的 Shiro 框架呢?

大家知道,Smart 是一款轻量级 Java Web 开发框架,此外,Smart 还提供了一系列的模块,之前开发过一款 Smart SSO 模块,它是不依赖于 Smart 框架的,可以在任何的 Web 项目中使用。

那么,能否再开发一款 Smart Security 模块呢?同样它也不依赖于 Smart 框架,仍然可以在其它 Web 项目中使用。也就是说,只需要拿到 smart-security.jar 这个包,并将其放入到 lib 目录下,最后只需少量的配置即可使用 Shiro 提供的安全控制功能。

为达到以上的目标,我首先创建了一个应用场景:

用户必须登录成功后,才能看到用户空间页面,否则跳转到登录页面要求用户进行身份认证。

这应该是一个很典型的案例,我们如何在普通的 Web 应用中使用 Shiro 呢?

在 Web 应用中使用 Shiro

首先,需要在 web.xml 中做如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <listener>
        <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>ShiroFilter</filter-name>
        <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ShiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

然后,需要在 classpath 下或 WEB-INF 目录下新增一个 shiro.ini 文件:

[main]
authc.loginUrl=/login

ds = org.apache.commons.dbcp.BasicDataSource
ds.driverClassName = com.mysql.jdbc.Driver
ds.url = jdbc:mysql://localhost:3306/sample
ds.username = root
ds.password = root

passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $ds
jdbcRealm.authenticationQuery = select password from user where username = ?
jdbcRealm.userRolesQuery = select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?
jdbcRealm.permissionsQuery = select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?
jdbcRealm.permissionsLookupEnabled = true
jdbcRealm.credentialsMatcher = $passwordMatcher
securityManager.realms = $jdbcRealm

cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager

[urls]
/ = anon
/space/** = authc

以上使用了 Shiro 的 JdbcRealm 来提供认证与授权服务,并提供了 Cache 功能,此外还有密码安全支持。

最后,就可以在代码里使用 Shiro 提供的 API 进行编程了。

Shiro 提供了几个常用 API,掌握了这些 API 的用法,基本上就会用 Shiro 了,这些 API 包括:

  • org.apache.shiro.SecurityUtils

  • org.apache.shiro.subject.Subject

  • org.apache.shiro.authc.UsernamePasswordToken

  • org.apache.shiro.authc.AuthenticationException

  • org.apache.shiro.authc.credential.PasswordService

  • org.apache.shiro.authc.credential.DefaultPasswordService

总结一下,我们需要在 web.xml 中整合 Shiro(一大堆的配置),需要提供一个 Shiro 的配置文件(又是一大堆配置),还需要在代码里使用 Shiro API(需要学习 Shiro 文档及其 JavaDoc)。

这简直就是在自找麻烦!

大家应该知道,Spring 有较强的系统整合能力,将 Shiro 整合到 Spring 中是否会变得简单呢?

Spring 与 Shiro 整合

首先,需要在 web.xml 中做如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring.xml
            classpath:spring-shiro.xml
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>shiroFilter</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>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

  1. 需要通过 ContextLoaderListener 这个监听器来读取 spring.xml 与 spring-shiro.xml 这两个 Spring 配置文件,而 Spring MVC 框架需要读取 spring-mvc.xml 配置文件。

  2. Spring 的 DelegatingFilterProxy 将拦截所有的请求,并将请求的委托给 ShiroFilter 来处理。

要实现请求委托,需要在 spring-shiro.xml 中做如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="filterChainDefinitions">
            <value>
                / = anon
                /space/** = authc
            </value>
        </property>
    </bean>

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="jdbcRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
    </bean>

    <bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
        <property name="dataSource" ref="dataSource"/>
        <property name="authenticationQuery" value="select password from user where username = ?"/>
        <property name="userRolesQuery" value="select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?"/>
        <property name="permissionsQuery" value="select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?"/>
        <property name="permissionsLookupEnabled" value="true"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="credentialsMatcher" ref="passwordMatcher"/>
    </bean>

    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>

    <bean id="passwordMatcher" class="org.apache.shiro.authc.credential.PasswordMatcher"/>

    <bean id="passwordService" class="org.apache.shiro.authc.credential.DefaultPasswordService"/>

</beans>

  1. 首先,通过 Spring 的 ShiroFilterFactoryBean 来创建 ShiroFilter,并在其中注入了 SecurityManager,此外只需在这里配置 loginUrl 与相关的 Filter Chain 即可。

  2. 随后,提供了相关 Bean 的定义,包括:SecurityManager、JdbcRealm、LifecycleBeanPostProcessor、CacheManager、PasswordMatcher、PasswordService 等。

  3. 需要注意的是,这里同时将 cacheManager 注入到 securityManager 与 jdbcRealm 中才能让 Cache 生效(目前尚未阅读源码,原因不详)。

  4. 必须提供 LifecycleBeanPostProcessor 才能让 Web 容器启动与关闭时调用 Shrio 的生命周期方法,同时需要确保 web.xml 中的 DelegatingFilterProxy 的 targetFilterLifecycle 参数要为 true 才行。

  5. 最后,其中 PasswordMatcher 与 PasswordService 是为密码加密与解密提供服务的。

看来 Spring 只是将 shiro.ini 中的配置转移到 spring-shiro.xml 中了,并没有对 Shiro 提供一个很好的封装,只是简单的集成而已。

一般情况下,我们不想做太多的配置,能够提供一个简单的安全控制功能即可,我们不需要它应有尽有,而需要它能够简单而实用。

这就是我的想法:

能否对 Shiro 的常用功能做一个封装呢?也就是说,只需对外提供统一的 API,让底层的具体实现变得更加透明,如果将来打算使用其它安全框架,只需改造这个框架即可,这样就无需在每个项目中进行重构了。

它就是 Smart Security 模块。

Smart Security

首先需要申明一下 Smart 模块的设计原则:

1. 提供最实用的功能,忽略不常用的功能,但需要提供扩展机制。
2. 隐藏具体实现细节,底层实现可以任意切换,从而不会影响到应用程序的改动。
3. 可以作为一个独立的模块,无需在 web.xml 里做任何配置,直接把 jar 包拿来就能用。

Smart Security 完全遵循以上设计原则。

配置

使用了 Smart Security 之后,再也不需要配置 web.xml 了。

此外,shiro.ini 也简单了:

[main]
authc.loginUrl = /login

[urls]
/ = anon
/space/** = authc

其它的配置将转移到 smart.properties 中:

app.package = demo.shiro
app.home_page = /index.jsp

jdbc.type = mysql
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/sample
jdbc.username = root
jdbc.password = root

security.realms = jdbc
security.jdbc.authc_query = select password from user where username = ?
security.jdbc.roles_query = select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?
security.jdbc.perms_query = select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?
security.cache = true

注意以上 security 开头的配置项,这样的配置是否更加简单呢?

使用

Smart Security 提供了一个 SmartSecurityHelper 类,包括以下静态方法:

public class SmartSecurityHelper {

    public static void login(String username, String password, boolean isRememberMe) throws LoginException {
        ...
    }

    public static void logout() {
        ...
    }

    public static String encrypt(String plaintext) {
        ...
    }
}

当然还可以扩展其它实用 API,目前只提供了几个常用方法。

我们可以在业务代码中这样使用:

@Service
public class UserServiceImpl implements UserService {

    @Override
    public void login(String username, String password, boolean isRememberMe) throws LoginException {
        SmartSecurityHelper.login(username, password, isRememberMe);
    }

    @Override
    public void register(Map<String, Object> fieldMap) throws RegisterException {
        // 获取表单数据
        String username = CastUtil.castString(fieldMap.get("username"));
        String password = CastUtil.castString(fieldMap.get("password"));

        // 在 user 表中根据 username 查询用户是否已存在
        Long userCount = DataSet.selectCount(User.class, "username = ?", username);
        if (userCount > 0) {
            throw new RegisterException();
        }

        // 加密密码
        password = SmartSecurityHelper.encrypt(password);

        // 插入 user 表
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        DataSet.insert(user);
    }
}

这样我们就通过 SmartSecurityHelper 屏蔽了 Shiro API 的细节了,代码量也精简了许多。

大家或许已经见识过 Shrio 的 JSP 标签了,不错,确实提供了很多有用的标签,但似乎又缺少了几个,导致我们不得不自行扩展,这样就会引用我们自定义的 Taglib,从而增加了前端开发人员的负担。

为了解决这个问题,不妨重新将 Shiro 的标签做一个整理,添加我们自定义的标签,这样前端开发人员只需面对 Smart Security 的 Taglib 就能工作了,如果不够用,我们可以随意扩展。

只需在 JSP 上使用以下 Taglib 定义即可:

<%@ taglib prefix="security" uri="/smart_security" %>

Smart Security 提供了一套比 Shiro 更为详尽的标签(少量扩展,大多数来自于 Shiro):

类型 标签 属性 功能
用户
<security:user> 判断当前用户是否已登录(已认证 或 已记住)

<security:guest> 判断当前用户是否未登录(为游客身份)

<security:authenticated> 判断当前用户是否已认证

<security:notAuthenticated> 判断当前用户是否未认证

<security:principal> type、property、defaultValue 显示当前用户的相关属性
角色 <security:hasRole> name 判断当前用户是否拥有某种角色

<security:lacksRole> name 判断当前用户是否缺少某种角色

<security:hasAnyRoles> name 判断当前用户是否拥有其中某一种角色(逗号分隔,或的关系)

<security:hasAllRoles> name 判断当前用户是否拥有其中所有的角色(逗号分隔,与的关系)
权限 <security:hasPermission> name 判断当前用户是否拥有某种权限

<security:lacksPermission> name 判断当前用户是否缺少某种权限

<security:hasAnyPermissions> name 判断当前用户是否拥有其中某一种权限(逗号分隔,或的关系)

<security:hasAllPermissions> name 判断当前用户是否拥有其中所有的权限(逗号分隔,与的关系)

在 JSP 中可以这样使用:

<%@ page pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="security" uri="/smart_security" %>
<html>
<head>
    <title>首页</title>
</head>
<body>

<h1>首页</h1>

<security:guest>
    <p>身份:游客</p>
    <a href="<c:url value="/login"/>">登录</a>
    <a href="<c:url value="/register"/>">注册</a>
</security:guest>

<security:user>
    <p>身份:<security:principal/></p>
    <a href="<c:url value="/space"/>">空间</a>
    <a href="<c:url value="/logout"/>">退出</a>
</security:user>

</body>
</html>

那么 Smart Security 应该如何实现呢?我将这个想法做了一个尝试:

http://git.oschina.net/huangyong/smart/tree/master/smart-security

此外,还有一个 Shiro Demo:

http://git.oschina.net/huangyong/shiro_demo

期待您的点评!


补充(2014-04-01)

经过今天上午大家在 QQ 群里的一番激烈讨论后,得到如下反馈:

1. 大多数人不喜欢在 properties 文件里定义那些 SQL 语句
2. Smart Security 可以不用依赖于 DBCP 连接池

今天下午,我开发了一个新特性,在 Smart Security 中提供了一个 ISmartSecurity 接口:

/**
 * Smart Security 接口
 */
public interface ISmartSecurity {

    /**
     * 根据用户名获取密码
     *
     * @param username 用户名
     * @return 密码
     */
    String getPassword(String username);

    /**
     * 根据用户名获取角色名集合
     *
     * @param username 用户名
     * @return 角色名集合
     */
    Set<String> getRoleNameSet(String username);

    /**
     * 根据角色名获取权限名集合
     *
     * @param roleName 角色名
     * @return 权限名集合
     */
    Set<String> getPermNameSet(String roleName);
}

在实际的项目中需要实现该接口,并将之前写在 properties 文件中的 SQL 语句放入实现类中执行,下面用 Smart API 写了一个实现类:

public class SmartSecurity implements ISmartSecurity {

    @Override
    public String getPassword(String username) {
        String sql = "select password from user where username = ?";
        return DatabaseHelper.queryColumn(sql, username);
    }

    @Override
    public Set<String> getRoleNameSet(String username) {
        String sql = "select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?";
        return DatabaseHelper.queryColumnSet(sql, username);
    }

    @Override
    public Set<String> getPermNameSet(String roleName) {
        String sql = "select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?";
        return DatabaseHelper.queryColumnSet(sql, roleName);
    }
}

只需要简单通过 Smart 提供的 DatabaseHelper 类即可执行这些 SQL 语句并返回相关结果。

最后需要做的事情,就是配置 smart.properties 了:

security.realms = custom
security.custom.class = demo.shiro.SmartSecurity
security.cache = true

只需三行配置即可,实际上只有两行,最后一行是可选的。

目前 security.realms 包括三种:jdbc、ad、custom,下表是详细的配置方式:

配置项 security.realms 种类
相关配置项
jdbc security.jdbc.authc_query
security.jdbc.roles_query
security.jdbc.perms_query
ad security.ad.url
security.ad.system_username
security.ad.system_password
security.ad.search_base
custom security.custom.class

此外,security.realms 可同时配置多个 Realm,用英文逗号分隔。

感谢大家提供的建议!尤其感谢 @哈库纳 的建议。

有兴趣的朋友欢迎加群讨论:120404320

© 著作权归作者所有

黄勇

黄勇

粉丝 6611
博文 121
码字总数 216155
作品 1
浦东
CTO(技术副总裁)
私信 提问
加载中

评论(11)

WhatUp
WhatUp
楼主。。。您说的那个群。。。满了36
黄勇
黄勇 博主

引用来自“joinwin”的评论

sample增加了shiro,不需要增加role,permision表也可以跑起来,SmartSecurity类有涉,不理解。

是的,role 与 permission 只有在 authorization 的时候才有用,smart smaple 目前并没有做这个功能的展示。 这里有个 shiro demo,也可以参考一下:http://git.oschina.net/huangyong/shiro_demo

joinwin
joinwin
sample增加了shiro,不需要增加role,permision表也可以跑起来,SmartSecurity类有涉,不理解。
黄勇
黄勇 博主

引用来自“webit”的评论

1. `security.cache = true`
这样的话 应该有个机制(API) (定时?手动?)清空缓存

2. 一定程度上,在Action/Service 上使用 权限annotation ,能使得 方法内的逻辑更简洁

谢谢!很好的建议。你说的正是我下一步要做的:)
zqq90
zqq90
1. `security.cache = true`
这样的话 应该有个机制(API) (定时?手动?)清空缓存

2. 一定程度上,在Action/Service 上使用 权限annotation ,能使得 方法内的逻辑更简洁
黄勇
黄勇 博主

引用来自“黄勇”的评论

补充(2014-04-01):感谢大家提供的建议!尤其感谢 @哈库纳 的建议。

引用来自“哈库纳”的评论

勇哥加油写呀,以后 Hasor 的权限控制就靠你啦。 等你写好了我直接移植过来,哈哈。

我是摸着石头过河,没有大家的帮助,我一个人也做不起来。

哈库纳
哈库纳

引用来自“黄勇”的评论

补充(2014-04-01):感谢大家提供的建议!尤其感谢 @哈库纳 的建议。

勇哥加油写呀,以后 Hasor 的权限控制就靠你啦。 等你写好了我直接移植过来,哈哈。

web菜鸟
web菜鸟
ttt
黄勇
黄勇 博主
补充(2014-04-01):感谢大家提供的建议!尤其感谢 @哈库纳 的建议。
dargoner
dargoner
写的很好,就像傻瓜相机一样平民化
黄勇/smart-framework

Smart Framework 简介 1. 它是一款轻量级 Java Web 框架 内置 IOC、AOP、ORM、DAO、MVC 等特性 基于 Servlet 3.0 规范 使用 Java 注解取代 XML 配置 2. 它使应用充分做到“前后端分离” 客户...

黄勇
2013/09/23
0
0
区块链安全:ANNI Token智能合约安全审计报告分析

     介绍   此博客文章介绍了由Blaze Information Security执行的智能合约的安全审核结果,并代表客户端Array.io(以前叫做Annihilat.io)公开这些结果。本文的内容包含了2017年12月底...

嘶吼RoarTalk
2018/07/22
0
0
行级安全(Row-Level Security)

通过授予和拒绝(Grant/Deny)命令控制用户的权限,只能控制用户对数据库对象的访问权限,这意味着,用户访问的粒度是对象整体,可以是一个数据表,或视图等,用户要么能够访问数据库对象,要...

长征6号
2015/05/14
0
0
springboot+security整合1

说明springboot版本2.0.3 一、 介绍   Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的B...

烦嚣的人
2018/07/23
0
0
第十节 spring could security实现OAuth2

使用spring could security实现OAuth2来控制服务中api的安全 首先创建一个安全服务spring security,用于控制身份验证和授权。 增加pom依赖 在启动类上增加@EnableResourceServer表示允许该服...

勃列日涅夫
2018/09/05
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Boot + Mybatis-Plus 集成与使用(二)

前言: 本章节介绍MyBatis-Puls的CRUD使用。在开始之前,先简单讲解下上章节关于Spring Boot是如何自动配置MyBatis-Plus。 一、自动配置 当Spring Boot应用从主方法main()启动后,首先加载S...

伴学编程
今天
7
0
用最通俗的方法讲spring [一] ──── AOP

@[TOC](用最通俗的方法讲spring [一] ──── AOP) 写这个系列的目的(可以跳过不看) 自己写这个系列的目的,是因为自己是个比较笨的人,我曾一度怀疑自己的智商不适合干编程这个行业.因为在我...

小贼贼子
今天
6
0
Flutter系列之在 macOS 上安装和配置 Flutter 开发环境

本文为Flutter开发环境在macOS下安装全过程: 一、系统配置要求 想要安装并运行 Flutter,你的开发环境需要最低满足以下要求: 操作系统:macOS(64位) 磁盘空间:700 MB(不包含 IDE 或其余...

過愙
今天
6
0
OSChina 周六乱弹 —— 早上儿子问我他是怎么来的

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @凉小生 :#今日歌曲推荐# 少点戾气,愿你和这个世界温柔以待。中岛美嘉的单曲《僕が死のうと思ったのは (曾经我也想过一了百了)》 《僕が死の...

小小编辑
今天
2.5K
16
Excption与Error包结构,OOM 你遇到过哪些情况,SOF 你遇到过哪些情况

Throwable 是 Java 中所有错误与异常的超类,Throwable 包含两个子类,Error 与 Exception 。用于指示发生了异常情况。 Java 抛出的 Throwable 可以分成三种类型。 被检查异常(checked Exc...

Garphy
今天
42
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部