基于Spring Security 的Java SaaS应用的权限管理
1. 概述
权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见。本文介绍的则是基于Saas系统架构的处理模型,SaaS应用的数据安全是目前大型企业比较担心的问题,因此,JSaaS的安全应用就显得非常重要。JSaaS平台不单是一款私有云的应用管理平台,更是一款可扩展开发的,适合于二次开发的租用的应用开发平台,如适合集团下有多个子公司多个子应用的开发。同时用于一个单位上使用,相当于只有一个租户的SaaS应用。本文从应用使用场景进行分析设计,并且基于Spring Security的开源安全框架上进行设计,以保证满足未来SaaS应用的数据安全要求。
2. Spring Security权限管理的原理
Spring Security 是一成熟的安全管理框架,大量应用于不同的系统中,其权限管理原理很简单,就是通过一组filter进行访问地址的拦截,通过判断用户的身份及其允许访问的权限,然后授权是否允许访问其下的资源。资源包括页面、逻辑代码中方法等。借用网上一图说明:
这些不同的Filter作用,请参考以下访问地址:
http://blog.163.com/yf_198407/blog/static/5138541120114272476265/
任何一个平台的数据访问都是需要授权的,授权只需要管理好两点,一是登录,另一个是授权访问需要的内容。Spring Security实现这两点非常容易,它已经提供了对应的接口及拦截点。
目前市面上大部分的平台都是基于角色控制访问的,因此,我们JSaas平台也是采用该办法,通过对角色或用户组进行授权,然后再把角色或用户组授权给用户即可,其原理图如下所示:
这些不同的Filter作用,请参考以下访问地址:
http://blog.163.com/yf_198407/blog/static/5138541120114272476265/
任何一个平台的数据访问都是需要授权的,授权只需要管理好两点,一是登录,另一个是授权访问需要的内容。Spring Security实现这两点非常容易,它已经提供了对应的接口及拦截点。
目前市面上大部分的平台都是基于角色控制访问的,因此,我们JSaas平台也是采用该办法,通过对角色或用户组进行授权,然后再把角色或用户组授权给用户即可,其原理图如下所示:
【说明】用户组包括的概念可以很广,如角色、部门、岗位、临时用户组等。我们在系统中只需要授权给用户组可访问哪一些资源,然后再把对应的用户组授权给对应的用户即可。为了后续以后平台对接其他用户的组织架构管理,我们对用户再进行一层隔离,即通过登录账号来实现登录的身份认证,而登录账号只需要通过关联用户即可。
【用户、用户组、资源之间的关系】
3. JSaaS平台的权限设计要点
3.1. 登录--身份认证
平台上有登录权限的实体我们称之为用户账号,也称之为身份认证,Security只需要实现UserDetails接口即可,登录的时候调用一下Security的安全认证接口即可。如下所示:
<!-- 认证管理器,实现用户认证的入口,主要实现UserDetailsService接口即可 --> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider user-service-ref="userDetailsProvider" /> </security:authentication-manager> <bean id="userDetailsProvider" class="com.redxun.saweb.security.provider.UserDetailsProvider" />UsesrDetailsProvider只需要实现UserDetailsService的loadByUsername(String username)方法即可,登录的实体实现UserDetails的方法即可。用户组实现GrantedAuthority接口即可。
虽然Spring Security提供了登录的实现Filter,但我们可以用它默认的实现,但为了我们平台的后续的更多扩展及灵活性,我们决定提供自定义的登录方式,但需要在登录后,通知Spring Security框架,即设置该框架需要的一些参数数据,以使得其后续可以通过对应的Filter访问到需要的资源。以下为我们的LoginController的实现方式:
@Controller @RequestMapping("/") public class LoginController extends BaseController{ @Resource AuthenticationManager authenticationManager; @Resource LoginManager loginManager; @RequestMapping("login") @ResponseBody public JsonResult login(HttpServletRequest request,HttpServletResponse response) throws Exception{ String username=request.getParameter("username"); String password=request.getParameter("password"); IUser user=loginManager.getLoginUser(username); if(user==null || !user.getUsername().equals(username) || !user.getPwd().equals(password.trim())){ return new JsonResult(false,"密码或用户名不正确!"); } if(user.getTenant()==null || !MStatus.ENABLED.toString().equals(user.getTenant().getStatus())){ return new JsonResult(false,"企业机构已经被禁用!"); } UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(username, password); authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(token); return new JsonResult(true,"Login Success"); } }以上特别说明一下,是登录的时候,进行了用户所在的账号的检查,以决定是否允许用户进入平台,当检查用户存在,并且密码也正确,然后产生一个带用户或及密码的令牌给Spring Security框架,让它通过后续的登录验证,否则后面的其他拦截器还是会进行拦截不允许用户访问。
3.2. 资源访问控制
3.2.1. 系统资源存储
平台上展示出来的页面、数据、按钮、后台允许访问的业务逻辑我们统一称之为系统资源,这些系统资源我们需要进行授权访问,以保证系统的安全性。那么我们如何来管理这些资源,这就需要我们进行系统的资源访问控制的设计。
平台上采用Spring MVC作为前端的控制框架,前端借用MiniUI来进行展示,因此,我们系统上的各种资源均可以用URL来表示,如下所示:
这些菜单下若有对应具体的功能及数据时,即带有具体的URL,这些URL对应的可以是具体的数据、也可以是操作按钮。以上图所示,系统中的资源包括:
-
菜单
-
按钮
-
页面或数据
-
子系统
【说明】
平台上除了具体的数据管理以外,其他的配置均可以在菜单管理中完成,包括管理机构下的非SaaS菜单以下SaaS菜单的管理。我们均把这些资源的数据存于SysMenu表中,以实现系统的资源的统一管理,同时为了兼顾系统的菜单展示模式,我们对菜单进行了树型的管理。
【子系统表结构】
【子菜单数据表结构】
3.2.2. 基于Spring Security上的扩展点:
用户登录后,需要对用户访问的资源进行安全拦截认证,我们通过在Spring Security的Filter Chain中增加我们的Filter,如下代码中的红色部分显示,在我们的Filter中,实现以下的功能要求即可:
<security:http entry-point-ref="authenticationProcessingFilterEntryPoint"> <security:intercept-url pattern="/login.do" access="ROLE_ANONYMOUS"/> <security:intercept-url pattern="/register.do" access="ROLE_ANONYMOUS"/> <security:intercept-url pattern="/captcha-image.do" access="ROLE_ANONYMOUS"/> <security:intercept-url pattern="/pub/**" access="ROLE_ANONYMOUS"/> <security:intercept-url pattern="/**" access="ROLE_PUB" /> <security:logout invalidate-session="true" logout-success-url="/login.jsp" logout-url="/j_spring_security_logout"/> <!--security:remember-me token-validity-seconds="3600" /--> <security:custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="securityInterceptorFilter" /> <security:custom-filter ref="loginFilter" position="FORM_LOGIN_FILTER" /> </security:http> <bean id="securityInterceptorFilter" class="com.redxun.saweb.filter.SecurityInterceptorFilter"> <property name="securityDataProvider" ref="securityDataProvider"/> </bean> <bean id="securityDataProvider" class="com.redxun.saweb.security.provider.SecurityDataSourceProvider"> <property name="sysMenuManager" ref="sysMenuManager"/> <property name="anonymousUrls"> <set> <value>/login.do</value> <value>/captcha-image.do</value> <value>/register.do</value> <value>/activeInst.do</value> <value>/pub/anony/imageView.do</value> <value>/pub/anony/imgUploadDialog.do</value> <value>/pub/anony/upload.do</value> <value>/pub/anony/previewImage.do</value> <value>/pub/anony/imageView.do</value> <value>/pub/anony/sysInst/regSuccess.do</value> </set> </property> <property name="publicUrls"> <set> <value>/index.do</value> </set> </property> </bean>SecurityInterceptorFilter完成以下几件重要的资源控制访问要点,用户的权限控制都包含在这个过滤器中。
-
如果用户尚未登陆,则抛出AuthenticationCredentialsNotFoundException“尚未认证异常”。
-
如果用户已登录,但是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。
-
如果用户已登录,也具有访问当前资源的权限,则放行。
3.2.3. 如何判断用户的访问资源的权限
我们需要清楚,如何通过当前登录的身份信息及拥有的用户组或角色,结合当前的访问URL,判断系统是否允许用户访问该URL。若允许,则放行,否则就抛出AccessDeniedException。
那如何通过拿到这两块信息来判断用户是否有访问资源的权限,这要求我们必须建议权限的统一数据中心,通过它来决定当前用户是否有权限访问。在此,我们在平台上建立了一个统一的数据中心,为了简化查询,我们通过HashMap来构造这个数据中心,其结构如下所示:
URL |
用户组ID |
/sys/core/subsys/list.do |
1,2,3 |
/sys/core/subsys/del.do |
2,3,5 |
/sys/core/subsys/save.do |
1,2,3 |
/sys/core/sysMenu/save.do |
4,5,8,10,22 |
/sys/core/sysMenu/del.do |
1,2 |
说明:该数据中心在Spring容器启动时进行构建,并且会进行缓存,当权限数据进行更新时,需要更新该数据中心的数据。
其判断逻辑如下所示:
其逻辑实现的代码如下所示:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String url=request.getRequestURI(); //若有contextPath,则切出来 if(org.springframework.util.StringUtils.hasLength(request.getContextPath())){ String contextPath=request.getContextPath(); int index=url.indexOf(contextPath); if(index!=-1){ url=url.substring(index+contextPath.length()); } } Authentication auth= SecurityContextHolder.getContext().getAuthentication();//取得认证器 //是否包括在匿名访问的地址中 if("anonymousUser".equals(auth.getPrincipal().toString())){ if(securityDataProvider.getAnonymousUrls().contains(url)){ doFilter(request, response, chain); return; } response.sendRedirect(request.getContextPath()+"/login.jsp"); return; } IUser user=ContextUtil.getCurrentUser(); //登录完成后,需要Spring后续的Filter进行处理 if(user==null){ doFilter(request, response, chain); return; } //是否为超级管理员 boolean isSuperUser=false; //是否为管理机构的超级管理员 if(WebAppUtil.isSaasMgrUser() && user.getUsername().startsWith("admin@")){ isSuperUser=true; } //若为超级管理员,则允许访问 if(isSuperUser){ doFilter(request, response, chain); return; } //是否为租户管理员 //允许访问所有的Saas菜单地址 if(user.getUsername().startsWith("admin@")){ if(securityDataProvider.getTenantUrlSet().contains(url)){ doFilter(request, response, chain); return; } } //公共URL if(securityDataProvider.getPublicUrls().contains(url)){ doFilter(request, response, chain); return; } //如果不包括在配置的菜单访问地址中,则默认允许访问 if(!securityDataProvider.getMenuGroupIdsMap().containsKey(url)){ doFilter(request, response, chain); return; } Set<String> groupIdSet=securityDataProvider.getMenuGroupIdsMap().get(url); boolean isIncludeGroupId=false; for(GrantedAuthority au:auth.getAuthorities()){ if(groupIdSet.contains(au.getAuthority())){ isIncludeGroupId=true; break; } } if(!isIncludeGroupId){ throw new AccessDeniedException("Access is denied! Url:" + url + " User:" + SecurityContextHolder.getContext().getAuthentication().getName()); } //进行下一个Filter chain.doFilter(request, response); }
从上面的实现的代码可以看到,关键是需要建立统一的权限数据中心,用户的权限认证只需要从中查找到匹配的用户ID即可,否则抛出禁止访问的异常。
4.数据库表的设计要素
4.1. 关于SaaS的租户表设计
JSaaS平台跟传统平台的一大区别是其引入租户的概念,我们把租户理解为使用平台的独立机构,该机构有独立的组织架构,在现行的中国法律中我们可以理解为有独立组织机构代码证的社会团队,如单位、
公司等。对于大型的集团公司,其下每个分公司也可以在使用本平台上,我们也可以用租户的概念来区分,即他们进入平台中使用功能及数据都是独立于其他租户的数据。为了适应小至中大型企业的不同运营的要求,我们采用的是共享数据库+独立数据库的方式来实现企业的不同云应用的需求。
当企业为比较大型,数据量比较大,这时我们可采用PAAS的方式为该企业提供独立的运行空间,数据库也将独立,即可认为其只有一个租户或几个相关的租户(如带有几个分公司的方式),这时对企业的数据安全是有非常有保障,也可以打消企业对自己的一些敏感数据的安全的一些顾虑。
当企业比较小时或数据量不多时,可以在平台上注册成为租户,即可以使用平台上的所有服务,这时不需要投入太多的资金,即可以实现企业的信息化管理,这种模式对中小企业是有吸引力的。
【说明】租户是由平台的管理机构实现统一管理,因此,其功能只是对平台运营商开放,不对租户开放该功能。
4.2. 组织架构设计
平台简化组织架构的管理,但又能适应复杂的组织架构管理需求,我们把人员的组织架构进行了如下方式的划分
1. 组织架构实体
-
用户组
为了管理不同的用户组,平台提供了用户组的维度管理,通过这种方式可以有效根据业务 需求对自己的用户组及用户进行不同的分类划分。
-
用户
平台的用户,可以理解平台的使用人员
a)用户与用户关系
实现用户与用户的关系定义,如我的直属领导,通过定义这种关系,可以为某个用户设置其直属领导是谁。
b)用户与用户组关系
定义用户与用户组的关系,如部门的领导,片区的负责人。这些均可以称之为用户与组的关系,通过定义这些关系,可以有效管理用户组下的用户。
c)用户组与用户组的关系定义用户组与用户组的关系在某些应用场景也会采用,如职务、部门。我们把挂在部门下的职务称之为岗位,通过岗位可以快速定位其属于哪个部门。
4.3. 系统权限涉及的表设计
表名 |
作用 |
OS_USER |
用户表 |
SYS_ACCOUNT |
用户的账号表 |
OS_GROUP |
用户组表,如部门、角色、岗位等,可为树型结构 |
OS_DIMENSION |
用户组维度,用于定义用户组的分类 |
SYS_MENU |
系统的菜单的资源,包括可访问的菜单、URL等 |
SYS_SUBSYS |
子系统 |
OS_GROUP_SYS |
用户组下授权访问的子系统 |
OS_GROUP_MENU |
用户组下授权访问的菜单 |
OS_REL_TYPE |
关系定义,包括各种用户关系、用户组关系、用户与用户的关系定义 |
OS_REL_INST |
关系实例表,存储各种关系,如用户从属于部门,需要从该表查询 |
5.关于SaaS的授权的扩展关键用例
平台把用户分为以下几方面,从管理的用例如下:
平台有一个比较重要的是SaaS管理员,它可以管理所有租户的信息,包括组织架构调用。当然为了数据安全,可以关闭一些对应的管理功能。
具体的访问效果如:
http://www.redxun.cn:8020/saweb/login.jsp
http://www.redxun.cn/?p=698 开发框架内容
推荐阅读
-
基于Spring Security前后端分离的权限控制系统问题
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现
-
基于Spring Security 的Java SaaS应用的权限管理
-
基于Spring Security 的Java SaaS应用的权限管理
-
基于struts2拦截器的权限管理之ThreadLocal在数据传递中的应用
-
Spring Security实战(一)-- 基于数据库的权限认证
-
一种Java Web应用开发框架的构建(基于Struts2+Spring+FreeMarker)之一
-
实现基于Spring框架应用的权限控制系统security
-
实现基于Spring框架应用的权限控制系统
-
译:使用Spring3.1和基于Java的配置启动web应用 part 1