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

2012年9月25日

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

很多项目需要对用户操作进行鉴权。它们的需求可以归纳为下面几点:
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)编辑 收藏
 
本文介绍如何更加轻松的编写代码。

为什么有的代码写起来让人头昏脑胀呢?就是因为逻辑太复杂。逻辑的复杂无非体现在两个方面,要么是步骤太长,要么是条件分支(也就是需要考虑的“例外情况 
”)太多。

人们处理复杂的事情,向来有一套方法,叫分而治之,它完全可以用在开发上。

使用分而治之的方式编写代码,好处就是关注点减少了。把一个长的步骤分成若干个短小的步骤,那么你每次只需要关注其中的一步就可以了;把一系列复杂的判断交给不同的对象去打理,那么你的思维又可以回到主流程上来。关注点减少的结果,一方面降低了出错的可能,另一方面不会令人绞尽脑汁弄得身心疲惫。

如何对复杂的逻辑分而治之呢?就本人的开发经验,我觉得有两点很重要:

1、关注顶层逻辑
任何方法都只应该体现其顶层逻辑。比如用户登录的顶层逻辑,只有三个步骤:1)检查参数格式;2)处理逻辑;3)返回结果。
public boolean login(String username, String password) {
    if (!checkParameters(username, password) {
        return false;
    }
    
    boolean result = checkUserPassword(username, password);
    
    return result;
}
这是顶层逻辑,将这三句话写好之后,我们再分别去实现每个步骤,而每个步骤当中又只包含顶层逻辑。这样做有效的减少了关注点,让代码写起来更加轻松。

2、职责分明
我们生活在一个分工精细的社会,任何事情都能(至少在名义上)找到相应的人对其负责。业务逻辑也是一样,字符串的处理、XML的解析、连接的打开关闭、日期时间的校验,这些逻辑都应该交给相应的类去处理,不要一窝蜂的都写在一个方法里面,这样写出来的东西是一团乱麻,写了一半可能你自己都不知道写到哪了。

我觉得只要做到上面这两点,面对任何复杂的逻辑都可以轻松对付,而且不易出错——你可以想象当别人埋头苦干的时候,你却在一边轻松的听歌喝咖啡了~~
posted @ 2012-09-25 20:23 捏造的信仰 阅读(199) | 评论 (0)编辑 收藏
CALENDER
<2012年9月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456

常用链接

留言簿

随笔档案

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜


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