锁定老帖子 主题:权限管理最佳实践:二,URL权限控制
精华帖 (4) :: 良好帖 (5) :: 新手帖 (10) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2010-09-02
-------------------------------------------- 总大纲 --------------------------------- Ralasafe开源有段时间了,大约有2个月了。根据社区的反馈,我打算围绕Ralasafe最佳实践,书写一系列BLOG。
大体内容有: 1, 登录控制: 哪些页面需要登录后才能访问,登录用户名、密码验证,登录转向页面; 2, URL权限控制:哪些页面访问需要进行角色权限验证,怎样验证最简单有效,如何处理验证失败情况; 3, 数据级权限管理方案探讨:选择中间件呢还是框架? 4, Ralasafe体系结构: 用户怎么读取,用户有哪些字段,怎样与应用基础; 5, 数据级查询权限管理: 如何给不同的人分配不同的查询数据权限,返回where条件呢,还是直接返回结果集? 6, 数据级决策权限管理: 如何给不同的人分配不同的数据操作权限,当用户不具备权限怎么办? 7, 其他细小的权限控制: 如下拉框显示内容;按钮、链接是否显示,图片是否显示等。 -------------------------------------------- ------- --------------------------------
今天说的URL权限控制,内容主要有:URL权限控制,当用户访问某URL时,进行角色权限验证。如果有相应权限,则允许其正常访问;否则,转到拒绝页面。 我们依然通过一个Filter来实现,这样就无需在代码中增加权限判断,也无需套用任何框架。对于整个权限管理系统来说,本节内容也非常简单。 理论分析当软件实施人员进行系统实施的时候,会将一些访问菜单定义为权限。然后定义角色,让角色拥有权限。然后再将权限赋给用户。 所以,当用户请求某个URL的时候,要不该URL需要权限验证,要不就是不需要权限验证。 检验标准就是:看权限表里面有没有该URL。检验的时候,唯一需要注意的是:URL参数,比如employeeManage?op=add。 数据库模型权限表:id<int>,name<varchar>,url<varchar>,description<varchar> | pk(id) 角色表:id<int>,name<varchar>,description<varchar> | pk(id) 角色-权限关系表:roleId<int>,privilegeId<int> | pk(roleId,privilegeId) 用户-角色关系表:userId<int>(根据你系统的情况,也可能是varchar等),roleId<int> | pk(userId,roleId) Ralasafe方案Ralasafe权限管理中间件(下载地址),既可以管理和控制功能级权限,也可以管理和控制数据级权限。开发者还可以根据需求,只选择功能级控制,或者只选择数据级控制。
当安装好用户元数据的时候,Ralasafe自动创建所有权限表。相关权限数据,都由Ralasafe界面进行管理(即录入)。
Ralasafe的管理界面,在功能权限方面可以做到: 1,管理权限界面; 2,管理角色界面,并给角色赋权限; 3,给用户分配角色界面。这里还需要注意:不同用户管理可以给不同范围的用户分配角色。比如:总公司的管理员可以给所有人分配角色;分公司管理员可以给本分公司及下属子公司用户分配角色。
Ralasafe将最后一点视为数据级权限。详见:http://www.ralasafe.org/zh/guide/reference/safe.html#ralasafe 和 http://www.ralasafe.org/jforum/posts/list/11.page
将org.ralasafe.webFilter.UrlAclFilter配置到web.xml即可,而且配置工作量极其少。
<filter> <filter-name>ralasafe/UrlAclFilter</filter-name> <filter-class>org.ralasafe.webFilter.UrlAclFilter</filter-class> <init-param> <param-name>loginPage</param-name> <param-value>/ralasafe/demo/login.jsp</param-value> </init-param> <init-param> <param-name>denyPage</param-name> <param-value>/ralasafe/demo/noPrivilege.jsp</param-value> </init-param> </filter> <filter-mapping> <filter-name>ralasafe/UrlAclFilter</filter-name> <url-pattern>/ralasafe/demo/*</url-pattern> </filter-mapping>
该Filter具有这些功能: 1,在用户具有权限的时候,正常访问; 2,在用户不具有权限的时候,转到拒绝页面; 3,如果用户没用登录,转到登录页面,让用户先登录。 其他这里我简单说说spring security。 spring security在控制功能权限的时候,还会帮助开发人员控制Dao/Service等组件。我个人认为这种控制是多余的。 因为,功能权限控制应该站在最终用户角度进行考虑。Dao/Service等编程开发级的组件,并不是最终用户关心的事情。所以无需进行功能权限控制。 另外,大家在使用spring security,我建议将功能级权限控制放在数据库里面,而不是annotation到java code里面。因为annotation到java code里面,最终用户就不能控制了。
注:ralasafe团队博客在javaeye/baidu/blogjava等空间,同步发布。ralasafe官方网站:http://www.ralasafe.org/zh
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-09-02
不好部署,能否有文档说明下部署流程。
|
|
返回顶楼 | |
发表时间:2010-09-03
spring security扩展,从数据库读取信息。从新实现UserDetailsService和FilterInvocationSecurityMetadataSource
public class HibernateUserDetailsServiceImpl extends HibernateDaoSupport implements UserDetailsService { //~ Methods ======================================================================================================== @SuppressWarnings("unchecked") public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException, DataAccessException { List results = getHibernateTemplate().find("from User as res left outer join fetch res.employee left outer join fetch res.authorities where userId = ?", new Object[] {userId}); if (results.size() < 1) { throw new UsernameNotFoundException(userId + "not found"); } return (UserDetails) results.get(0); } } public class HibernateFilterInvocationSecurityMetadataSource extends HibernateDaoSupport implements FilterInvocationSecurityMetadataSource{ //~ Static fields ================================================================================================== public static final String CACHE_KEY = "FilterInvoSecMetaKey"; //~ Instance fields ================================================================================================ protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @javax.annotation.Resource(name="starFrameCache") private Cache cache; private Map<Object, Collection<ConfigAttribute>> httpMap = null; private UrlMatcher urlMatcher = new AntUrlPathMatcher(true); private boolean stripQueryStringFromUrls; //~ Constructors =================================================================================================== //~ Methods ======================================================================================================== /** * 初始化,加载资源权限 */ @SuppressWarnings("unchecked") @PostConstruct public void loadSecurityMetadataSource() throws Exception { logger.info("Loading Security Metadata Source..."); httpMap = new LinkedHashMap<Object, Collection<ConfigAttribute>>(); Session session = getHibernateTemplate().getSessionFactory().openSession(); Query query = session.createQuery("from Resource as res left outer join fetch res.roles where res.enabled = :enabled and res.type <> :type order by res.priority"); query.setCharacter("enabled", 'Y'); //不等于method类型 query.setString("type", Resource.ResourceType.METHOD.getType()); List<Resource> resources = query.list(); for(Resource resource : resources) { String pattern = resource.getAction(); //如果资源是菜单类型时,需要去掉前缀 if(pattern.startsWith("./")) pattern = pattern.substring(1); else if(pattern.startsWith("../")) pattern = pattern.substring(2); while(pattern.startsWith("/..")) { pattern = pattern.substring(3); } List<ConfigAttribute> attrs = new ArrayList<ConfigAttribute>(); Set<Role> roles = resource.getRoles(); for(Role role : roles) { attrs.add(new SecurityConfig(role.getCode())); } httpMap.put(urlMatcher.compile(pattern), attrs); if (logger.isDebugEnabled()) { logger.debug("Added URL pattern: " + pattern + "; attributes: " + attrs); } } session.close(); cache.putItem(CACHE_KEY, httpMap); } /** * */ public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } /** * 通过FilterInvocation对象查找适合的ConfigAttribute * * @param object * @return */ public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { if ((object == null) || !this.supports(object.getClass())) { throw new IllegalArgumentException("Object must be a FilterInvocation"); } String url = ((FilterInvocation) object).getRequestUrl(); return lookupAttributes(url); } /** * * @param url * @return */ public final Collection<ConfigAttribute> lookupAttributes(String url) { if (stripQueryStringFromUrls) { // Strip anything after a question mark symbol, as per SEC-161. See also SEC-321 int firstQuestionMarkIndex = url.indexOf("?"); if (firstQuestionMarkIndex != -1) { url = url.substring(0, firstQuestionMarkIndex); } } if (urlMatcher.requiresLowerCaseUrl()) { url = url.toLowerCase(); if (logger.isDebugEnabled()) { logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'"); } } // Obtain the map of request patterns to attributes for this method and lookup the url. Object obj = cache.getItem(CACHE_KEY); if(obj == null) { try { loadSecurityMetadataSource(); } catch (Exception e) { e.printStackTrace(); } } Collection<ConfigAttribute> attributes = extractMatchingAttributes(url, httpMap); return attributes; } /** * * @param url * @param map * @return */ private Collection<ConfigAttribute> extractMatchingAttributes(String url, Map<Object, Collection<ConfigAttribute>> map) { final boolean debug = logger.isDebugEnabled(); for (Map.Entry<Object, Collection<ConfigAttribute>> entry : map.entrySet()) { Object p = entry.getKey(); boolean matched = urlMatcher.pathMatchesUrl(entry.getKey(), url); if (debug) { logger.debug("Candidate is: '" + url + "'; pattern is " + p + "; matched=" + matched); } if (matched) { return entry.getValue(); } } return null; } /** * */ public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } public void setCache(Cache cache) { this.cache = cache; } } |
|
返回顶楼 | |
发表时间:2010-09-03
|
|
返回顶楼 | |
发表时间:2010-09-04
粒度太粗了,这让我想起当年研究RBAC的日子。
那时候的导师要求需要将每个页面的每一个按钮都作为可控制部分, 而我那时也只是简单用url作为最小粒度,不过现在想一想还真对不起RBAC。 |
|
返回顶楼 | |
发表时间:2010-09-07
权限是最讨厌的
|
|
返回顶楼 | |
发表时间:2010-09-08
替楼主叫屈
看不明白那些投新手帖的 权限真的那么简单吗? |
|
返回顶楼 | |
浏览 19026 次