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

2012年11月27日

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

很多项目需要对用户操作进行鉴权。它们的需求可以归纳为下面几点:
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 捏造的信仰 阅读(2281) | 评论 (0)编辑 收藏

2012年11月10日

要想使 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 捏造的信仰 阅读(3424) | 评论 (0)编辑 收藏

2012年10月26日

     摘要: 下面是一个例子,演示如何执行一个进程(类似于在命令行下键入命令),读取进程执行的输出,并根据进程的返回值判断是否执行成功。一般来说,进程返回 0 表示执行成功,其他值表示失败。  阅读全文
posted @ 2012-10-26 20:14 捏造的信仰 阅读(5274) | 评论 (0)编辑 收藏

2012年9月25日

本文介绍如何更加轻松的编写代码。

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

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

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

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

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 捏造的信仰 阅读(120) | 评论 (0)编辑 收藏

2012年9月21日

本文介绍如何更加轻松的阅读代码。

有人会说,阅读代码是否轻松,取决于代码的可读性吧。是的,而且请不要误会,本文不是要介绍编码规范。不管代码本身的可读性如何,你都可以找到更好的手段去帮助阅读。下面是几个可供参考的建议:

1、用丰富的颜色区分类型、成员、方法、变量、参数等等。例如下面两张图片,你觉得哪个阅读起来更轻松呢?

颜色的区分不仅仅是为了好看,颜色越多,代码展示的信息就越丰富。特别是当有时候变量名称覆盖了成员名称的时候,一眼就能看出来。

2、适当用空行将逻辑隔开。在下面的例子中,第一段的代码没有使用空行,看起来比较凌乱;第二段代码只是加了几个空行,逻辑马上变得清晰起来,有利于阅读。


3、为逻辑区块添加注释。有人说好的代码不需要注释,但一方面好的代码太少,另一方面英语不是我们母语,所以适当的加上注释是有必要的。

上面的代码没有加注释,虽然逻辑做了一定的整理,但要看懂还是不容易。

加了注释之后,看起来更轻松了。

另外,如果代码当中的变量命名晦涩,可以用重构的方式对变量重命名,以方便阅读。
posted @ 2012-09-21 13:09 捏造的信仰 阅读(115) | 评论 (0)编辑 收藏

2012年9月19日

这里所说的“提高开发效率”不是指在相同的时间内做更多的事情,而是:
用更少的脑力完成同样多的事情。

为什么要用更少的脑力完成同样多的事情?因为一个人的脑力劳动同体力劳动一样,劳动强度越高,能够坚持的时间越少,人越觉得疲惫。

想想一天只有24小时,除去8小时睡眠,8小时工作,花在自己和家人上的时间最多也就8小时(这还是很理想的情况)。如果工作很累的话,你还有多少精力留给自己和家人?

作为开发人员,我们花费精力的事情无非就是三个:阅读代码,编写代码,运行代码。在这三件事情上减少脑力负担可以令工作更轻松。我希望能通过一系列文章帮助大家用更少的脑力阅读、编写和运行代码。

不过在这之前,有一件任务必须先完成,那就是:优化工作环境。

你的工作环境有多大的优化余地?请尝试考虑一下下面的问题:
  • 你的椅子过高还是过低?
  • 脖子、肩膀、腰和手腕是否酸痛?
  • 你的周围环境是否过于嘈杂?
  • 你桌面上的物件是整洁的还是凌乱的?
  • 你的鼠标是否不灵?
  • 你的显示屏上是否有层灰?
  • 你的笔记本是否热到让你不敢把手放在键盘上?
  • 你的内存是否够用?
  • 你的硬盘是否足够快?
这些方面都应该尽可能的做调整,不要让这些细枝末节打断你工作中的思路。

比如说硬盘不够快的问题,如果你用 Windows 7,那么它的 ReadyBoost 特性应该好好利用,它可以加快读取文件的速度。要知道 Java IDE 通常都是非常大的,每次打开来要读取半天。当插上一支 U 盘并将其用于 ReadyBoost 之后,不管是打开项目还是编译运行,速度都会加快很多。你想象过打开 Word 文档就像打开记事本一样快吗?

我将在接下来的文章中介绍如何用更少的脑力阅读、编写和运行代码。
posted @ 2012-09-19 23:06 捏造的信仰 阅读(179) | 评论 (0)编辑 收藏
CALENDER
<2019年8月>
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567

常用链接

留言簿

随笔档案

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜


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