Spring Security 2 配置精讲

关键字: spring security acegi 配置 数据库

原文链接:http://downpour.iteye.com/blog/319965

论坛上看了不少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.<!-- Spring security Filter -->
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.<http auto-config='true'>
2. <intercept-url pattern="/**" access="ROLE_USER" />
3.</http>


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

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

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


Xml代码
1.<authentication-provider>
2. <user-service>
3. <user name="downpour" password="downpour" authorities="ROLE_USER, ROLE_ADMIN" />
4. <user name="robbin" password="robbin" authorities="ROLE_USER" />
5. <user name="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-url pattern="/**" 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-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" default-target-url="/work" />


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


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


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

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

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

3. 过滤器链在哪里

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


Xml代码
1.<bean id="filterChainProxy"
2. class="org.acegisecurity.util.FilterChainProxy">
3. <property name="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:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter">
2. <custom-filter position="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.CREATE TABLE users (
2. username VARCHAR(50) NOT NULL PRIMARY KEY,
3. password VARCHAR(50) NOT NULL,
4. enabled BIT NOT NULL
5.);
6.
7.CREATE TABLE authorities (
8. username VARCHAR(50) NOT NULL,
9. authority VARCHAR(50) NOT NULL
10.);


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



Java代码
1.CREATE TABLE `user` (
2. `id` int(11) NOT NULL auto_increment,
3. `name` varchar(255) default NULL,
4. `password` varchar(255) default NULL,
5. `disabled` int(1) NOT NULL,
6. PRIMARY KEY (`id`)
7.) ENGINE=InnoDB DEFAULT CHARSET=utf8;
8.
9.CREATE TABLE `role` (
10. `id` int(11) NOT NULL auto_increment,
11. `name` varchar(255) default NULL,
12. PRIMARY KEY (`id`)
13.) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14.
15.CREATE TABLE `user_role` (
16. `user_id` int(11) NOT NULL,
17. `role_id` int(11) NOT NULL,
18. PRIMARY KEY (`user_id`,`role_id`),
19. UNIQUE KEY `role_id` (`role_id`),
20. KEY `FK143BF46AF6AD4381` (`user_id`),
21. KEY `FK143BF46A51827FA1` (`role_id`),
22. CONSTRAINT `FK143BF46A51827FA1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
23. CONSTRAINT `FK143BF46AF6AD4381` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
24.) ENGINE=InnoDB DEFAULT CHARSET=utf8;


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

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



Xml代码
1.<authentication-provider>
2. <jdbc-user-service data-source-ref="dataSource"
3. users-by-username-query="SELECT U.username, U.password, U.accountEnabled AS 'enabled' FROM User U where U.username=?"
4. authorities-by-username-query="SELECT U.username, R.name as 'authority' FROM User U JOIN Authority A ON u.id = A.userId JOIN Role R ON R.id = A.roleId WHERE U.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.public interface UserDetails extends Serializable {
2.
3. GrantedAuthority[] getAuthorities();
4.
5. String getPassword();
6.
7. String getUsername();
8.
9. boolean isAccountNonExpired();
10.
11. boolean isAccountNonLocked();
12.
13. boolean isCredentialsNonExpired();
14.
15. boolean isEnabled();
16.}



Java代码
1.public interface UserDetailsService {
2. UserDetails loadUserByUsername(String username)
3. throws UsernameNotFoundException, DataAccessException;
4.}


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

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


Java代码
1.@Entity
2.@Proxy(lazy = false)
3.@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
4.public class User {
5.
6. private static final long serialVersionUID = 8026813053768023527L;
7.
8. @Id
9. @GeneratedValue
10. private Integer id;
11.
12. private String name;
13.
14. private String password;
15.
16. private boolean disabled;
17.
18. @ManyToMany(targetEntity = Role.class, fetch = FetchType.EAGER)
19. @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
20. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
21. private Set<Role> roles;
22.
23. // setters and getters
24.}



Java代码
1.@Entity
2.@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
3.public class Role {
4.
5. @Id
6. @GeneratedValue
7. private Integer id;
8.
9. private String name;
10.
11. // setters and getters
12.}


请注意这里的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.public class User implements UserDetails {
5.
6. private static final long serialVersionUID = 8026813053768023527L;
7.
8. @Id
9. @GeneratedValue
10. private Integer id;
11.
12. private String name;
13.
14. private String password;
15.
16. private boolean disabled;
17.
18. @ManyToMany(targetEntity = Role.class, fetch = FetchType.EAGER)
19. @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
20. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
21. private Set<Role> roles;
22.
23. /**
24. * The default constructor
25. */
26. public User() {
27.
28. }
29.
30. /* (non-Javadoc)
31. * @see org.springframework.security.userdetails.UserDetails#getAuthorities()
32. */
33. public GrantedAuthority[] getAuthorities() {
34. List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(roles.size());
35. for(Role role : roles) {
36. grantedAuthorities.add(new GrantedAuthorityImpl(role.getName()));
37. }
38. return grantedAuthorities.toArray(new GrantedAuthority[roles.size()]);
39. }
40.
41. /* (non-Javadoc)
42. * @see org.springframework.security.userdetails.UserDetails#getPassword()
43. */
44. public String getPassword() {
45. return password;
46. }
47.
48. /* (non-Javadoc)
49. * @see org.springframework.security.userdetails.UserDetails#getUsername()
50. */
51. public String getUsername() {
52. return name;
53. }
54.
55. /* (non-Javadoc)
56. * @see org.springframework.security.userdetails.UserDetails#isAccountNonExpired()
57. */
58. public boolean isAccountNonExpired() {
59. return true;
60. }
61.
62. /* (non-Javadoc)
63. * @see org.springframework.security.userdetails.UserDetails#isAccountNonLocked()
64. */
65. public boolean isAccountNonLocked() {
66. return true;
67. }
68.
69. /* (non-Javadoc)
70. * @see org.springframework.security.userdetails.UserDetails#isCredentialsNonExpired()
71. */
72. public boolean isCredentialsNonExpired() {
73. return true;
74. }
75.
76. /* (non-Javadoc)
77. * @see org.springframework.security.userdetails.UserDetails#isEnabled()
78. */
79. public boolean isEnabled() {
80. return !this.disabled;
81. }
82.
83. // setters and getters
84.}


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


Java代码
1./* (non-Javadoc)
2. * @see org.springframework.security.userdetails.UserDetails#getAuthorities()
3. */
4.public GrantedAuthority[] getAuthorities() {
5. List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(roles.size());
6. for(Role role : roles) {
7. grantedAuthorities.add(new GrantedAuthorityImpl(role.getName()));
8. }
9. return grantedAuthorities.toArray(new GrantedAuthority[roles.size()]);
10.}


这个函数的实际作用是根据User返回这个User所拥有的权限列表。如果以上面曾经用过的例子来说,如果当前User是downpour,我需要得到ROLE_USER和ROLE_ADMIN;如果当前User是robbin,我需要得到ROLE_USER。

了解了含义,实现就变得简单了,由于User与Role是多对多的关系,我们可以通过User得到所有这个User所对应的Role,并把这些Role的name拼装起来返回。

由此可见,实现UserDetails接口,并没有什么神秘的地方,它只是实际上在一定程度上只是代替了使用配置文件的硬编码:


Xml代码
1.<user name="downpour" password="downpour" authorities="ROLE_USER, ROLE_ADMIN" />


3. 实现UserDetailsService接口


Java代码
1.@Repository("securityManager")
2.public class SecurityManagerSupport extends HibernateDaoSupport implements UserDetailsService {
3.
4. /**
5. * Init sessionFactory here because the annotation of Spring 2.5 can not support override inject
6. *
7. * @param sessionFactory
8. */
9. @Autowired
10. public void init(SessionFactory sessionFactory) {
11. super.setSessionFactory(sessionFactory);
12. }
13.
14. public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
15. List<User> users = getHibernateTemplate().find("FROM User user WHERE user.name = ? AND user.disabled = false", userName);
16. if(users.isEmpty()) {
17. throw new UsernameNotFoundException("User " + userName + " has no GrantedAuthority");
18. }
19. return users.get(0);
20. }
21.}


这个实现非常简单,由于我们的User对象已经实现了UserDetails接口。所以我们只要使用Hibernate,根据userName取出相应的User对象即可。注意在这里,由于我们对于User的关联对象Roles都设置了lazy="false",所以我们无需担心lazy loading的问题。

4. 配置文件

有了上面的代码,一切都变得很简单,重新定义authentication-provider节点即可。如果你使用Spring 2.5的Annotation配置功能,你甚至可以不需要在配置文件中定义securityManager的bean。



Xml代码
1.<authentication-provider user-service-ref="securityManager">
2. <password-encoder hash="md5"/>
3.</authentication-provider>


使用数据库对资源进行管理

在完成了使用数据库来进行用户和权限的管理之后,我们再来看看http配置的部分。在实际应用中,我们不可能使用类似/**的方式来指定URL与权限ROLE的对应关系,而是会针对某些URL,指定某些特定的ROLE。而URL与ROLE之间的映射关系最好可以进行扩展和配置。而URL属于资源的一种,所以接下来,我们就来看看如何使用数据库来对权限和资源的匹配关系进行管理,并且将认证匹配加入到Spring Security中去。

权限和资源的设计

上面我们讲到,用户(User)和权限(Role)之间是一个多对多的关系。那么权限(Role)和资源(Resource)之间呢?其实他们之间也是一个典型的多对多的关系,我们同样用3张表来表示:



Java代码
1.CREATE TABLE `role` (
2. `id` int(11) NOT NULL auto_increment,
3. `name` varchar(255) default NULL,
4. `description` varchar(255) default NULL,
5. PRIMARY KEY (`id`)
6.) ENGINE=InnoDB DEFAULT CHARSET=utf8;
7.
8.CREATE TABLE `resource` (
9. `id` int(11) NOT NULL auto_increment,
10. `type` varchar(255) default NULL,
11. `value` varchar(255) default NULL,
12. PRIMARY KEY (`id`)
13.) ENGINE=InnoDB DEFAULT CHARSET=utf8;
14.
15.CREATE TABLE `role_resource` (
16. `role_id` int(11) NOT NULL,
17. `resource_id` int(11) NOT NULL,
18. PRIMARY KEY (`role_id`,`resource_id`),
19. KEY `FKAEE599B751827FA1` (`role_id`),
20. KEY `FKAEE599B7EFD18D21` (`resource_id`),
21. CONSTRAINT `FKAEE599B751827FA1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
22. CONSTRAINT `FKAEE599B7EFD18D21` FOREIGN KEY (`resource_id`) REFERENCES `resource` (`id`)
23.) ENGINE=InnoDB DEFAULT CHARSET=utf8;


在这里Resource可能分成多种类型,比如MENU,URL,METHOD等等。

针对资源的认证

针对资源的认证,实际上应该由Spring Security中的FilterSecurityInterceptor这个过滤器来完成。不过内置的FilterSecurityInterceptor的实现往往无法满足我们的要求,所以传统的Acegi的方式,我们往往会替换FilterSecurityInterceptor的实现,从而对URL等资源进行认证。

不过在Spring Security中,由于默认的拦截器链内置了FilterSecurityInterceptor,而且上面我们也提到过,这个实现无法被替换。这就使我们犯了难。我们如何对资源进行认证呢?

实际上,我们虽然无法替换FilterSecurityInterceptor的默认实现,不过我们可以再实现一个类似的过滤器,并将我们自己的过滤器作为一个customer-filter,加到默认的过滤器链的最后,从而完成整个过滤检查。

接下来我们就来看看一个完整的例子:

1. 建立权限(Role)和资源(Resource)之间的关联关系

修改上面的权限(Role)的Entity定义:


Java代码
1.@Entity
2.@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
3.public class Role {
4.
5. @Id
6. @GeneratedValue
7. private Integer id;
8.
9. private String name;
10.
11. @ManyToMany(targetEntity = Resource.class, fetch = FetchType.EAGER)
12. @JoinTable(name = "role_resource", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "resource_id"))
13. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
14. private Set<Resource> resources;
15.
16. // setters and getter
17.}


增加资源(Resource)的Entity定义:


Java代码
1.@Entity
2.@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
3.
4.public class Resource {
5.
6. @Id
7. @GeneratedValue
8. private Integer id;
9.
10. private String type;
11.
12. private String value;
13.
14. @ManyToMany(mappedBy = "resources", targetEntity = Role.class, fetch = FetchType.EAGER)
15. @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
16. private Set<Role> roles;
17.
18. /**
19. * The default constructor
20. */
21. public Resource() {
22.
23. }
24.}


注意他们之间的多对多关系,以及他们之间关联关系的缓存和lazy属性设置。

2. 在系统启动的时候,把所有的资源load到内存作为缓存

由于资源信息对于每个项目来说,相对固定,所以我们可以将他们在系统启动的时候就load到内存作为缓存。这里做法很多,我给出的示例是将资源的存放在servletContext中。



Java代码
1.public class ServletContextLoaderListener implements ServletContextListener {
2.
3. /* (non-Javadoc)
4. * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
5. */
6. public void contextInitialized(ServletContextEvent servletContextEvent) {
7. ServletContext servletContext = servletContextEvent.getServletContext();
8. SecurityManager securityManager = this.getSecurityManager(servletContext);
9.
10. Map<String, String> urlAuthorities = securityManager.loadUrlAuthorities();
11. servletContext.setAttribute("urlAuthorities", urlAuthorities);
12. }
13.
14.
15. /* (non-Javadoc)
16. * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
17. */
18. public void contextDestroyed(ServletContextEvent servletContextEvent) {
19. servletContextEvent.getServletContext().removeAttribute("urlAuthorities");
20. }
21.
22. /**
23. * Get SecurityManager from ApplicationContext
24. *
25. * @param servletContext
26. * @return
27. */
28. protected SecurityManager getSecurityManager(ServletContext servletContext) {
29. return (SecurityManager) WebApplicationContextUtils.getWebApplicationContext(servletContext).getBean("securityManager");
30. }
31.
32.}


这里,我们看到了SecurityManager,这是一个接口,用于权限相关的逻辑处理。还记得之前我们使用数据库管理User的时候所使用的一个实现类SecurityManagerSupport嘛?我们不妨依然借用这个类,让它实现SecurityManager接口,来同时完成url的读取工作。



Java代码
1.@Service("securityManager")
2.public class SecurityManagerSupport extends HibernateDaoSupport implements UserDetailsService, SecurityManager {
3.
4. /**
5. * Init sessionFactory here because the annotation of Spring 2.5 can not support override inject
6. *
7. * @param sessionFactory
8. */
9. @Autowired
10. public void init(SessionFactory sessionFactory) {
11. super.setSessionFactory(sessionFactory);
12. }
13.
14. /* (non-Javadoc)
15. * @see org.springframework.security.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
16. */
17. public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
18. List<User> users = getHibernateTemplate().find("FROM User user WHERE user.name = ? AND user.disabled = false", userName);
19. if(users.isEmpty()) {
20. throw new UsernameNotFoundException("User " + userName + " has no GrantedAuthority");
21. }
22. return users.get(0);
23. }
24.
25. /* (non-Javadoc)
26. * @see com.javaeye.sample.security.SecurityManager#loadUrlAuthorities()
27. */
28. public Map<String, String> loadUrlAuthorities() {
29. Map<String, String> urlAuthorities = new HashMap<String, String>();
30. List<Resource> urlResources = getHibernateTemplate().find("FROM Resource resource WHERE resource.type = ?", "URL");
31. for(Resource resource : urlResources) {
32. urlAuthorities.put(resource.getValue(), resource.getRoleAuthorities());
33. }
34. return urlAuthorities;
35. }
36.}


3. 编写自己的FilterInvocationDefinitionSource实现类,对资源进行认证


Java代码
1.public class SecureResourceFilterInvocationDefinitionSource implements FilterInvocationDefinitionSource, InitializingBean {
2.
3. private UrlMatcher urlMatcher;
4.
5. private boolean useAntPath = true;
6.
7. private boolean lowercaseComparisons = true;
8.
9. /**
10. * @param useAntPath the useAntPath to set
11. */
12. public void setUseAntPath(boolean useAntPath) {
13. this.useAntPath = useAntPath;
14. }
15.
16. /**
17. * @param lowercaseComparisons
18. */
19. public void setLowercaseComparisons(boolean lowercaseComparisons) {
20. this.lowercaseComparisons = lowercaseComparisons;
21. }
22.
23. /* (non-Javadoc)
24. * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
25. */
26. public void afterPropertiesSet() throws Exception {
27.
28. // default url matcher will be RegexUrlPathMatcher
29. this.urlMatcher = new RegexUrlPathMatcher();
30.
31. if (useAntPath) { // change the implementation if required
32. this.urlMatcher = new AntUrlPathMatcher();
33. }
34.
35. // Only change from the defaults if the attribute has been set
36. if ("true".equals(lowercaseComparisons)) {
37. if (!this.useAntPath) {
38. ((RegexUrlPathMatcher) this.urlMatcher).setRequiresLowerCaseUrl(true);
39. }
40. } else if ("false".equals(lowercaseComparisons)) {
41. if (this.useAntPath) {
42. ((AntUrlPathMatcher) this.urlMatcher).setRequiresLowerCaseUrl(false);
43. }
44. }
45.
46. }
47.
48. /* (non-Javadoc)
49. * @see org.springframework.security.intercept.ObjectDefinitionSource#getAttributes(java.lang.Object)
50. */
51. public ConfigAttributeDefinition getAttributes(Object filter) throws IllegalArgumentException {
52.
53. FilterInvocation filterInvocation = (FilterInvocation) filter;
54. String requestURI = filterInvocation.getRequestUrl();
55. Map<String, String> urlAuthorities = this.getUrlAuthorities(filterInvocation);
56.
57. String grantedAuthorities = null;
58. for(Iterator<Map.Entry<String, String>> iter = urlAuthorities.entrySet().iterator(); iter.hasNext();) {
59. Map.Entry<String, String> entry = iter.next();
60. String url = entry.getKey();
61.
62. if(urlMatcher.pathMatchesUrl(url, requestURI)) {
63. grantedAuthorities = entry.getValue();
64. break;
65. }
66.
67. }
68.
69. if(grantedAuthorities != null) {
70. ConfigAttributeEditor configAttrEditor = new ConfigAttributeEditor();
71. configAttrEditor.setAsText(grantedAuthorities);
72. return (ConfigAttributeDefinition) configAttrEditor.getValue();
73. }
74.
75. return null;
76. }
77.
78. /* (non-Javadoc)
79. * @see org.springframework.security.intercept.ObjectDefinitionSource#getConfigAttributeDefinitions()
80. */
81. @SuppressWarnings("unchecked")
82. public Collection getConfigAttributeDefinitions() {
83. return null;
84. }
85.
86. /* (non-Javadoc)
87. * @see org.springframework.security.intercept.ObjectDefinitionSource#supports(java.lang.Class)
88. */
89. @SuppressWarnings("unchecked")
90. public boolean supports(Class clazz) {
91. return true;
92. }
93.
94. /**
95. *
96. * @param filterInvocation
97. * @return
98. */
99. @SuppressWarnings("unchecked")
100. private Map<String, String> getUrlAuthorities(FilterInvocation filterInvocation) {
101. ServletContext servletContext = filterInvocation.getHttpRequest().getSession().getServletContext();
102. return (Map<String, String>)servletContext.getAttribute("urlAuthorities");
103. }
104.
105.}


4. 配置文件修改

接下来,我们来修改一下Spring Security的配置文件,把我们自定义的这个过滤器插入到过滤器链中去。


Xml代码
1.<beans:beans xmlns="http://www.springframework.org/schema/security"
2. xmlns:beans="http://www.springframework.org/schema/beans"
3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
5. http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
6.
7. <beans:bean id="loggerListener" class="org.springframework.security.event.authentication.LoggerListener" />
8.
9. <http access-denied-page="/403.jsp" >
10. <intercept-url pattern="/static/**" filters="none" />
11. <intercept-url pattern="/template/**" filters="none" />
12. <intercept-url pattern="/" filters="none" />
13. <intercept-url pattern="/login.jsp" filters="none" />
14. <form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" default-target-url="/index" />
15. <logout logout-success-url="/login.jsp"/>
16. <http-basic />
17. </http>
18.
19. <authentication-manager alias="authenticationManager"/>
20.
21. <authentication-provider user-service-ref="securityManager">
22. <password-encoder hash="md5"/>
23. </authentication-provider>
24.
25. <beans:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
26. <beans:property name="allowIfAllAbstainDecisions" value="false"/>
27. <beans:property name="decisionVoters">
28. <beans:list>
29. <beans:bean class="org.springframework.security.vote.RoleVoter"/>
30. <beans:bean class="org.springframework.security.vote.AuthenticatedVoter"/>
31. </beans:list>
32. </beans:property>
33. </beans:bean>
34.
35. <beans:bean id="resourceSecurityInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
36. <beans:property name="authenticationManager" ref="authenticationManager"/>
37. <beans:property name="accessDecisionManager" ref="accessDecisionManager"/>
38. <beans:property name="objectDefinitionSource" ref="secureResourceFilterInvocationDefinitionSource" />
39. <beans:property name="observeOncePerRequest" value="false" />
40. <custom-filter after="LAST" />
41. </beans:bean>
42.
43. <beans:bean id="secureResourceFilterInvocationDefinitionSource" class="com.javaeye.sample.security.interceptor.SecureResourceFilterInvocationDefinitionSource" />
44.
45.</beans:beans>


请注意,由于我们所实现的,是FilterSecurityInterceptor中的一个开放接口,所以我们实际上定义了一个新的bean,并通过<custom-filter after="LAST" />插入到过滤器链中去。

Spring Security对象的访问

1. 访问当前登录用户

Spring Security提供了一个线程安全的对象:SecurityContextHolder,通过这个对象,我们可以访问当前的登录用户。我写了一个类,可以通过静态方法去读取:



Java代码
1.public class SecurityUserHolder {
2.
3. /**
4. * Returns the current user
5. *
6. * @return
7. */
8. public static User getCurrentUser() {
9. return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
10. }
11.
12.}


2. 访问当前登录用户所拥有的权限

通过上面的分析,我们知道,用户所拥有的所有权限,其实是通过UserDetails接口中的getAuthorities()方法获得的。只要实现这个接口,就能实现需求。在我的代码中,不仅实现了这个接口,还在上面做了点小文章,这样我们可以获得一个用户所拥有权限的字符串表示:



Java代码
1./* (non-Javadoc)
2. * @see org.springframework.security.userdetails.UserDetails#getAuthorities()
3. */
4.public GrantedAuthority[] getAuthorities() {
5. List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(roles.size());
6. for(Role role : roles) {
7. grantedAuthorities.add(new GrantedAuthorityImpl(role.getName()));
8. }
9. return grantedAuthorities.toArray(new GrantedAuthority[roles.size()]);
10.}
11.
12./**
13. * Returns the authorites string
14. *
15. * eg.
16. * downpour --- ROLE_ADMIN,ROLE_USER
17. * robbin --- ROLE_ADMIN
18. *
19. * @return
20. */
21.public String getAuthoritiesString() {
22. List<String> authorities = new ArrayList<String>();
23. for(GrantedAuthority authority : this.getAuthorities()) {
24. authorities.add(authority.getAuthority());
25. }
26. return StringUtils.join(authorities, ",");
27.}


3. 访问当前登录用户能够访问的资源

这就涉及到用户(User),权限(Role)和资源(Resource)三者之间的对应关系。我同样在User对象中实现了一个方法:


Java代码
1./**
2. * @return the roleResources
3. */
4.public Map<String, List<Resource>> getRoleResources() {
5. // init roleResources for the first time
6. if(this.roleResources == null) {
7. this.roleResources = new HashMap<String, List<Resource>>();
8.
9. for(Role role : this.roles) {
10. String roleName = role.getName();
11. Set<Resource> resources = role.getResources();
12. for(Resource resource : resources) {
13. String key = roleName + "_" + resource.getType();
14. if(!this.roleResources.containsKey(key)) {
15. this.roleResources.put(key, new ArrayList<Resource>());
16. }
17. this.roleResources.get(key).add(resource);
18. }
19. }
20.
21. }
22. return this.roleResources;
23.}


这里,会在User对象中设置一个缓存机制,在第一次取的时候,通过遍历User所有的Role,获取相应的Resource信息。

代码示例

在附件中,我给出了一个简单的例子,把我上面所讲到的所有内容整合在一起,是一个eclipse的工程,大家可以下载进行参考。


SpringSecurity.zip (48.7 KB)
下载次数: 11206
查看图片附件


忘记李刚,一步一步跟我学Struts2 —— 拦 ... | 忘记李刚,一步一步跟我学Struts2 —— MVC ...

15:19
浏览 (9130)
论坛浏览 (108566)
评论 (30)
分类: Java
收藏
相关推荐


评论


30 楼 disturbed 2010-01-20 引用

受益匪浅。比较专业,而且很详细。


29 楼 wyzchn 2010-01-13 引用

楼上我提的这个问题困扰了好几天,今天无意中解决了。
就是在web.xml文件中的spring security部分的配置文件放到struts和spring,Hibernate等配置文件前面就好了。原来我放在最后面。真服了!


28 楼 wyzchn 2010-01-12 引用

我照着这个配置,其他都好,就是用SecurityUserHolder 取不到当前用户,
打印:SecurityContextHolder.getContext()为:
org.springframework.security.context.SecurityContextImpl@ffffffff: Null authentication,
再往下取就是Null了。
我没配置缓存,其他都几乎一样,请问是缓存的原因吗?还是别的原因?


27 楼 erikchang 2009-12-31 引用

什么时候能够升级到ss3?


26 楼 polaris1119 2009-12-25 引用

楼主这篇文章真是讲得太好了。网上一堆文章都很差,看了云里雾里的,看了这篇后豁然开朗……


25 楼 yanjunzheng 2009-12-03 引用

为什么下载的工程里面没有jar呢 还要自己去下载配置 多麻烦啊···


24 楼 Vstar 2009-11-20 引用

Spring security确实比以前的acegi精简了很多,只是还有一个问题。

我之前开发过一个项目,是一个学术会议的注册管理系统,客户的要求是前后台完全分离。前台供会议参加者注册,登陆,提交修改论文及相关信息等,而后台是对各种信息的管理。前后台不是同一个登陆页面,而且两者的session互不影响,也就是说同一个浏览器下,用户登陆前台页面后,再转到后台的URL时,还需要用管理员的帐户登陆后台。

这个应该属于Multiple Login Page的问题,而这个模型的使用非常普遍,不过我仅在http://forum.springsource.org/showthread.php?t=77030上找到了一些解决的办法。这个办法通过两个filter-chain入口将入口分离,并针对每个入口分别使用不同的配置,似乎还要像之前acegi的配置一样繁琐,有没有更简单的实现方法。


23 楼 yaozhen12 2009-11-19 引用

按楼主的配置,已经成功实现了数据配置权限,谢谢指点,但是还有一个问题请教,
我在context安全文件中配置了两个custom filter,
<beans:bean id="myAuthenticationFilter" class="com.myframe.security.web.servlet.MyAuthenticationProcessingFilter">
<custom-filter position="AUTHENTICATION_PROCESSING_FILTER"/>
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationFailureUrl" value="/error.jsp" />
<beans:property name="defaultTargetUrl" value="/workbench.do"/>
<beans:property name="securityManager" ref="securityManager"/>
</beans:bean>
<beans:bean id="myLogoutFilter" class="com.myframe.security.web.servlet.MyLogoutFilter">
<custom-filter position="LOGOUT_FILTER"/>
<beans:constructor-arg value="/logout.do"/>
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.ui.logout.SecurityContextLogoutHandler" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
可是为什么登陆时会找到“LOGOUT_FILTER”而不是“AUTHENTICATION_PROCESSING_FILTER”


22 楼 海风308 2009-11-13 引用


downpour 写道


海风308 写道

请问楼主,这样修改User类,是不是侵入性太高了?我觉得PO应该尽量保持干净。
还有你实现InitializingBean里的afterPropertiesSet()有什么用意?


如果仅用Spring Security做用户的认证,并且Spring Security默认的功能能够满足你的要求,你是不需要像我这样改User类的。

不过既然你用了Spring Security做你的权限管理,又要不入侵,请你来教大家怎么做。

不好意思楼主,我没有别的意思,我只是有这个想法而已,我也是刚学SpringSecurity。还有,你还没告诉我你
InitializingBean里的afterPropertiesSet()有什么用意呢,这个方法里的内容有点看不懂。
再有一个问题,就是我按照你的做法,只不过我的<http>中没有配置资源和对应的权限,而是从数据库度的。我确认数据没问题,因为之前用其他例子的JDBC方式实验过,而用楼主的方法读取资源和对应权限时,访问项目地址总是能够进入index.jsp页面,而这个页面的权限最少要登录才对。请问是什么原因。我跟踪了下断点,
public ConfigAttributeDefinition getAttributes(Object filter)返回null。

21 楼 downpour 2009-11-13 引用


海风308 写道

请问楼主,这样修改User类,是不是侵入性太高了?我觉得PO应该尽量保持干净。
还有你实现InitializingBean里的afterPropertiesSet()有什么用意?


如果仅用Spring Security做用户的认证,并且Spring Security默认的功能能够满足你的要求,你是不需要像我这样改User类的。

不过既然你用了Spring Security做你的权限管理,又要不入侵,请你来教大家怎么做。

20 楼 海风308 2009-11-13 引用

请问楼主,这样修改User类,是不是侵入性太高了?我觉得PO应该尽量保持干净。
还有你实现InitializingBean里的afterPropertiesSet()有什么用意?


19 楼 releasa 2009-09-24 引用

粗略读了一下,貌似非常不错,俺细细研读去。。


18 楼 alymail 2009-07-15 引用

代码只是读取了当前用户的角色信息,但是如何限制某个用户访问某个资源呢?在resource表中将/**修改为别的吗,我试了一下不行,比如如何做到限制robbin访问index.jsp页面,希望指点一下。
找了一下,好像有,在SecureResourceFilterInvocationDefinitionSource类中,每当用户到达资源之前都会运行getAttributes(Object filter)方法,如果没有对这个资源授权,即使返回为null,用户还是可以访问此资源,该如果设置呢?


17 楼 songjingjing520 2009-07-15 引用

哈哈,精华啊


16 楼 SARA520 2009-07-09 引用

写的不清楚啊
我新手
你写的那些配置最好要说是要放在哪个文件里
要不我们新手搞不懂要放哪


15 楼 daquan198163 2009-07-01 引用

楼上的干啥呢?


14 楼 SINCE1978 2009-07-01 引用

“垃圾”或者“无聊”这样的质疑一直有之,包括老外的论坛上也有不少人谈到acegi繁琐的配置复杂的代码实现一点点功能是否值得,最知名的就是“每当你使用一次acegi就会有一个仙女死掉”...可以肯定acegi当然比不上ssh中任何一个的见效,它只是个子项目,解决一个小领域的问题。还是认为acegi的过滤链方式引入更多复杂性和潜在bug,而且感觉使用过滤器链方式有点唯技术论,另外在ss2中的基于schema的配置是有明显缺陷的,非得使用链式处理、就带进来别的一大堆问题、在花费功夫去解决...
文章是好文,楼主是牛人,学到很多。不过楼主还没回答第一页我的问题呢:)


13 楼 downpour 2009-06-20 引用


starfeng 写道

从03年发现这个垃圾后,他竟然改头换面成了spring security, 我真不知道它好处在哪?
啰嗦的一大堆配置,就实现了一个url的过滤.带条件的url过滤能做么?,记录级的控制能做么?可读可写某一个字段能做么?全都不能...

其核心功能就是, 请填写interface1,interface2,interface3...
然后. 他可以帮我做一句话:
if (interface1.xxx()) { //认证匹配
interface2.xxx(); // 角色装入
}

接着, 来一个:
inteface.xxx();//请认证

事实上,这几块还全得自已来. 最无聊的产品了.


我觉得你的判断有点武断,总体来说,spring security还是一个不错的产品,当然也有这样那样的缺陷,这个就需要我们自己在应用程序中完成。类似“垃圾”或者“无聊”这样的字眼的评价,似乎有点过了。

12 楼 starfeng 2009-06-20 引用

从03年发现这个垃圾后,他竟然改头换面成了spring security, 我真不知道它好处在哪?
啰嗦的一大堆配置,就实现了一个url的过滤.带条件的url过滤能做么?,记录级的控制能做么?可读可写某一个字段能做么?全都不能...

其核心功能就是, 请填写interface1,interface2,interface3...
然后. 他可以帮我做一句话:
if (interface1.xxx()) { //认证匹配
interface2.xxx(); // 角色装入
}

接着, 来一个:
inteface.xxx();//请认证

事实上,这几块还全得自已来. 最无聊的产品了.


11 楼 daquan198163 2009-06-18 引用


xuzhfa123 写道

查了这么多springsecurity相关资料,权限控制基本上是到模块,如果粒度再细一下,也就是说,控制增,删,改,查之类操作,能否做到?方案是什么?

怎么会是到模块呢,那粒度太粗了
springsecurity可以很自然的控制对url的访问和组件方法的调用,
这不就对应了你说的增,删,改,查之类操作么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值