您好,欢迎来到爱go旅游网。
搜索
您的当前位置:首页SpringSecurity

SpringSecurity

来源:爱go旅游网
用SpringSecurity保护Web的安全

王健-吐血奉献

重要说明,由于本人用的是SpringSecurity3.1版本,3.1版本与3.0版本在配置上发生了一些变化,在配置时,本人都已经全部注明区别和使用方法。

1、SpringSecurity的体系结构

SpringSecurity由一系列的过虑器组成,核心过虑器为为:org.springframework.web.filter.DelegatingFilterProxy,它代理其他所 有的过虑器,此类必须要配置到web.xml中,且名称必须取名是springSecurityFilterChain。与spring的bean配置文件中的配置的元素id具有相同的名称。

DelegatingFilterProxy将顺序让以下过虑器工作: Security filter chain: [

SecurityContextPersistenceFilter LogoutFilter

UsernamePasswordAuthenticationFilter DefaultLoginPageGeneratingFilter BasicAuthenticationFilter RequestCacheAwareFilter

SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ]

说明:1:以上信息,是通过spring-security-3.1.xsd的命名空间,配置在后台输出的信息获得。

2:关于上面过虑器DelegatingFilterProxy的说明请见spring-security3.1.pdf文档第8.2节的具体讲解。 对二上面的过虑都系统都给出来别名,见spring-security-3.1.pdf文档的第20页: 以下截图来即来自于spring-security3.1.pdf

2、准备开发的资源包

本人使用spring3.1和spring security3.1进行示例。

注意里面包含有aopalliance.jar和aspectj两个外部jar包。这也是spring依赖的包。还有就是log4j和logging.jar.

3、以下配置非数据库方式下安全保护

准备好所有的jar文件。创建一个完整的web项目。建议使用utf-8编码。

第一步:在web.xml中添加以下配置

xmlns=\"http://java.sun.com/xml/ns/javaee\"

xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\">

contextConfigLocation

classpath:spring-beans.xml, classpath:spring-security.xml

encoding

org.springframework.web.filter.CharacterEncodingFilter

encoding UTF-8

springSecurityFilterChain

org.springframework.web.filter.DelegatingFilterProxy

encoding /*

springSecurityFilterChain /*

org.springframework.web.context.ContextLoaderListener

org.springframework.security.web.session.HttpSessionEventPublisher

index.jsp

第二步:完善项目结构

添加完成jar文件,及配置好web.xml文件后的项目结构如下:

第三步:在spring-secrity.xml中配置安全

xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd\">

注意:

上面的配置项目中,安全所使用的命名空间为spring-security-3.1.xsd。它与3.0.xsd有一些区别,由于项目用的是3.1的jar包,所以必须要使用3.1的命名空间。同时,3.0与3.1在配置上存在一些差别,请多加注意。

1、说明

上面的配置中即自动配置,它的所有默认值为:

以下是来自于spring-security.pdf第12页的说明:

2、access=”hasRole(‘ROLE_ADMIN’)是什么意思?

hasRole方法及permitAll()方法都来自于类:org.springframework.security.web.access.expression.WebSecurityExpressionRoot。此类中及其父类中定义了若干的方法,提供验证功能。它的类层次结构为:

在类SecurityExpressionRoot中提供了若干的验证方法如下:

关于更多请查看它的源代码。

3、元素的作用是什么?

此元素提供用户登录认证,及提供角色列表。可以在配置文件中直接配置用户名和密码,也可以配置从数据库加载用户和。这是我们以后动大手术的地方。 此元素定义后,默认的id值为:org.springframework.security.authenticationManager且不建议修改,因为元素默认引用它。但我们可以给它取一个别名即alias=”authenticationManager”。

4、元素

此元素是专门提供用户名和密码的地方。目前我们并没有给用户名进行加密。以后我们可选的可以给密码MD5加密。这是以后我们要修改的地方。只要提供一个能可以从数据库读取用户名和密码的userDetailService即可以从数据库加载用户。 注意,在上面的配置中,我们让admin用户,拥有了ROLE_ADMIN,ROLE_USER两个角色,所以admin用户可以访问所有用户的资源。而jack不可以访问admin的资源。一旦访问,将转到访问被拒绝页面上去。

5、一些默认的配置

配置中,包含了非常多的默认配置项目。只要用户不配置,spring就会选择自己的默认配置,这倒是非常符合spring的默认配置原则。如: 提供默认的登录页面、提供默认的登录错误以后的返回页面、提供默认的错误提示等。

6、access-denied-page选项

上面配置了访问被拒绝时的转发页面。

需要说明的是,在ie6浏览器上,错误页面字节数量必须要大于1024bytes才可以被正常转向。

第四步:运行测试

配置完成上面的程序后,就可以运行测试了。 以下是运行的截图:

Springsecurity提供的默认登录页面:

1、登录成功以后,如何获取用户信息

在登录成功以后,SpringSecurity会向session中存放一个key值为SPRING_SECURITY_CONTEXT的对象。此对象为SecurityContext类型,实现类为SecurityContextImpl。这里面包含了安全认证及安全验证的所有信息。 且Spring默认为我们实现了一个User类。此类是UserDetails的子类。此类在登录成功后,被封装成User对象放到SecurityContext中,然后将SecurityContext对象以SPRING_SECURITY_CONTEXT为key值放到session中。

以下是User类的部分代码:可见,此类封装了用户名和密码,并没有提供我们最为关心的id。这也是我们以后要重要扩展的对象。

public class User implements UserDetails, CredentialsContainer {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; //~ Instance fields

================================================================================================ private String password; private final String username;

private final Set authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled;

}

所以,我们可以按以下方式获取User类的实例:

SecurityContext ctx = (SecurityContext)//从session中获取SPRING_SECURITY_CONTEXT对象并从中获取Principal session().getAttribute(\"SPRING_SECURITY_CONTEXT\"); User user=(User) ctx.getAuthentication().getPrincipal();//注意是如何获取最后用户对象的

按Spring的习惯,总会给我们提供一个工具类,以方便获取SecurityContext对象。(如Spring中的WebApplicationContextUtils). 此处,SpringSecurity所提供的工具类为:SecurityContextHolder

以下是用SecurityContextHolder类获取SecurityContext对象的示例:

Object o = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); System.err.println(\"o>:\"+o);

User u = (User) o;//强制类型转换成User对象即可

//SecurityContextHolder的好处是在任何地方,你都可以获取用户的信息,因为它是通过静态方法获取SecurityContext对象的。

2、登录成功以后,如何获取用户的信息-2

虽然我们可以通过SecurityContextHolder来获取用户的信息,或是通过session.getAttribute(“SPRING_SECURITY_CONTEXT”),但这样做会让程序员感觉有些蹩脚。直接将User对象放到Session中,这是我们通常的做法。即然,我们已经知道Spring帮助我们登录成功以后,即会向Session中添加key为SPRING_SECURITY_CONTEXT的key对象。则我们可以书写一个HttpSessionAttributeListener的: 示例代码:以下必须要配置到web.xml中:

package cn.listener;

import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent;

import org.springframework.security.core.context.SecurityContext; import cn.domain.MyUser; /**

* 此类用于监控是否向session中放入了SPRING_Security_Context的key值。

* 此值只有当用户通过spring的security通过安全认证时,由spring的过虑器向session中放数据
* 因此我们完全可以拦截此key值,并从session中获取已经放入的SecurityContext对象。
* 并从中获取用户信息
* @author 王健

* @version 2012-4-29 */

public class MySessionListener implements HttpSessionAttributeListener{ }

public void attributeAdded(HttpSessionBindingEvent e) { } /**

* 也同时删除 */

public void attributeRemoved(HttpSessionBindingEvent e) { } /**

* 记得还要同时替换呀 */

public void attributeReplaced(HttpSessionBindingEvent e) { }

if(e.getName().equals(\"SPRING_SECURITY_CONTEXT\")){ }

e.getSession().removeAttribute(\"user\");

if(e.getName().equals(\"SPRING_SECURITY_CONTEXT\")){//判断是否放入了SPRING_SECURITY_CONTEXT的key值 }

System.err.println(\"认证通过。。。。。\");

SecurityContext ctx = (SecurityContext)//从session中获取SPRING_SECURITY_CONTEXT对象并从中获取Principal e.getSession().getAttribute(\"SPRING_SECURITY_CONTEXT\"); MyUser user=(MyUser) ctx.getAuthentication().getPrincipal(); e.getSession().setAttribute(\"user\",user);

第五步:编写退出请求

Spring Security默认提供的退出请求为:/j_spring_security_logout

<%@ page language=\"java\" import=\"java.util.*\" pageEncoding=\"UTF-8\"%> <%@ taglib uri=\"http://java.sun.com/jsp/jstl/core\" prefix=\"c\"%>

This is my JSP page.

\">管理员安全页面


\">普通人员安全页面

\">退出

\">登录

第六步:配置自己的登录页面,同时配置了退出和cookie信息

修改spring-security.xml配置文件。添加元素如下:

//这儿原来有些配置,略去了,请参考上面的实现 default-target-url=\"/index.jsp\" password-parameter=\"j_password\" username-parameter=\"j_username\"

authentication-failure-url=\"/jsps/login.jsp?error=1\" />

logout-url=\"/j_spring_security_logout\"/>

请注意登录验证不成功返回的页面,只是在后面多了一个参数而已。 第七步:书写登录页面

Spring security的登录请求url为/j_spring_security_check:

<%@ page language=\"java\" import=\"java.util.*\" pageEncoding=\"UTF-8\"%> <%@ taglib uri=\"http://java.sun.com/jsp/jstl/core\" prefix=\"c\"%>

用户名或密码错误。

这是登录页面

\" method=\"post\">

用户名:
密码:

记住我:

第八步:小总结

经过上面的配置,我们已经通过硬编码实现了用户的管理,安全路径资源的管理。且认识了以下几个工具类和配置: SecurityContextHolder类。

必须要在外部单独配置。 配置更加明细的资源管理

- 默认将用户信息全部保存到内存中。它们都是UserDetailService的子类:

以上源代码位于:security3项目中。

4、将用户的信息保存到数据库中-暂只使用用户和角色表

本示例,只使用用户和角色表,我将在后面的课程中再讲解如何使用资源表。这一个慢慢学习的过程。

按照我个人的习惯,定义用户-角色-资源三个表。并能过两个中间表建立多对多多的关系,本人采用MySql数据库。 第一步:定义数据结构 E-R图如下:

完整的DDL如下:

create database security3 character set UTF8; use security3; /*用户列表*/

create table users(

user_id varchar(32) primary key, user_name varchar(50), user_pwd varchar(32), user_status char(1) );

/*角色列表*/

create table roles(

role_id varchar(32) primary key, role_name varchar(30), role_desc varchar(50) );

/*用户角色对应表*/

create table roleuser( ru_userid varchar(32), ru_roleid varchar(32),

constraint ru_pk primary key(ru_userid,ru_roleid),

constraint ru_fk1 foreign key(ru_userid) references users(user_id), constraint ru_fk2 foreign key(ru_roleid) references roles(role_id) );

/*资源列表*/

create table resources( );

/*资源角色对应表*/ create table func(

func_roleid varchar(32), func_resid varchar(32),

constraint func_pk primary key(func_roleid,func_resid),

constraint func_fk1 foreign key(func_roleid) references roles(role_id), constraint func_fk2 foreign key(func_resid) references resources(res_id) );

/*写入一些初始化数据,暂时先不加密*/

insert into users values('U1','admin','1234','1'); insert into users values('U2','guest','1234','1'); /*写入角色信息,默认情况下,角色都以ROLE_开头*/

insert into roles values('R1','ROLE_ADMIN','管理员'); insert into roles values('R2','ROLE_USER','普通用户'); /*用户角色对应*/

insert into roleuser values('U1','R1'); insert into roleuser values('U1','R2'); insert into roleuser values('U2','R2');

res_id varchar(32) primary key, res_name varchar(50), res_url varchar(100)

/*暂时先不定义资源信息*/

/*现在开始定义资源信息,并设置资源对应的角色*/

insert into resources values('S1','超级管理','/jsps/secu/secu.jsp'); insert into resources values('S2','普通应用','/jsps/user/user.jsp'); /*定义角色与资源的对应关系*/

insert into func(func_roleid,func_resid) values('R1','S1'); insert into func(func_roleid,func_resid) values('R1','S2'); insert into func(func_roleid,func_resid) values('R2','S2');

/*查询某人拥有某资源*/

SELECT u.user_name,s.res_name,s.res_url

FROM users u INNER JOIN roleuser ru ON u.user_id=ru.ru_userid INNER JOIN roles r ON ru.ru_roleid=r.role_id INNER JOIN func f ON r.role_id=f.func_roleid INNER JOIN resources s ON f.func_resid=s.res_id; /*使用distinct关键字*/

SELECT DISTINCT u.user_name,s.res_name,s.res_url

FROM users u INNER JOIN roleuser ru ON u.user_id=ru.ru_userid INNER JOIN roles r ON ru.ru_roleid=r.role_id INNER JOIN func f ON r.role_id=f.func_roleid

INNER JOIN resources s ON f.func_resid=s.res_id;

第二步:在配置文件中连接数据库

修改spring-beans.xml文件,在这儿连接数据库:

注意,为了区别,我在这儿默认使用bean为默认命名空间,而在spring-security.xml中默认使用spring-security为默认命名空间。

xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd\">

第三步:修改元素-从数据库加载用户

这一步,最为关键,我们通过查看InMemoryDaoImpl的源代码可知,此类是UserDetailsService的子类。InMemoryDaoImpl负责从配置文件中读取硬编码的用户名和密码。 1、分析spring-security的源代码1

通过查看UserDetailService的继承关系我们也可以知道,它还有一个子类JdbcDaoImpl,它负责从数据库中加载用户的信息。但此类要求使用Spring给我们定义好数据结构。此数据结构并不是很灵活,不能满足我们的要求。 以下是UserDetailsService的继承关系:

虽然JdbcDaoImpl不能满足我们的要求,但我们可以从中学习到Spring是如何加载用户认证的,从而去实现自己的UserDetailsService。以下是JdbcDaoImpl的部分源代码:

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService { //~ Static fields/initializers

=====================================================================================

public static final String DEF_USERS_BY_USERNAME_QUERY = \"select username,password,enabled \" + \"from users \" + \"where username = ?\";

//请自己去查看更多源代码。 //其中就是定义了很多查询语句而已 }

关于JdbcDaoImpl使用什么样的表结构,在SpringSecurity的pdf文档中已经给出,请参考spring-security.pdf第126页的建表语句。

2、分析spring-security的源代码2

UserDetailsService接口的源代码如下:

public interface UserDetailsService {

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

可见,只要我们实现此类,并提供一个此方法的实现,并返回UserDetails即可。

又通过查看User类的源代码我们知道。它实现了UserDetails接口。并封装了一系列的用户信息:

public class User implements UserDetails, CredentialsContainer { private String password; private final String username;

private final Set authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled;

}

通过这些,我们就知道了。自己完全可以继承User类,或是直接实现UserDetails接口。在loadUserByUsername方法中根据用户名查询用户是否存在。

3、User类实现UserDetails接口

实现UserDetails接口以后,有一系列的方法让我们实现,先来看一个添加了所有空实现的实现:

package cn.itcast.domain; import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails; public class User implements UserDetails { }

private static final long serialVersionUID = 1L;

public Collection getAuthorities() { }

public String getPassword() { }

public String getUsername() { }

//以下方法必须返回真

public boolean isAccountNonExpired() { }

public boolean isAccountNonLocked() { }

public boolean isCredentialsNonExpired() { }

public boolean isEnabled() { }

return true; return true; return true; return true; return null; return null; return null;

4、完善自己开发的User类

package cn.itcast.domain; import java.util.ArrayList; import java.util.Collection; import java.util.List;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; public class User implements UserDetails {

private static final long serialVersionUID = 1L; private String id; private String username; private String password;

private List roles;//定义角色 /**

* 返回认证的角色列表,但返的一不是我们的Role对象,

* 其实它只需要一个名称。所以,我们可以将Role中的name放到GrantedAuthority中返回 * 即可。 */

public Collection getAuthorities() { }

//定义两个构造方法 public User() {}

public User(String id, String username, String password, List roles) { }

this.id = id;

this.username = username; this.password = password; this.roles = roles;

List list = new ArrayList(); for(Role r:roles){//将角色名称放到Authority中返回即可 list.add(new SimpleGrantedAuthority(r.getName())); }

return list;

//以下是getter/setter略

}

上面用到了Role类,也同时给出Role类的源代码:

package cn.itcast.domain; /**

* 定义角色类的Bean * @author 王健

* @version 2012-4-29 */

public class Role {

private String id; private String name; private String desc; //以下是getter/setter略

}

5、实现UserDetailsService接口

实现UserDetailsService接口的原理就是先根据用户名查询此用户是否存在。如果存在则查询此用户所拥有的所有角色。 强烈建议查询代码应该在DaoJdbc中实现,你懂的! 代码:

package cn.itcast.security; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List;

import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException; import cn.itcast.domain.Role; import cn.itcast.domain.User; /**

* 就实现一个方法即可

* 因为需要操作数据库,所以要注入 datasource * 应该注入一个dao,此处我就略了 * @author 王健

* @version 2012-4-29 */

public class DbUserDetailsService extends JdbcDaoSupport implements UserDetailsService {

//本人强烈建议在dao中查询 /**

* 根据用户名查询此用户是否存在 */

public UserDetails loadUserByUsername(String username)

throws UsernameNotFoundException { User user = null;

//查询此用户

String sql = \"select * from users where user_name=?\";

System.err.println(\"查询语句为:\"+sql+\+username);

user = getJdbcTemplate().queryForObject(sql, new RowMapper(){

public User mapRow(ResultSet rs, int row) throws SQLException { }

User user = new User(rs.getString(\"user_id\"),

rs.getString(\"user_name\"), rs.getString(\"user_pwd\"), null);//先设置角色为null

System.err.println(\"有没有:\"+user); return user;

},username);

System.err.println(\"user对象为\"+user);

if(user!=null){//说明此用户存在,则查询此用户所属性于的所有角色,根据id查即可

sql = \"select role_id,role_name,role_desc \" +//只关联角色和中间表即可 \"from roles inner join roleuser on roles.role_id=roleuser.ru_roleid\" +

}

}

}

\" where roleuser.ru_userid=?\";

System.err.println(\"角色查询为:\"+sql+\+user.getId());

List roles = getJdbcTemplate().query(sql,new RowMapper(){

public Role mapRow(ResultSet rs, int row) }

throws SQLException { Role role = new Role();

role.setId(rs.getString(\"role_id\")); role.setName(rs.getString(\"role_name\")); role.setDesc(rs.getString(\"role_desc\")); return role;

},user.getId()); user.setRoles(roles);

return user;

6、小小的修改以下配置文件与数据库的角色对应

由于本人的角色表已经设置成了以下信息:,即不是以ROLE_xx开头的名称了,的所以,要做一点小小的修改: 表数据:

修改spring-security.xml如下:注意里面的对应关系,即为admin

第四步:小总结

本部分我们主要学习了以下两个类:

UserDetailsService接口,及如何实现它里面的方法。

UserDetails接口,即用户,及如何实现一个自己的User对象。 配置中的access=”hasRole(„ROLE_ADMIN)”与角色表的对应关系。

5、实现资源也从数据库进行验证

目前情况下,实现到上面的代码,就已经非常好了。如果再向下实现,将会是更加细粒度的控制。 继承吧!。。。。 如果前面的配置你都已经看明白了,那我恭喜你。后面的配置将比上面的所有配置复杂的多。 第一步:分析存在的问题 – 在配置文件中资源硬编码问题

第上例中,我们已经实现不同的资源只能某些用户访问如:

上面两个定义了,secu下的资源必须要拥有admin角色才可以访问,suer下的资源,必须要具有user角色才可以访问。而上面的配置全部是是通过硬编码方式实现的。如果也能将资源配置转移到数据库中岂不是更好吗!

第二步:在resources表中保存资源信息-越详细越好

同样使用之前的表结构:

/*现在开始定义资源信息,并设置资源对应的角色*/

insert into resources values('S1','超级管理','/jsps/secu/secu.jsp'); insert into resources values('S2','普通应用','/jsps/user/user.jsp'); /*定义角色与资源的对应关系*/

insert into func(func_roleid,func_resid) values('R1','S1'); insert into func(func_roleid,func_resid) values('R1','S2'); insert into func(func_roleid,func_resid) values('R2','S2');

现在让我们做一个查询,查询出某人拥有某资源:

/*查询某人拥有某资源*/

SELECT u.user_name,s.res_name,s.res_url

FROM users u INNER JOIN roleuser ru ON u.user_id=ru.ru_userid INNER JOIN roles r ON ru.ru_roleid=r.role_id INNER JOIN func f ON r.role_id=f.func_roleid INNER JOIN resources s ON f.func_resid=s.res_id;

查询的结果:

不难发现,里面有重复的数据。 为此我们使用distinct关键字:

/*使用distinct关键字*/

SELECT DISTINCT u.user_name,s.res_name,s.res_url

FROM users u INNER JOIN roleuser ru ON u.user_id=ru.ru_userid INNER JOIN roles r ON ru.ru_roleid=r.role_id INNER JOIN func f ON r.role_id=f.func_roleid INNER JOIN resources s ON f.func_resid=s.res_id;

查询的结果:

可见,重复的数据已经没有了。

上例的查询操作,一般用于用户登录时,显示用户的菜单。

第三步:配置自己的核心过虑器-核心工作

前面的配置默认情况下,启动了一系列的过虑器,启动过程可以通过配置元素查看后台的输出日志:

Security filter chain: [

SecurityContextPersistenceFilter LogoutFilter

UsernamePasswordAuthenticationFilter BasicAuthenticationFilter RequestCacheAwareFilter

SecurityContextHolderAwareRequestFilter RememberMeAuthenticationFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ]

最后一个过虑器FilterSecurityInterceptor则是我们真正核心的安全过虑器。要想真正的使用自己的验证规则,则必须要覆盖此类的实现。此类的源代码及继承关系图如下:

1、三个重要的属性-必不可少

要想重写FilterSecurityInterceptor必须要为其注入三个必不可少的属性,它们是:

1、authenticationManager认证管理器,从内存或是从数据库获取用户的信息。此属性我们已经配置好了:

2、accessDecisionManager访问判断管理器,它使用投票机制决定用户是否具体访问某资源的权限。系统已经帮我们做好了默认的实现。但需要我们给出定义。

3、securityMatadataSourcer所有被保护的资源信息,一般为一个url,也可以是一个类或是一个方法。如中的信息。

此类需要我们自己的去实现,即读取所有资源并缓存起来。

为了更加清楚安全认证的过程,我们再次分析FilterSecurityInterceptor的源代码。 通过分析源码,我们得到如下信息:

它实现了javax.servlet.Filter接口,是一个标准的Servlet 过滤器从AbstractSecurityInterceptor 继承而来,定义了FilterInvocationSecurityMetadataSource(接口)属性,属性名为securityMetadataSource,并提供了getter/setter方法。而AbstractSecurityInterceptor 中定义了accessDecisionManager属性,并提供getter/setter方法,类型为AccessDecisionManager(接口),还有authenticationManager属性,并提供了getter/setter方法,类型为AuthenticationManager(接口). 过滤器中三个主要属性都找到了。 accessDecisionManager属性:此属性的类型为AccessDecisionManager接口类型,它的子类如下:

可见。它拥有一个抽象子类和三个具体子类,AccessDecisionManager接口的源代码如下:

public interface AccessDecisionManager {

//决定是否允许访问某个资源,资源可以是一个url,也可以是一个类或是方法。

//如果没有访问权限则会直接抛出一个AccessDeniedException异常

void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; /*

* @return true if this AccessDecisionManager can support the passed configuration attribute */

boolean supports(ConfigAttribute attribute); /*

* @return true if the implementation can process the indicated class */

boolean supports(Class clazz); }

2、三种不同的验证方式-投票

上例中AccessDecisionManager接口的三个实现类分别为三种不同的验证方式:

AffrimativeBased类,验证用户是否具体所需要的角色中的一个。如访问/user/user.jsp资源,需要具有ROLE_ADMIN或是ROLE_USER角色,只要用户具有其中一个角色,即可以访问资源。应该是or的关系。也可以理解成,只要有一个人投同意票就算是通过。

ConsensusBased类,当用户拥有的角色大于等于所需要的角色数量/2,即可访问资源。即有一半以上的角色时,即可以访问资源。也可以理解成,当有一半以上的人投同意票时才通过。

UnanimousBased类,这也是最为严格的验证。必须要所有的投票者都投同意票时才通过。而每一个AccessDecisionVoter的子类只能是一个投票者。

都通过AccessDecisionVoter接口的子类来实现投票:

RoleVoter类是我们所需要的一个类。

3、启动过程和访问过程分析

4、先完成Resource领域模型

public class Resource {

private String id; private String name; private String url; private List roles; //getters/setters略

}

5、书写securityMetadataSource的实现类继承

package cn.itcast.security; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set;

import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper;

import org.springframework.jdbc.core.support.JdbcDaoSupport;

import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation;

import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import cn.itcast.domain.Resource; import cn.itcast.domain.Role; /**

* 从数据库中加载需要认证的资源

* 本人强烈建议在Dao中实现从数据库中的查询,但为了让你看明白我还在这儿实现
* 以后再做优化

* @author 王健

* @version 2012-4-30 */

public class DBSecurityMetadataSource extends JdbcDaoSupport implements FilterInvocationSecurityMetadataSource{

private Map> resourcesMap=new HashMap* init方法,用于在项目启动时,即读取所有资源放到resources中去 * 在配置文件中通过init-method指定此方法在加载此bean完成后运行 */

public void init(){

String sql = \"select res_id,res_name,res_url from resources\";//查询所有受保护的资源 JdbcTemplate jt = getJdbcTemplate();

List ress = jt.query(sql,new RowMapper(){

public Resource mapRow(ResultSet rs, int row) }

throws SQLException { Resource res = new Resource(); res.setId(rs.getString(\"res_id\")); res.setName(rs.getString(\"res_name\")); res.setUrl(rs.getString(\"res_url\")); return res;

Collection>();

});

//查询每一资源所对应的角色

sql = \"select role_id,role_name,role_desc \" + }

//将List转成Map>

//即一个资源所对应的角色是什么即:{\"/secu.jsp\":[\"ROLE_ADMIN\ for(Resource rs:ress){ }

String key = rs.getUrl();//以受保护的资源为key

Set config = new HashSet(); for(Role r:rs.getRoles()){ }

resourcesMap.put(key,config);

ConfigAttribute att = new SecurityConfig(r.getName());//直接将角色信息放到config中即可 config.add(att);

\"from roles inner join func on role_id=func_roleid\" + \" where func_resid=?\";

List roles = jt.query(sql,new RowMapper(){

public Role mapRow(ResultSet rs, int arg1) }

throws SQLException { Role role = new Role();

role.setId(rs.getString(\"role_id\")); role.setName(rs.getString(\"role_name\")); role.setDesc(rs.getString(\"role_desc\")); return role;

for(Resource res:ress){

},res.getId()); res.setRoles(roles);

System.err.println(\"启动完成:\"+resourcesMap); } /**

* 查询所有的资源 */

public Collection getAllConfigAttributes() {

}

}

Collection all = new HashSet();

//遍历获取所有的资源

for(Map.Entry> en : resourcesMap.entrySet()){ all.addAll(en.getValue());//一次性部分添加 }

System.err.println(\"获取所有可用的角色信息:\"+all); return all;

/**

* 根据给定的资源获取某一个资源的角色信息 */

public Collection getAttributes(Object obj) }

public boolean supports(Class arg0) { }

return true;

throws IllegalArgumentException {

System.err.println(\"所有资源为:\"+resourcesMap); FilterInvocation invo =(FilterInvocation) obj; String url = invo.getRequestUrl();

System.err.println(\"用spring的方式获取url为:\"+url);

url = invo.getRequest().getRequestURI();//获取项目名和请求的路径 System.err.println(\"自己的方式获取:\"+url);

String contextPath = invo.getRequest().getContextPath(); url = url.replace(contextPath,\"\"); System.err.println(\"自己处理以后:\"+url);

if(url.indexOf(\";\")!=-1){//说明里面有jsessionid }

Collection attr = resourcesMap.get(url);//因为我们的url是完全匹配模式所以,直接从map中获取即可 //如果没有获取到则直接返回null,如果返回null则直接放行。 return attr;

System.err.println(\"去掉jsessionid:\"); url = url.substring(0,url.indexOf(\";\"));

6、修改spring-security.xml文件的配置

xsi:schemaLocation=\"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd\">

default-target-url=\"/index.jsp\" password-parameter=\"j_password\" username-parameter=\"j_username\"

authentication-failure-url=\"/jsps/login.jsp?error=1\" />

logout-url=\"/j_spring_security_logout\"/>

7、运行测试 大功告成。 第四步:小总结

修改SpringSecurityInterceptor核心过虑器。提供自己的实现。

三种投票方式:一个人同意即放行,一半以上同意才放行,全部同意才放行。

自己实现securityMetadataSource。提供查询语句,并最终将数据封装成Spring security需要的数据类型Map>。 核心过虑器三个必须的属性是哪三个,必须要牢记: authentationManager – 自己实现从数据库加载。我们自己实现。

accessDecisionManager – 使用系统的实现,需要我们给出配置。 securityMetadataSource – 安全数据源是哪些,需要我们自己实现。

6、为密码MD5加密

这样就需要预先对数据进行加密后再保存到数据库中:

Md5PasswordEncoder md5 =

new Md5PasswordEncoder();

String ss = md5.encodePassword(\"1234\",\"guest\"); System.err.println(ss);

保存到数据库后的结果为:

7、总结:如果上面的配置你都已经成功,那恭喜你,你已经可以完全的使用spring security了

经过上面的学习,我们应该了解了以下内容:

1:在web.xml配置的核心过虑器DelegatingFilterProxy。它与spring-security.xml配置文件中的http元素对应。且名称必须要叫:springSecurityFilterChain。 2:用户基本配置

InMemoryDaoImpl在配置文件中直接配置用户:

3:使用数据库进行验证

需要声明一个实现了UserDetailsService接口的类,并提供查询语句:

在MyUserDetailsService子类中实现以下一个方法: UserDetails loadUserByUsername(String userName){ //需要根据用户名查询出此用户。

//需要根据用户名查询此用户所拥有的所有角色 //返回此UserDetails对象即可。

//可以实现一个UserDetails的子类,以封装自己更加有用的信息 }

4、将安全验证过程全部转移到过虑器中。 此过程的关键是修改FilterSecurityInterceptor类,并给它传递三个重要的属性: accessDecisionManager – 以投票方式用于决定是否拥有访问权限。

authenticationManager – 之前我们已经定义,即用所定义的元素。

securityMetadataSource – 主要用于查询所有资源及所对应的角色信息。实现此类,必须要实现接口FilterInvocationSecurityMetadataSource。且应该实现一个初始化方法,在初始时一次加载所有资源及资源所对应的角色。

实现里面的两个方法。具体请参考上面的实现。

8、重要提示

必须要配置对登录页面不过虑。 即:

否则会抛出cn.itcast.security.DBSecurityMetadataSource 不能转换成

org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource类的异常。

9、spring security FilterChain详解

9.1. HttpSessionContextIntegrationFilter

位于过滤器顶端,第一个起作用的过滤器。

用途一,在执行其他过滤器之前,率先判断用户的session中是否已经存在一个SecurityContext了。如果存在,就把 SecurityContext拿出来,放到SecurityContextHolder中,供Spring Security的其他部分使用。如果不存在,就创建一个SecurityContext出来,还是放到SecurityContextHolder中,供Spring Security的其他部分使用。

用途二,在所有过滤器执行完毕后,清空SecurityContextHolder,因为SecurityContextHolder是基于ThreadLocal的,如果在操作完成后清空ThreadLocal,会受到服务器的线程池机制的影响。

9.2. LogoutFilter

只处理注销请求,默认为/j_spring_security_logout。

用途是在用户发送注销请求时,销毁用户session,清空SecurityContextHolder,然后重定向到注销成功页面。可以与rememberMe之类的机制结合,在注销的同时清空用户cookie。

9.3. AuthenticationProcessingFilter

处理form登陆的过滤器,与form登陆有关的所有操作都是在此进行的。

默认情况下只处理/j_spring_security_check请求,这个请求应该是用户使用form登陆后的提交地址,form所需的其他参数可以参考:???。

此过滤器执行的基本操作时,通过用户名和密码判断用户是否有效,如果登录成功就跳转到成功页面(可能是登陆之前访问的受保护页面,也可能是默认的成功页面),如果登录失败,就跳转到失败页面。

9.4. DefaultLoginPageGeneratingFilter

此过滤器用来生成一个默认的登录页面,默认的访问地址为/spring_security_login,这个默认的登录页面虽然支持用户输入用户名,密码,也支持rememberMe功能,但是因为太难看了,只能是在演示时做个样子,不可能直接用在实际项目中。

如果想自定义登陆页面,可以参考:第 4 章 自定义登陆页面。

9.5. BasicProcessingFilter

此过滤器用于进行basic验证,功能与AuthenticationProcessingFilter类似,只是验证的方式不同。有关basic验证的详细情况,我们会在后面的章节中详细介绍。

有关basic验证的详细信息,可以参考:第 12 章 basic认证。

9.6. SecurityContextHolderAwareRequestFilter

此过滤器用来包装客户的请求。目的是在原始请求的基础上,为后续程序提供一些额外的数据。比如getRemoteUser()时直接返回当前登陆的用户名之类的。

9.7. RememberMeProcessingFilter

此过滤器实现RememberMe功能,当用户cookie中存在rememberMe的标记,此过滤器会根据标记自动实现用户登陆,并创建SecurityContext,授予对应的权限。

有关rememberMe功能的详细信息,可以参考:第 14 章 自动登录。

9.8. AnonymousProcessingFilter

为了保证操作统一性,当用户没有登陆时,默认为用户分配匿名用户的权限。

有关匿名登录功能的详细信息,可以参考:第 15 章 匿名登录。

9.9. ExceptionTranslationFilter

此过滤器的作用是处理中FilterSecurityInterceptor抛出的异常,然后将请求重定向到对应页面,或返回对应的响应错误代码。

9.10. SessionFixationProtectionFilter

防御会话伪造攻击。有关防御会话伪造的详细信息,可以参考:第 16 章 防御会话伪造。

9.11. FilterSecurityInterceptor

用户的权限控制都包含在这个过滤器中。

功能一:如果用户尚未登陆,则抛出AuthenticationCredentialsNotFoundException“尚未认证异常”。

功能二:如果用户已登录,但是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。

功能三:如果用户已登录,也具有访问当前资源的权限,则放行。

至此,我们完全展示了默认情况下Spring Security中使用到的过滤器,以及每个过滤器的应用场景和显示功能,下面我们会对这些过滤器的配置和用法进行逐一介绍。

10、角色投票(翻译)

以下内容见:page76。 RoleVoter

The most commonly used AccessDecisionVoter provided with Spring Security is the simple

RoleVoter, which treats configuration attributes as simple role names and votes to grant access if the user has been assigned that role.

It will vote if any ConfigAttribute begins with the prefix ROLE_. It will vote to grant access if there is a GrantedAuthority which returns a String representation (via the getAuthority() method)

exactly equal to one or more ConfigAttributes starting with the prefix ROLE_. If there is no exact match of any ConfigAttribute starting with ROLE_, the RoleVoter will vote to deny access. If no ConfigAttribute begins with ROLE_, the voter will abstain.

应用最广泛的访问投票。如果角色名称以ROLE_头,它将投票。如果有任一的角色与之匹配它将投意票。如果没有匹配的将投拒绝票。如果没有以ROLE_开头将投弃权票。

11、UnanimousBased全票通过决策器详解:

为了更好的验证UnanimousBased决策器。我们自己开发一个投票者,且它总是投反对票:

class=\"org.springframework.security.access.vote.UnanimousBased\">

MyVoter类:

package cn.itcast.utils; import java.util.Collection;

import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.core.Authentication; public class MyVoter implements AccessDecisionVoter{

public boolean supports(ConfigAttribute attribute) {

System.err.println(\"属性性:\"+attribute); return true;

}

}

public boolean supports(Class clazz) { }

public int vote(Authentication authentication, Object object,

Collection attributes) {

System.err.println(\"反对......\"+authentication+\+object+\+attributes); System.err.println(\"类信息是\"+clazz); return true;

return -1;//不可以访问 }

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- igat.cn 版权所有 赣ICP备2024042791号-1

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务