SaaS 应用程序实例java版Word文档格式.docx
《SaaS 应用程序实例java版Word文档格式.docx》由会员分享,可在线阅读,更多相关《SaaS 应用程序实例java版Word文档格式.docx(23页珍藏版)》请在冰豆网上搜索。
该解决方案结合使用了SpringSecurity(一个经久不衰的开源安全框架)和ApacheDirectoryServer(一个基于Java的流行服务器,它是开源的并且遵从LightweightDirectoryAccessProtocolv3,即LDAPv3)。
本文提出的解决方案是一个
示例JavaWeb应用程序,它既可以部署到ApacheTomcat,也可以部署到ApacheGeronimo。
本文重点介绍SaaS模型内的身份验证和授权机制。
其他有关SaaS安全性的概念和技术—比如数据保密与隔离、法规、审计和密码等,超出了本文的范围。
多承租的SaaS应用程序中的身份验证与授权
身份验证和授权是现实应用程序的安全性概念中主要的两个:
∙身份验证允许一个应用程序在连接时验证一个人(或一个应用程序、智能卡等)是否与它声明的一样。
∙授权定义一个用户在一个系统上的权利与权限。
用户身份验证通过之后,授权会决定该用户在系统上有权做什么。
因此,授权应该发生在身份验证之后。
身份验证和授权在SaaS应用程序中很复杂。
在一个安全性SaaS解决方案中,底层的身份验证和授权基础设施有两种设计方法:
集中式或联邦式。
本文提出的解决方案使用集中式身份验证系统(LDAP服务器)。
集中式的身份验证系统并不排除支持分布式目录的可能性,分布式目录储存了可以分区和复制的信息。
本文不考虑采用另一种分散式处理方法,即
联邦身份管理。
在SaaS领域用联邦身份管理会给安全性带来很多新的挑战。
(典型的用例会涉及到跨域、基于Web的单点登录、跨域用户帐户供应、跨域授权管理和跨域用户属性交换等。
详细信息请参见
参考资料,里面的文章链接“MeetingtheSaaSSecurityChallenge”有详细的解释)。
回页首
SpringSecurity简介
SpringSecurity:
比Acegi的功能更强大
SpringSecurity从2003年开始作为Spring的AcegiSecuritySystem,直到2007年年末它才成为一个官方的Spring项目,并重命名为SpringSecurity。
我们推荐使用SpringSecurity(而不是Acegi)有以下几点原因:
∙它具备Acegi的所有优点,并且有额外的优点。
∙它利用自定义Spring2.0配置名称空间。
∙复杂的安全性细节现在隐藏在更简单的XML配置之后。
∙它具有自动配置的能力。
在默认情况下,JavaEnterpriseEdition(JavaEE)5安全机制不支持承租者ID(tenantID)等自定义属性,不管它们是什么样的验证者类型(basic,form,digest或clientcertificate)。
要支持多承租就必须要实现自定义的解决方案。
在本文中,我们将展示如何使用SpringSecurity来构建这样的解决方案。
SpringSecurity提供了一个综合的安全解决方案,这个方案大大地简化了在JavaEE应用程序中开发安全措施的工作。
它提供了更高级的摘要,能够让您插入不同的身份验证模型,同时还支持丰富的身份验证功能。
此外,它还在不同的应用程序服务器之间提供了高度可移植性。
SpringSecurity有以下特性:
∙声明性安全性
∙支持各种身份验证和授权机制,如basic、form、digest、JDBC和LDAP
∙支持方法级别的安全性以及JSR-250安全性注释
∙支持单点登录
∙支持容器集成
∙支持匿名对话、并行对话、remember-me、通道加强等
本文的重点是直接集成SpringSecurity和LDAP。
其他的部署场景可能会考虑到其他的方法,如Java身份验证和授权服务(JavaAuthenticationandAuthorizationService,JAAS),或者是由SpringSecurity框架提供的容器适配器进行的容器管理身份验证。
LDAP与ApacheDirectory概述
在企业中,管理用户和角色的常见方法是使用LDAP服务器。
有几个开源的商业LDAP解决方案可供选择(参见
考虑到有些读者可能不熟悉LDAP或ApacheDirectoryServer,接下来我们对其进行概述。
LDAP的核心
LDAP本质上就是一个数据库。
但它趋向于包含更多描述性的、基于属性的信息。
由于LDAP目录中的信息的读多于写,所以LDAP被设计为读最优化。
最常见的例子就是电话簿,它里面的每一人都附有地址和电话号码。
作为身份验证和授权源,LDAP与关系数据库管理系统性相比有以下优点:
∙有线协议;
无需驱动器
∙灵活的模式
∙以身份为中心
o身份验证、授权和审计
o组
o安全性(密码、证书)
ApacheDirectoryServer的核心
ApacheDirectoryServer是一个可嵌入的、可扩展的、遵从标准的开源LDAP服务器,它由Java语言编写而成。
我们为本文的解决方案选择ApacheDirectoryServer的理由是它的简单性,因为它是一个纯Java实现。
对于现实中的应用程序,您一定要正确衡量哪一个LDAP解决方案最符合您的业务和技术需求。
ApacheDirectory项目提供了一个ApacheDirectoryServer,它遵从LDAPv3和ApacheDirectoryStudio,后者是一组基于Eclipse的目录工具(参见
在多承租的环境中集成SpringSecurity和ApacheDirectoryServer
在一般情况下,配置SpringSecurity使其能够协同LDAP服务器进行工作很简单。
虽然在多承租的环境中集成它们也相对容易,但还是比一般情况复杂些。
我们首先论述如何在ApacheDirectoryServer中创建一个多承租用户注册表,然后再展示一个动态LDAP路由解决方案如何为一个有效的多承租安全性解决方案提供便利。
多承租ApacheDirectoryServer用户注册表
本小节描述一个示例多承租用户注册表,它由两个安全性区域组成,名为tenant1和tenant2,每一区域个都有两组不同的用户:
管理员和访问者。
每一组都链接着许多用户,这些用户共用一个特定角色,而且都属于该组的相应安全区域。
不同区域的用户凭证储存于一个ApacheDirectoryServer用户注册表中,位于不同的子树下,如
图1所示。
将不同的LDAP后缀分配给不同的安全区域。
例如,tenant1的基本专有名称(BaseDistinguishedName,DN)是[dc=tenant1,dc=com],tenant2的基本DN为[dc=tenant2,dc=com]。
图1.多承租LDAP用户注册表示例
然后,不同的用户组(在现实中转换成了用户角色)会被分配到对应的每一个安全区域。
例如,组[cn=adm,ou=groups]和组[cn=gst,ou=groups](分别转换成管理员和访问者)属于基本DN为[dc=tenant1,dc=com]的tenant1安全区域。
反过来,不同的用户条目与一个特定安全区域下的特定组相关联。
例如,用户[uid=tenant1admin,ou=people]被赋予管理员的角色[cn=adm,ou=groups],属于安全区域[dc=tenant1,dc=com]。
一定要在ApacheDirectoryServer中的server.xml配置文件(ApacheDirectoryServerInstallDirectory/instances/default/conf/server.xml)中为
图1中的每一个安全区域创建一个新的分区和安全上下文条目,如
清单1所示:
清单1.ApacheDirectoryServer的server.xml文件
<
?
xmlversion="
1.0"
encoding="
UTF-8"
>
<
spring:
beansxmlns:
spring="
http:
//xbean.apache.org/schemas/spring/1.0"
xmlns:
s="
//www.springframework.org/schema/beans"
xmlns="
//apacheds.org/config/1.0"
...
jdbmPartitionid="
tenant1"
cacheSize="
100"
suffix="
dc=tenant1,dc=com"
optimizerEnabled="
true"
syncOnWrite="
indexedAttributes>
jdbmIndexattributeId="
1.3.6.1.4.1.18060.0.4.1.2.1"
/>
1.3.6.1.4.1.18060.0.4.1.2.2"
1.3.6.1.4.1.18060.0.4.1.2.3"
1.3.6.1.4.1.18060.0.4.1.2.4"
1.3.6.1.4.1.18060.0.4.1.2.5"
10"
1.3.6.1.4.1.18060.0.4.1.2.6"
1.3.6.1.4.1.18060.0.4.1.2.7"
dc"
ou"
krb5PrincipalName"
uid"
objectClass"
/indexedAttributes>
contextEntry>
#tenant1ContextEntry<
/contextEntry>
/jdbmPartition>
beanid="
tenant1ContextEntry"
class="
org.springframework.beans.factory.config.MethodInvokingFactoryBean"
propertyname="
targetObject"
reflocal='
directoryService'
/spring:
property>
targetMethod"
value>
newEntry<
arguments"
list>
valuexmlns="
objectClass:
top
domain
extensibleObject
dc:
tenant1
dc=tenant1,dc=com<
bean>
beans>
同样地,要像tenant1那样为tenant2添加一个新的分区和安全性上下文条目。
示例Web应用程序中有一个简单的示例.server.xml文件。
创建了安全区域后,就可以使用LDIFImportWizard——ApacheDirectoryStudioEclipse插件中的特色工具(参见
参考资料)——将类似于
清单2中的LDAPDateInterchangeFormat(LDIF)文件的内容导入到其相应的安全性区域中。
示例Web应用程序提供示例LDIF文件。
清单2.tenant1_users.ldif
dn:
ou=groups,dc=tenant1,dc=com
objectclass:
organizationalUnit
ou:
groups
dn:
ou=people,dc=tenant1,dc=com
people
uid=tenant1admin,ou=people,dc=tenant1,dc=com
person
organizationalPerson
inetOrgPerson
cn:
tenant1admin
sn:
uid:
userPassword:
uid=tenant1guest,ou=people,dc=tenant1,dc=com
tenant1guest
cn=gst,ou=groups,dc=tenant1,dc=com
groupOfNames
gst
member:
cn=adm,ou=groups,dc=tenant1,dc=com
adm
SpringSecurity的动态LDAP路由
动态LDAP路由的原理是在运行时根据查找密钥动态地选择LDAP安全性上下文的可能性(参见
图2)。
在一个多承租环境中,这针对一个LDAP源(根据承租者的ID动态生成)转换成身份验证和授权。
图2.多承租动态LDAP路由
Spring本身不提供动态LDAP路由,所以需要亲自构建。
我们的想法受到类似解决方案—Spring的
AbstractRoutingDataSource
—(参见
参考资料)的启发。
我们通过封装三个主要的类来实现动态LDAP路由:
∙清单3中所示的
AbstractRoutingSpringSecurityContextSource是一个抽象的实现,它基于Spring的LdapContextSource类。
它引用一组“真实的”安全性上下文源(参见
targetSpringSecurityContextSources),它的目的是根据固定的查找密钥将调用路由到众多的目标安全性上下文源之一(参见
getResolvedContextSource())。
清单3.AbstractRoutingSpringSecurityContextSource.java
publicabstractclassAbstractRoutingSpringSecurityContextSource<
T
extendsSerializable>
extendsLdapContextSource
implementsSpringSecurityContextSource,InitializingBean{
privateMap<
T,DefaultSpringSecurityContextSource>
targetSpringSecurityContextSources;
/**Determinethecurrentlookupkey.Thiswilltypicallybe
implementedtocheckathread-boundcontext.*/
protectedabstractTdetermineCurrentLookupKey();
/**Determinethe'
real'
securitycontextsourcedynamically
atruntimebaseduponalookupkey.*/
protectedDefaultSpringSecurityContextSourcegetResolvedContextSource(){
TlookupKey=determineCurrentLookupKey();
DefaultSpringSecurityContextSourcespringSecurityContextSource=
this.targetSpringSecurityContextSources.get(lookupKey);
if(springSecurityContextSource==null){
thrownewIllegalStateException(
"
CannotdeterminetargetSpringSecurityContextSourceforlookupkey["
+
lookupKey+"
]"
);
}
returnspringSecurityContextSource;
publicvoidsetTargetSpringSecurityContextSources(
Map<
targetSpringSecurityContextSources){
this.targetSpringSecurityContextSources=targetSpringSecurityContextSources;
publicvoidafterPropertiesSet()throwsException{
if(this.targetSpringSecurityContextSources==null){
thrownewIllegalArgumentException(
targetSpringSecurityContextSourcesisrequired"
publicDirContextgetReadWriteContext(StringuserDn,Objectcredentials){
returnthis.getResolvedContextSource().getReadWriteContext(userDn,credentials);
@Override
publicDirContextgetReadOnlyContext(){
returnthis.getResolvedContextSource().getReadOnlyContext();
publicDirContextgetReadWriteContext(){
returnthis.getReso