A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 小江哥 于 2018-12-22 16:54 编辑

第 1 章 一个简单的HelloWorld

Spring Security中可以使用Acegi-1.x时代的普通配置方式,也可以使用从2.0时代才出现的命名空间配置方式,实际上这两者实现的功能是完全一致的,只是新的命名空间配置方式可以把原来需要几百行的配置压缩成短短的几十行。我们的教程中都会使用命名空间的方式进行配置,凡事务求最简。

1.1. 配置过滤器

为了在项目中使用Spring Security控制权限,首先要在web.xml中配置过滤器,这样我们就可以控制对这个项目的每个请求了。

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

所有的用户在访问项目之前,都要先通过Spring Security的检测,这从第一时间把没有授权的请求排除在系统之外,保证系统资源的安全。关于过滤器配置的更多讲解可以参考http://www.family168.com/tutoria ... tml#jsp-ch-07-03-01

1.2. 使用命名空间

在applicationContext.xml中使用Spring Security提供的命名空间进行配置。

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/s ... -security-2.0.4.xsd">

    <http auto-config='true'>
        <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
        <intercept-url pattern="/**" access="ROLE_USER" />
    </http>

    <authentication-provider>
        <user-service>
            <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
            <user name="user" password="user" authorities="ROLE_USER" />
        </user-service>
    </authentication-provider>

</beans:beans>


声明在xml中使用Spring Security提供的命名空间。

http部分配置如何拦截用户请求。auto-config='true'将自动配置几种常用的权限控制机制,包括form, anonymous, rememberMe。

我们利用intercept-url来判断用户需要具有何种权限才能访问对应的url资源,可以在pattern中指定一个特定的url资源,也可以使用通配符指定一组类似的url资源。例子中定义的两个intercepter-url,第一个用来控制对/admin.jsp的访问,第二个使用了通配符/**,说明它将控制对系统中所有url资源的访问。
在实际使用中,Spring Security采用的是一种就近原则,就是说当用户访问的url资源满足多个intercepter-url时,系统将使用第一个符合条件的intercept-url进行权限控制。在我们这个例子中就是,当用户访问/admin.jsp时,虽然两个intercept-url都满足要求,但因为第一个intercept-url排在上面,所以Spring Security会使用第一个intercept-url中的配置处理对/admin.jsp的请求,也就是说,只有那些拥有了ROLE_ADMIN权限的用户才能访问/admin.jsp。
access指定的权限部分比较有趣,大家可以注意到这些权限标示符都是以ROLE_开头的,实际上这与Spring Security中的Voter机制有着千丝万缕的联系,只有包含了特定前缀的字符串才会被Spring Security处理。目前来说我们只需要记住这一点就可以了,在教程以后的部分中我们会详细讲解Voter的内容。

user-service中定义了两个用户,admin和user。为了简便起见,我们使用明文定义了两个用户对应的密码,这只是为了当前演示的方便,之后的例子中我们会使用Spring Security提供的加密方式,避免用户密码被他人窃取。
最最重要的部分是authorities,这里定义了这个用户登陆之后将会拥有的权限,它与上面intercept-url中定义的权限内容一一对应。每个用户可以同时拥有多个权限,例子中的admin用户就拥有ROLE_ADMIN和ROLE_USER两种权限,这使得admin用户在登陆之后可以访问ROLE_ADMIN和ROLE_USER允许访问的所有资源。
与之对应的是,user用户就只拥有ROLE_USER权限,所以他只能访问ROLE_USER允许访问的资源,而不能访问ROLE_ADMIN允许访问的资源。
1.3. 完善整个项目

因为Spring Security是建立在Spring的基础之上的,所以web.xml中除了需要配置我们刚刚提到的过滤器,还要加上加载Spring的相关配置。最终得到的web.xml看起来像是这样:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext*.xml</param-value>
    </context-param>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

演示不同权限的用户登陆之后可以访问不同的资源,我们为项目添加了两个jsp文件,admin.jsp和index.jsp。其中admin.jsp只有那些拥有ROLE_ADMIN权限的用户才能访问,而index.jsp只允许那些拥有ROLE_USER权限的用户才能访问。

最终我们的整个项目会变成下面这样:

+ ch001/
  + src/
    + main/
      + resources/
        * applicationContext.xml
      + webapp/
        + WEB-INF/
          * web.xml
        * admin.jsp
        * index.jsp
    + test/
      + resources/
  * pom.xml

1.4. 运行示例

首先确保自己安装了Maven2。如果之前没用过Maven2,可以参考我们的Maven2教程http://www.family168.com/oa/maven2/html/index.html

安装好Maven2之后,进入ch001目录,然后执行mvn。

信息: Root WebApplicationContext: initialization completed in 1578 ms
2009-05-28 11:37:50.171::INFO:  Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
[INFO] Starting scanner at interval of 10 seconds.

等到项目启动完成后。打开浏览器访问http://localhost:8080/ch001/就可以看到登陆页面。


图 1.1. 用户登陆


这个简陋的页面是Spring Security自动生成的,一来为了演示的方便,二来避免用户自己编写登陆页面时犯错,Spring Security为了避免可能出现的风险,连测试用的登录页面都自动生成出来了。在这里我们就省去编写登陆页面的步骤,直接使用默认生成的登录页面进行演示吧。

首先让我们输入一个错误用的用户名或密码,这里我们使用test/test,当然这个用户是不存在的,点击提交之后我们会得到这样一个登陆错误提示页面。


图 1.2. 登陆失败


如果输入的是正确的用户名和密码,比如user/user,系统在登陆成功后会默认跳转到index.jsp。


图 1.3. 登陆成功


这时我们可以点击admin.jsp链接访问admin.jsp,也可以点击logout进行注销。

如果点击了logout,系统会注销当前登陆的用户,然后跳转至登陆页面。如果点击了admin.jsp链接就会显示如下页面。


图 1.4. 拒绝访问


很遗憾,user用户是无法访问/admin.jsp这个url资源的,这在上面的配置文件中已经有过深入的讨论。我们在这里再简要重复一遍:user用户拥有ROLE_USER权限,但是/admin.jsp资源需要用户拥有ROLE_ADMIN权限才能访问,所以当user用户视图访问被保护的/admin.jsp时,Spring Security会在中途拦截这一请求,返回拒绝访问页面。

为了正常访问admin.jsp,我们需要先点击logout注销当前用户,然后使用admin/admin登陆系统,然后再次点击admin.jsp链接就会显示出admin.jsp中的内容。


图 1.5. 显示admin.jsp


根据我们之前的配置,admin用户拥有ROLE_ADMIN和ROLE_USER两个权限,因为他拥有ROLE_USER权限,所以可以访问/index.jsp,因为他拥有ROLE_ADMIN权限,所以他可以访问/admin.jsp。

至此,我们很高兴的宣布,咱们已经正式完成,并运行演示了一个最简单的由Spring Security保护的web系统,下一步我们会深入讨论Spring Security为我们提供的其他保护功能,多姿多彩的特性。



第 2 章 使用数据库管理用户权限
上一页         部分 I. 基础篇         下一页
第 2 章 使用数据库管理用户权限

上一章节中,我们把用户信息和权限信息放到了xml文件中,这是为了演示如何使用最小的配置就可以使用Spring Security,而实际开发中,用户信息和权限信息通常是被保存在数据库中的,为此Spring Security提供了通过数据库获得用户权限信息的方式。

2.1. 修改配置文件

为了从数据库中获取用户权限信息,我们所需要的仅仅是修改配置文件中的authentication-provider部分。

将上一章配置文件中的user-service替换为jdbc-user-service,替换内容如下所示:

<authentication-provider>
    <user-service>
        <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
        <user name="user" password="user" authorities="ROLE_USER" />
    </user-service>
</authentication-provider>

将上述红色部分替换为下面黄色部分。

<authentication-provider>
    <jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

现在只要再为jdbc-user-service提供一个dataSource就可以让Spring Security使用数据库中的权限信息了。在此我们使用spring创建一个演示用的dataSource实现,这个dataSource会连接到hsqldb数据库,从中获取用户权限信息。[1]

<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
    <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/>
    <beans:property name="username" value="sa"/>
    <beans:property name="password" value=""/>
</beans:bean>

最终的配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/s ... -security-2.0.4.xsd">

    <http auto-config='true'>
        <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
        <intercept-url pattern="/**" access="ROLE_USER" />
    </http>

    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource"/>
    </authentication-provider>

    <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/>
        <beans:property name="username" value="sa"/>
        <beans:property name="password" value=""/>
    </beans:bean>
</beans:beans>

2.2. 数据库表结构

Spring Security默认情况下需要两张表,用户表和权限表。以下是hsqldb中的建表语句:

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(50) not null,
    enabled boolean not null
);

create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username,authority);

users:用户表。包含username用户登录名,password登陆密码,enabled用户是否被禁用三个字段。
其中username用户登录名为主键。

authorities:权限表。包含username用户登录名,authorities对应权限两个字段。
其中username字段与users用户表的主键使用外键关联。

对authorities权限表的username和authority创建唯一索引,提高查询效率。
Spring Security会在初始化时,从这两张表中获得用户信息和对应权限,将这些信息保存到缓存中。其中users表中的登录名和密码用来控制用户的登录,而权限表中的信息用来控制用户登陆后是否有权限访问受保护的系统资源。

我们在示例中预先初始化了一部分数据:

insert into users(username,password,enabled) values('admin','admin',true);
insert into users(username,password,enabled) values('user','user',true);

insert into authorities(username,authority) values('admin','ROLE_ADMIN');
insert into authorities(username,authority) values('admin','ROLE_USER');
insert into authorities(username,authority) values('user','ROLE_USER');

上述sql中,我们创建了两个用户admin和user,其中admin拥有ROLE_ADMIN和ROLE_USER权限,而user只拥有ROLE_USER权限。这和我们上一章中的配置相同,因此本章实例的效果也和上一章完全相同,这里就不再赘述了。

实例见ch002。


[1] javax.sql.DataSource是一个用来定义数据库连接池的统一接口。当我们想调用任何实现了javax.sql.DataSource接口的连接池,只需要调用接口提供的getConnection()就可以获得连接池中的jdbc连接。javax.sql.DataSource可以屏蔽连接池的不同实现,我们使用的连接池即可能由第三方包单独提供,也可能是由j2ee容器统一管理提供的。





第 3 章 自定义数据库表结构
上一页         部分 I. 基础篇         下一页
第 3 章 自定义数据库表结构

Spring Security默认提供的表结构太过简单了,其实就算默认提供的表结构很复杂,也无法满足所有企业内部对用户信息和权限信息管理的要求。基本上每个企业内部都有一套自己的用户信息管理结构,同时也会有一套对应的权限信息体系,如何让Spring Security在这些已有的数据结构之上运行呢?

3.1. 自定义表结构

假设我们实际使用的表结构如下所示:

-- 角色
create table role(
    id bigint,
    name varchar(50),
    descn varchar(200)
);
alter table role add constraint pk_role primary key(id);
alter table role alter column id bigint generated by default as identity(start with 1);

-- 用户
create table user(
    id bigint,
    username varchar(50),
    password varchar(50),
    status integer,
    descn varchar(200)
);
alter table user add constraint pk_user primary key(id);
alter table user alter column id bigint generated by default as identity(start with 1);

-- 用户角色连接表
create table user_role(
    user_id bigint,
    role_id bigint
);
alter table user_role add constraint pk_user_role primary key(user_id, role_id);
alter table user_role add constraint fk_user_role_user foreign key(user_id) references user(id);
alter table user_role add constraint fk_user_role_role foreign key(role_id) references role(id);

上述共有三张表,其中user用户表,role角色表为保存用户权限数据的主表,user_role为关联表。user用户表,role角色表之间为多对多关系,就是说一个用户可以有多个角色。ER图如下所示:


图 3.1. 数据库表关系


3.2. 初始化数据

创建两个用户,admin和user。admin用户拥有“管理员”角色,user用户拥有“用户”角色。

insert into user(id,username,password,status,descn) values(1,'admin','admin',1,'管理员');
insert into user(id,username,password,status,descn) values(2,'user','user',1,'用户');

insert into role(id,name,descn) values(1,'ROLE_ADMIN','管理员角色');
insert into role(id,name,descn) values(2,'ROLE_USER','用户角色');

insert into user_role(user_id,role_id) values(1,1);
insert into user_role(user_id,role_id) values(1,2);
insert into user_role(user_id,role_id) values(2,2);

3.3. 获得自定义用户权限信息

现在我们要在这样的数据结构基础上使用Spring Security,Spring Security所需要的数据只是为了处理两种情况,一是判断登录用户是否合法,二是判断登陆的用户是否有权限访问受保护的系统资源。

我们所要做的工作就是在现有数据结构的基础上,为Spring Security提供这两种数据。

3.3.1. 处理用户登陆

当用户登陆时,系统需要判断用户登录名是否存在,登陆密码是否正确,当前用户是否被禁用。我们使用下列SQL来提取这三个信息。

select username,password,status as enabled
  from user
where username=?

3.3.2. 检验用户权限

用户登陆之后,系统需要获得该用户的所有权限,根据用户已被赋予的权限来判断哪些系统资源可以被用户访问,哪些资源不允许用户访问。

以下SQL就可以获得当前用户所拥有的权限。

select u.username,r.name as authority
  from user u
  join user_role ur
    on u.id=ur.user_id
  join role r
    on r.id=ur.role_id
where u.username=?"/>

将这两条SQL语句配置到xml中,就可以让Spring Security从我们自定义的表结构中提取数据了。最终配置文件如下所示:

    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource"
            users-by-username-query="select username,password,status as enabled
                                         from user
                                        where username=?"
            authorities-by-username-query="select u.username,r.name as authority
                                             from user u
                                             join user_role ur
                                               on u.id=ur.user_id
                                             join role r
                                               on r.id=ur.role_id
                                            where u.username=?"/>
    </authentication-provider>


users-by-username-query为根据用户名查找用户,系统通过传入的用户名查询当前用户的登录名,密码和是否被禁用这一状态。

authorities-by-username-query为根据用户名查找权限,系统通过传入的用户名查询当前用户已被授予的所有权限。



第 4 章 自定义登陆页面
上一页         部分 I. 基础篇         下一页
第 4 章 自定义登陆页面

Spring Security虽然默认提供了一个登陆页面,但是这个页面实在太简陋了,只有在快速演示时才有可能它做系统的登陆页面,实际开发时无论是从美观还是实用性角度考虑,我们都必须实现自定义的登录页面。
4.1. 实现自定义登陆页面

自己实现一个login.jsp,放在src/main/webapp/目录下。
+ ch004/
  + src/
    + main/
      + resources/
        * applicationContext.xml
      + webapp/
        + WEB-INF/
          * web.xml
        * admin.jsp
        * index.jsp
        * login.jsp
    + test/
      + resources/
  * pom.xml

4.2. 修改配置文件

在xml中的http标签中添加一个form-login标签。
<http auto-config='true'>
    <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <form-login login-page="/login.jsp"authentication-failure-url="/login.jsp?error=true"default-target-url="/" />
</http>


让没登陆的用户也可以访问login.jsp。[2]
这是因为配置文件中的“/**”配置,要求用户访问任意一个系统资源时,必须拥有ROLE_USER角色,/login.jsp也不例外,如果我们不为/login.jsp单独配置访问权限,会造成用户连登陆的权限都没有,这是不正确的。

login-page表示用户登陆时显示我们自定义的login.jsp。
这时我们访问系统显示的登陆页面将是我们上面创建的login.jsp。

authentication-failure-url表示用户登陆失败时,跳转到哪个页面。
当用户输入的登录名和密码不正确时,系统将再次跳转到/login.jsp,并添加一个error=true参数作为登陆失败的标示。

default-target-url表示登陆成功时,跳转到哪个页面。[3]
4.3. 登陆页面中的参数配置

以下是我们创建的login.jsp页面的主要代码。
<div class="error ${param.error == true ? '' : 'hide'}">
  登陆失败<br>
  ${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}
</div>
<form action="${pageContext.request.contextPath}/j_spring_security_check" style="width:260px;text-align:center;">
  <fieldset>
    <legend>登陆</legend>
    用户: <input type="text" name="j_username" style="width:150px;" value="${sessionScope['SPRING_SECURITY_LAST_USERNAME']}"/><br />
    密码: <input type="password" name="j_password" style="width:150px;" /><br />
    <input type="checkbox" name="_spring_security_remember_me" />两周之内不必登陆<br />
    <input type="submit" value="登陆"/>
    <input type="reset" value="重置"/>
  </fieldset>
</form>


/j_spring_security_check,提交登陆信息的URL地址。
自定义form时,要把form的action设置为/j_spring_security_check。注意这里要使用绝对路径,避免登陆页面存放的页面可能带来的问题。[4]

j_username,输入登陆名的参数名称。

j_password,输入密码的参数名称

_spring_security_remember_me,选择是否允许自动登录的参数名称。
可以直接把这个参数设置为一个checkbox,无需设置value,Spring Security会自行判断它是否被选中。
以上介绍了自定义页面上Spring Security所需的基本元素,这些参数名称都采用了Spring Security中默认的配置值,如果有特殊需要还可以通过配置文件进行修改。
4.4. 测试一下

经过以上配置,我们终于使用了一个自己创建的登陆页面替换了原来Spring Security默认提供的登录页面了。我们不仅仅是做个样子,而是实际配置了各个Spring Security所需的参数,真正将自定义登陆页面与Spring Security紧紧的整合在了一起。以下是使用自定义登陆页面实际运行时的截图。

图 4.1. 进入登录页面
第 5 章 使用数据库管理资源
上一页         部分 I. 基础篇         下一页
第 5 章 使用数据库管理资源

国内对权限系统的基本要求是将用户权限和被保护资源都放在数据库里进行管理,在这点上Spring Security并没有给出官方的解决方案,为此我们需要对Spring Security进行扩展。
5.1. 数据库表结构

这次我们使用五张表,user用户表,role角色表,resc资源表相互独立,它们通过各自之间的连接表实现多对多关系。
-- 资源
create table resc(
    id bigint,
    name varchar(50),
    res_type varchar(50),
    res_string varchar(200),
    priority integer,
    descn varchar(200)
);
alter table resc add constraint pk_resc primary key(id);
alter table resc alter column id bigint generated by default as identity(start with 1);

-- 角色
create table role(
    id bigint,
    name varchar(50),
    descn varchar(200)
);
alter table role add constraint pk_role primary key(id);
alter table role alter column id bigint generated by default as identity(start with 1);

-- 用户
create table user(
    id bigint,
    username varchar(50),
    password varchar(50),
    status integer,
    descn varchar(200)
);
alter table user add constraint pk_user primary key(id);
alter table user alter column id bigint generated by default as identity(start with 1);

-- 资源角色连接表
create table resc_role(
    resc_id bigint,
    role_id bigint
);
alter table resc_role add constraint pk_resc_role primary key(resc_id, role_id);
alter table resc_role add constraint fk_resc_role_resc foreign key(resc_id) references resc(id);
alter table resc_role add constraint fk_resc_role_role foreign key(role_id) references role(id);

-- 用户角色连接表
create table user_role(
    user_id bigint,
    role_id bigint
);
alter table user_role add constraint pk_user_role primary key(user_id, role_id);
alter table user_role add constraint fk_user_role_user foreign key(user_id) references user(id);
alter table user_role add constraint fk_user_role_role foreign key(role_id) references role(id);

user表中包含用户登陆信息,role角色表中包含授权信息,resc资源表中包含需要保护的资源。
ER图如下所示:

图 5.1. 数据库表关系

5.2. 初始化数据

创建的两个用户分别对应“管理员”角色和“用户”角色。而“管理员”角色可以访问“/admin.jsp”和“/**”,“用户”角色只能访问“/**”。
insert into user(id,username,password,status,descn) values(1,'admin','admin',1,'管理员');
insert into user(id,username,password,status,descn) values(2,'user','user',1,'用户');

insert into role(id,name,descn) values(1,'ROLE_ADMIN','管理员角色');
insert into role(id,name,descn) values(2,'ROLE_USER','用户角色');

insert into resc(id,name,res_type,res_string,priority,descn) values(1,'','URL','/admin.jsp',1,'');
insert into resc(id,name,res_type,res_string,priority,descn) values(2,'','URL','/**',2,'');

insert into resc_role(resc_id,role_id) values(1,1);
insert into resc_role(resc_id,role_id) values(2,1);
insert into resc_role(resc_id,role_id) values(2,2);

insert into user_role(user_id,role_id) values(1,1);
insert into user_role(user_id,role_id) values(1,2);
insert into user_role(user_id,role_id) values(2,2);

5.3. 实现从数据库中读取资源信息

Spring Security没有提供从数据库获得获取资源信息的方法,实际上Spring Security甚至没有为我们留一个半个的扩展接口,所以我们这次要费点儿脑筋了。
首先,要搞清楚需要提供何种类型的数据,然后,寻找可以让我们编写的代码替换原有功能的切入点,实现了以上两步之后,就可以宣布大功告成了。
5.3.1. 需要何种数据格式

从配置文件上可以看到,Spring Security所需的数据应该是一系列URL网址和访问这些网址所需的权限:
<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />

Spring Security所做的就是在系统初始化时,将以上XML中的信息转换为特定的数据格式,而框架中其他组件可以利用这些特定格式的数据,用于控制之后的验证操作。
现在这些资源信息都保存在数据库中,我们可以使用上面介绍的SQL语句从数据中查询。
  select re.res_string,r.name
    from role r
    join resc_role rr
      on r.id=rr.role_id
    join resc re
      on re.id=rr.resc_id
order by re.priority

下面要开始编写实现代码了。
搜索数据库获得资源信息。
我们通过定义一个MappingSqlQuery实现数据库操作。
private class ResourceMapping extends MappingSqlQuery {
    protected ResourceMapping(DataSource dataSource,
        String resourceQuery) {
        super(dataSource, resourceQuery);
        compile();
    }

    protected Object mapRow(ResultSet rs, int rownum)
        throws SQLException {
        String url = rs.getString(1);
        String role = rs.getString(2);
        Resource resource = new Resource(url, role);

        return resource;
    }
}

这样我们可以执行它的execute()方法获得所有资源信息。
protected Map<String, String> findResources() {
    ResourceMapping resourceMapping = new ResourceMapping(getDataSource(),
            resourceQuery);

    Map<String, String> resourceMap = new LinkedHashMap<String, String>();

    for (Resource resource : (List<Resource>) resourceMapping.execute()) {
        String url = resource.getUrl();
        String role = resource.getRole();

        if (resourceMap.containsKey(url)) {
            String value = resourceMap.get(url);
            resourceMap.put(url, value + "," + role);
        } else {
            resourceMap.put(url, role);
        }
    }

    return resourceMap;
}

使用获得的资源信息组装requestMap。
protected LinkedHashMap<RequestKey, ConfigAttributeDefinition> buildRequestMap() {
    LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap = null;
    requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>();

    ConfigAttributeEditor editor = new ConfigAttributeEditor();

    Map<String, String> resourceMap = this.findResources();

    for (Map.Entry<String, String> entry : resourceMap.entrySet()) {
        RequestKey key = new RequestKey(entry.getKey(), null);
        editor.setAsText(entry.getValue());
        requestMap.put(key,
            (ConfigAttributeDefinition) editor.getValue());
    }

    return requestMap;
}

使用urlMatcher和requestMap创建DefaultFilterInvocationDefinitionSource。
public Object getObject() {
    return new DefaultFilterInvocationDefinitionSource(this
        .getUrlMatcher(), this.buildRequestMap());
}

这样我们就获得了DefaultFilterInvocationDefinitionSource,剩下的只差把这个我们自己创建的类替换掉原有的代码了。
完整代码如下所示:
package com.family168.springsecuritybook.ch005;

import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.FactoryBean;

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

import org.springframework.security.ConfigAttributeDefinition;
import org.springframework.security.ConfigAttributeEditor;
import org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource;
import org.springframework.security.intercept.web.FilterInvocationDefinitionSource;
import org.springframework.security.intercept.web.RequestKey;
import org.springframework.security.util.AntUrlPathMatcher;
import org.springframework.security.util.UrlMatcher;


public class JdbcFilterInvocationDefinitionSourceFactoryBean
    extends JdbcDaoSupport implements FactoryBean {
    private String resourceQuery;

    public boolean isSingleton() {
        return true;
    }

    public Class getObjectType() {
        return FilterInvocationDefinitionSource.class;
    }

    public Object getObject() {
        return new DefaultFilterInvocationDefinitionSource(this
            .getUrlMatcher(), this.buildRequestMap());
    }

    protected Map<String, String> findResources() {
        ResourceMapping resourceMapping = new ResourceMapping(getDataSource(),
                resourceQuery);

        Map<String, String> resourceMap = new LinkedHashMap<String, String>();

        for (Resource resource : (List<Resource>) resourceMapping.execute()) {
            String url = resource.getUrl();
            String role = resource.getRole();

            if (resourceMap.containsKey(url)) {
                String value = resourceMap.get(url);
                resourceMap.put(url, value + "," + role);
            } else {
                resourceMap.put(url, role);
            }
        }

        return resourceMap;
    }

    protected LinkedHashMap<RequestKey, ConfigAttributeDefinition> buildRequestMap() {
        LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap = null;
        requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>();

        ConfigAttributeEditor editor = new ConfigAttributeEditor();

        Map<String, String> resourceMap = this.findResources();

        for (Map.Entry<String, String> entry : resourceMap.entrySet()) {
            RequestKey key = new RequestKey(entry.getKey(), null);
            editor.setAsText(entry.getValue());
            requestMap.put(key,
                (ConfigAttributeDefinition) editor.getValue());
        }

        return requestMap;
    }

    protected UrlMatcher getUrlMatcher() {
        return new AntUrlPathMatcher();
    }

    public void setResourceQuery(String resourceQuery) {
        this.resourceQuery = resourceQuery;
    }

    private class Resource {
        private String url;
        private String role;

        public Resource(String url, String role) {
            this.url = url;
            this.role = role;
        }

        public String getUrl() {
            return url;
        }

        public String getRole() {
            return role;
        }
    }

    private class ResourceMapping extends MappingSqlQuery {
        protected ResourceMapping(DataSource dataSource,
            String resourceQuery) {
            super(dataSource, resourceQuery);
            compile();
        }

        protected Object mapRow(ResultSet rs, int rownum)
            throws SQLException {
            String url = rs.getString(1);
            String role = rs.getString(2);
            Resource resource = new Resource(url, role);

            return resource;
        }
    }
}

5.3.2. 替换原有功能的切入点

在spring中配置我们编写的代码。
<beans:bean id="filterInvocationDefinitionSource"
    class="com.family168.springsecuritybook.ch005.JdbcFilterInvocationDefinitionSourceFactoryBean">
    <beans:property name="dataSource" ref="dataSource"/>
    <beans:property name="resourceQuery" value="
        select re.res_string,r.name
          from role r
          join resc_role rr
            on r.id=rr.role_id
          join resc re
            on re.id=rr.resc_id
      order by re.priority
    "/>
</beans:bean>

下一步使用这个filterInvocationDefinitionSource创建filterSecurityInterceptor,并使用它替换系统原来创建的那个过滤器。
<beans:bean id="filterSecurityInterceptor"
    class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType">
    <custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
    <beans:property name="objectDefinitionSource" ref="filterInvocationDefinitionSource" />
</beans:bean>

注意这个custom-filter标签,它表示将filterSecurityInterceptor放在框架原来的FILTER_SECURITY_INTERCEPTOR过滤器之前,这样我们的过滤器会先于原来的过滤器执行,因为它的功能与老过滤器完全一样,所以这就等于把原来的过滤器替换掉了。
完整的配置文件如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/s ... -security-2.0.4.xsd">

    <http auto-config="true"/>

    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource"
            users-by-username-query="select username,password,status as enabled
                                       from user
                                      where username=?"
            authorities-by-username-query="select u.username,r.name as authority
                                             from user u
                                             join user_role ur
                                               on u.id=ur.user_id
                                             join role r
                                               on r.id=ur.role_id
                                            where u.username=?"/>
    </authentication-provider>

    <beans:bean id="filterSecurityInterceptor"
        class="org.springframework.security.intercept.web.FilterSecurityInterceptor" autowire="byType">
        <custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
        <beans:property name="objectDefinitionSource" ref="filterInvocationDefinitionSource" />
    </beans:bean>

    <beans:bean id="filterInvocationDefinitionSource"
        class="com.family168.springsecuritybook.ch05.JdbcFilterInvocationDefinitionSourceFactoryBean">
        <beans:property name="dataSource" ref="dataSource"/>
        <beans:property name="resourceQuery" value="
            select re.res_string,r.name
              from role r
              join resc_role rr
                on r.id=rr.role_id
              join resc re
                on re.id=rr.resc_id
          order by re.priority
        "/>
    </beans:bean>

    <beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <beans:property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <beans:property name="url" value="jdbc:hsqldb:res:/hsqldb/test"/>
        <beans:property name="username" value="sa"/>
        <beans:property name="password" value=""/>
    </beans:bean>
</beans:beans>

实例见ch05。
目前存在的问题是,系统会在初始化时一次将所有资源加载到内存中,即使在数据库中修改了资源信息,系统也不会再次去从数据库中读取资源信息。这就造成了每次修改完数据库后,都需要重启系统才能时资源配置生效。
解决方案是,如果数据库中的资源出现的变化,需要刷新内存中已加载的资源信息时,使用下面代码:
<%@page import="org.springframework.context.ApplicationContext"%>
<%@page import="org.springframework.web.context.support.WebApplicationContextUtils"%>
<%@page import="org.springframework.beans.factory.FactoryBean"%>
<%@page import="org.springframework.security.intercept.web.FilterSecurityInterceptor"%>
<%@page import="org.springframework.security.intercept.web.FilterInvocationDefinitionSource"%>
<%
    ApplicationContext ctx =  WebApplicationContextUtils.getWebApplicationContext(application);
    FactoryBean factoryBean = (FactoryBean) ctx.getBean("&filterInvocationDefinitionSource");
    FilterInvocationDefinitionSource fids = (FilterInvocationDefinitionSource) factoryBean.getObject();
    FilterSecurityInterceptor filter = (FilterSecurityInterceptor) ctx.getBean("filterSecurityInterceptor");
    filter.setObjectDefinitionSource(fids);
%>
<jsp:forward page="/"/>

目前还不支持对方法调用和ACL资源的动态管理,相关讨论请参考手册后面的部分第 39 章 动态资源管理。
第 6 章 控制用户信息
上一页         部分 I. 基础篇         下一页
第 6 章 控制用户信息

让我们来研究一些与用户信息相关的功能,包括为用户密码加密,缓存用户信息,获得系统当前登陆的用户,获得登陆用户的所有权限。
6.1. MD5加密

任何一个正式的企业应用中,都不会在数据库中使用明文来保存密码的,我们在之前的章节中都是为了方便起见没有对数据库中的用户密码进行加密,这在实际应用中是极为幼稚的做法。可以想象一下,只要有人进入数据库就可以看到所有人的密码,这是一件多么恐怖的事情,为此我们至少要对密码进行加密,这样即使数据库被攻破,也可以保证用户密码的安全。
最常用的方法是使用MD5算法对密码进行摘要加密,这是一种单项加密手段,无法通过加密后的结果反推回原来的密码明文。
首先我们要把数据库中原来保存的密码使用MD5进行加密:
INSERT INTO USERS VALUES('admin','21232f297a57a5a743894a0e4a801fc3',TRUE)
INSERT INTO USERS VALUES('user','ee11cbb19052e40b07aac0ca060c23ee',TRUE)

现在密码部分已经面目全非了,即使有人攻破了数据库,拿到这种“乱码”也无法登陆系统窃取客户的信息。
下一步为了让Spring Security支持MD5加密,我们需要修改一下配置文件。
<authentication-provider>
    <password-encoder hash="md5"/>
    <jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

上述代码中新增的黄色部分,将启用MD5算法。用户登录时,输入的密码是明文,需要使用password-encoder将明文转换成md5形式,然后再与数据库中的已加密密码进行比对。
这些配置对普通客户不会造成任何影响,他们只需要输入自己的密码,Spring Security会自动加以演算,将生成的结果与数据库中保存的信息进行比对,以此来判断用户是否可以登陆。
这样,我们只添加了一行配置,就为系统带来了密码加密的功能。
6.2. 盐值加密

实际上,上面的实例在现实使用中还存在着一个不小的问题。虽然md5算法是不可逆的,但是因为它对同一个字符串计算的结果是唯一的,所以一些人可能会使用“字典攻击”的方式来攻破md5加密的系统[5]。这虽然属于暴力解密,却十分有效,因为大多数系统的用户密码都不回很长。
实际上,大多数系统都是用admin作为默认的管理员登陆密码,所以,当我们在数据库中看到“21232f297a57a5a743894a0e4a801fc3”时,就可以意识到admin用户使用的密码了。因此,md5在处理这种常用字符串时,并不怎么奏效。
为了解决这个问题,我们可以使用盐值加密“salt-source”。
修改配置文件:
<authentication-provider>
    <password-encoder hash="md5">
        <salt-source user-property="username"/>
    </password-encoder>
    <jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

在password-encoder下添加了salt-source,并且指定使用username作为盐值。
盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再使用md5对合并后的内容进行演算,这样一来,就算密码是一个很常见的字符串,再加上用户名,最后算出来的md5值就没那么容易猜出来了。因为攻击者不知道盐值的值,也很难反算出密码原文。
我们这里将每个用户的username作为盐值,最后数据库中的密码部分就变成了这样:
INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE)
INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE)

6.3. 用户信息缓存

介于系统的用户信息并不会经常改变,因此使用缓存就成为了提升性能的一个非常好的选择。Spring Security内置的缓存实现是基于ehcache的,为了启用缓存功能,我们要在配置文件中添加相关的内容。
<authentication-provider>
    <password-encoder hash="md5">
        <salt-source user-property="username"/>
    </password-encoder>
    <jdbc-user-service data-source-ref="dataSource" cache-ref="userCache"/>
</authentication-provider>

我们在jdbc-user-service部分添加了对userCache的引用,它将使用这个bean作为用户权限缓存的实现。对userCache的配置如下所示:
<beans:bean id="userCache" class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache">
    <beans:property name="cache" ref="userEhCache"/>
</beans:bean>

<beans:bean id="userEhCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <beans:property name="cacheManager" ref="cacheManager"/>
    <beans:property name="cacheName" value="userCache"/>
</beans:bean>

<beans:bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>

EhCacheBasedUserCache是Spring Security内置的缓存实现,它将为jdbc-user-service提供缓存功能。它所引用的userEhCache来自spring提供的EhCacheFactoryBean和EhCacheManagerFactoryBean,对于userCache的缓存配置放在ehcache.xml中:
<ehcache>
    <diskStore path="java.io.tmpdir"/>

    <defaultCache
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
    />

    <cache
        name="userCache"
        maxElementsInMemory="100"
        eternal="false"
        timeToIdleSeconds="600"
        timeToLiveSeconds="3600"
        overflowToDisk="true"
    />
</ehcache>


内存中最多存放100个对象。

不是永久缓存。

最大空闲时间为600秒。

最大活动时间为3600秒。

如果内存对象溢出则保存到磁盘。
如果想了解有关ehcache的更多配置,可以访问它的官方网站http://ehcache.sf.net/
这样,我们就为用户权限信息设置好了缓存,当一个用户多次访问应用时,不需要每次去访问数据库了,ehcache会将对应的信息缓存起来,这将极大的提高系统的相应速度,同时也避免数据库符合过高的风险。
注意

cache-ref隐藏着一个陷阱,如果不看代码,我们也许会误认为cache-ref会在JdbcUserDetailsManager中设置对应的userCache,然后只要直接执行JdbcUserDetailsManager中的方法,就可以自动维护用户缓存。
可惜,cache-ref实际上是在JdbcUserDetailsManager的基础上,生成了一个CachingUserService,这个CachedUserDetailsService会拦截loadUserByUsername()方法,实现读取用户信息的缓存功能。我们在cache-ref中引用的UserCache实际上是放在CacheUserDetailsService中,而不是放到了原有的JdbcUserDetailsManager中,这就会导致JdbcUserDetailsManager中对用户缓存的操作全部失效。
6.4. 获取当前用户信息

如果只是想从页面上显示当前登陆的用户名,可以直接使用Spring Security提供的taglib。
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<div>username : <sec:authentication property="name"/></div>

如果想在程序中获得当前登陆用户对应的对象。
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
    .getAuthentication()
    .getPrincipal();

如果想获得当前登陆用户所拥有的所有权限。
GrantedAuthority[] authorities = userDetails.getAuthorities();

关于UserDetails是如何放到SecuirtyContext中去的,以及Spring Security所使用的TheadLocal模式,我们会在后面详细介绍。这里我们已经了解了如何获得当前登陆用户的信息。

[5] 所谓字典攻击,就是指将大量常用字符串使用md5加密,形成字典库,然后将一段由md5演算得到的未知字符串,在字典库中进行搜索,当发现匹配的结果时,就可以获得对应的加密前的字符串内容。
第 8 章 动态管理资源结合自定义登录页面
上一页         部分 I. 基础篇         下一页
第 8 章 动态管理资源结合自定义登录页面

如果想将动态管理资源与自定义登录页面一起使用,最简单的办法就是在数据库中将登录页面对应的权限设置为IS_AUTHENTICATED_ANONYMOUSLY。
因此在数据库中添加一条资源信息。
INSERT INTO RESC VALUES(1,'','URL','/login.jsp*',1,'')

这里的/login.jsp*就是我们自定义登录页面的地址。
然后为匿名用户添加一条角色信息:
INSERT INTO ROLE VALUES(3,'IS_AUTHENTICATED_ANONYMOUSLY','anonymous')

最后为这两条记录进行关联即可。
INSERT INTO RESC_ROLE VALUES(1,3)

这样就实现了将动态管理资源与自定义登录页面进行结合。
第 33 章 保护方法调用
上一页         部分 III. 内部机制篇         下一页
第 33 章 保护方法调用

提示

因为Spring Security对方法调用和ACL权限控制的实现都是基于Spring的AOP,所以只能保护定义在applicationContext.xml中的Java类,对直接new出来的对象是无法保护的。
这里有三种方式可以选择:
33.1. 控制全局范围的方法权限

使用global-method-security和protect-point标签来管理全局范围的方法权限。
为了在spring中使用AOP,我们要为项目添加几个依赖库。
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib-nodep</artifactId>
  <version>2.1_3</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.6.5</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.6.5</version>
</dependency>

首先来看看我们将要保护的java类。
package com.family168.springsecuritybook.ch201;

public class MessageServiceImpl implements MessageService {
    public String adminMessage() {
        return "admin message";
    }

    public String adminDate() {
        return "admin " + System.currentTimeMillis();
    }
    public String userMessage() {
        return "user message";
    }

    public String userDate() {
        return "user " + System.currentTimeMillis();
    }
}

这里使用的是spring-2.0中的aop语法,对MessageService中所有以admin开头的方法进行权限控制,限制这些方法只能由ROLE_ADMIN调用。
<global-method-security>
    <protect-pointcut
        expression="execution(* com.family168.springsecuritybook.ch201.MesageServiceImpl.admin*(..))"
        access="ROLE_ADMIN"/>
</global-method-security>

现在只有拥有ROLE_ADMIN权限的用户才能调用MessageService中以admin开头的方法了,当我们以user/user登陆系统时,尝试调用MessageService类的adminMessage()会跑出一个“访问被拒绝”的异常。
33.2. 控制某个bean内的方法权限

在bean中嵌入intercept-methods和protect标签。
这需要改造配置文件。
<beans:bean id="messageService" class="com.family168.springsecuritybook.ch201.MessageServiceImpl">
    <intercept-methods>
        <protect access="ROLE_ADMIN" method="userMessage"/>
    </intercept-methods>
</beans:bean>

现在messageService中的userMessage()方法只允许拥有ROLE_ADMIN权限的用户才能调用了。
使用intercept-methods面临着几个问题

首先,intercept-methods只能使用jdk14的方式拦截实现了接口的类,而不能用cglib直接拦截无接口的类。
其次,intercept-methods和global-method-security一起使用,同时使用时,global-method-security一切正常,intercept-methods则会完全不起作用。
33.3. 使用annotation控制方法权限

借助jdk5以后支持的annotation,我们直接在代码中设置某一方法的调用权限。
现在有两种选择,使用Spring Security提供的Secured注解,或者使用jsr250规范中定义的注解。
33.3.1. 使用Secured

首先修改global-method-security中的配置,添加支持annotation的参数。
<global-method-security secured-annotations="enabled"/>

然后添加依赖包。
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-core-tiger</artifactId>
  <version>2.0.5.RELEASE</version>
</dependency>

现在我们随便在java代码中添加注解了。
package com.family168.springsecuritybook.ch201;

import org.springframework.security.annotation.Secured;

public class MessageServiceImpl implements MessageService {
    @Secured({"ROLE_ADMIN", "ROLE_USER"})
    public String userMessage() {
        return "user message";
    }
}

在Secured中设置了ROLE_ADMIN和ROLE_USER两个权限,只要当前用户拥有其中任意一个权限都可以调用这个方法。
33.3.2. 使用jsr250

首先还是要修改配置文件。
<global-method-security secured-annotations="enabled"
                           jsr250-annotations="enabled"/>

然后添加依赖包。
<dependency>
  <groupId>javax.annotation</groupId>
  <artifactId>jsr250-api</artifactId>
  <version>1.0</version>
</dependency>

现在可以在代码中使用jsr250中的注解了。
package com.family168.springsecuritybook.ch201;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;

public class MessageServiceImpl implements MessageService {
    @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})
    public String userMessage() {
        return "user message";
    }

    @DenyAll
    public String userMessage2() {
        return "user message";
    }

    @PermitAll
    public String userMessage2() {
        return "user message";
    }
}


RolesAllowed与前面的Secured功能相同,用户只要满足其中定义的权限之一就可以调用方法。

DenyAll拒绝所有的用户调用方法。

PermitAll允许所有的用户调用方法。
从实际使用上来讲,jsr250里多出来的DenyAll和PermitAll纯属浪费,谁会定义谁也不能调用的方法呢?实际上,要是annotation支持布尔操作就好了,比如逻辑并,逻辑或,逻辑否之类的。
还有jsr250中未被支持的RunAs注解,如果能利用起来估计更有趣。
实例在ch201。
TODO: 需要演示一下如何在数据库中配置方法权限控制。
第 34 章 权限管理的基本概念
上一页         部分 III. 内部机制篇         下一页
第 34 章 权限管理的基本概念

34.1. 认证与验证

Spring Security作为权限管理框架,其内部机制可分为两大部分,其一是认证授权auhorization,其二是权限校验authentication。
认证授权authorization是指,根据用户提供的身份凭证,生成权限实体,并为之授予相应的权限。
权限校验authentication是指,用户请求访问被保护资源时,将被保护资源所需的权限和用户权限实体所拥护的权限二者进行比对,如果校验通过则用户可以访问被保护资源,否则拒绝访问。
我们之前讲过的form-login,http-basic, digest都属于认证授权authorization部分的概念,用户可以通过这些机制登录到系统中,系统会为用户生成权限主体,并授予相应的权限。
与之相对的,FilterSecurityInterceptor,Method保护,taglib,@Secured都属于权限校验authentication,无论是对URL的请求,对方法的调用,页面信息的显示,都要求用户拥有相应的权限才能访问,否则请求即被拒绝。
34.2. SecurityContext安全上下文

为使所有的组件都可以通过同一方式访问当前的权限实体,Spring Security特别提供了SecurityContext作为安全上下文,可以直接通过SecurityContextHolder获得当前线程中的SecurityContext。
SecurityContext securityContext = SecurityContextHolder.getContext();

默认情况下,SecurityContext的实现基于ThreadLocal,系统会在每次用户请求时将SecurityContext与当前Thread进行绑定,这在web系统中是很常用的使用方式,服务器维护的线程池允许多个用户同时并发访问系统,而ThreadLocal可以保证隔离不同Thread之间的信息。
当时对于单机应用来说,因为只有一个人使用,并不存在并发的情况,所以完全可以让所有Thread都共享同一个SecurityContext,因此Spring Security为我们提供了不同的策略模式,我们可以通过设置系统变量的方式选择希望使用的策略类。
java -Dspring.security.strategy=MODE_GLOBAL com.family168.springsecuritybook.Main

也可以调用SecurityContextHolder的setStrategyName()方法来修改系统使用的策略。
SecurityContextHolder.setStrategyName("MODE_GLOBAL");

34.3. Authentication验证对象

SecurityContext中保存着实现了Authentication接口的对象,如果用户尚未通过认证,那么SecurityContext.getAuthenticaiton()方法就会返回null。
可以使用Authentication接口中定义的几个方法,获得当前权限实体的信息。
public interface Authentication extends Principal, Serializable {

    GrantedAuthority[] getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated)
        throws IllegalArgumentException;
}

默认情况下,会在某一个进行认证的过滤器中生成一个UsernamePasswordAuthenticationToken实例,并将此实例放到SecurityContext中。

获得权限主体拥有的权限。
权限实体拥有的权限,GrantedAuthority接口内只有一个方法getAuthority(),它的返回值是一个字符串,这个字符串用来标识系统中的某一权限。用户认证后权限实体将拥有一个保存了一系列GrantedAuthority对象的数组,之后可以用于进行验证用户是否可以访问系统中被保护资源。

获得权限主体的凭证,此凭证应该可以唯一标示权限主体。
默认情况下,凭证是用户的登录密码。

获得验证请求有关的附加信息。
默认情况下,附加信息是WebAuthenticationDetails的一个实例,其中保存了客户端ip和sessionid。

获得权限主体。
默认情况下,权限主体是一个实现了UserDetails接口的对象。
第 37 章 用户信息
上一页         部分 III. 内部机制篇         下一页
第 37 章 用户信息

37.1. UserDetails

Spring Security中的UserDetails被作为一个通用的权限主体,凡是涉及到username和password的情况,都会使用到UserDetails和它对应的服务。
常用的服务有从内存中读取用户信息的InMemoryDaoImpl和用数据库中读取用户信息的JdbcDaoImp。它们都实现了UserDetailsService,因此都可以使用loadUserByUsername()方法获得对应用户的信息。
如果使用了LDAP,还会接触到LdapUserDetailsService,它用来从LDAP中获取用户信息。
在org.springframework.security.userdetails包下还包含一个check目录,它主要用来校验用户是否过期,是否被锁定,是否被禁用。
还可以看到一个hierarchicalroles,它的作用是处理角色继承关系,如果希望使用角色继承策略,需要将原始的UserDetailsService通过UserDetailsServiceWrapper进行一下封装,从而获得由UserDetailsWrapper封装的UserDetails,以此来实现角色继承机制。
37.2. 使用角色继承

在Spring Security中,我们可以指定角色间的继承关系,这样可以重用角色权限,减少配置的代码量,让权限配置整体上显得更清晰。
为了使用角色继承功能,我们需要对原有的配置文件进行一些修改。
<authentication-provider user-service-ref="userDetailsServiceWrapper"/>

<beans:bean id="userDetailsServiceWrapper"
    class="org.springframework.security.userdetails.hierarchicalroles.UserDetailsServiceWrapper">
    <beans:property name="userDetailsService" ref="userDetailsService"/>
    <beans:property name="roleHierarchy">
        <beans:bean class="org.springframework.security.userdetails.hierarchicalroles.RoleHierarchyImpl">
            <beans:property name="hierarchy" value="ROLE_ADMIN > ROLE_USER"/>
        </beans:bean>
    </beans:property>
</beans:bean>

<user-service id="userDetailsService">
    <user name="admin" password="admin" authorities="ROLE_ADMIN" />
    <user name="user" password="user" authorities="ROLE_USER" />
</user-service>

我们将原有的user-service单独抽离出来,在userDetailsService的基础上生成一个userDetailsServiceWrapper,这个wrapper的作用就是在原有的user-service的基础上启用角色继承功能。
我们使用RoleHierarchyImpl为userDetailsServiceWrapper配置了角色继承的策略,ROLE_ADMIN > ROLE_USER表示ROLE_ADMIN将继承ROLE_USER所有用的所有角色,只要是允许ROLE_USER访问的资源,ROLE_ADMIN也都有权限进行访问。这样我们在user-service中的配置就可以从ROLE_ADMIN,ROLE_USER简化为ROLE_ADMIN了,而intercept-url中的配置也可以从ROLE_ADMIN,ROLE_USER改为ROLE_USER了。
如果希望配置更多继承关系,可以使用换行进行分隔,比如:
<property name="hierarchy">
    <value>
        ROLE_A > ROLE_B
        ROLE_B > ROLE_AUTHENTICATED
        ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED
    </value>
</property>

实例在ch205。
37.3. 为ACL添加角色继承

目前,直至Spring Security-3.0.0.M1都不支持在acl中使用RoleHierarchy,不过在官网的jira上有人提交了一个patch,如果情况顺利的话,这个patch应该在Spring Security-3.0.0.RC1中被应用到svn中,我们就可以为acl实现角色继承了。
http://jira.springframework.org/browse/SEC-1049
如果希望在Spring Security-2.x中在acl部分实现角色继承,需要进行如下配置。
首先根据jira上的patch自己创建一个SidRoleHierarchyRetrievalStrategyImpl.java。
/* Copyright 2008 Thomas Champagne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.acls.sid;

import java.util.List;

import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.userdetails.hierarchicalroles.RoleHierarchy;
import org.springframework.util.Assert;

/**
* Extended SidRetrievalStrategyImpl which uses a {@link RoleHierarchy} definition to determine the
* roles allocated to the current user.
* @author Thomas Champagne
*/
public class SidRoleHierarchyRetrievalStrategyImpl extends SidRetrievalStrategyImpl {
    private RoleHierarchy roleHierarchy = null;

    public SidRoleHierarchyRetrievalStrategyImpl(RoleHierarchy roleHierarchy) {
        Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
        this.roleHierarchy = roleHierarchy;
    }

    /**
     * Calls the <tt>RoleHierarchy</tt> to obtain the complete set of user authorities.
     */
    GrantedAuthority[] extractAuthorities(Authentication authentication) {
        return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
    }

    public Sid[] getSids(Authentication authentication) {
        GrantedAuthority[] authorities = this.extractAuthorities(authentication);
        Sid[] sids = new Sid[authorities.length + 1];

        sids[0] = new PrincipalSid(authentication);

        for (int i = 1; i <= authorities.length; i++) {
            sids = new GrantedAuthoritySid(authorities[i - 1]);
        }

        return sids;
    }
}

然后在acl的配置文件中配置bean,并在AclEntryVoter,AclEntryAfterInvocationProvider和AclEntryAfterInvocationCollectionFilteringProvider中替换默认的SidRetrievalStrategy。
<bean id="sidRetrievalStrategy"
    class="org.springframework.security.acls.sid.SidRoleHierarchyRetrievalStrategyImpl">
    <constructor-arg ref="roleHierarchy"/>
</bean>

<bean id="afterAclRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationProvider">
    <sec:custom-after-invocation-provider/>
    <constructor-arg ref="aclService"/>
    <constructor-arg>
        <list>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.READ"/>
        </list>
    </constructor-arg>
    <property name="sidRetrievalStrategy" ref="sidRetrievalStrategy"/>
</bean>

<bean id="afterAclCollectionRead" class="org.springframework.security.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">
    <sec:custom-after-invocation-provider/>
    <constructor-arg ref="aclService"/>
    <constructor-arg>
        <list>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.READ"/>
        </list>
    </constructor-arg>
    <property name="sidRetrievalStrategy" ref="sidRetrievalStrategy"/>
</bean>

这样就在acl中添加了对角色继承的支持。
37.4. PasswordEncoder和SaltValue

默认提供的PasswordEncoder包含plaintext, sha, sha-256, md5, md4, {sha}, {ssha}。其中{sha}和{ssha}是专门为ldap准备的,plaintext意味着不对密码进行加密,如果我们不设置PasswordEncoder,默认就会使用它。
SaltValue是为了让密码加密更加安全,它有两种策略可以选择。user-property, system-wide分别对应着ReflectionSaltSource和SystemWideSaltSource,它们的区别是ReflectionSaltSource会使用反射,从用户的Principal对象汇总取出一个对应的属性来作为盐值,而SystemWideSaltSource会为所有用户都设置相同的盐值。
使用了PasswordEncoder和SaltValue的结果就是数据库中的密码变得难以辨认了,这就要注意在添加用户时要使用相同的策略对密码进行加密,这才能保证新用户可以正常登陆。
第 39 章 动态资源管理
上一页         部分 III. 内部机制篇         下一页
第 39 章 动态资源管理

之前在第 5 章 使用数据库管理资源中,我们简要讨论过使用数据库管理资源,为了使手册开始的部分保持简洁,我们没有再深入讨论这个话题,包括实例中存在的一些问题也都没有解决,这一章中,我们会尝试进行更深层次的讨论。
39.1. 基本知识

对应的数据库结构与ER图,可以参考第 5 章 使用数据库管理资源。
拦截器与所需的权限配置数据格式,可以参考第 36 章 拦截器。
所有,我们需要做的就是把数据库中的数据读取出来,组装成拦截器所需的格式,然后把权限配置数据放到拦截器里。
39.2. 读取资源

为了区分URL和METHOD,我们在resc表中使用res_type字段来区分这两种不同的被保护资源。
res_type="URL"对应将在FilterSecurityInterceptor中使用的被保护网址。
INSERT INTO RESC VALUES(1,'','URL','/admin.jsp',1,'')

这里将/admin.jsp作为一个网址进行保护,随后它将被设置到FilterSecurityInterceptor中。
res_type="METHOD"对应将在MethodSecurityInterceptor中使用的被保护方法。
INSERT INFO RESC VALUES(3,'','METHOD','com.family168.springsecuritybook.ch207.MessageService.adminMessage',3,'');

这里将com.family168.springsecuriytbook.ch207.Message的adminMessage()方法设置为被保护资源,随后它将被设置到MethodSecurityInterceptor中。
我们使用如下sql语句从数据库中分别读取被保护的url和method信息。
读取被保护url信息。
  select re.res_string,r.name
    from role r
    join resc_role rr
      on r.id=rr.role_id
    join resc re
      on re.id=rr.resc_id
   where re.res_type='URL'
order by re.priority

读取被保护method信息。
  select re.res_string,r.name
    from role r
    join resc_role rr
      on r.id=rr.role_id
    join resc re
      on re.id=rr.resc_id
   where re.res_type='METHOD'
order by re.priority


为了实现资源的统一配置,我们创建了名为ResourceDetailsMonitor的类用来管理数据库中的被保护资源信息,它负责从数据库中读取原始信息,并转换成FilterSecurityInterceptor和MethodInterceptor所需的数据格式。
39.3. URL资源扩展点

为了动态设置FilterSecurityInterceptor中的资源配置,ResouceDetailsMonitor中直接将组装后的FilterInvocationDefinitionSource使用setObjectDefinitionSource()方法设置到FilterSecurityInterceptor中。
FilterInvocationDefinitionSource source = resourceDetailsBuilder
    .createUrlSource(queryUrl, getUrlMatcher());
filterSecurityInterceptor.setObjectDefinitionSource(source);

之后,FilterSecurityInterceptor就会根据我们设置的资源信息控制用户可以访问哪些资源。
39.4. METHOD资源扩展点

MethodSecurityInterceptor的情况有些复杂,因为涉及到spring中aop的pointcut部分特性,所以直接为MethodSecurityInterceptor设置objectDefinitionSource是不会起作用的。
我们需要获取delegatingMethodDefinitionSource,将数据库中读取的资源信息设置到它里面才能使MethodSecurityInterceptor和动态生成的pointcut都是用我们最新的资源信息。
MethodDefinitionSource source = resourceDetailsBuilder
    .createMethodSource(queryMethod);
List<MethodDefinitionSource> sources = new ArrayList<MethodDefinitionSource>();
sources.add(source);
delegatingMethodDefinitionSource.setMethodDefinitionSources(sources);

因为ACL实际上也是借助于MethodSecurityInterceptor来实现的,所以可以将ACL_READ和AFTER_ACL_READ配置在res_type="METHOD"的资源中。
实例在ch207中。
第 40 章 扩展UserDetails
上一页         部分 III. 内部机制篇         下一页
第 40 章 扩展UserDetails

如果希望扩展登录时加载的用户信息,最简单直接的办法就是实现UserDetails接口,定义一个包含所有业务数据的对象。我们下面演示如何将用户邮箱加入UserDetails中。
40.1. 实现UserDetails接口

UserDetails接口中总共声明了六个方法:
public interface UserDetails extends Serializable {
    GrantedAuthority[] getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}


用户拥有的权限

用户名

密码

用户账号是否过期

用户账号是否被锁定

用户密码是否过期

用户是否可用
我们的任务就是实现这六个接口,同时添加一个getEmail()方法,用以获得用户的邮箱地址。
最初我们的打算是直接继承Spring Security中默认提供的实现类User,但是User为了避免用户信息被外部程序篡改,被设计为只能通过构造方法来为内部数据赋值,没有提供setter方法对其中数据进行修改,因此为了之后演示的方便,我们仿照User类自行实现了一个BaseUserDetails类,在BaseUserDetails中所有属性都被定义为protected,可以暴露给子类进行操作。
在BaseUserDetails的基础上,我们实现了UserInfo类,在它里面添加有关email的属性和方法。
package com.family168.springsecuritybook.ch208;

import org.springframework.security.GrantedAuthority;

public class UserInfo extends BaseUserDetails {

    private static final long serialVersionUID = 1L;

    private String email;

    public UserInfo(String username, String password, boolean enabled, GrantedAuthority[] authorities)
        throws IllegalArgumentException {
        super(username, password, enabled, authorities);
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

40.2. 实现UserDetailsService接口

为了将UserInfo提供给权限系统,我们还需要实现自定义的UserDetailsService,这个接口只包含一个方法:
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException, DataAccessException;
}

实际运行中,系统会通过这个方法获得登录用户的信息。
下面我们直接实现UserDetailsService接口,在其中创建UserInfo的对象。
public class UserInfoService implements UserDetailsService {

    private Map<String, UserInfo> userMap = null;

    public UserInfoService() {
        userMap = new HashMap<String, UserInfo>();
        UserInfo userInfo = null;
        userInfo = new UserInfo("user", "user", true, new GrantedAuthority[]{
            new GrantedAuthorityImpl("ROLE_USER")
        });
        userInfo.setEmail("user@family168.com");
        userMap.put("user", userInfo);
        userInfo = new UserInfo("admin", "admin", true, new GrantedAuthority[]{
            new GrantedAuthorityImpl("ROLE_ADMIN"),
            new GrantedAuthorityImpl("ROLE_USER")
        });
        userInfo.setEmail("admin@family168.com");
        userMap.put("admin", userInfo);
    }

    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException, DataAccessException {
        return userMap.get(username);
    }
}

40.3. 修改配置文件

将UserInfoService添加到配置文件中:
<authentication-provider user-service-ref="userDetailsService"/>

<beans:bean id="userDetailsService" class="com.family168.springsecuritybook.ch208.UserInfoService"/>

定义userDetailsService之后,然后使用user-service-ref为authentication-provider设置对UserDetailsService的引用,这样在系统中就会从我们自定义的UserInfoService中获取用户信息了。
40.4. 测试运行

修改过配置文件后,在ch208中启动mvn,还是通过登录页面进入系统,在登录成功页面中就可以看到用户对应的邮箱地址了。

图 40.1. 显示邮箱地址信息

这时保存在SecurityContext中的Principal已经变为了UserInfo类型的对象,我们可以直接使用taglib获得启动的邮件信息。
email : <sec:authentication property="principal.email"/>

如果希望获得UserInfo对象,可以使用如下代码:
UserInfo userInfo = (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马