`
yaowei06252009
  • 浏览: 85443 次
  • 性别: Icon_minigender_1
  • 来自: 蚌埠
社区版块
存档分类
最新评论

Spring Security 2 配置精讲

阅读更多

论坛上看了不少Spring Security的相关文章。这些文章基本上都还是基于Acegi-1.X的配置方式,而主要的配置示例也来自于SpringSide的贡献。

众所周知,Spring Security针对Acegi的一个重大的改进就在于其配置方式大大简化了。所以如果配置还是基于Acegi-1.X这样比较繁琐的配置方式的话,那么我们还不如直接使用Acegi而不要去升级了。所以在这里,我将结合一个示例,重点讨论一下Spring Security 2是如何进行配置简化的。

搭建基础环境

首先我们为示例搭建基本的开发环境,环境的搭建方式,可以参考我的另外一篇文章:http://www.iteye.com/wiki/struts2/1321-struts2-development-environment-to-build

整个环境的搭建包括:创建合适的目录结构、加入了合适的Library,加入了基本的Jetty启动类、加入基本的配置文件等。最终的项目结构,可以参考我的附件。

参考文档

这里主要的参考文档是Spring Security的自带的Reference。网络上有一个它的中文翻译,地址如下:http://www.family168.com/tutorial/springsecurity/html/springsecurity.html

除此之外,springside有一个比较完整的例子,不过是基于Acegi的,我也参阅了其中的一些实现。

Spring Security基本配置

Spring Security是基于Spring的的权限认证框架,对于Spring和Acegi已经比较熟悉的同学对于之前的配置方式应该已经非常了解。接下来的例子,将向大家展示Spring Security基于schema的配置方式。

最小化配置

1. 在web.xml文件中加入Filter声明

Xml代码 复制代码
  1. <!--SpringsecurityFilter-->
  2. <filter>
  3. <filter-name>springSecurityFilterChain</filter-name>
  4. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  5. </filter>
  6. <filter-mapping>
  7. <filter-name>springSecurityFilterChain</filter-name>
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>



这个Filter会拦截所有的URL请求,并且对这些URL请求进行Spring Security的验证。

注意,springSecurityFilterChain这个名称是由命名空间默认创建的用于处理web安全的一个内部的bean的id。所以你在你的Spring配置文件中,不应该再使用这个id作为你的bean。

与Acegi的配置不同,Acegi需要自行声明一个Spring的bean来作为Filter的实现,而使用Spring Security后,无需再额外定义bean,而是使用<http>元素进行配置。

2. 使用最小的<http>配置

Xml代码 复制代码
  1. <httpauto-config='true'>
  2. <intercept-urlpattern="/**"access="ROLE_USER"/>
  3. </http>



这段配置表示:我们要保护应用程序中的所有URL,只有拥有ROLE_USER角色的用户才能访问。你可以使用多个<intercept-url>元素为不同URL的集合定义不同的访问需求,它们会被归入一个有序队列中,每次取出最先匹配的一个元素使用。 所以你必须把期望使用的匹配条件放到最上边。

3. 配置UserDetailsService来指定用户和权限

接下来,我们来配置一个UserDetailsService来指定用户和权限:

Xml代码 复制代码
  1. <authentication-provider>
  2. <user-service>
  3. <username="downpour"password="downpour"authorities="ROLE_USER,ROLE_ADMIN"/>
  4. <username="robbin"password="robbin"authorities="ROLE_USER"/>
  5. <username="QuakeWang"password="QuakeWang"authorities="ROLE_ADMIN"/>
  6. </user-service>
  7. </authentication-provider>



在这里,downpour拥有ROLE_USER和ROLE_ADMIN的权限,robbin拥有ROLE_USER权限,QuakeWang拥有ROLE_ADMIN的权限

4. 小结

有了以上的配置,你已经可以跑简单的Spring Security的应用了。只不过在这里,我们还缺乏很多基本的元素,所以我们尚不能对上面的代码进行完整性测试。

如果你具备Acegi的知识,你会发现,有很多Acegi中的元素,在Spring Security中都没有了,这些元素包括:表单和基本登录选项、密码编码器、Remember-Me认证等等。

接下来,我们就来详细剖析一下Spring Security中的这些基本元素。

剖析基本配置元素

1. 有关auto-config属性

在上面用到的auto-config属性,其实是下面这些配置的缩写:

Xml代码 复制代码
  1. <http>
  2. <intercept-urlpattern="/**"access="ROLE_USER"/>
  3. <form-login/>
  4. <anonymous/>
  5. <http-basic/>
  6. <logout/>
  7. <remember-me/>
  8. </http>



这些元素分别与登录认证,匿名认证,基本认证,注销处理和remember-me对应。 他们拥有各自的属性,可以改变他们的具体行为。

这样,我们在Acegi中所熟悉的元素又浮现在我们的面前。只是在这里,我们使用的是命名空间而已。

2. 与Acegi的比较

我们仔细观察一下没有auto-config的那段XML配置,是不是熟悉多了?让我们来将基于命名空间的配置与传统的Acegi的bean的配置做一个比较,我们会发现以下的区别:

1) 基于命名空间的配置更加简洁,可维护性更强

例如,基于命名空间进行登录认证的配置代码,可能像这样:

Xml代码 复制代码
  1. <form-loginlogin-page="/login.jsp"authentication-failure-url="/login.jsp?error=true"default-target-url="/work"/>



如果使用老的Acegi的Bean的定义方式,可能像这样:

Xml代码 复制代码
  1. <beanid="authenticationProcessingFilter"
  2. class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
  3. <propertyname="authenticationManager"
  4. ref="authenticationManager"/>
  5. <propertyname="authenticationFailureUrl"
  6. value="/login.jsp?error=1"/>
  7. <propertyname="defaultTargetUrl"value="/work"/>
  8. <propertyname="filterProcessesUrl"
  9. value="/j_acegi_security_check"/>
  10. <propertyname="rememberMeServices"ref="rememberMeServices"/>
  11. </bean>



这样的例子很多,有兴趣的读者可以一一进行比较。

2) 基于命名空间的配置,我们无需再担心由于过滤器链的顺序而导致的错误

以前,Acegi在缺乏默认内置配置的情况下,你需要自己来定义所有的bean,并指定这些bean在过滤器链中的顺序。一旦顺序错了,很容易发生错误。而现在,过滤器链的顺序被默认指定,你不需要在担心由于顺序的错误而导致的错误。

3. 过滤器链在哪里

到目前为止,我们都还没有讨论过整个Spring Security的核心部分:过滤器链。在原本Acegi的配置中,我们大概是这样配置我们的过滤器链的:

Xml代码 复制代码
  1. <beanid="filterChainProxy"
  2. class="org.acegisecurity.util.FilterChainProxy">
  3. <propertyname="filterInvocationDefinitionSource">
  4. <value>
  5. CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
  6. PATTERN_TYPE_APACHE_ANT
  7. /common/**=#NONE#
  8. /css/**=#NONE#
  9. /images/**=#NONE#
  10. /js/**=#NONE#
  11. /login.jsp=#NONE#
  12. /**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,exceptionTranslationFilter,filterSecurityInterceptor
  13. </value>
  14. </property>
  15. </bean>



其中,每个过滤器链都将对应于Spring配置文件中的bean的id。

现在,在Spring Security中,我们将看不到这些配置,这些配置都被内置在<http>节点中。让我们来看看这些默认的,已经被内置的过滤器:


这些过滤器已经被Spring容器默认内置注册,这也就是我们不再需要在配置文件中定义那么多bean的原因。

同时,过滤器顺序在使用命名空间的时候是被严格执行的。它们在初始化的时候就预先被排好序。不仅如此,Spring Security规定,你不能替换那些<http>元素自己使用而创建出的过滤器,比如HttpSessionContextIntegrationFilter, ExceptionTranslationFilter 或 FilterSecurityInterceptor

当然,这样的规定是否合理,有待进一步讨论。因为实际上在很多时候,我们希望覆盖过滤器链中的某个过滤器的默认行为。而Spring Security的这种规定在一定程度上限制了我们的行为。

不过Spring Security允许你把你自己的过滤器添加到队列中,使用custom-filter元素,并且指定你的过滤器应该出现的位置:

Xml代码 复制代码
  1. <beans:beanid="myFilter"class="com.mycompany.MySpecialAuthenticationFilter">
  2. <custom-filterposition="AUTHENTICATION_PROCESSING_FILTER"/>
  3. </beans:bean>



不仅如此,你还可以使用after或before属性,如果你想把你的过滤器添加到队列中另一个过滤器的前面或后面。 可以分别在position属性使用"FIRST"或"LAST"来指定你想让你的过滤器出现在队列元素的前面或后面。

这个特性或许能够在一定程度上弥补Spring Security的死板规定,而在之后的应用中,我也会把它作为切入点,对资源进行管理。

另外,我需要补充一点的是,对于在http/intercept-url中没有进行定义的URL,将会默认使用系统内置的过滤器链进行权限认证。所以,你并不需要在http/intercept-url中额外定义一个类似/**的匹配规则。

使用数据库对用户和权限进行管理

一般来说,我们都有使用数据库对用户和权限进行管理的需求,而不会把用户写死在配置文件里。所以,我们接下来就重点讨论使用数据库对用户和权限进行管理的方法。

用户和权限的关系设计

在此之前,我们首先需要讨论一下用户(User)和权限(Role)之间的关系。Spring Security在默认情况下,把这两者当作一对多的关系进行处理。所以,在Spring Security中对这两个对象所采用的表结构关系大概像这样:

Java代码 复制代码
  1. CREATETABLEusers(
  2. usernameVARCHAR(50)NOTNULLPRIMARYKEY,
  3. passwordVARCHAR(50)NOTNULL,
  4. enabledBITNOTNULL
  5. );
  6. CREATETABLEauthorities(
  7. usernameVARCHAR(50)NOTNULL,
  8. authorityVARCHAR(50)NOTNULL
  9. );



不过这种设计方式在实际生产环境中基本上不会采用。一般来说,我们会使用逻辑主键ID来标示每个User和每个Authorities(Role)。而且从典型意义上讲,他们之间是一个多对多的关系,我们会采用3张表来表示,下面是我在MySQL中建立的3张表的schema示例:

Java代码 复制代码
  1. CREATETABLE`user`(
  2. `id`int(11)NOTNULLauto_increment,
  3. `name`varchar(255)defaultNULL,
  4. `password`varchar(255)defaultNULL,
  5. `disabled`int(1)NOTNULL,
  6. PRIMARYKEY(`id`)
  7. )ENGINE=InnoDBDEFAULTCHARSET=utf8;
  8. CREATETABLE`role`(
  9. `id`int(11)NOTNULLauto_increment,
  10. `name`varchar(255)defaultNULL,
  11. PRIMARYKEY(`id`)
  12. )ENGINE=InnoDBDEFAULTCHARSET=utf8;
  13. CREATETABLE`user_role`(
  14. `user_id`int(11)NOTNULL,
  15. `role_id`int(11)NOTNULL,
  16. PRIMARYKEY(`user_id`,`role_id`),
  17. UNIQUEKEY`role_id`(`role_id`),
  18. KEY`FK143BF46AF6AD4381`(`user_id`),
  19. KEY`FK143BF46A51827FA1`(`role_id`),
  20. CONSTRAINT`FK143BF46A51827FA1`FOREIGNKEY(`role_id`)REFERENCES`role`(`id`),
  21. CONSTRAINT`FK143BF46AF6AD4381`FOREIGNKEY(`user_id`)REFERENCES`user`(`id`)
  22. )ENGINE=InnoDBDEFAULTCHARSET=utf8;



通过配置SQL来模拟用户和权限

有了数据库的表设计,我们就可以在Spring Security中,通过配置SQL,来模拟用户和权限,这依然通过<authentication-provider>来完成:

Xml代码 复制代码
  1. <authentication-provider>
  2. <jdbc-user-servicedata-source-ref="dataSource"
  3. users-by-username-query="SELECTU.username,U.password,U.accountEnabledAS'enabled'FROMUserUwhereU.username=?"
  4. authorities-by-username-query="SELECTU.username,R.nameas'authority'FROMUserUJOINAuthorityAONu.id=A.userIdJOINRoleRONR.id=A.roleIdWHEREU.username=?"/>
  5. </authentication-provider>



这里给出的是一个使用SQL进行模拟用户和权限的示例。其中你需要为运行SQL准备相应的dataSource。这个dataSource应该对应于Spring中的某个bean的定义。

从这段配置模拟用户和权限的情况来看,实际上Spring Security对于用户,需要username,password,accountEnabled三个字段。对于权限,它需要的是username和authority2个字段。

也就是说,如果我们能够通过其他的方式,模拟上面的这些对象,并插入到Spring Security中去,我们同样能够实现用户和权限的认证。接下来,我们就来看看我们如何通过自己的实现,来完成这件事情。

通过扩展Spring Security的默认实现来进行用户和权限的管理

事实上,Spring Security提供了2个认证的接口,分别用于模拟用户和权限,以及读取用户和权限的操作方法。这两个接口分别是:UserDetails和UserDetailsService。

Java代码 复制代码
  1. publicinterfaceUserDetailsextendsSerializable{
  2. GrantedAuthority[]getAuthorities();
  3. StringgetPassword();
  4. StringgetUsername();
  5. booleanisAccountNonExpired();
  6. booleanisAccountNonLocked();
  7. booleanisCredentialsNonExpired();
  8. booleanisEnabled();
  9. }


Java代码 复制代码
  1. publicinterfaceUserDetailsService{
  2. UserDetailsloadUserByUsername(Stringusername)
  3. throwsUsernameNotFoundException,DataAccessException;
  4. }



非常清楚,一个接口用于模拟用户,另外一个用于模拟读取用户的过程。所以我们可以通过实现这两个接口,来完成使用数据库对用户和权限进行管理的需求。在这里,我将给出一个使用Hibernate来定义用户和权限之间关系的示例。

1. 定义User类和Role类,使他们之间形成多对多的关系

Java代码 复制代码
  1. @Entity
  2. @Proxy(lazy=false)
  3. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
  4. publicclassUser{
  5. privatestaticfinallongserialVersionUID=8026813053768023527L;
  6. @Id
  7. @GeneratedValue
  8. privateIntegerid;
  9. privateStringname;
  10. privateStringpassword;
  11. privatebooleandisabled;
  12. @ManyToMany(targetEntity=Role.class,fetch=FetchType.EAGER)
  13. @JoinTable(name="user_role",joinColumns=@JoinColumn(name="user_id"),inverseJoinColumns=@JoinColumn(name="role_id"))
  14. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
  15. privateSet<Role>roles;
  16. //settersandgetters
  17. }


Java代码 复制代码
  1. @Entity
  2. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
  3. publicclassRole{
  4. @Id
  5. @GeneratedValue
  6. privateIntegerid;
  7. privateStringname;
  8. //settersandgetters
  9. }



请注意这里的Annotation的写法。同时,我为User和Role之间配置了缓存。并且将他们之间的关联关系设置的lazy属性设置成false,从而保证在User对象取出之后的使用不会因为脱离session的生命周期而产生lazy loading问题。

2. 使User类实现UserDetails接口

接下来,我们让User类去实现UserDetails接口:

Java代码 复制代码
  1. @Entity
  2. @Proxy(lazy=false)
  3. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
  4. publicclassUserimplementsUserDetails{
  5. privatestaticfinallongserialVersionUID=8026813053768023527L;
  6. @Id
  7. @GeneratedValue
  8. privateIntegerid;
  9. privateStringname;
  10. privateStringpassword;
  11. privatebooleandisabled;
  12. @ManyToMany(targetEntity=Role.class,fetch=FetchType.EAGER)
  13. @JoinTable(name="user_role",joinColumns=@JoinColumn(name="user_id"),inverseJoinColumns=@JoinColumn(name="role_id"))
  14. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
  15. privateSet<Role>roles;
  16. /**
  17. *Thedefaultconstructor
  18. */
  19. publicUser(){
  20. }
  21. /*(non-Javadoc)
  22. *@seeorg.springframework.security.userdetails.UserDetails#getAuthorities()
  23. */
  24. publicGrantedAuthority[]getAuthorities(){
  25. List<GrantedAuthority>grantedAuthorities=newArrayList<GrantedAuthority>(roles.size());
  26. for(Rolerole:roles){
  27. grantedAuthorities.add(newGrantedAuthorityImpl(role.getName()));
  28. }
  29. returngrantedAuthorities.toArray(newGrantedAuthority[roles.size()]);
  30. }
  31. /*(non-Javadoc)
  32. *@seeorg.springframework.security.userdetails.UserDetails#getPassword()
  33. */
  34. publicStringgetPassword(){
  35. returnpassword;
  36. }
  37. /*(non-Javadoc)
  38. *@seeorg.springframework.security.userdetails.UserDetails#getUsername()
  39. */
  40. publicStringgetUsername(){
  41. returnname;
  42. }
  43. /*(non-Javadoc)
  44. *@seeorg.springframework.security.userdetails.UserDetails#isAccountNonExpired()
  45. */
  46. publicbooleanisAccountNonExpired(){
  47. returntrue;
  48. }
  49. /*(non-Javadoc)
  50. *@seeorg.springframework.security.userdetails.UserDetails#isAccountNonLocked()
  51. */
  52. publicbooleanisAccountNonLocked(){
  53. returntrue;
  54. }
  55. /*(non-Javadoc)
  56. *@seeorg.springframework.security.userdetails.UserDetails#isCredentialsNonExpired()
  57. */
  58. publicbooleanisCredentialsNonExpired(){
  59. returntrue;
  60. }
  61. /*(non-Javadoc)
  62. *@seeorg.springframework.security.userdetails.UserDetails#isEnabled()
  63. */
  64. publicbooleanisEnabled(){
  65. return!this.disabled;
  66. }
  67. //settersandgetters
  68. }



实现UserDetails接口中的每个函数,其实没什么很大的难度,除了其中的一个函数我需要额外强调一下:

Java代码 复制代码
  1. /*(non-Javadoc)
  2. *@seeorg.springframework.security.userdetails.UserDetails#getAuthorities()
  3. */
  4. publicGrantedAuthority[]getAuthorities(){
  5. List<GrantedAuthority>grantedAuthorities=newArrayList<GrantedAuthority>(ro
    分享到:
    评论

相关推荐

Global site tag (gtag.js) - Google Analytics