随笔:6 文章:1 评论:0 引用:0
BlogJava 首页 发新随笔
发新文章 联系 聚合管理

2012年10月26日

(本文沿着思路来写,不会一开始就给出最后结论。)

很多项目需要对用户操作进行鉴权。它们的需求可以归纳为下面几点:
1、基于角色的权限控制,一个用户可能被授予多个角色;
2、用户对同一条记录的不同操作(如查看和修改)需要分别授权;
3、记录之间可能存在父子关系,子记录的权限自动从父记录继承,不需要明确授权。

所以,可以看出这个权限模型存在四个基本要素:用户、角色、操作、记录。
用户:实际进行操作的单位。
角色:用户进行操作时的身份。
操作:用户执行的与记录有关的动作。
记录:被操作的对象。

因为鉴权只针对角色而非用户,所以一个鉴权应该可以描述为:判断某角色是否对某记录拥有某操作的权限

可以看出,每个权限都是一个“角色—操作—记录”的三要素关联关系。如果我们要设计一张表保存所有授权,那么这张表至少应该包含这三个字段。

那么问题来了:随着记录的增加,这张表的记录数将呈级数增长,特别是记录之间存在父子关系的情况下,为父记录授权就意味着同样要为所有子记录授权,删除一条授权也会造成大量的查询和更新,这张表的维护将成为噩梦,这样的表设计没有实用性。

造成这种情况的根本原因是,记录是经常变化的,而鉴权规则很少改变,二者之间存在脱节。

在这里我们不得不重新思考授权的本质:授权本来就不是针对某条具体的记录的。以博客系统为例,“作者有权删除其创建的文章”这个规则中,角色是“作者”,操作是“删除”,而记录呢?“作者创建的文章”并不是一条具体的记录。所以说,授权是对规则的描述,它针对的不是具体的记录,而是更抽象的东西。

这种更抽象的东西,我们暂把它叫做“资源”。那么鉴权的三要素,应该称作“角色—操作—资源”。资源是对记录的抽象,就如角色是对用户的抽象一样。这样,一条权限就变成了完全抽象的:它既不针对具体的某个用户,也不针对具体的某条记录,它完全是对规则的描述。当一个用户对一条记录的操作需要鉴权时,需要进行映射,将用户映射到角色,将记录映射到资源,然后再搜索是否寻在允许的授权。

因为“用户—角色”是多对多的关系,“记录—资源”也是多对多的关系(比如一篇博客文章可能是“我的文章”,也可能是“别人的文章”),所以“用户—操作—记录”先要被映射到“角色[]—操作—资源[]”([]表示有多个),然后再从匹配关系的组合中搜索是否存在允许的授权。如果存在则表示鉴权通过,否则鉴权不通过。

至此为止,权限表设计已经从“角色—操作—记录”改为“角色—操作—资源”,它符合“授权的本质”,所以不会有大量的级联查询和更新。但它仍然存在两个问题:1)因为存在角色与资源的多对多组合,所以每次鉴权需要进行大量的判断。2)我们没有办法实现从记录到资源的映射,因为两者并没有直接关联。为什么这么说呢?以博客系统为例,一篇文章在作者面前可以被映射为“我的文章”,在管理员面前可以被映射为“普通文章”,也许还会存在其他的映射,这是无法确定的。我们需要仔细考察这里面的关系。

经过仔细考察,我发现这两个问题其实是有关联的。我们需要重新审视资源的概念:资源不是独立存在的,而是与角色关联的,不同的角色需要面对不同的资源。比如博客系统中,管理员面对用户、角色等资源,而普通用户则面对文章、博客等资源。所以记录到资源的映射与角色有着密切关系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“别人的文章”,而在管理员面前要么是“普通文章”,要么是“其他文章”。这是很合理的:一条记录在不同的角色面前以不同的角度呈现。

所以通过角色,我们可以将“用户—操作—记录”映射到“角色—操作—资源”的步骤改为:首先完成用户到角色的映射,然后对每种角色,列出记录到资源的映射,然后搜索是否存在允许的授权。这样能够明显减少判断的次数。

我们还要考虑记录的继承关系。这个逻辑不复杂:当一条记录搜索不到授权时,还要获取其父记录并搜索父记录的授权。直到所有的父记录都找不到授权,才能返回授权不通过。

然后我们要考虑同步:对同一个“角色(不是用户,因为这样同步范围更大)—操作—记录(不是资源,因为这样搜索更精准)”的鉴权需要进行同步锁,这样可以避免重复搜索浪费资源。

最后我们要考虑如何缓存。通常为了提高鉴权效率,所有已经判断的授权都要缓存起来可以避免重复搜索。缓存是对记录(而非资源)授权的缓存,即缓存“用户/角色—操作—记录”,这样可以避免重复搜索父记录授权。

当授权变更时,缓存如何维护?这个问题也要考虑。当添加一条授权时,我们不需要关心,因为经过一段时间的搜索,相应的缓存记录就会自动补充起来;当一条“角色—操作—资源”授权被删除时,需要删除:1)所有对应的“角色—操作—记录”缓存;2)找到角色对应的用户,删除所有对应的“用户—操作—记录”缓存。这个过程效率当然可能不高,但考虑到删除授权本身不会很频繁,所以应该能够接受。

至此为止,表设计和鉴权的逻辑过程我们都清晰了,然后是如何实现。鉴权过程的实现关键在于映射,特别是从记录到资源的映射。这个映射是不可能光靠数据库配置来完成的,必须要有代码逻辑,比如博客系统的文章映射到“我的文章”,就需要判断该用户是不是文章的作者。这样的逻辑我们可以抽象为一个接口:

/**
 * 将记录映射到资源的接口。不同类型的记录应该有不同的实现类
 
*/
public interface ResourceMapper {

    /**
     * 根据角色将记录映射到资源
     * 
@param recordId 记录ID
     * 
@param roleId   角色ID
     * 
@return 资源ID
     
*/
    Long getResourceId(Long recordId, Long roleId);

    /**
     * 搜索父记录,如果不存在则返回 null
     * 
@param recordId  记录ID
     * 
@return 父记录ID,如果不存在则返回 null
     
*/
    Long getParent(Long recordId);
}

至于其他的部分,本文就不赘述了。我要赶紧去实现一个看看。

最后总结一下这个鉴权系统的主要部分:
1、数据库设计:角色表(ID,名称),鉴权表(角色,操作,资源),资源表(ID,名称,角色【如果为空则表示对所有角色可见】)
2、鉴权的核心逻辑:映射 + 搜索
3、鉴权的外围逻辑:缓存
4、需要用户实现的逻辑:映射接口 ResourceMapper
posted @ 2012-11-27 12:21 捏造的信仰 阅读(2702) | 评论 (0)编辑 收藏
 
要想使 DefaultHttpClient 对象使用自定义的 DNS 解析(比如将 blogjava.net 关联到 127.0.0.1,使其访问 "http://blogjava.net" 时请求本地服务器),可以用下面的办法(我在官网上没找到相关文章,是看了源代码自己琢磨出来的,也不是道是不是标准做法)

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionOperator;
import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.BasicClientConnectionManager;
import org.apache.http.impl.conn.DefaultClientConnectionOperator;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

/**
 * 在 httpclient 4.2 中使用自定义的 DNS 解析
 
*/
public class CustomDnsResolverDemo {

    public static void main(String[] args) throws Exception {

        // 创建自定义的 ConnectionManager
        BasicClientConnectionManager connectionManager = new BasicClientConnectionManager() {

            @Override
            protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) {
                return new DefaultClientConnectionOperator(schreg, new MyDnsResolver());
            }
        };

        // 创建 HttpClient 对象
        DefaultHttpClient httpclient = new DefaultHttpClient(connectionManager);

        // 构造请求
        HttpGet httpget = new HttpGet("https://www.google.com/");
        System.out.println(httpget.getRequestLine());

        // 发送请求并返回结果
        HttpResponse response = httpclient.execute(httpget);
        System.out.println(response.getEntity().getContentType());
        System.out.println(response.getStatusLine());

        // 这句不是必须的,只是让程序结束更快点
        httpclient.getConnectionManager().shutdown();
    }

    // 自定义的 DNS 解析类
    private static class MyDnsResolver implements DnsResolver {

        private static final Map<String, InetAddress[]> MAPPINGS = new HashMap<String, InetAddress[]>();

        static {
            addResolve("www.google.com", "74.125.134.138");
        }

        private static void addResolve(String host, String ip) {
            try {
                MAPPINGS.put(host, new InetAddress[]{InetAddress.getByName(ip)});
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
        }

        @Override
        public InetAddress[] resolve(String host) throws UnknownHostException {
            return MAPPINGS.containsKey(host) ? MAPPINGS.get(host) : new InetAddress[0];
        }
    }
}
posted @ 2012-11-10 19:18 捏造的信仰 阅读(3615) | 评论 (0)编辑 收藏
 
     摘要: 下面是一个例子,演示如何执行一个进程(类似于在命令行下键入命令),读取进程执行的输出,并根据进程的返回值判断是否执行成功。一般来说,进程返回 0 表示执行成功,其他值表示失败。  阅读全文
posted @ 2012-10-26 20:14 捏造的信仰 阅读(5715) | 评论 (0)编辑 收藏
CALENDER
<2012年10月>
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

常用链接

留言簿

随笔档案

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜


Powered By: 博客园
模板提供沪江博客