在jsp web项目中实现透明的权限控制

现在想到的是在Filter中做……

目标:通过配置来实现权限管理,在业务代码中无需再考虑权限问题
参考:类似tomcat的realm,在进到项目前,已经由容器判断了权限。权限管理通过配置来实现,在业务代码中无需再考虑权限问题。不足是,servlet规范中用URL Pattern配置访问级别,不够灵活
办法:在Filter中做权限判断?
我做了一个接口

boolean isUserHasPerm(String user, String url)

MyFilter调用接口

if (!isUserHasPerm(...))
response.sendError(403, "Forbidden");

isUserHasPerm接口已经初步实现,???现在的问题是Filter好像不是放权限判断的地方???,因为签名是这样的:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)

其中request 是ServletRequest, 不是HttpServletRequest,
response是ServletResponse,不是HttpServletResponse
这样就没有request.getSession().getAttribute("userinfo"),//得到权限判断的who要素
没有response.sendError() //这样没有权限,该如何处理?
???当然可以强行转换,tomcat下也不会cast错误,但始终不是个办法……???

-----------------------------------------------
另外,说明一下isUserHasPerm的实现
相关联的对象图

核心是ACL表,实际操作给出role,function,resource三要素,如果ACL表中有记录,说明可以访问,因为三要素唯一确定一条ACL记录(function通过resoure type和operate type确定)

优化后,剩三个表,User,Role,ACL(当然,因为User,Role是多对多,还要一个关联表)
ACL表包含四个字段:
Role, Component, Instance, OP(参考了PostNuke的权限设置)

Component就是resource type,
Instance就是resource
OP就是function

合并进Component和OP,而不是functionID的好处是,可以通过通配符来设置权限,比如设置一个全局的管理权限,只需要一条记录


role=admins, component=*, instance=*, OP=*

另外,OP字段还可以等于DENY,有优先权,如果找到一条匹配的DENY记录,直接就是没有权限了。有了DENY,如果需要赋大部分权限,排除一小部分,也比较容易配置

isUserHasPerm的伪码


(1)
select * from ACL
where Role in (user.getRoles())
and Component in (url.getComponent, *)
and Instance in (url.getInst, *)
and OP = DENY

(2)
if (结果集>=0)
return false;

(3)
将(1)的OP条件改为
and OP in (url.getOP, *)

(4)
if (结果集>0)
return true;
else
return false;

*注:从url中得到权限判断的要素,当然需要一些规则了,比如
category.do?op=add&id=1
表示component是category
op是add
instance是1
role当然是从session中拿

稍微看了一下,如果我的意见和事实不符合请见谅。

我觉得用filter来执行权限判断是不可能的,因为基于数据库应用的URL经常是一致的,只是后面的参数不一样。有些参数还是表单发送的。
所以,用它来执行中文化是不错的,但是来实现ACL功能好象不行,tomcat可以是因为它的各部分是不同的URL吧。

我觉得windows2000的许可和角色和树状继承很不错,不过在应用中实现还是有难度。

filter概念实际就是Proxy模式实现。

ACL是肯定要用Proxy模式实现,关键是如何实现比较巧妙,象Jive那样,为每个数据Model建立Proxy(filter)的做法是不可取的,动态Proxy好像是比较好的方法,根据操作方法名称,如果get,表示读取操作,update表示更新,缺点是,要求对EJB或Service操作方法实现统一命名限制。

通过EJB容器的安全机制,在ejb-jar.xml中配置ACL也是目前比较好的办法,缺点是需要管理员修改ejb-jar.xml,那么在此基础上,编制管理程序操作修改ejb-jar.xml可以达到方便ACL设置吧。

>只是后面的参数不一样。有些参数还是表单发送的。
其实在filter中传入的参数是request,参数可以通过getRequestURI,getParameter来获取,

问题是如何通过request提取出
role,component,instance,op, 这个规则不太好定:
能要求URI就是component?
instance一定要用名为id的parameter来确定?
要显式的给出op参数?

而且,定了这样的规则,方案就不透明了

下面给出isUserHasPerm的初步实现

一、数据库,mssqlserver


drop table hive_acl;
drop table hive_account_role;
drop table hive_account;
drop table hive_role;

create table hive_account (
id int identity,
account varchar(50) unique not null,
password varchar(16) not null,
primary key (id),
);

create table hive_role (
id int identity,
role varchar(50) unique not null,
primary key (id),
);

create table hive_account_role (
account_id int,
role_id int,
primary key (account_id, role_id),
foreign key (account_id) references hive_account(id),
foreign key (role_id) references hive_role(id),
);

create table hive_acl (
role_id int,
component varchar(50),
instance varchar(50),
op varchar(50),
primary key (role_id, component, instance, op),
foreign key (role_id) references hive_role(id),
);

insert into hive_account (account, password) values ('admin', 'abc');
insert into hive_account (account, password) values ('amen', 'abc');
insert into hive_account (account, password) values ('anonymous', '');

insert into hive_role (role) values ('admins');
insert into hive_role (role) values ('users');
insert into hive_role (role) values ('guests');

insert into hive_account_role (account_id, role_id) values (1,1);
insert into hive_account_role (account_id, role_id) values (1,2);
insert into hive_account_role (account_id, role_id) values (2,2);
insert into hive_account_role (account_id, role_id) values (3,3);

insert into hive_acl (role_id, component, instance, op) values (1, '*', '*', '*');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', 'manager', 'none');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', '*', 'read');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', 'public', 'write');
insert into hive_acl (role_id, component, instance, op) values (3, 'category', 'public', 'read');

测试权限的核心语句,其实后面的java方法只是另一种实现
1) 查询相应的op=none的记录,如果记录数大于则没有权限
2) 查询匹配记录,如果记录数等于0(没有acl记录)则没有权限


-- 判断是否有op=none的acl记录
declare @account varchar(50);
declare @component varchar(50);
declare @instance varchar(50);
declare @op varchar(50);

select @account = 'anonymous';
select @component = 'category';
select @instance = 'manager';
select @op = 'write';

select * from hive_acl
where role_id in (
select role_id
from hive_account_role inner join hive_account
on hive_account_role.account_id = hive_account.id
where account = @account
)
and component in (@component, '*')
and instance in (@instance, '*')
and op ='none'
;


-- 判断是否有匹配的acl记录
declare @account varchar(50);
declare @component varchar(50);
declare @instance varchar(50);
declare @op varchar(50);

select @account = 'amen';
select @component = 'category';
select @instance = 'manager';
select @op = 'write';

select * from hive_acl
where role_id in (
select role_id
from hive_account_role inner join hive_account
on hive_account_role.account_id = hive_account.id
where account = @account
)
and component in (@component, '*')
and instance in (@instance, '*')
and op in (@op, '*')
;

将由filter调用


public interface PermissionManager {

public boolean isUserHasPerm(HttpServletRequest request);

}



public class PermissionFilter implements Filter {
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {

try {
HttpServletRequest req = (HttpServletRequest)request;
if (!HiveFactory.getDefaultHive().getPermissionManager().isUserHasPerm(req)) {
HttpServletResponse rep = (HttpServletResponse)response;
rep.sendError(403);
}

} catch (Exception e) {
}

try {

filterChain.doFilter(request, response);
} catch (Exception e) {
}
}
}



public class PermissionManagerImpl implements PermissionManager {

public boolean isUserHasPerm(HttpServletRequest request) {
HttpSession session = request.getSession();
Account user = (Account)session.getAttribute("userinfo");
if (user == null)
user =
HiveFactory
.getDefaultHive()
.getUserManager()
.getAnonymousUser();

String component, instance, op;
// get component from URI
{
String uri = request.getRequestURI();
int i, j;
i = uri.lastIndexOf(
"/");
j = uri.indexOf(
".do", i);
component = uri.substring(i+1, j).toLowerCase();
}
// get instance
instance = request.getParameter(
"id");
// get op
op = request.getParameter(
"op");
return isUserHasPerm(user.getAccount(), component, instance, op);
}


private boolean isUserHasPerm(
String account,
String component,
String instance,
String op) {
try {
Connection cn = HiveFactory.getDefaultHive().getConnection();
if (cn == null)
return false;
String sql;
PreparedStatement pstm;
ResultSet rs;
int count;

sql =
"select count(*) from hive_acl"
+
" where role_id in ("
+
" select role_id"
+
" from hive_account_role inner join hive_account"
+
" on hive_account_role.account_id = hive_account.id"
+
" where account = ?"
+
" )"
+
" and component in (?, '*')"
+
" and instance in (?, '*')"
+
" and op ='none'";
pstm = cn.prepareStatement(sql);
pstm.setString(1, account);
pstm.setString(2, component);
pstm.setString(3, instance);
rs = pstm.executeQuery();
rs.next();
count = rs.getInt(1);
if (count >= 1) {
cn.close();
return false;
}

sql =
"select count(*) from hive_acl"
+
" where role_id in ("
+
" select role_id"
+
" from hive_account_role inner join hive_account"
+
" on hive_account_role.account_id = hive_account.id"
+
" where account = ?"
+
" )"
+
" and component in (?, '*')"
+
" and instance in (?, '*')"
+
" and op in (?, '*')";
pstm = cn.prepareStatement(sql);
pstm.setString(1, account);
pstm.setString(2, component);
pstm.setString(3, instance);
pstm.setString(4, op);
rs = pstm.executeQuery();
rs.next();
count = rs.getInt(1);

cn.close();
if (count == 0) {
return false;
}
return true;

} catch (SQLException e) {
e.printStackTrace();
return false;
}
}

}

在web.xml中,PermissionFilter设置过滤"/secu/*"的请求


<web-app>
<filter>
<filter-name>permissionfilter</filter-name>
<filter-class>com.stalent.hive.permission.PermissionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>permissionfilter</filter-name>
<url-pattern>/secu/*</url-pattern>
</filter-mapping>

前面


insert into hive_acl (role_id, component, instance, op) values (1, '*', '*', '*');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', 'manager', 'none');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', '*', 'read');
insert into hive_acl (role_id, component, instance, op) values (2, 'category', 'public', 'write');
insert into hive_acl (role_id, component, instance, op) values (3, 'category', 'public', 'read');

前面insert的数据表示:
假想有一种“category”的资源(可以想象成目录、论坛的板块之类)
有public,manager以及其他的一些实例
role_id=1是admins,2是users,3是guests
admins有所有权限
users可以读除manager外的所有资源,并对public可写
guests只能读public

以users登录
/hiveweb/secu/category.do?op=read&id=manager
403 Forbidden

/hiveweb/secu/category.do?op=read&id=public
400 Invalid path

……

一、没有owner的概念,比如要让用户对自己创建的资源有编辑权,怎么办?……

二、PermissionFilter,如果设为过滤"/*",又是根目录下的项目,tomcat启动时也会调用doFilter方法,这不是我想要的

三、PermissionFilter中,downcast ServletRequest到HttpServletRequest,会不会有隐患?上传文件通过mutipart form提交的request会不会转换错?还有没有其他不能cast的情况?

四、用资源类型、操作类型能唯一确定一个功能吗?

可以参见 seraph
http://opensource.atlassian.com/seraph

Woo~
Seraph is a 'gate keeper' for J2EE applications - thus was named after Seraph from the Matrix Reloaded.

thumbup!;)