Spring Security使用手册

JPass

贡献于2011-10-15

字数:0 关键词: Spring Spring Security 安全相关框架 手册

1 Spring Security 2 目录 Spring Security..................................................................................................................................1 第 1 章 一个简单的HelloWorld...............................................................................................9 1.1. 配置过滤器........................................................................................................................9 1.2. 使用命名空间....................................................................................................................9 1.3. 完善整个项目..................................................................................................................11 1.4. 运行示例..........................................................................................................................13 第 2 章 使用数据库管理用户权限........................................................................................17 2.1. 修改配置文件..................................................................................................................17 2.2. 数据库表结构..................................................................................................................19 第 3 章 自定义数据库表结构................................................................................................21 3.1. 自定义表结构..................................................................................................................21 3.2. 初始化数据......................................................................................................................22 3.3. 获得自定义用户权限信息..............................................................................................23 3.3.1. 处理用户登陆.......................................................................................................23 3.3.2. 检验用户权限.......................................................................................................24 第 4 章 自定义登陆页面........................................................................................................26 4.1. 实现自定义登陆页面......................................................................................................26 4.2. 修改配置文件..................................................................................................................26 4.3. 登陆页面中的参数配置..................................................................................................27 4.4. 测试一下..........................................................................................................................28 第 5 章 使用数据库管理资源................................................................................................30 5.1. 数据库表结构..................................................................................................................30 5.2. 初始化数据......................................................................................................................32 5.3. 实现从数据库中读取资源信息......................................................................................33 5.3.1. 需要何种数据格式...............................................................................................33 5.3.2. 替换原有功能的切入点.......................................................................................39 第 6 章 控制用户信息............................................................................................................43 6.1. MD5 加密 ........................................................................................................................43 6.2. 盐值加密..........................................................................................................................44 6.3. 用户信息缓存..................................................................................................................46 6.4. 获取当前用户信息..........................................................................................................48 第 7 章 自定义访问拒绝页面................................................................................................50 第 8 章 动态管理资源结合自定义登录页面........................................................................52 第 9 章 中文用户名................................................................................................................53 第 10 章 判断用户是否登录..................................................................................................54 第 11 章 图解过滤器..............................................................................................................57 11.1. HttpSessionContextIntegrationFilter..............................................................................57 11.2. LogoutFilter ...................................................................................................................59 11.3. AuthenticationProcessingFilter......................................................................................59 11.4. DefaultLoginPageGeneratingFilter ................................................................................60 11.5. BasicProcessingFilter.....................................................................................................61 11.6. SecurityContextHolderAwareRequestFilter...................................................................62 3 11.7. RememberMeProcessingFilter.......................................................................................63 11.8. AnonymousProcessingFilter ..........................................................................................64 11.9. ExceptionTranslationFilter.............................................................................................65 11.10. SessionFixationProtectionFilter ...................................................................................65 11.11. FilterSecurityInterceptor ..............................................................................................66 第 12 章 管理会话..................................................................................................................69 12.1. 添加监听器....................................................................................................................69 12.2. 添加过滤器....................................................................................................................69 12.3. 控制策略........................................................................................................................70 12.3.1. 后登 陆的将先登录的踢出系统.........................................................................70 12.3.2. 后面 的用户禁止登陆.........................................................................................71 第 13 章 单点登录..................................................................................................................73 13.1. 配置JA-SIG ...................................................................................................................73 13.2. 配置Spring Security.......................................................................................................75 13.2.1. 添加 依赖.............................................................................................................75 13.2.2. 修改applicationContext.xml ...............................................................................76 13.3. 运行配置了cas的子系统...............................................................................................78 13.4. 为cas配置SSL................................................................................................................80 13.4.1. 生成 密钥.............................................................................................................81 13.4.2. 为je tty配置SSL...................................................................................................82 13.4.3. 为t omcat配置SSL...............................................................................................83 第 14 章 basic认证 .................................................................................................................84 14.1. 配置basic验证 ...............................................................................................................84 14.2. 编程实现basic客户端 ...................................................................................................85 第 15 章 标签库......................................................................................................................87 15.1. 配置taglib.......................................................................................................................87 15.2. authenticaiton.................................................................................................................87 15.3. authorize.........................................................................................................................88 15.4. acl/accesscontrollist .......................................................................................................88 15.5. 为不同用户显示各自的登陆成功页面........................................................................89 第 16 章 自动登录..................................................................................................................90 16.1. 默认策略........................................................................................................................90 16.2. 持久化策略....................................................................................................................91 第 17 章 匿名登录..................................................................................................................94 17.1. 配置文件........................................................................................................................94 17.2. 修改默认用户名............................................................................................................96 17.3. 匿名用户的限制............................................................................................................97 第 18 章 防御会话伪造........................................................................................................100 18.1. 攻击场景......................................................................................................................100 18.2. 解决会话伪造..............................................................................................................100 第 19 章 预先认证................................................................................................................102 19.1. 为jet ty配置Realm ........................................................................................................102 19.2. 配置Spring Security.....................................................................................................103 第 20 章 切换用户................................................................................................................107 4 20.1. 配置方式......................................................................................................................107 20.2. 实例演示......................................................................................................................108 第 21 章 信道安全................................................................................................................109 21.1. 设置信道安全..............................................................................................................109 21.2. 指定http和https的端口................................................................................................109 第 22 章 digest认证..............................................................................................................111 22.1. 配置digest验证............................................................................................................111 22.2. 使用ajax实现digest认证..............................................................................................113 22.3. 编程实现digest客户端 ................................................................................................113 第 23 章 通过LDAP获取用户信息 .....................................................................................115 第 24 章 通过OpenID进行登录...........................................................................................118 24.1. 配置..............................................................................................................................118 24.2. 系统时间问题..............................................................................................................120 第 25 章 使用X509 登录......................................................................................................122 25.1. 生成证书......................................................................................................................122 25.2. 配置服务器使用双向加密..........................................................................................123 25.3. 配置X509 认证............................................................................................................123 第 26 章 使用NTLM登录(无法成功登陆).....................................................................125 第 27 章 使用JAAS机制 ......................................................................................................129 第 28 章 使用HttpInvoker ....................................................................................................134 第 29 章 使用rmi ..................................................................................................................135 第 30 章 控制portal的权限 ..................................................................................................136 第 31 章 保存登录之前的请求............................................................................................138 第 32 章 记录操作日志........................................................................................................141 第 33 章 保护方法调用........................................................................................................143 33.1. 控制全局范围的方法权限..........................................................................................143 33.2. 控制某个bean内的方法权限 ......................................................................................144 33.3. 使用annotation控制方法权限.....................................................................................145 33.3.1. 使用Secured......................................................................................................145 33.3.2. 使用jsr250.........................................................................................................146 第 34 章 权限管理的基本概念............................................................................................149 34.1. 认证与验证..................................................................................................................149 34.2. SecurityContext安全上下文........................................................................................149 34.3. Authentication验证对象..............................................................................................150 第 35 章 Vo t e r表决者 ...........................................................................................................153 35.1. Voter表决者 .................................................................................................................153 35.2. RoleVoter .....................................................................................................................154 35.3. AuthenticatedVoter.......................................................................................................154 35.4. AbstractAclVoter..........................................................................................................155 第 36 章 拦截器....................................................................................................................156 36.1. 权限配置数据源..........................................................................................................156 36.2. 权限管理器..................................................................................................................158 36.3. 后置调用管理器..........................................................................................................159 36.4. 临时分配额外权限......................................................................................................159 5 第 37 章 用户信息................................................................................................................161 37.1. UserDetails...................................................................................................................161 37.2. 使用角色继承..............................................................................................................161 37.3. 为ACL添加角色继承..................................................................................................163 37.4. PasswordEncoder和SaltValue......................................................................................166 第 38 章 集成jcaptcha ..........................................................................................................168 第 39 章 动态资源管理........................................................................................................170 39.1. 基本知识......................................................................................................................170 39.2. 读取资源......................................................................................................................170 39.3. URL资源扩展点..........................................................................................................172 39.4. METHOD资源扩展点.................................................................................................172 第 40 章 扩展UserDetails.....................................................................................................174 40.1. 实现UserDetails接口...................................................................................................174 40.2. 实现UserDetailsService接口.......................................................................................176 40.3. 修改配置文件..............................................................................................................177 40.4. 测试运行......................................................................................................................177 第 41 章 锁定用户................................................................................................................179 第 42 章 设置过滤器链........................................................................................................182 第 43 章 自定义过滤器........................................................................................................185 第 44 章 使用用户组............................................................................................................188 44.1. 数据库结构..................................................................................................................188 44.2. 修改配置文件..............................................................................................................190 第 45 章 在JSF中使用Spring Security.................................................................................191 45.1. 修改过滤器支持forward .............................................................................................191 45.2. 自定义登录页面..........................................................................................................191 45.3. 显示密码错误信息......................................................................................................193 第 46 章 自定义会话管理....................................................................................................195 46.1. 默认策略的缺陷..........................................................................................................195 46.2. 记录用户名与ip...........................................................................................................196 46.3. 改造控制类..................................................................................................................197 46.4. 修改配置文件..............................................................................................................198 第 47 章 匹配URL地址........................................................................................................201 47.1. AntUrlPathMatcher ......................................................................................................201 47.2. RegexUrlPathMatcher..................................................................................................202 47.3. lowercase-comparisons ................................................................................................203 第 48 章 配置过滤器............................................................................................................204 48.1. 标准过滤器..................................................................................................................204 48.2. 在ht tp中启用标准过滤器............................................................................................205 48.3. 为自定义过滤器设置位置..........................................................................................207 第 49 章 监控会话过期........................................................................................................209 49.1. 实现原理......................................................................................................................209 49.2. 代码实现......................................................................................................................210 49.3. 目前实现的缺陷..........................................................................................................213 第 50 章 多个登陆页面........................................................................................................214 6 50.1. 未登录自动跳转到对应的登录页面..........................................................................214 50.2. 密码出错时返回对应页面..........................................................................................217 第 51 章 角色继承................................................................................................................218 51.1. 使用RoleHierarchyVoter..............................................................................................218 51.2. 使用数据库实现RoleHierarchy ..................................................................................219 第 52 章 设置方法拦截器....................................................................................................222 第 53 章 ACL基本操作........................................................................................................224 53.1. 准备数据库和aclService .............................................................................................224 53.1.1. 为acl配置cache................................................................................................. 224 53.1.2. 配置lookupStrategy ..........................................................................................225 53.1.3. 配置aclService ..................................................................................................226 53.2. 使用aclService管理acl信息 ........................................................................................226 53.3. 使用acl控制delete操作................................................................................................227 53.4. 控制用户可以看到哪些信息......................................................................................230 第 54 章 管理acl...................................................................................................................233 54.1. 管理多个domain类......................................................................................................233 54.2. 动态授权与收回授权..................................................................................................234 54.2.1. 获得 对象的acl权限 ..........................................................................................234 54.2.2. 添加 授权...........................................................................................................236 54.2.3. 收回 授权...........................................................................................................237 第 55 章 acl自动提醒...........................................................................................................239 55.1. 自动创建acl.................................................................................................................239 55.2. 自动删除acl.................................................................................................................241 55.3. 根据id删除acl..............................................................................................................243 第 56 章 最简控制台............................................................................................................247 56.1. 平台搭建......................................................................................................................247 56.2. 用户登录......................................................................................................................249 56.3. 用户信息列表..............................................................................................................250 56.4. 添加用户......................................................................................................................252 56.5. 修改用户信息..............................................................................................................254 56.6. 修改自己的密码..........................................................................................................256 第 57 章 用户组控制台........................................................................................................259 57.1. 添加对用户组的支持..................................................................................................259 57.2. 浏览用户组..................................................................................................................259 57.3. 创建用户组..................................................................................................................260 57.4. 修改用户组..................................................................................................................261 附录:...................................................................................................................................264 A:常见问题解答................................................................................................................264 B:数据库表结构................................................................................................................267 C:异常................................................................................................................................270 D:事件................................................................................................................................271 F:RBAC模型 .....................................................................................................................272 F1. RBAC模型介绍..............................................................................................................273 F2. 有关概念 ........................................................................................................................273 7 F2.1. 什么是角色 .........................................................................................................274 F2.2. 角色与用户组 .....................................................................................................275 F3. 基本模型RBAC0 ............................................................................................................277 F3.1. RBAC0 模型的形式定义如下.............................................................................279 F4. 角色分级模型RBAC1 ....................................................................................................280 F4.1. 定义 2:RBAC1 由以下内容确定......................................................................281 5. 限制模型RBAC2 ..............................................................................................................282 F5.1. 定义 3: ..............................................................................................................282 F6. 统一模型RBAC3 ............................................................................................................285 F7. 定义 4 .............................................................................................................................287 F8. 在ARBAC97 中,包括三种组件..................................................................................288 F9. RBAC模型的特点..........................................................................................................290 F10. 基于party的模型 ..........................................................................................................291 F11. 有关operation ...............................................................................................................292 8 部分 I 基础篇 在一开始,我们主要谈谈怎么配置 Spring Security,怎么使用 Spring Security。 为了避免在每个例子中重复包含所有的第三方依赖,要知道 Spring.jar 就有 2M 多,所以我们使用了 Maven2 管理项目。 我们用使用的第三方依赖库关系如下所示: [INFO] [dependency:tree] INFO] com.family168.springsecuritybook:ch001:war:0.1 INFO] \- org.springframework.security:spring-security-taglibs:jar:2.0.5.RELEASE:compile INFO] +- org.springframework.security:spring-security-core:jar:2.0.5.RELEASE:compile INFO] | +- org.springframework:spring-core:jar:2.0.8:compile INFO] | +- org.springframework:spring-context:jar:2.0.8:compile INFO] | | \- aopalliance:aopalliance:jar:1.0:compile INFO] | +- org.springframework:spring-aop:jar:2.0.8:compile INFO] | +- org.springframework:spring-support:jar:2.0.8:runtime INFO] | +- commons-logging:commons-logging:jar:1.1.1:compile INFO] | +- commons-codec:commons-codec:jar:1.3:compile INFO] | \- commons-collections:commons-collections:jar:3.2:compile INFO] +- org.springframework.security:spring-security-acl:jar:2.0.5.RELEASE:compile INFO] | \- org.springframework:spring-jdbc:jar:2.0.8:compile INFO] | \- org.springframework:spring-dao:jar:2.0.8:compile INFO] \- org.springframework:spring-web:jar:2.0.8:compile INFO] \- org.springframework:spring-beans:jar:2.0.8:compile 9 第 1 章 一个简单的HelloWorld Spring Security 中可以使用 Acegi-1.x 时代的普通配置方式,也可以使用 从 2.0 时代才出现的命名空间配置方式,实际上这两者实现的功能是完 全一致的,只是新的命名空间配置方式可以把原来需要几百行的配置压 缩成短短的几十行。我们的教程中都会使用命名空间的方式进行配置, 凡事务求最简。 1.1. 配置过滤器 为了在项目中使用 Spring Security 控制权限,首先要在 web.xml 中配置 过滤器,这样我们就可以控制对这个项目的每个请求了。 springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* 所有的用户在访问项目之前,都要先通过 Spring Security 的检测,这从 第一时间把没有授权的请求排除在系统之外,保证系统资源的安全。关 于过滤器配置的更多讲解可以参考。 1.2. 使用命名空间 10 在 applicationContext.xml 中使用 Spring Security 提供的命名空间进行配 置。 声明在 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 都满足要求,但因为第一个 11 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 看起来像是这样: contextConfigLocation classpath:applicationContext*.xml 12 springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* org.springframework.web.context.ContextLoaderListener 演示不同权限的用户登陆之后可以访问不同的资源,我们为项目添加了 两个 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 13 1.4. 运行示例 首先确保自己安装了 Maven2。 安装好 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 为了避免可能 出现的风险,连测试用的登录页面都自动生成出来了。在这里我们就省 去编写登陆页面的步骤,直接使用默认生成的登录页面进行演示吧。 14 首先让我们输入一个错误用的用户名或密码,这里我们使用 test/test, 当然这个用户是不存在的,点击提交之后我们会得到这样一个登陆错误 提示页面。 图 1.2. 登陆失败 如果输入的是正确的用户名和密码,比如 user/user,系统在登陆成功后 会默认跳转到 index.jsp。 图 1.3. 登陆成功 这时我们可以点击 admin.jsp 链接访问 admin.jsp,也可以点击 logout 进 行注销。 15 如果点击了 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 16 根据我们之前的配置,admin 用户拥有 ROLE_ADMIN 和 ROLE_USER 两个权限,因为他拥有 ROLE_USER 权限,所以可以访问/index.jsp,因 为他拥有 ROLE_ADMIN 权限,所以他可以访问/admin.jsp。 至此,我们很高兴的宣布,咱们已经正式完成,并运行演示了一个最简 单的由 Spring Security 保护的 web 系统,下一步我们会深入讨论 Spring Security 为我们提供的其他保护功能,多姿多彩的特性。 17 第 2 章 使用数据库管理用户权限 上一章节中,我们把用户信息和权限信息放到了 xml 文件中,这是为了 演示如何使用最小的配置就可以使用 Spring Security,而实际开发中, 用户信息和权限信息通常是被保存在数据库中的,为此 Spring Security 提供了通过数据库获得用户权限信息的方式。 2.1. 修改配置文件 为了从数据库中获取用户权限信息,我们所需要的仅仅是修改配置文件 中的 authentication-provider 部分。 将上一章配置文件中的 user-service 替换为 jdbc-user-service,替换内容 如下所示: 将上述红色部分替换为下面黄色部分。 18 现在只要再为jdbc-user-service提供一个dataSource就可以让Spring Security使用数据库中的权限信息了。在此我们使用spring创建一个演示 用的dataSource实现,这个dataSource会连接到hsqldb数据库,从中获取 用户权限信息。[1] 最终的配置文件如下所示: 19 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 表中的登录名和密码用来控制用 20 户的登录,而权限表中的信息用来控制用户登陆后是否有权限访问受保 护的系统资源。 我们在示例中预先初始化了一部分数据: 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容器统一管理提供的。 21 第 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 22 ); 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. 初始化数据 23 创建两个用户,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=? 24 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 从我们自定 义的表结构中提取数据了。最终配置文件如下所示: users-by-username-query 为根据用户名查找用户,系统通过传入的用户名 查询当前用户的登录名,密码和是否被禁用这一状态。 25 authorities-by-username-query 为根据用户名查找权限,系统通过传入的 用户名查询当前用户已被授予的所有权限。 实例见 ch003。 26 第 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 标签。 让没登陆的用户也可以访问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 页面的主要代码。
登陆失败
${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}
登陆 用户:
密码:
两周之内不必登陆
28 /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. 进入登录页面 29 图 4.2. 用户登陆失败 实例见 ch004。 30 第 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), 31 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 图如下所示: 32 图 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); 33 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 网址和访问这些网址所需的权限: Spring Security 所做的就是在系统初始化时,将以上 XML 中的信息转换 为特定的数据格式,而框架中其他组件可以利用这些特定格式的数据, 用于控制之后的验证操作。 34 现在这些资源信息都保存在数据库中,我们可以使用上面介绍的 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 下面要开始编写实现代码了。 1. 搜索数据库获得资源信息。 我们通过定义一个 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()方法获得所有资源信息。 35 protected Map findResources() { ResourceMapping resourceMapping = new ResourceMapping(getDataSource(), resourceQuery); Map resourceMap = new LinkedHashMap(); for (Resource resource : (List) 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; } 2. 使用获得的资源信息组装 requestMap。 3. protected LinkedHashMap buildRequestMap() { 4. LinkedHashMap requestMap = null; 5. requestMap = new LinkedHashMap(); 6. 7. ConfigAttributeEditor editor = new ConfigAttributeEditor(); 8. 9. Map resourceMap = this.findResources(); 10. 11. for (Map.Entry entry : resourceMap.entrySet()) { 12. RequestKey key = new RequestKey(entry.getKey(), null); 13. editor.setAsText(entry.getValue()); 14. requestMap.put(key, 15. (ConfigAttributeDefinition) editor.getValue()); 16. } 17. 18. return requestMap; 19. } 36 20. 使用 urlMatcher 和 requestMap 创建 DefaultFilterInvocationDefinitionSource。 21. public Object getObject() { 22. return new DefaultFilterInvocationDefinitionSource(this 23. .getUrlMatcher(), this.buildRequestMap()); 24. } 这样我们就获得了 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; 37 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 findResources() { ResourceMapping resourceMapping = new ResourceMapping(getDataSource(), resourceQuery); Map resourceMap = new LinkedHashMap(); for (Resource resource : (List) 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 buildRequestMap() { LinkedHashMap requestMap = null; 38 requestMap = new LinkedHashMap(); ConfigAttributeEditor editor = new ConfigAttributeEditor(); Map resourceMap = this.findResources(); for (Map.Entry 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, 39 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 中配置我们编写的代码。 下一步使用这个 filterInvocationDefinitionSource 创建 filterSecurityInterceptor,并使用它替换系统原来创建的那个过滤器。 40 注意这个 custom-filter 标签,它表示将 filterSecurityInterceptor 放在框架 原来的 FILTER_SECURITY_INTERCEPTOR 过滤器之前,这样我们的 过滤器会先于原来的过滤器执行,因为它的功能与老过滤器完全一样, 所以这就等于把原来的过滤器替换掉了。 完整的配置文件如下所示: 41 实例见 ch05。 目前存在的问题是,系统会在初始化时一次将所有资源加载到内存中, 即使在数据库中修改了资源信息,系统也不会再次去从数据库中读取资 42 源信息。这就造成了每次修改完数据库后,都需要重启系统才能时资源 配置生效。 解决方案是,如果数据库中的资源出现的变化,需要刷新内存中已加载 的资源信息时,使用下面代码: <%@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); %> 43 第 6 章 控制用户信息 让我们来研究一些与用户信息相关的功能,包括为用户密码加密,缓存 用户信息,获得系统当前登陆的用户,获得登陆用户的所有权限。 6.1. MD5 加密 任何一个正式的企业应用中,都不会在数据库中使用明文来保存密码 的,我们在之前的章节中都是为了方便起见没有对数据库中的用户密码 进行加密,这在实际应用中是极为幼稚的做法。可以想象一下,只要有 人进入数据库就可以看到所有人的密码,这是一件多么恐怖的事情,为 此我们至少要对密码进行加密,这样即使数据库被攻破,也可以保证用 户密码的安全。 最常用的方法是使用 MD5 算法对密码进行摘要加密,这是一种单项加 密手段,无法通过加密后的结果反推回原来的密码明文。 首先我们要把数据库中原来保存的密码使用 MD5 进行加密: INSERT INTO USERS VALUES('admin','21232f297a57a5a743894a0e4a801fc3',TRUE) INSERT INTO USERS VALUES('user','ee11cbb19052e40b07aac0ca060c23ee',TRUE) 现在密码部分已经面目全非了,即使有人攻破了数据库,拿到这种“乱 码”也无法登陆系统窃取客户的信息。 44 下一步为了让 Spring Security 支持 MD5 加密,我们需要修改一下配置 文件。 上述代码中新增的黄色部分,将启用 MD5 算法。用户登录时,输入的 密码是明文,需要使用 password-encoder 将明文转换成 md5 形式,然后 再与数据库中的已加密密码进行比对。 这些配置对普通客户不会造成任何影响,他们只需要输入自己的密码, Spring Security 会自动加以演算,将生成的结果与数据库中保存的信息 进行比对,以此来判断用户是否可以登陆。 这样,我们只添加了一行配置,就为系统带来了密码加密的功能。 6.2. 盐值加密 实际上,上面的实例在现实使用中还存在着一个不小的问题。虽然md5 算法是不可逆的,但是因为它对同一个字符串计算的结果是唯一的,所 以一些人可能会使用“字典攻击”的方式来攻破md5 加密的系统[5]。这 虽然属于暴力解密,却十分有效,因为大多数系统的用户密码都不回很 长。 45 实际上,大多数系统都是用 admin 作为默认的管理员登陆密码,所以, 当我们在数据库中看到“21232f297a57a5a743894a0e4a801fc3”时,就 可以意识到 admin 用户使用的密码了。因此,md5 在处理这种常用字符 串时,并不怎么奏效。 为了解决这个问题,我们可以使用盐值加密“salt-source”。 修改配置文件: 在 password-encoder 下添加了 salt-source,并且指定使用 username 作为 盐值。 盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再 使用 md5 对合并后的内容进行演算,这样一来,就算密码是一个很常 见的字符串,再加上用户名,最后算出来的 md5 值就没那么容易猜出 来了。因为攻击者不知道盐值的值,也很难反算出密码原文。 我们这里将每个用户的 username 作为盐值,最后数据库中的密码部分 就变成了这样: INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE) INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE) 46 6.3. 用户信息缓存 介于系统的用户信息并不会经常改变,因此使用缓存就成为了提升性能 的一个非常好的选择。Spring Security 内置的缓存实现是基于 ehcache 的,为了启用缓存功能,我们要在配置文件中添加相关的内容。 我们在 jdbc-user-service 部分添加了对 userCache 的引用,它将使用这个 bean 作为用户权限缓存的实现。对 userCache 的配置如下所示: EhCacheBasedUserCache 是 Spring Security 内置的缓存实现,它将为 jdbc-user-service 提供缓存功能。它所引用的 userEhCache 来自 spring 提 47 供的 EhCacheFactoryBean 和 EhCacheManagerFactoryBean,对于 userCache 的缓存配置放在 ehcache.xml 中: 内存中最多存放 100 个对象。 不是永久缓存。 最大空闲时间为 600 秒。 最大活动时间为 3600 秒。 如果内存对象溢出则保存到磁盘。 如果想了解有关ehcache的更多配置,可以访问它的官方网站 http://ehcache.sf.net/。 48 这样,我们就为用户权限信息设置好了缓存,当一个用户多次访问应用 时,不需要每次去访问数据库了,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" %>
username :
如果想在程序中获得当前登陆用户对应的对象。 UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); 49 如果想获得当前登陆用户所拥有的所有权限。 GrantedAuthority[] authorities = userDetails.getAuthorities(); 关于 UserDetails 是如何放到 SecuirtyContext 中去的,以及 Spring Security 所使用的 TheadLocal 模式,我们会在后面详细介绍。这里我们已经了 解了如何获得当前登陆用户的信息。 [5] 所谓字典攻击,就是指将大量常用字符串使用md5 加密,形成字典库,然后将一段由md5 演算 得到的未知字符串,在字典库中进行搜索,当发现匹配的结果时,就可以获得对应的加密前的字 符串内容。 50 第 7 章 自定义访问拒绝页面 在我们的例子中,user 用户是不能访问/admin.jsp 页面的,当我们使用 user 用户登录系统之后,访问/admin.jsp 时系统默认会返回 403 响应。 图 7.1. 403 响应 如果我们希望自定义访问拒绝页面,只需要随便创建一个 jsp 页面,让 后将这个页面的位置放到配置文件中。 下面创建一个 accessDenied.jsp <%@ page contentType="text/html;charset=UTF-8"%> Access Denied 51

Access Denied


访问被拒绝
${requestScope['SPRING_SECURITY_403_EXCEPTION'].message}

下一步修改配置文件,添加自定义访问拒绝页面的地址。 现在访问拒绝的页面就变成了下面这样: 图 7.2. 自定义访问拒绝页面 实例在 ch007。 52 第 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) 这样就实现了将动态管理资源与自定义登录页面进行结合。 实例在 ch008。 53 第 9 章 中文用户名 只要使用统一编码——一般是 UTF-8,避免了乱码的问题就行了。不存 在用户名无法使用中文的情况。 实例在 ch009。 54 第 10 章 判断用户是否登录 有些情况,只要用户登录就可以访问某些资源,而不需要具体要求用户 拥有哪些权限,这时候可以使用 IS_AUTHENTICATED_FULLY,配置 如下所示: 这样除了/admin.jsp 之外所有的 URL,只要是登录用户就可以访问了。 实例在 ch010。 55 部分 II 保护 web 篇 信息: FilterChainProxy: FilterChainProxy[ UrlMatcher = org.springframework.security.util.AntUrlPathMatcher[requiresLowerCase='true']; Filter Chains: { /**=[ org.springframework.security.context.HttpSessionContextIntegrationFilter[ order=200 ;], org.springframework.security.ui.logout.LogoutFilter[ order=300; ], org.springframework.security.ui.webapp.AuthenticationProcessingFilter[ order=700; ], org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter[ order=900; ], org.springframework.security.ui.basicauth.BasicProcessingFilter[ order=1000; ], org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter[ order =1100; ], org.springframework.security.ui.rememberme.RememberMeProcessingFilter[ order=1200; ], org.springframework.security.providers.anonymous.AnonymousProcessingFilter[ order=1 300; ], org.springframework.security.ui.ExceptionTranslationFilter[ order=1400; ], org.springframework.security.ui.SessionFixationProtectionFilter[ order=1600; ], org.springframework.security.intercept.web.FilterSecurityInterceptor@e2fbeb ] } ] 56 Spring Security 一启动就会包含这样一批负责各种安全管理的过滤器, 这部分的任务就是详细讲解每个过滤器的功能和用法,并讨论与之相关 的各种控制权限的方法。 57 第 11 章 图解过滤器 图 11.1. auto-config='true'时的过滤器列表 11.1. HttpSessionContextIntegrationFilter 58 图 11.2. org.springframework.security.context.HttpSessionContextInteg rationFilter 位于过滤器顶端,第一个起作用的过滤器。 用途一,在执行其他过滤器之前,率先判断用户的 session 中是否已经 存在一个 SecurityContext 了。如果存在,就把 SecurityContext 拿出来, 放到 SecurityContextHolder 中,供 Spring Security 的其他部分使用。如 果不存在,就创建一个 SecurityContext 出来,还是放到 SecurityContextHolder 中,供 Spring Security 的其他部分使用。 59 用途二,在所有过滤器执行完毕后,清空 SecurityContextHolder,因为 SecurityContextHolder 是基于 ThreadLocal 的,如果在操作完成后清空 ThreadLocal,会受到服务器的线程池机制的影响。 11.2. LogoutFilter 图 11.3. org.springframework.security.ui.logout.LogoutFilter 只处理注销请求,默认为/j_spring_security_logout。 用途是在用户发送注销请求时,销毁用户 session,清空 SecurityContextHolder,然后重定向到注销成功页面。可以与 rememberMe 之类的机制结合,在注销的同时清空用户 cookie。 11.3. AuthenticationProcessingFilter 60 图 11.4. org.springframework.security.ui.webapp.AuthenticationProces singFilter 处理 form 登陆的过滤器,与 form 登陆有关的所有操作都是在此进行的。 默认情况下只处理/j_spring_security_check 请求,这个请求应该是用户 使用 form 登陆后的提交地址,form 所需的其他参数可以参考:???。 此过滤器执行的基本操作时,通过用户名和密码判断用户是否有效,如 果登录成功就跳转到成功页面(可能是登陆之前访问的受保护页面,也 可能是默认的成功页面),如果登录失败,就跳转到失败页面。 11.4. DefaultLoginPageGeneratingFilter 61 图 11.5. org.springframework.security.ui.webapp.DefaultLoginPageGen eratingFilter 此过滤器用来生成一个默认的登录页面,默认的访问地址为 /spring_security_login,这个默认的登录页面虽然支持用户输入用户名, 密码,也支持 rememberMe 功能,但是因为太难看了,只能是在演示时 做个样子,不可能直接用在实际项目中。 如果想自定义登陆页面,可以参考:第 4 章 自定义登陆页面。 11.5. BasicProcessingFilter 62 图 11.6. org.springframework.security.ui.basicauth.BasicProcessingFilt er 此过滤器用于进行 basic 验证,功能与 AuthenticationProcessingFilter 类 似,只是验证的方式不同。有关 basic 验证的详细情况,我们会在后面 的章节中详细介绍。 有关basic验证的详细信息,可以参考:第 14 章 basic认证。 11.6. SecurityContextHolderAwareRequestFilter 63 图 11.7. org.springframework.security.wrapper.SecurityContextHolder AwareRequestFilter 此过滤器用来包装客户的请求。目的是在原始请求的基础上,为后续程 序提供一些额外的数据。比如 getRemoteUser()时直接返回当前登陆的用 户名之类的。 11.7. RememberMeProcessingFilter 64 图 11.8. org.springframework.security.ui.rememberme.RememberMeP rocessingFilter 此过滤器实现 RememberMe 功能,当用户 cookie 中存在 rememberMe 的标记,此过滤器会根据标记自动实现用户登陆,并创建 SecurityContext,授予对应的权限。 有关rememberMe功能的详细信息,可以参考:第 16 章 自动登录。 11.8. AnonymousProcessingFilter 图 11.9. org.springframework.security.providers.anonymous.Anonymo usProcessingFilter 为了保证操作统一性,当用户没有登陆时,默认为用户分配匿名用户的 权限。 65 有关匿名登录功能的详细信息,可以参考:第 17 章 匿名登录。 11.9. ExceptionTranslationFilter 图 11.10. org.springframework.security.ui.ExceptionTranslationFilter 此过滤器的作用是处理中 FilterSecurityInterceptor 抛出的异常,然后将 请求重定向到对应页面,或返回对应的响应错误代码。 11.10. SessionFixationProtectionFilter 66 图 11.11. org.springframework.security.ui.SessionFixationProtectionFil ter 防御会话伪造攻击。有关防御会话伪造的详细信息,可以参考: 第 18 章 防御会话伪造。 11.11. FilterSecurityInterceptor 67 图 11.12. org.springframework.security.intercept.web.FilterSecurityInt erceptor 用户的权限控制都包含在这个过滤器中。 功能一:如果用户尚未登陆,则抛出 AuthenticationCredentialsNotFoundException“尚未认证异常”。 功能二:如果用户已登录,但是没有访问当前资源的权限,则抛出 AccessDeniedException“拒绝访问异常”。 功能三:如果用户已登录,也具有访问当前资源的权限,则放行。 68 至此,我们完全展示了默认情况下 Spring Security 中使用到的过滤器, 以及每个过滤器的应用场景和显示功能,下面我们会对这些过滤器的配 置和用法进行逐一介绍。 69 第 12 章 管理会话 多个用户不能使用同一个账号同时登陆系统。 12.1. 添加监听器 在 web.xml 中添加一个监听器,这个监听器会在 session 创建和销毁的 时候通知 Spring Security。 org.springframework.security.ui.session.HttpSessionEventPublisher 这种监听 session 生命周期的监听器主要用来收集在线用户的信息,比 如统计在线用户数之类的事。 12.2. 添加过滤器 在 xml 中添加控制同步 session 的过滤器。 70 因为 Spring Security 的作者不认为控制会话是一个大家都经常使用的功 能,所以 concurrent-session-control 没有包含在默认生成的过滤器链中, 在我们需要使用它的时候,需要自己把它添加到 http 元素中。 这个 concurrent-session-control 对应的过滤器类是 org.springframework.security.concurrent.ConcurrentSessionFilter,它的排 序代码是 100,它会被放在过滤器链的最顶端,在所有过滤器使用之前 起作用。 12.3. 控制策略 12.3.1. 后登陆的将先登录的踢出系统 默认情况下,后登陆的用户会把先登录的用户踢出系统。 想测试一下的话,先打开 firefox 使用 user/user 登陆系统,然后再打开 ie 使用 user/user 登陆系统。这时 ie 下的 user 用户会登陆成功,进入登 陆成功页面。而 firefox 下的用户如何刷新页面,就会显示如下信息: This session has been expired (possibly due to multiple concurrent logins being attempted as the same user). 这是因为先登录的用户已经被强行踢出了系统,如果他再次使用 user/user 登陆,ie 下的用户也会被踢出系统了。 71 12.3.2. 后面的用户禁止登陆 如果不想让之前登录的用户被自动踢出系统,需要为 concurrent-session-control 设置一个参数。 这个参数用来控制是否在会话数目超过最大限制时抛出异常,默认值是 false,也就是不抛出异常,而是把之前的 session 都销毁掉,所以之前 登陆的用户就会被踢出系统了。 现在我们把这个参数改为 true,再使用同一个账号同时登陆一下系统, 看看会发生什么现象。 图 12.1. 禁止同一账号多次登录 72 很好,现在只要有一个人使用 user/user 登陆过系统,其他人就不能再次 登录了。这样可能出现一个问题,如果有人登陆的时候因为某些问题没 有进行 logout 就退出了系统,那么他只能等到 session 过期自动销毁之 后,才能再次登录系统。 实例在 ch102。 73 第 13 章 单点登录 所谓单点登录,SSO(Single Sign On),就是把 N 个应用的登录系统整合 在一起,这样一来无论用户登录了任何一个应用,都可以直接以登录过 的身份访问其他应用,不用每次访问其他系统再去登陆一遍了。 Spring Security没有实现自己的SSO,而是整合了耶鲁大学单点登陆 (JA-SIG),这是当前使用很广泛的一种SSO实现,它是基于中央认证服 务CAS(Center Authentication Service)的结构实现的,可以访问它们的官 方网站获得更详细的信息 http://www.jasig.org/cas。 在了解过这些基础知识之后,我们可以开始研究如何使用 Spring Security 实现单点登录了。 13.1. 配置JA-SIG 从JA-SIG的官方网站下载cas-server,本文写作时的最新稳定版为 3.3.2。 http://www.ja-sig.org/downloads/cas/cas-server-3.3.2-release.zip。 将下载得到的 cas-server-3.3.2-release.zip 文件解压后,可以得到一大堆 的目录和文件,我们这里需要的是 modules 目录下的 cas-server-webapp-3.3.2.war。 把cas-server-webapp-3.3.2.war放到ch103\server目录下,然后执行run.bat 就可启动 CAS 中央认证服务器。 74 我们已在pom.xml中配置好了启用SSL所需的配置,包括使用的server.jks 和对应密码,之后我们可以通过 https://localhost:9443/cas/login访问CAS 中央认证服务器。 图 13.1. 登陆页面 默认情况下,只要输入相同的用户名和密码就可以登陆系统,比如我们 使用 user/user 进行登陆。 75 图 13.2. 登陆成功 这就证明中央认证服务器已经跑起来了。下一步我们来配置 Spring Security,让它通过中央认证服务器进行登录。 13.2. 配置Spring Security 13.2.1. 添加依赖 首先要添加对 cas 的插件和 cas 客户端的依赖库。因为我们使用了 maven2,所以只需要在 pom.xml 中添加一个依赖即可。 org.springframework.security spring-security-cas-client 2.0.5.RELEASE 76 如果有人很不幸的没有使用 maven2,那么就需要手工把去下面这些依 赖库了。 spring-security-cas-client-2.0.5.RELEASE.jar spring-dao-2.0.8.jar aopalliance-1.0.jar cas-client-core-3.1.5.jar 大家可以去 spring 和 ja-sig 的网站去寻找这些依赖库。 13.2.2. 修改applicationContext.xml 首先修改 http 部分。 添加一个 entry-point-ref 引用 cas 提供的 casProcessingFilterEntryPoint,这样在验证用户登录时就用上 cas 提供 的机制了。 修改注销页面,将注销请求转发给 cas 处理。 Logout of CAS 然后要提供 userService 和 authenticationManager,二者会被注入到 cas 的类中用来进行登录之后的用户授权。 77 为了演示方便,我们将用户信息直接写在了配置文件中,之后 cas 的类就可 以通过 id 获得 userService,以此获得其中定义的用户信息和对应的权限。 对于 authenticationManager 来说,我们没有创建一个新实例,而是使用了 “别名”(alias),这是因为在之前的 namespace 配置时已经自动生成了 authenticationManager 的实例,cas 只需要知道这个实例的别名就可以直 接调用。 创建 cas 的 filter, entryPoint, serviceProperties 和 authenticationProvider。 78 casProcessingFilter 最终是要放到 Spring security 的安全过滤器链中才 能发挥作用的。这里使用的 customer-filter 就会把它放到 CAS_PROCESSING_FILTER 位置的后面。 这个位置具体是在 LogoutFilter 和 AuthenticationProcessingFilter 之 间,这样既不会影响注销操作,又可以在用户进行表单登陆之前拦截用户请 求进行 cas 认证了。 当用户尚未登录时,会跳转到这个 cas 的登录页面进行登录。 用户在 cas 登录成功后,再次跳转回原系统时请求的页面。 CasProcessingFilter 会处理这个请求,从 cas 获得已登录的用户信息,并 对用户进行授权。 使用 custom-authentication-provider 之后,Spring Security其他的权限 模块会从这个 bean 中获得权限验证信息。 系统需要验证当前用户的 tickets 是否有效。 经过了这么多的配置,我们终于把 cas 功能添加到 spring security 中了, 看着一堆堆一串串的配置文件,好似又回到了 acegi 的时代,可怕啊。 下面运行系统,尝试使用了 cas 的权限控制之后有什么不同。 13.3. 运行配置了cas的子系统 首先要保证 cas 中央认证服务器已经启动了。子系统的 pom.xml 中也已 经配置好了 SSL,所以可以进入 ch103 执行 run.bat 启动子系统。 79 现在直接访问 http://localhost:8080/ch103/不再会弹出登陆页面,而是会 跳转到 cas 中央认证服务器上进行登录。 图 13.3. 登陆页面 输入 user/user 后进行登录,系统不会做丝毫的停留,直接跳转回我们的 子系统,这时我们已经登录到系统中了。 图 13.4. 登陆成功 80 我们再来试试注销,点击 logout 会进入 cas-logout.jsp。 图 13.5. cas-logout.jsp 在此点击 Logout of CAS 会跳转至 cas 进行注销。 图 13.6. 注销成功 现在我们完成了 Spring Security 中 cas 的配置,enjoy it。 13.4. 为cas配置SSL 81 在使用 cas 的时候,我们要为 cas 中央认证服务器和子系统都配置上 SSL,以此来对他们之间交互的数据进行加密。这里我们将使用 JDK 中 包含的 keytool 工具生成配置 SSL 所需的密钥。 13.4.1. 生成密钥 首先生成一个 key store。 keytool -genkey -keyalg RSA -dname "cn=localhost,ou=family168,o=www.family168.com,l=china,st=beijing,c=cn" -alias server -keypass password -keystore server.jks -storepass password 我们会得到一个名为 server.jks 的文件,它的密码是 password,注意 cn=localhost 部分,这里必须与 cas 服务器的域名一致,而且不能使用 ip, 因为我们是在本地 localhost 测试 cas,所以这里设置的就是 cn=localhost, 在实际生产环境中使用时,要将这里配置为 cas 服务器的实际域名。 导出密钥 keytool -export -trustcacerts -alias server -file server.cer -keystore server.jks -storepass password 将密钥导入 JDK 的 cacerts keytool -import -trustcacerts -alias server -file server.cer -keystore D:/apps/jdk1.5.0_15/jre/lib/security/cacerts -storepass password 82 这里需要把使用实际 JDK 的安装路径,我们要把密钥导入到 JDK 的 cacerts 中。 我们在 ch103/certificates/下放了一个 genkey.bat,这个批处理文件中已经 包含了上述的所有命令,运行它就可以生成我们所需的密钥。 13.4.2. 为jetty配置SSL jetty 的配置可参考 ch103 中的 pom.xml 文件。 9443 ../certificates/server.jks password password ../certificates/server.jks password true false javax.net.ssl.trustStore ../certificates/server.jks javax.net.ssl.trustStorePassword password 83 13.4.3. 为tomcat配置SSL 要运行支持 SSL 的 tomcat,把 server.jks 文件放到 tomcat 的 conf 目录下, 然后把下面的连接器添加到 server.xml 文件中。 如果你希望客户端没有提供证书的时候 SSL 链接也能成功,也可以把 clientAuth 设置成 want。 实例在 ch103。 84 第 14 章 basic认证 basic 认证是另一个常用的认证方式,与表单认证不同的是,basic 认证 常用于无状态客户端的验证,比如 HttpInvoker 或者 Web Service 的认证, 这种场景的特点是客户端每次访问应用时,都在请求头部携带认证信 息,一般就是用户名和密码,因为 basic 认证会传递明文,所以最好使 用 https 传输数据。 14.1. 配置 basic 验证 如果在 http 中配置了 auto-config="true"我们就不用再添加任何配置了, 默认配置中已经包含了 Basic 认证功能。但是这同时也会激活 form-login,因此我们将演示仅有 basic 验证的场景,为此需要去掉配置 文件中的 auto-config="true"。 删除了 auto-config="true"之后,还要记得添加 http-basic 标签,这样我们 的系统将仅仅使用 basic 认证方式来实现用户登录。 现在我们访问系统时,不会再进入之前的登录页面,而是会显示浏览器 原生的登录对话框。 85 图 14.1. basic 登录 登录成功之后,我们可以在 HTTP 请求头部看到 basic 验证所需的属性 Authorization。 图 14.2. HTTP 请求头 最后需要注意的是,因为 basic 认证不使用 session,所以无法与 rememberMe 功用。 14.2. 编程实现 basic 客户端 86 下面我们来示范一下如何使用 basic 认证。假设我们在 basic.jsp 中需要 远程调用 http://localhost:8080/ch104/admin.jsp 的内容。这时为了能够通 过 Spring Security 的权限检测,我们需要在请求的头部加上 basic 所需 的认证信息。 String username = "admin"; String password = "admin"; byte[] token = (username + ":" + password).getBytes("utf-8"); String authorization = "Basic " + new String(Base64.encodeBase64(token), "utf-8"); URL url = new URL("http://localhost:8080/ch104/admin.jsp"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("Authorization", authorization); 我们先将用户名和密码拼接成一个字符串,两者之间使用“:”分隔。 然后使用 commons-codec 的 Base64 将这个字符串加密。在进行 basic 认证 的时候 Spring Security 会使用 commons-codec 把这段字符串反转成用户名 和密码,再进行认证操作。 下一步为加密后得到的字符串添加一个前缀"Basic ",这样Spring Security 就可以通过这个判断客户端是否使用了 basic 认证。 将上面生成的字符串设置到请求头部,名称为“Authorization”。Spring Security 会在认证时,获取头部信息进行判断。 有关 basic 代码可以在/ch104/basic.jsp 找到,可以运行 ch104,然后访问 http://localhost:8080/ch104/basic.jsp。它会使用上述的代码,通过 Spring Security 的认证,成功访问到 admin.jsp 的信息。 实例在 ch104。 87 第 15 章 标签库 Spring Security 提供的标签库,主要的用途是为了在视图层直接访问用 户信息,再者就是为了对显示的内容进行权限管理。 15.1. 配置 taglib 如果需要使用 taglib,首先要把 spring-security-taglibs-2.0.5.RELEASE.jar 放到项目的 classpath 下,这在文档附带的实例中已经配置好了依赖。剩 下 的只要在 jsp 上添加 taglib 的定义就可以使用标签库了。 <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 15.2. authenticaiton authentication 的功能是从 SecurityContext 中获得一些权限相关的信息。 可以用它获得当前登陆的用户名: 获得当前用户所有的权限,把权限列表放到 authorities 变量里,然后循 环输出权限信息: ${authority.authority} 88 15.3. authorize authorize 用来判断当前用户的权限,然后根据指定的条件判断是否显示 内部的内容。 admin and user admin or user not admin ifAllGranted,只有当前用户同时拥有 ROLE_ADMIN 和 ROLE_USER 两个权限 时,才能显示标签内部内容。 ifAnyGranted,如果当前用户拥有 ROLE_ADMIN 或 ROLE_USER 其中一个权限 时,就能显示标签内部内容。 ifNotGranted,如果当前用户没有 ROLE_ADMIN 时,才能显示标签内部内容。 15.4. acl/accesscontrollist 用于判断当前用户是否拥有指定的 acl 权限。 | Remove 我们将当前显示的对象作为参数传入 acl 标签,然后指定判断的权限为 8(删除)和 16(管理),当前用户如果拥有对这个对象的删除和管理权限 89 时,就会显示对应的 remove 超链接,用户才可以通过此链接对这条记 录进行删除操作。 关于ACL的知识,请参考 第 53 章 ACL基本操作。 15.5. 为不同用户显示各自的登陆成功页面 一个常见的需求是,普通用户登录之后显示普通用户的工作台,管理员 登陆之后显示后台管理页面。这个功能可以使用 taglib 解决。 其实只要在登录成功后的 jsp 页面中使用 taglib 判断当前用户拥有的权 限进行跳转就可以。 <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%response.sendRedirect("admin.jsp");%> <%response.sendRedirect("user.jsp");%> 当用户拥有 ROLE_ADMIN 权限时,既跳转到 admin.jsp 显示管理后台。 当用户没有 ROLE_ADMIN 权限时,既跳转到 user.jsp 显示普通用户工作台。 这里我们只做最简单的判断,只区分当前用户是否为管理员。可以根据 实际情况做更加复杂的跳转,当用户具有不同权限时,跳到对应的页面, 甚至可以根据用户 username 跳转到各自的页面。 实例在 ch105。 90 第 16 章 自动登录 如果用户一直使用同一台电脑上网,那么他可能希望不要每次上网都要 进行登录这道程序,他们希望系统可以记住自己一段时间,这样用户就 可以无需登录直接登录系统,使用其中的功能。rememberMe 就给我们 提供了这样一种便捷途径。 16.1. 默认策略 在配置文件中使用 auto-config="true"就会自动启用 rememberMe,之后, 只要用户在登录时选中 checkbox 就可以实现下次无需登录直接进入系 统的功能。 图 16.1. 选中 rememberMe 默认有效时间是两周,启用 rememberMe 之后的两周内,用户都可以直 接跳过系统,直接进入系统。 91 实际上,Spring Security 中的 rememberMe 是依赖 cookie 实现的,当用 户在登录时选择使用 rememberMe,系统就会在登录成功后将为用户生 成一个唯一标识,并将这个标识保存进 cookie 中,我们可以通过浏览器 查看用户电脑中的 cookie。 图 16.2. rememberMe cookie 从上图中,我们可以看到 Spring Security 生成的 cookie 名称是 SPRING_SECURITY_REMEMBER_ME_COOKIE,它的内容是一串加 密的字符串,当用户再次访问系统时,Spring Security 将从这个 cookie 读取用户信息,并加以验证。如果可以证实 cookie 有效,就会自动将用 户登录到系统中,并为用户授予对应的权限。 16.2. 持久化策略 rememberMe 的默认策略会将 username 和过期时间保存到客户主机上的 cookie 中,虽然这些信息都已经进行过加密处理,不过我们还可以使用 安全级别更高的持久化策略。在持久化策略中,客户主机 cookie 中保存 的不再用 username,而是由系统自动生成的序列号,在验证时系统会将 客户 cookie 中保存的序列号与数据库中保存的序列号进行比对,以确认 客户请求的有效性,之后在比对成功后才会从数据库中取出对应的客户 92 信息,继续进行认证和授权等工作。这样即使客户本地的 cookie 遭到破 解,攻击者也只能获得一个序列号,而不是用户的登录账号。 如果希望使用持久化策略,我们需要先在数据库中创建 rememberMe 所 需的表。 create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null ); 然后要为配置文件中添加与数据库的链接。 最后修改http中的配置,为remember-me添加data-source-ref即可,Spring Security 会在初始化时判断是否存在 data-source-ref 属性,如果存在就会 使用持久化策略,否则会使用上述的默认策略。 93 注意:默认策略和持久化策略是不能混用的,如果你首先在应用中使用过默认策 略的 rememberMe,未等系统过期便换成了持久化策略,之前保留的 cookie 也无 法通过系统验证,实际上系统会将 cookie 当做无效标识进行清除。同样的,持 久化策略中生成的 cookie 也无法用在默认策略下。 实例在 ch106。 94 第 17 章 匿名登录 匿名登录,即用户尚未登录系统,系统会为所有未登录的用户分配一个 匿名用户,这个用户也拥有自己的权限,不过他是不能访问任何被保护 资源的。 设置一个匿名用户的好处是,我们在进行权限判断时,可以保证 SecurityContext 中永远是存在着一个权限主体的,启用了匿名登录功能 之后,我们所需要做的工作就是从 SecurityContext 中取出权限主体,然 后对其拥有的权限进行校验,不需要每次去检验这个权限主体是否为空 了。这样做的好处是我们永远认为请求的主体是拥有权限的,即便他没 有登录,系统也会自动为他赋予未登录系统角色的权限,这样后面所有 的安全组件都只需要在当前权限主体上进行处理,不用一次一次的判断 当前权限主体是否存在。这就更容易保证系统中操作的一致性。 17.1. 配置文件 在配置文件中使用 auto-config="true"就会启用匿名登录功能。在启用匿 名登录之后,如果我们希望允许未登录就可以访问一些资源,可以在进 行如下配置。 95 在 access 中指定 IS_AUTHENTICATED_ANONYMOUSLY 后,系统就 知道此资源可以被匿名用户访问了。当未登录时访问系统的“/”,就 会被自动赋以匿名用户的身份。我们可以使用 taglib 获得用户的权限主 体信息。 这里的 IS_AUTHENTICATED_ANONYMOUSLY 将会交由 AuthenticatedVoter 处理,内部会依据 AuthenticationTrustResolver 判断当 前登录的用户是否是匿名用户。
username : | authorities: ${authority.authority}
当用户访问系统时,就会看到如下信息,这时他还没有进行登录。 图 17.1. 匿名登录 这里显示的是分配给所有未登录用户的一个默认用户名 roleAnonyMous,拥有的权限是 ROLE_ANONYMOUS。我们可以看到 96 系统已经把匿名用户当做了一个合法有效的用户进行处理,可以获得它 的用户名和拥有的权限,而不需判断 SecurityContext 中是否为空。 实际上,我们完全可以把匿名用户像一个正常用户那样进行配置,我们 可以在配置文件中直接使用 ROLE_ANONYMOUS 指定它可以访问的 资源。 不过,为了更明显的将匿名用户与系统中的其他用户区分开,我们推荐 在配置时尽量使用 IS_AUTHENTICATED_ANONYMOUSLY 来指定匿 名用户可以访问的资源。 17.2. 修改默认用户名 我们通常可以看到这种情况,当一个用户尚未登录系统时,在页面上应 当显示用户名的部分显示的是“游客”。这样操作更利于提升客户体验, 他看到自己即使没有登录也可以使用“游客”的身份享受系统的服务, 而且使用了“游客”作为填充内容,也避免了原本显示用户名部分留空, 影响页面布局。 如果没有匿名登录的功能,我们就被迫要在所有显示用户名的部分,判 断当前用户是否登录,然后根据登录情况显示登录用户名或默认的“游 97 客”字符。匿名登录功能帮我们解决了这个问题,既然未登录用户都拥 有匿名用户的身份,那么在显示用户名时就不必去区分用户登录状态, 直接显示当前权限主体的名称即可。 我们需要做的只是为匿名设置默认的用户名而已,默认的名称 roleAnonymous 可以通过配置文件中的 anonymous 元素修改。 这样,匿名用户的默认名称就变成了“Guest”,我们在页面上依然使 用相同的 taglib 显示用户名即可。 图 17.2. 修改匿名用户名称 17.3. 匿名用户的限制 虽然匿名用户无论在配置授权时,还是在获取权限信息时,都与已登录 用户的操作一模一样,但它应该与未登录用户是等价,当我们以匿名用 户的身份进入“/”后,点击 admin.jsp 链接,系统会像处理未登录用户 98 时一样,跳转到登录用户,而不是像处理以登录用户时显示拒绝访问页 面。 但是匿名用户与未登录用户之间也有很大的区别,比如,我们将“/” 设置为不需要过滤器保护,而不是设置匿名用户。 filters="none"表示当我们访问“/”时,是不会使用任何一个过滤器去处 理这个请求的,它可以实现无需登录即可访问资源的效果,但是因为没 有使用过滤器对请求进行处理,所以也无法利用安全过滤器为我们带来 的好处,最简单的,这时 SecurityContext 内再没有保存任何一个权限主 体了,我们也无法从中取得主体名称以及对应的权限信息。 图 17.3. 跳过过滤器的情况 因此,使用 filters="none"忽略所有过滤器会提升性能,而是用匿名登录 功能可以实现权限操作的一致性,具体应用时还需要大家根据实际情况 自行选择了。 99 实例在 ch107。 100 第 18 章 防御会话伪造 18.1. 攻击场景 session fixation 会话伪造攻击是一个蛮婉转的过程。 比如,当我要是使用 session fixation 攻击你的时候,首先访问这个网站, 网站会创建一个会话,这时我可以把附有 jsessionid 的 url 发送给你。 http://unsafe/index.jsp;jsessionid=1pjztz08i2u4i 你使用这个网址访问网站,结果你和我就会公用同一个 jsessionid 了, 结果就是在服务器中,我们两人使用的是同一个 session。 这时我只要祈求你在 session 过期之前登陆系统,然后我就可以使用 jsessionid 直接进入你的后台了,然后可以使用你在系统中的授权做任何 事情。 简单来说,我创建了一个 session,然后把 jsessionid 发给你,你傻乎乎 的就使用我的 session 进行了登陆,结果等于帮我的 session 进行了授权 操作,结果就是我可以使用一开始创建的 session 进入系统做任何事情 了。 与会话伪造的详细信息可以参考 http://en.wikipedia.org/wiki/Session_fixation。 18.2. 解决会话伪造 101 解决 session fix 的问题其实很简单,只要在用户登录成功之后,销毁用 户的当前 session,并重新生成一个 session 就可以了。 Spring Security 默认就会启用 session-fixation-protection,这会在登录时 销毁用户的当前 session,然后为用户创建一个新 session,并将原有 session 中的所有属性都复制到新 session 中。 如果希望禁用 session-fixation-protection,可以在 http 中将 session-fixation-protection 设置为 none。 session-fixation-protection 的值共有三个可供选择,none,migrateSession 和 newSession。默认使用的是 migrationSession,如同我们上面所讲的, 它会将原有 session 中的属性都复制到新 session 中。上面我们也见到了 使用 none 来禁用 session-fixation 功能的场景,最后剩下的 newSession 会在用户登录时生成新 session,但不会复制任何原有属性。 实例在 ch108。 102 第 19 章 预先认证 预先认证是指用户在进入系统之前,就已经通过某种机制进行过身份认 证,请求中已经附带了身份认证的信息,这时我们只需要从获得这些身 份认证信息,并对用户进行授权即可。CAS, X509 等都属于这种情况。 Spring Security 中专门为这种系统外预先认证的情况提供了工具类,这 一章我们来看一下如何使用 Pre-Auth 处理使用容器 Realm 认证的用户。 19.1. 为 jetty 配置 Realm 首先在 pom.xml 中配置 jetty 所需的 Realm。 Preauth Realm realm.properties 用户,密码,以及权限信息都保存在 realm.properties 文件中。 admin: admin,ROLE_ADMIN,ROLE_USER user: user,ROLE_USER test: test 我们配置了三个用户,分别是 admin, user 和 test,其中 admin 拥有 ROLE_ADMIN 和 ROLE_USER 权限,user 拥有 ROLE_USER 权限,而 test 没有任何权限。 103 下一步在 src/webapp/WEB-INF/web.xml 中配置登录所需的安全权限。 BASIC Preauth Realm ROLE_USER ROLE_ADMIN All areas /* ROLE_USER 这里我们将 login-config 中的 realm-name 配置为 Preauth Realm,这与刚 刚在 pom.xml 中配置的名称是相同的。而后我们配置了两个安全权限 ROLE_USER 和 ROLE_ADMIN,最后我们现在访问所有资源都需要使 用 ROLE_USER 这个权限。 自此,服务器与应用中的 Realm 配置完毕,下一步我们需要使用 Spring Security 与 Realm 对接。 19.2. 配置 Spring Security 104 因为使用容器 Realm 的 Pre-Auth 并没有对应的命名空间,所以需要去 掉 auto-config="true"并引用对应的安全入口。 这次我们会使用 j2eePreAuthFilter 执行用户认证,所有默认那些 form-login, basic-login, rememberMe 都没了用武之地。 而为了使用 j2eePreAuthFilter,我们需要进行如下配置: 105 这里,我们要配置 Pre-Auth 所需的 AuthenticationProvider, EntryPoint, AuthenticatedUserDetailsService 并最终组装成一个 j2eePreAuthFilter。其 106 中 j2eeMappableRolesRetriever 会读取我们之前配置的 web.xml,从中获 得权限信息。 这样,当用户登录时,请求会先被 Realm 拦截,并要求用户进行登录: 图 19.1. Realm 登录 登录成功后,Realm 会将用户身份信息绑定到请求中,j2eePreAuthFilter 就会从请求中读取身份信息,结合 web.xml 中定义的权限信息对用户进 行授权,并将授权信息录入 SecurityContext,之后对用户验证时与之前 已没有了任何区别。 这里的 preauthEntryPoint 会在用户权限不足时起作用,它只会简单返回 一个 401 的拒绝访问响应。 在此我们并不推荐实际中使用这项功能,因为需要对容器进行配置,影 响应用的灵活性。 实例在 ch109。 107 第 20 章 切换用户 Spring Security 提供了一种称为切换用户的机制,可以使管理员免于进 过登录的操作,直接切换当前用户,从而改变当前的操作权限。因为按 照责权分离的原则,系统内的超级管理员应该只有管理权限,而没有操 作权限,所以为了在改变操作后可以测试系统的操作,需要降低权限才 可以进入操作界面,这时就可以使用切换用户的功能。 20.1. 配置方式 在 xml 中添加 SwitchUser 的配置。 它需要引用系统中的 userDetailsService 在切换用户时,根据对应的 username 获得切换后用户的信息和权限,我们还要使用 custom-filter 将 该过滤器放到过滤器链中,注意必须放在用来验证权限的 FilterSecurityInterceptor 之后,这样可以控制当前用户是否拥有切换用户 的权限。 现在,我们可以在系统中使用切换用户这一功能了,我们可以通过 /j_spring_security_switch_user?j_username=user 切换到 j_username 指定 108 的用户,这样可以快捷的获得目标用户的信息和权限。当需要返回管理 员用户时,只需要通过/j_spring_security_exit_user 就可以还原到切换前 的状态。 20.2. 实例演示 现在我们进入实例,通过登录页面进行登录。因为实现了权责分离, admin/admin 用户只能访问管理页面 admin.jsp,不能访问 user.jsp, user/user 用户只能访问操作页面 user.jsp,不能访问 admin.jsp。 如果我们以管理员身份登录后,希望切换到 user/user 用户,可以调用 /j_spring_security_switch_user?j_username=user 切换到 user/user 用户,然 后就可以使用 user 的权限访问 user.jsp 了。 在切换用户后,我们可以看到当前登录用户的权限中多了一个 ROLE_PREVIOUS_ADMINISTRATOR,这其中就保存着前用户的权限 信息,当我们通过/j_spring_security_exit_user 退出切换用户模式时,系 统就会从 ROLE_PREVIOUS_ADMINISTRATOR 中获得原始用户信息, 重新进行授权。 实例在 ch110。 109 第 21 章 信道安全 21.1. 设置信道安全 为了加强安全级别,我们可以限制默写资源必须通过 https 协议才能访 问。 可以为/admin.jsp单独设置必须使用https才能访问,如果用户使用了http 协议访问该网址,系统会强制用户使用 https 协议重新进行请求。 这里我们可以选使用 https, http 或者 any 三种数值,其中 any 为默认值, 表示无论用户使用何种协议都可以访问资源。 21.2. 指定http和https的端口 因为 http 和 https 协议的访问端口不同,Spring Security 在处理信道安全 时默认会使用 80/443 和 8080/8443 对访问的网址进行转换。如果服务器 对 http 和 https 协议监听的端口进行了修改,则需要修改配置文件让系 统了解 http 和 https 的端口信息。 我们可以使用 port-mappings 自定义端口映射。 110 上述配置文件中,我们定义了 9000 与 9443 的映射,现在系统会在强制 使用 http 协议的网址时使用 9000 作为端口号,在强制使用 https 协议的 网址时使用 9443 作为端口号,这些端口号会反映在重定向后生成网址 中。 实例在 ch111。 111 第 22 章 digest认证 digest 认证比 form-login 和 http-basic 更安全的一种认证方式,尤其适用 于不能使用 https 协议的场景。它与 http-basic 一样,都是不基于 session 的无状态认证方式。 22.1. 配置 digest 验证 因为 digest 不包含在命名空间中,所以我们需要配置额外的过滤器和验 证入口。 然后记得删除 auto-config="true",去除默认的 form-login 和 http-basic 认 证,并添加对验证入口的引用。 112 现在我们访问系统时,不会再进入之前的登录页面,而是会显示浏览器 原生的登录对话框。 图 22.1. digest 登录 登录成功之后,我们可以在 HTTP 请求头部看到 basic 验证所需的属性 Authorization。 图 22.2. HTTP 请求头 113 最后需要注意的是,因为 digest 认证不使用 session,所以无法与 rememberMe 功用。 22.2. 使用 ajax 实现 digest 认证 可以直接使用 ajax 来进行 digest 认证,完全不需要任何额外的配置,只 需要在 open 的时候传入用户名和密码就可以完成认证。 oRequest.open("get", "index.jsp", false, "user", "user"); oRequest.setRequestHeader("Content-type", "text/xml; charset=utf-8"); oRequest.send(""); 这样就完成的认证过程,之后再使用普通方式访问其他页面也没问题 了。 22.3. 编程实现 digest 客户端 如果希望自己编写客户端进行 digest 认证,可以参考 RFC 2617,它是 对 RFC 2069 这个早期摘要式认证标准的更新。 在 HTTP 请求头中将包含这样一个 Authorization,它包含了 username, realm, nonce, uri, responseDigest, qop, nc 和 cnonce 八个部分。其中 nonce 是 digest 认证的中心,它的组成结构如下所示: base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key )) 其中 expirationTime 是 nonce 的过期时间,单位是毫秒。 key 是放置 nonce 修改的私钥。 114 如果服务器生成的 nonce 已经过期(但是摘要还是有效), DigestProcessingFilterEntryPoint 会发送一个"stale=true"头信息。 这告诉 用户代理,这里不再需要打扰用户(像是密码和用户其他都是正确的), 只是简单尝试使用一个新 nonce。 实例在 ch112。 115 第 23 章 通过LDAP获取用户信息 很多企业内部使用 LDAP 保存用户信息,这章我们来看一下如何从 LDAP 中获取 Spring Security 所需的用户信息。 首先在 pom.xml 中添加 ldap 所需的依赖。 org.apache.directory.server apacheds-server-jndi 1.0.2 org.springframework.ldap spring-ldap 1.2.1 然后修改配置文件,使用内嵌的 ldap 服务器和 ldap-authentication-provider。 这里配置内嵌的 ldap 服务器从 users.ldif 文件中读取初始化数据,端口 使用 33389,查询目录的根目录设置为 dc=family168,dc=com。 116 ldap-authentication-provider 设置查找组和用户的配置,分别使用 ou=groups 表示组,使用 ou=people 表示用户。 用于保存 ldap 初始信息的文件内容如下: dn: ou=groups,dc=family168,dc=com objectclass: top objectclass: organizationalUnit ou: groups dn: ou=people,dc=family168,dc=com objectclass: top objectclass: organizationalUnit ou: people dn: uid=user,ou=people,dc=family168,dc=com objectclass: top objectclass: person objectclass: organizationalPerson objectclass: inetOrgPerson cn: FirstName LastName sn: LastName uid: user userPassword: user dn: uid=admin,ou=people,dc=family168,dc=com objectclass: top objectclass: person objectclass: organizationalPerson objectclass: inetOrgPerson cn: FirstName LastName sn: LastName uid: admin userPassword: admin dn: cn=user,ou=groups,dc=family168,dc=com objectclass: top objectclass: groupOfNames cn: ROLE_USER member: uid=user,ou=people,dc=family168,dc=com member: uid=admin,ou=people,dc=family168,dc=com 117 dn: cn=admin,ou=groups,dc=family168,dc=com objectclass: top objectclass: groupOfNames cn: ROLE_ADMIN member: uid=admin,ou=people,dc=family168,dc=com 这里在 dc=family168,dc=com 目录下创建了 groups 和 people 两个目录, 然后在 people 目录下创建了 user 和 admin 两个用户。在 groups 目录下 创建了 admin 和 user 两个目录,并将 user 和 admin 两个用户与 groups 的 user 目录关联,又将 admin 用户与 groups 的 admin 目录关联。 在系统初始化后,Spring Security 会在 people 下读取用户信息,而对应 的权限信息是对应用户所关联的 groups 信息,Spring Security 会将查询 到的权限信息加上 ROLE_前缀,如 cn=admin 最终会转换为 ROLE_ADMIN。 实例在 ch113。 118 第 24 章 通过OpenID进行登录 OpenID 是一种网上身份识别服务,它的目标是让用户可以网上使用同 一身份登录不同的系统。 这一章我们讲解如何使用 Spring Security 支持 OpenID。 24.1. 配置 首先在 pom.xml 中添加使用 OpenID 所需的依赖。 org.springframework.security spring-security-openid 2.0.5.RELEASE 然后在 xml 中添加 OpenID 所需的配置。 119 只需要添加 openid-login 标签,并引用一个 userService。userService 中 需要配置 OpenID 中的账号,以及对应的权限。它会在用户登录成功后 对用户进行授权。 现在我们可以启动应用,进入登陆页面。 图 24.1. 登录页面 在登录页面中输入 http://family168.myopenid.com/,这是我们已经注册过 的一个 OpenID 账号,点击提交之后会跳转到 myopenid.com 网站。 120 图 24.2. myopenid.com 在此处输入密码:password。登录成功之后就会跳转回我们的系统。 图 24.3. 登录成功 这时我们可以看到当前登录系统的用户是 http://family168.openid.com/。 24.2. 系统时间问题 121 因为 OpenID 服务器会根据当前时间生成 nonce 进行权限校验,所以一 定要保证服务器的当前时间是正确的,如果服务器当前时间比正常时间 快了哪怕只是几秒钟,也会导致在进行校验时抛出 nonce is too old 异常, 这会导致一直无法正常登陆。 实例在 ch114。 122 第 25 章 使用X509 登录 X509 是一种基于双向加密的身份认证方式,它要基于 SSL,并要求开 启客户端证书,是一种非常强力的安全手段。 25.1. 生成证书 我们要为 X509 生成服务器端和客户端使用的证书。 首先生成服务端证书 server.jks。 keytool -genkey -keyalg RSA -dname "cn=localhost,ou=family168,o=www.family168.com,l=china,st=beijing,c=cn" -alias server -keypass password -keystore server.jks -storepass password 然后生成客户端证书,并将客户端证书导入到 server.jks 中。 keytool -genkey -v -alias user -keyalg RSA -storetype PKCS12 -keystore user.p12 -dname "cn=user,ou=family168,o=www.family168.com,l=china,st=beijing,c=cn" -storepass password -keypass password keytool -export -alias user -keystore user.p12 -storetype PKCS12 -storepass password -rfc -file user.cer keytool -import -v -file user.cer -keystore server.jks -storepass password 最后将服务端证书导出,加入 jre 安全证书中。 keytool -export -trustcacerts -alias server -file server.cer -keystore server.jks -storepass password keytool -import -trustcacerts -alias server -file server.cer -keystore "%JAVA_HOME%/JRE/LIB/SECURITY/CACERTS" -storepass changeit 123 25.2. 配置服务器使用双向加密 在 pom.xml 中对 jetty 进行配置,使用刚刚生成的服务端证书 server.js 配置 SSL,并使用 needClientAuth="true"启用双向加密啊。 8080 3600000 8443 certificates/server.jks password password certificates/server.jks password true 10 ../webdefault.xml javax.net.ssl.trustStore certificates/server.jks javax.net.ssl.trustStorePassword password 25.3. 配置X509 认证 修改配置文件,添加 x509 配置方式 124 x509 中,subject-principal-regex 会从客户端证书中获取用户名,将 CN 部分当做 username 来使用,因为服务器端会绝对相信客户端证书中的 信息,所以不会再去使用 userService 中的密码对用户进行校验,一旦证 书校验通过,直接对用户进行授权。 现在系统已经配置好了 x509 认证方式,只要将生成的客户端证书 user.p12 导入到浏览器中,直接访问系统就会发现已经用 user 用户登录 到系统中了。 如果使用 IE 浏览器,可以直接双击 user.p12 进行导入。 如果使用 FireFox 浏览器,需要点击浏览器工具条上的“工具”->“选 项”->“高级”->“查看证书”,在“证书管理器”中导入 user.p12。 实例在 ch115。 125 第 26 章 使用NTLM登录(无法成功登陆) NTLM 是 NT Lan Manager,即 Window NT 局域网管理器。它会以本地 系统当前用户尝试访问远程主机,基本工作流程请去网上搜索。 NTLM 验证允许 Windows 用户使用当前登录系统的身份进行认证,当 前用户应该是登陆在一个域(domain)上,他的身份是可以自动通过浏 览器传递给服务器的。它是一种单点登录的策略,系统可以通过 NTLM 重用登录到 Windows 系统中的用户凭证,不用再次要求用户输入密码 进行认证。 不过,它只能用在 IE 中。使用 Firefox 时,你会被要求输入用户名和密 码,这时可以使用下面的配置启用 NTLM。 • 在 Firefox 的地址栏中输入“about:config”。 • 这时可以看到 Firefox 的所有配置,现在找到 “network.automatic-ntlm-auth.trusted-uris”。 • 输入主机名称,比如“host1.domain.com, host2.domain.com”,也可 以直接输入“.domain.com”。 现在我们开始在 Spring Security 中配置 NTLM。 首先在 pom.xml 中添加依赖: org.springframework.security spring-security-ntlm 2.0.5.RELEASE 126 然后修改 xml 文件,添加 NTLM 的过滤器和入口点。 我们还需要创建一个 UserDetailsAuthenticationProvider,可以直接通过 username 从 UserDetailsService 中获得用户信息与相应的权限。 package com.family168.springsecuritybook.ch116; import org.springframework.dao.DataAccessException; 127 import org.springframework.security.AuthenticationException; import org.springframework.security.AuthenticationServiceException; import org.springframework.security.providers.UsernamePasswordAuthenticationToken; import org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvide r; import org.springframework.security.userdetails.UserDetails; import org.springframework.security.userdetails.UserDetailsService; public class UserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private UserDetailsService userDetailsService; protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { loadedUser = this.getUserDetailsService().loadUserByUsername(username); } catch (DataAccessException repositoryProblem) { throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem); } if (loadedUser == null) throw new AuthenticationServiceException("User cannot be null"); return loadedUser; } protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException{ } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } } 128 现在的问题就是如何在 NTLM 中配置了,如何配置才能让 ch116 正常 登陆。 实例在 ch116。 129 第 27 章 使用JAAS机制 可以在 Spring Security 中使用 JAAS 机制进行用户的身份认证。 JAAS 即 Java Authentication and Authorization Service,它 是 JDK 自带的 一套专门用于处理用户认证和授权的标准 API,Spring Security 中可以 使用 API 作为 AuthenticationProvider 处理用户认证与授权。 配置文件中,我们使用 JaasAuthenticationProvider 作为 AuthenticationProvider。 130 注意不能在 http 标签中使用 auto-config="true"或是在 http 标签中包含 rememberMe,因为 rememberMe 需要引用 userDetailsService,而在使用 JaasAuthenticationProvider 时,用户数据校验是交由 LoginModule 处理 的,不会使用 userDetailsService,所以 rememberMe 会抛出异常。 我们将 JAAS 所需的配置文件放在/WEB-INF/login.config。 JAASTest { com.family168.springsecuritybook.ch117.LoginModuleImpl required; }; 并在配置文件中指明使用 JAASTest 作为登陆上下文。 现在要创建 LoginModuleImpl 用来处理用户登录。 package com.family168.springsecuritybook.ch117; import java.security.Principal; import java.util.Map; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.TextInputCallback; import javax.security.auth.login.LoginException; 131 import javax.security.auth.spi.LoginModule; public class LoginModuleImpl implements LoginModule { private String password; private String user; private Subject subject; public boolean abort() throws LoginException { return true; } public boolean commit() throws LoginException { return true; } public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; try { TextInputCallback textCallback = new TextInputCallback("prompt"); NameCallback nameCallback = new NameCallback("prompt"); PasswordCallback passwordCallback = new PasswordCallback("prompt", false); callbackHandler.handle(new Callback[] {textCallback, nameCallback, passwordCallback}); password = new String(passwordCallback.getPassword()); user = nameCallback.getName(); } catch (Exception e) { throw new RuntimeException(e); } } public boolean login() throws LoginException { if (!user.equals("user")) { throw new LoginException("Bad User"); } if (!password.equals("user")) { throw new LoginException("Bad Password"); } 132 subject.getPrincipals().add(new Principal() { public String getName() { return "TEST_PRINCIPAL"; } }); subject.getPrincipals().add(new Principal() { public String getName() { return "NULL_PRINCIPAL"; } }); return true; } public boolean logout() throws LoginException { return true; } } 当用户登录成功时,会通过 authorityGranters 为权限主体授权,这一步 也要自己实现 AuthorityGranter 接口。 package com.family168.springsecuritybook.ch117; import java.security.Principal; import java.util.HashSet; import java.util.Set; import org.springframework.security.providers.jaas.AuthorityGranter; public class AuthorityGranterImpl implements AuthorityGranter { public Set grant(Principal principal) { Set rtnSet = new HashSet(); if (principal.getName().equals("TEST_PRINCIPAL")) { rtnSet.add("ROLE_USER"); rtnSet.add("ROLE_ADMIN"); } 133 return rtnSet; } } 至此,JAAS 与 Spring Security 结合进行认证授权的功能已经完成,每 一步都要件功能写死在代码里,让人感觉很不舒服。 实例在 ch117。 134 第 28 章 使用HttpInvoker Spring Security 提供了 AuthenticationSimpleHttpInvokerRequestExecutor, 可以在调用 HttpInvoker 时,自动根据当前权限主体生成 basic 认证所需 的 http 请求头,以此来通过远程服务器的认证,从而访问 httpInvoker 暴露的远程资源。 只需要为 HttpInvokerProxyFactoryBean 配置 httpInvokerRequestExecutor 属性。 实例在 ch118。 135 第 29 章 使用rmi Spring Security 提供了 ContextPropagatingRemoteInvocationFactory,可以 在调用 rmi 时,将当前 SecurityContext 当做 rmi 的一部分传递到远程服 务器,并使用这个 SecurityContext 在远程服务器中进行权限校验,这也 就是 RunAsManager 的作用所在,当一个用户访问一个方法时,需要自 动调用远程服务,这个远程服务中需要一个角色才可以通过校验,为了 让调用此方法的用户都拥有调用远程资源的权限,才使用了 RunAsManager 为所有访问该方法的用户自动添加一个角色。 实际使用时,只需要为 RmiProxyFactoryBean 配置 remoteInvocationFactory 属性。 实例在 ch119。 136 第 30 章 控制portal的权限 实际是只是为 portlet 添加权限拦截器,它会为 portlet 进行 Pre-Auth 式 认证,就是说它会从当前 SecurityContext 中取出权限实体,用来进行 portlet 的校验。 在 portlet 对应的配置文件中配置如下拦截器,用于校验对应 portlet 的权 限。 tomcat admin manager Administrator Guest User Power User 137 实例在 ch120。 138 第 31 章 保存登录之前的请求 经常会碰到一种情况,用户花费大量时间编辑信息,但是 session 超时 失效导致用户自动退出系统,安全过滤器会强制用户再次登录,但这也 会使用户提交的信息全部丢失。 为了解决这个问题,Spring Security 提供了一种称作 SavedRequest 功能, 可以在未登录用户访问资源时,将用户请求保存起来,当用户登录成功 之后 SecurityContextHolderAwareRequestFilter 会使用之前保存的请求, 结合当前用户的请求生成一个新的请求对象,而这个请求对象中就保存 了用户登录之前提交的信息。 SavedRequest 功能默认就会被 Spring Security 启用,不需任何配置就可 以重用登陆之前请求提交的数据。 如果希望尝试 SavedRequest 的功能,可以运行 ch121 中的实例。 进入系统后会显示一个表单,可以在表单中填写信息,然后进行提交。 提交的目标页面是 user.jsp。 图 31.1. 未登录用户提交信息 139 但是因为用户尚未登录,所以请求会被转发到登陆页面,当我们登录成 功后,系统会自动跳转到用户登录之前请求的页面,user.jsp。 图 31.2. 进入登陆页面 因为我们使用了 SavedRequest 功能,所以现在就能在 user.jsp 看到我们 登陆之前提交的信息。 图 31.3. 使用登录之前提交的信息 如果希望禁用 SavedRequet 这一功能,只需要在 http 标签中设置 servlet-api-provision 参数。 140 实例在 ch121。 141 第 32 章 记录操作日志 官方提供了 LoggerListener 用来向控制台输出日志。如果我们希望在数 据库中保存用户访问日志,可以实现自己的 AuditListener,实现 ApplicationListener 接口,然后 Spring Security 就会自动以事件的形式发 送审计日志了。 实例在 ch122。 142 部分 III 内部机制篇 Spring Security 使用 AOP 对方法调用进行权限控制,这部分内容基本都 是来自于 Spring 提供的 AOP 功能,Spring Security 进行了自己的封装, 我们可以使用声明和编程两种方式进行权限管理。 143 第 33 章 保护方法调用 提示:因为 Spring Security 对方法调用和 ACL 权限控制的实现都是基于 Spring 的 AOP,所以只能保护定义在 applicationContext.xml 中的 Java 类,对直接 new 出来的对象是无法保护的。 这里有三种方式可以选择: 33.1. 控制全局范围的方法权限 使用 global-method-security 和 protect-point 标签来管理全局范围的方法 权限。 为了在 spring 中使用 AOP,我们要为项目添加几个依赖库。 cglib cglib-nodep 2.1_3 org.aspectj aspectjrt 1.6.5 org.aspectj aspectjweaver 1.6.5 首先来看看我们将要保护的 java 类。 package com.family168.springsecuritybook.ch201; 144 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 调用。 现在只有拥有 ROLE_ADMIN 权限的用户才能调用 MessageService 中以 admin 开头的方法了,当我们以 user/user 登陆系统时,尝试调用 MessageService 类的 adminMessage()会跑出一个“访问被拒绝”的异常。 33.2. 控制某个bean内的方法权限 145 在 bean 中嵌入 intercept-methods 和 protect 标签。 这需要改造配置文件。 现在 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 的参数。 146 然后添加依赖包。 org.springframework.security spring-security-core-tiger 2.0.5.RELEASE 现在我们随便在 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 首先还是要修改配置文件。 然后添加依赖包。 147 javax.annotation jsr250-api 1.0 现在可以在代码中使用 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 允许所有的用户调用方法。 148 从实际使用上来讲,jsr250 里多出来的 DenyAll 和 PermitAll 纯属浪费, 谁会定义谁也不能调用的方法呢?实际上,要是 annotation 支持布尔操 作就好了,比如逻辑并,逻辑或,逻辑否之类的。 还有 jsr250 中未被支持的 RunAs 注解,如果能利用起来估计更有趣。 实例在 ch201。 149 第 34 章 权限管理的基本概念 34.1. 认证与验证 Spring Security 作为权限管理框架,其内部机制可分为两大部分,其一 是认证授权 auhorization,其二是权限校验 authentication。 认证授权 authorization 是指,根据用户提供的身份凭证,生成权限实体, 并为之授予相应的权限。 权限校验 authentication 是指,用户请求访问被保护资源时,将被保护资 源所需的权限和用户权限实体所拥护的权限二者进行比对,如果校验通 过则用户可以访问被保护资源,否则拒绝访问。 我们之前讲过的 form-login,http-basic, digest 都属于认证授权 authorization 部分的概念,用户可以通过这些机制登录到系统中,系统 会为用户生成权限主体,并授予相应的权限。 与之相对的,FilterSecurityInterceptor,Method 保护,taglib,@Secured 都属于权限校验 authentication,无论是对 URL 的请求,对方法的调用, 页面信息的显示,都要求用户拥有相应的权限才能访问,否则请求即被 拒绝。 34.2. SecurityContext 安全上下文 150 为使所有的组件都可以通过同一方式访问当前的权限实体,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 验证对象 151 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 对象的数组,之后可以用于进行验证用户是否可以访问系统中被保护资源。 获得权限主体的凭证,此凭证应该可以唯一标示权限主体。 默认情况下,凭证是用户的登录密码。 152 获得验证请求有关的附加信息。 默认情况下,附加信息是 WebAuthenticationDetails 的一个实例,其中保 存了客户端 ip 和 sessionid。 获得权限主体。 默认情况下,权限主体是一个实现了 UserDetails 接口的对象。 153 第 35 章 Voter表决者 35.1. Voter表决者 实际上并没有翻译的字面含义那么有血有肉,实际上就是一些条件,判 断权限的时候,这些条件有三个状态。弃权,通过,禁止。最后通过你 在 xml 里配置的策略来决定到底是不是让你访问这个需要验证的对象。 Spring Security 提供的策略有三个 • UnanimousBased.java 只要有一个 Voter 不能完全通过权限要求, 就禁止访问。这个太可怕了,我今天晚上就载在它上面了。就因为我给 所有的资源设置了两个角色,但当前的用户只拥有其中一个角色,就导 致这个用户因为权限不够,所以无法继续访问资源了。简直无法理喻啊。 • AffirmativeBased.java 只要有一个 Voter 可以通过权限要求,就可 以访问。这里应该是一个最小通过,就是说至少满足里其中一个条件就 可以通过了。 • ConsensusBased.java 只要通过的 Voter 比禁止的 Voter 数目多就可 以访问了。嘿嘿。 最后我当然选择 AffirmativeBased.java,这样,我给一个资源配置几个 角色,用户只要满足其中一个角色就可以访问啦。这样更正常一些啊。 默认提供的 Voter 继承关系如下。 AccessDecisionVoter 154 AbstractAclVoter AclEntryVoter(acls) BasicAclEntryVoter LabelBasedAclVoter RoleVoter RoleHierarchyVoter AuthenticatedVoter Jsr250Voter(tiger) 35.2. RoleVoter 默认角色名称都是以 ROLE_开头 稍微注意一下,默认角色名称都要以 ROLE_开头,否则不会被计入权 限控制,如果需要修改,就在 xml 里配个什么前缀的。可以用过配置 roleVoter 的 rolePrefix 来改变这个前缀。 35.3. AuthenticatedVoter AuthenticatedVoter 用于判断 ConfigAttribute 上是否拥有 IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED 或 IS_AUTHENTICATED_ANONYMOUSLY 之类的配置。 155 如果配置为 IS_AUTHENTICATED_FULLY,那么只有 AuthenticationTrustResolver 的 isAnonymous()和 isRememberMe()都返回 false 时才能通过验证。 如果配置为 IS_AUTHENTICATED_REMEMBERED,那么会在 AuthenticationTrustResolver 的 isAnonymous()返回 false 时通过验证。 如果配置为 IS_AUTHENTICATED_ANONYMOUSLY,就可以在 AuthenticationTrustResolver 的 isAnonymous()和 isRememberMe()两个方 法返回任意值时都可以通过验证。 35.4. AbstractAclVoter BasicAclEntryVoter, LabelBasedAclVoter, AclEntryVoter 用来在处理 ACL 中的权限控制。在 Spring Security 中的 ACL 都是基于特定 POJO 类的,每个 AclVoter 都会分配给一个特定的 POJO 类,并用来专门控制 方法中对这个 POJO 类操作的权限。实际上所有 AclVoter 都是用于处理 方法调用的,它会检测方法调用时传递的每个参数,当某个参数的类型 与 AclVoter 对应的 POJO 类一致时,就会采用配置好的元数据进行权限 校验。 156 第 36 章 拦截器 无论是 Filter,MethodInterceptor,ACL 都要用到 AOP,实际上都是拦 截器的概念,其中要用到 AbstractSecurityInterceptor 总拦截器, AfterInvocationManager 后置拦截,authenticationManager 验证管理器, 可能还要用上 RunAsManager。 关于 RunAsManager,官方文档给出的解释是,在 HttpInvoker 或者 Web Service 的情况下,当前用户的一些身份要转换成其他身份,这时就是用 RunAsManager,默认将以 RUN_AS_开头的权限名,改变成 ROLE_RUN_AS_开头的权限名,然后重新赋予当前认证主体是用。现 在的问题是不清楚具体使用在什么场景。 36.1. 权限配置数据源 处于继承树顶端的 AbstractSecurityInterceptor 有三个实现类: • FilterSecurityInterceptor,负责处理 FilterInvocation,实现对 URL 资源的拦截。 • MethodSecurityInterceptor,负责处理 MethodInvocation,实现对方 法调用的拦截。 • AspectJSecurityInterceptor,负责处理 JoinPoint,主要也是用于对 方法调用的拦截。 157 为了限制用户访问被保护资源,Spring Security 提供了一套元数据,用 于定义被保护资源的访问权限,这套元数据主要体现为 ConfigAttribute 和 ConfigAttributeDefinition。每个 ConfigAttribute 中只包含一个字符串, 而一个 ConfigAttributeDefinition 中可以包含多个 ConfigAttribute。对于 系统来说,每个被保护资源都将对应一个 ConfigAttributeDefinition,这 个 ConfigAttributeDefinition 中包含的多个 ConfigAttribute 就是访问该资 源所需的权限。 实际应用中,ConfigAttributeDefinition 会保存在 ObjectDefinitionSource 中,这是一个主要接口,FilterSecurityInterceptor 所需的 DefaultFilterInvocationDefinitionSource 和 MethodSecurityInterceptor 所需 的 MethodDefinitionAttributes 都实现了这个接口。ObjectDefinitionSource 可以看做是 Spring Security 中权限配置的源头,框架内部所有的验证组 件都是从 ObjectDefintionSource 中获得数据,来对被保护资源进行权限 控制的。 为了从 xml 中将用户配置的访问权限转换成 ObjectDefinitionSource 类型 的对象,Spring Security 专门扩展了 Spring 中提供的 PropertyEditor 实现 了 ConfigAttributeEditor,它可以把以逗号分隔的一系列字符串转换成包 含多个 ConfigAttribute 的 ConfigAttributeDefintion 对象。 "ROLE_ADMIN,ROLE_USER" ↓ ConfigAttributeDefinition 158 ConfigAttribute["ROLE_ADMIN"] ConfigAttribute["ROLE_USER"] 对于 FilterSecurityInterceptor 来说,最终生成的就是一个包含了 url pattern 和 ConfigAttributeConfiguration 的 ObjectDefinitionSource。 ↓ ConfigAttributeDefinition "/admin.jsp" → ConfigAttribute["ROLE_ADMIN"] ConfigAttribute["ROLE_USER"] 换而言之,无论我们将权限配置的原始数据保存在什么地方,只要最终 可以将其转换为 ObjectDefintionSource 就可以提供给验证组件进行调 用,实现权限控制。 36.2. 权限管理器 AbstractSecurityInterceptor 中将几个权限管理器组合应用, AuthenticationManager, AccessDecisionManager, AfterInvocationManager 和 RunAsManager。 AuthenticationManager 用来对用户请求进行认证工作,默认情况下,我 们使用的实现类为 NamespaceAuthenticationManager。它内部将包含一 个 AuthenticationProvider 队列,在实际进行权限校验的时候顺序执行这 个队列实现对用户的认证功能。AuthenticationProvider 中最常见的实现 159 是 DaoAuthenticationProvider,它可以根据用户的 username 和 password 对用户有效性进行验证,如果通过校验就会从 userDetailsService 中获取 用户信息,并为用户授予对应的权限。 AccessDecisionManager 用于控制资源的访问权限,它下面有三个实现类 可以选择 AffirmativeBased, UnanimousBased 和 ConsensusBased,分别对 应,一票通过,一票否决,多数通过。每个 AccessDecisionManager 内 部拥有多个 Voter,每个 Voter 会进行表决,表决的结果有 ACCESS_GRANTED 赞成, ACCESS_DENIED 反对, ACCESS_ABSTAIN 弃权三种。AccessDecisionManager 会根据最终投票 的结果,结合实现类的策略判断用户是否可以访问当前资源。 36.3. 后置调用管理器 AfterInvocationManager 用来在方法调用完成后,根据用户的权限,对方 法返回的结果进行筛选,它主要是用在 ACL 中的。 AfterInvocationManager 有两个实现类。 BasicAclEntryAfterInvocationCollectionFilteringProvider 用于过滤返回结 果为集合的方法,BasicAclEntryAfterInvocationProvider 用于控制返回结 果为对象的方法。 36.4. 临时分配额外权限 160 我们可以在某一方法调用过程中,使用 RunAsManager 为用户临时分配 额外的权限。据说这个功能是为了在方法内部远程调用被保护资源而实 现的,为实现这一功能,我们首先要在 AbstractMethodInterceptor 中设 置 RunAsManagerImpl,并且要在 ObjectDefintionSource 中配置 RUN_AS_开头的权限,这样,当用户访问这个方法时,就会自动将 SecurityContext 中保存的 Authenticaton 对象替换为 RUN_AS 对象,并 在其中附加额外的权限。 现在还不能在 2.x 版本的命名空间中调用 RunAsManager,3.0.0.M1 中提 供了如下方法。 161 第 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. 使用角色继承 162 在 Spring Security 中,我们可以指定角色间的继承关系,这样可以重用 角色权限,减少配置的代码量,让权限配置整体上显得更清晰。 为了使用角色继承功能,我们需要对原有的配置文件进行一些修改。 我们将原有的 user-service 单独抽离出来,在 userDetailsService 的基础 上生成一个 userDetailsServiceWrapper,这个 wrapper 的作用就是在原有 的 user-service 的基础上启用角色继承功能。 我们使用 RoleHierarchyImpl 为 userDetailsServiceWrapper 配置了角色继 承的策略,ROLE_ADMIN > ROLE_USER 表示 ROLE_ADMIN 将继承 ROLE_USER所有用的所有角色,只要是允许ROLE_USER访问的资源, 163 ROLE_ADMIN 也都有权限进行访问。这样我们在 user-service 中的配置 就可以从 ROLE_ADMIN,ROLE_USER 简化为 ROLE_ADMIN 了,而 intercept-url 中的配置也可以从 ROLE_ADMIN,ROLE_USER 改为 ROLE_USER 了。 如果希望配置更多继承关系,可以使用换行进行分隔,比如: ROLE_A > ROLE_B ROLE_B > ROLE_AUTHENTICATED ROLE_AUTHENTICATED > ROLE_UNAUTHENTICATED 实例在 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 部分实现角色继承,需要进行如 下配置。 164 首先根据 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; } /** 165 * Calls the RoleHierarchy 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[i] = new GrantedAuthoritySid(authorities[i - 1]); } return sids; } } 然后在 acl 的配置文件中配置 bean,并在 AclEntryVoter, AclEntryAfterInvocationProvider 和 AclEntryAfterInvocationCollectionFilteringProvider 中替换默认的 SidRetrievalStrategy。 166 这样就在 acl 中添加了对角色继承的支持。 37.4. PasswordEncoder和SaltValue 默认提供的 PasswordEncoder 包含 plaintext, sha, sha-256, md5, md4, {sha}, {ssha}。其中{sha}和{ssha}是专门为 ldap 准备的,plaintext 意味 着不对密码进行加密,如果我们不设置 PasswordEncoder,默认就会使 用它。 167 SaltValue 是为了让密码加密更加安全,它有两种策略可以选择。 user-property, system-wide 分别对应着 ReflectionSaltSource 和 SystemWideSaltSource,它们的区别是 ReflectionSaltSource 会使用反射, 从用户的 Principal 对象汇总取出一个对应的属性来作为盐值,而 SystemWideSaltSource 会为所有用户都设置相同的盐值。 使用了 PasswordEncoder 和 SaltValue 的结果就是数据库中的密码变得难 以辨认了,这就要注意在添加用户时要使用相同的策略对密码进行加 密,这才能保证新用户可以正常登陆。 168 第 38 章 集成jcaptcha 使用 jcaptcha 实现彩色验证码,这是一个被 spring security 2 放弃的组件, 据说本来维护 acegi 版本的作者不见了,所以组件就放到了 sandbox 中, 最终被废弃了。 首先在 pom.xml 中添加依赖。 com.octo.captcha jcaptcha 1.0 commons-lang commons-lang 2.4 然后,将 jcaptcha 集成相关的代码复制到 src/main/java 目录下。 下一步,修改配置文件,添加 jcaptcha 的过滤器与 provider,代码太多, 请参考示例中的实际配置。 最终自定义登录页面,显示效果如下所示: 169 图 38.1. jcaptcha 实例在 ch206。 170 第 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 中。 171 res_type="METHOD"对应将在 MethodSecurityInterceptor 中使用的被保 护方法。 INSERT INFO RESC VALUES(3,'','METHOD','com.family168.springsecuritybook.ch207.MessageService.adminMe ssage',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' 172 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 是不会起作用的。 173 我们需要获取 delegatingMethodDefinitionSource,将数据库中读取的资 源信息设置到它里面才能使 MethodSecurityInterceptor 和动态生成的 pointcut 都是用我们最新的资源信息。 MethodDefinitionSource source = resourceDetailsBuilder .createMethodSource(queryMethod); List sources = new ArrayList(); sources.add(source); delegatingMethodDefinitionSource.setMethodDefinitionSources(sources); 因为 ACL 实际上也是借助于 MethodSecurityInterceptor 来实现的,所以 可以将 ACL_READ 和 AFTER_ACL_READ 配置在 res_type="METHOD"的资源中。 实例在 ch207 中。 174 第 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()方法,用以 获得用户的邮箱地址。 175 最初我们的打算是直接继承 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; } } 176 40.2. 实现 UserDetailsService 接口 为了将 UserInfo 提供给权限系统,我们还需要实现自定义的 UserDetailsService,这个接口只包含一个方法: public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException; } 实际运行中,系统会通过这个方法获得登录用户的信息。 下面我们直接实现 UserDetailsService 接口,在其中创建 UserInfo 的对 象。 public class UserInfoService implements UserDetailsService { private Map userMap = null; public UserInfoService() { userMap = new HashMap(); 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 { 177 return userMap.get(username); } } 40.3. 修改配置文件 将 UserInfoService 添加到配置文件中: 定义 userDetailsService 之后,然后使用 user-service-ref 为 authentication-provider 设置对 UserDetailsService 的引用,这样在系统中 就会从我们自定义的 UserInfoService 中获取用户信息了。 40.4. 测试运行 修改过配置文件后,在 ch208 中启动 mvn,还是通过登录页面进入系统, 在登录成功页面中就可以看到用户对应的邮箱地址了。 图 40.1. 显示邮箱地址信息 178 这时保存在 SecurityContext 中的 Principal 已经变为了 UserInfo 类型的对 象,我们可以直接使用 taglib 获得启动的邮件信息。 email : 如果希望获得 UserInfo 对象,可以使用如下代码: UserInfo userInfo = (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 实例在 ch208 中。 179 第 41 章 锁定用户 这里我们通过一个常见的功能需求来演示如何锁定一个用户的账号。 下面我们需要实现这样一个功能,当一个用户输错三次密码后,就锁定 这个账号。 首先我们要注意到,虽然 UserDetails 中提供了 isAccountNonLocked()方 法,框架中也提供了 LockedException,但是默认提供的 User 实现类不 太容易实现对用户锁定的操作,为此,我们需要借用???中定义的 UserInfo 来实现对用户的锁定。 为了监听用户输入错误密码的事件,我们需要自定义一个事件监听器 LockUserListener,它的主体代码如下所示: public void onApplicationEvent(ApplicationEvent event) { if (event instanceof AuthenticationFailureBadCredentialsEvent) { AuthenticationFailureBadCredentialsEvent authEvent = (AuthenticationFailureBadCredentialsEvent) event; Authentication authentication = (Authentication) authEvent.getSource(); String username = (String) authentication.getPrincipal(); addCount(username); } if (event instanceof AuthenticationSuccessEvent) { AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event; Authentication authentication = (Authentication) authEvent.getSource(); UserInfo userInfo = (UserInfo) authentication.getPrincipal(); String username = userInfo.getUsername(); cleanCount(username); } } 180 其中,AuthenticationFailureBadCredentialsEvent 事件表示用户输入了错 误的密码,AuthenticationSuccessEvent 表示用户登录成功。 将 LockUserListener 添加到配置文件中即可监听权限校验的事件。 在获得 AuthenticationFailureBadCredentialsEvent 事件,也即用户输入错 密码时,我们首先要获得对应的用户名,然后将用户名对应的输入密码 错误次数加一。当获得 AuthenticationSuccessEvent 事件时,说明用户已 经成功登陆了,这时我们要把用户之前输入错误的密码次数清零,让他 再次登录时还可以享有三次错误的机会。 这里为了演示方便,直接将用户输入密码错误的次数记录在 ServletContext 中,代码如下所示: protected void addCount(String username) { Map lockUserMap = getLockUserMap(); Integer count = lockUserMap.get(username); if (count == null) { lockUserMap.put(username, Integer.valueOf(1)); } else { int resultCount = count.intValue() + 1; if (resultCount >= 3) { UserInfo userInfo = (UserInfo) userInfoService.loadUserByUsername(username); 181 userInfo.lockAccount(); } else { lockUserMap.put(username, Integer.valueOf(resultCount)); } } } 当用户输错密码超过三次时,就会从 UserInfoService 中取出对应的 UserInfo 对象,使用 lockAccount()方法将该用户账号锁定,之后用户就 会在登录上看到对应的错误信息。 图 41.1. 用户已被锁定 之后用户再怎么尝试也无法登陆到系统中了。 实例在 ch209 中。 用户过期与密码过期的处理方法与锁定用户类似,等以后有机会再详细 介绍。 182 第 42 章 设置过滤器链 对于不是从 acegi 升级到 spring security 的同志们,用多了命名空间这种 配置方式,肯定在抱怨它的扩展性不够。现在我们就来展示一下在 acegi 中屡遭诟病的自定义过滤器链配置方式。 警告:acegi 当年就是以这种配置方式,被冠以“每使用一次 acegi,世界的某个 地方就会有一个精灵死掉”的称号,请各位慎用。 既然不再使用 http 标签,我们就需要在配置文件中手工声明一个 springSecurityFilterChain,这个 bean 会由 web.xml 中的 DelegatingFilterProxy 调用,注意 bean 的 id 必须为 springSecurityFilterChain,否则系统启动时会报错。 下面我们就在 springSecurityFilterChain 中配置多个过滤器链。 这里我们配置了三套过滤器链,loginPageFilter 负责处理 /spring_security_login, 183 httpSessionContextIntegrationFilter,authenticationProcessingFilter 用来处 理/j_spring_security_check*,最后由其他三个过滤器处理其外的所有 URL 请求。 loginPageFilter 用来生成登录页面,authenticationProcessingFilter 用来处 理用户登录请求,只是它还需要与 httpSessionContextIntegrationFilter 一 同起作用才能完成用户登录。这三个过滤器的配置如下: 其余的所有 URL 请求都使用 httpSessionContextIntegrationFilter,exceptionTranslationFilter,filterInvocati onInterceptor 这三个过滤器的组合来处理,它们就是用来实际控制权限 的部分。虽然这里只要配置三个过滤器,但实际上它们还需要和其他附 属功能部件一起工作才能完成全县控制的功能。 184 比如 exceptionTranslationFilter 就需要 authenticationEntryPoint 来控制抛 出异常时响应的策略和跳转的 URL 地址。 而 filterInvocationInterceptor 作为最核心的权限控制拦截器还需要 authenticationManager 和 decisionManager 的配合,这里我们为了不多害 死其他精灵,所以就不贴配置内容了。 最后我们可以看到,使用了这种 acegi 中古老的配置方法能给我们控制 每个组件的权力,但是整整 82 行代码的配置所实现功能,也就相当于 使用了命名空间配置的 20 行代码,明显还是使用命名空间配置方式在 可维护性上更有优势。 实例在 ch210 中。 185 第 43 章 自定义过滤器 常见的问题就是要在登录时多加几个参数时,默认的 AuthenticationProcessFilter 既不支持保存额外参数,也没有提供扩展点 来实现这个功能,实际上就算是 Spring Security-3.x 中也因为只能配置 一个 handler,实际扩展时还是比较麻烦。所以这个时候一般的选择就是 自定义过滤器了。 我们的目标是在登录时除了填写用户名和密码之外,再添加一个 mark 参数。 图 43.1. 登录时附加一个 mark 参数 我们的目的是在登录时将这个参数保存到 session 中,以备后用。为此 我们要扩展 AuthenticationProcessFilter: public class LoginFilter extends AuthenticationProcessingFilter { public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { Authentication authentication = super.attemptAuthentication(request); String mark = request.getParameter("mark"); 186 request.getSession().setAttribute("mark", "mark"); return authentication; } } 实际上我们只需要重写 attemptAuthentication()这个方法,先调用 super.attemptAuthentication()获得生成的 Authentication,如果这部分没有 抛出异常,我们下面再去从 request 中获得 mark 参数,再把这个参数保 存到 session 里。最后返回 authentication 对象即可。 下面修改配置文件,在 xml 中添加一个名为 loginFilter 的 bean,使用 custom-filter 将它加入到过滤器链中,放到原来的 form-login 的前面。 这样我们自定义的 LoginFilter 就会取代原本的 AuthenticationProcessFilter 处理用户登录,并在用户登录成功时将额外 的 mark 参数保存到 session 中。 之后在 jsp 中,我们就可以直接通过${sessionScope['mark']}来获得 mark 的参数值。 187 图 43.2. 显示额外的参数 实例在 ch211 中。 188 第 44 章 使用用户组 Spring Security 提供了用户组机制,可以将多个用户归纳在一个组中, 进行统一授权。下面我们来研究一下如何在数据库中使用用户组保存用 户的权限信息。 44.1. 数据库结构 在原数据库的基础上添加用户组所需的三张表: create table groups ( id bigint generated by default as identity(start with 0) primary key, group_name varchar_ignorecase(50) not null ); create table group_authorities ( group_id bigint not null, authority varchar(50) not null, constraint fk_group_authorities_group foreign key(group_id) references groups(id) ); create table group_members ( id bigint generated by default as identity(start with 0) primary key, username varchar(50) not null, group_id bigint not null, constraint fk_group_members_group foreign key(group_id) references groups(id) ); ER 图如下所示: 189 图 44.1. 用户组 ER 图 下面开始初始化数据: insert into users(username,password,enabled) values('admin','admin',true); insert into users(username,password,enabled) values('user','user',true); insert into groups(id,group_name) values(1,'admin'); insert into groups(id,group_name) values(2,'user'); insert into group_authorities(group_id,authority) values(1,'ROLE_ADMIN'); insert into group_authorities(group_id,authority) values(2,'ROLE_USER'); insert into group_members(id,username,group_id) values(1,'admin',1); insert into group_members(id,username,group_id) values(2,'admin',2); 190 insert into group_members(id,username,group_id) values(3,'user',2); 创建两个用户 admin 和 user。 创建两个组 admin 和 user。 为用户组授权,让 admin 组中的所有用户都拥有 ROLE_ADMIN 权限,user 组 中的所有用户都拥有 ROLE_USER 权限。 admin 用户加入 admin 和 user 两个用户组,将 user 用户加入 user 用户组。 44.2. 修改配置文件 为 jdbc-user-service 添加一个参数就可以打开用户组功能。 这样系统就会使用这条 sql 语句从用户组表中查询用户拥有的权限。 之后可以启动实例 ch212,使用 admin 和 user 用户测试授权情况。 实例在 ch212 中。 191 第 45 章 在JSF中使用Spring Security 在网上看到一个同志说不知道如何在 JSF 中使用 Spring Security,这里 特别做了一个例子演示一下,预先声明一下咱们对 JSF 并不太了解,例 子略显简单,将就着用吧。 45.1. 修改过滤器支持forward 第一步就是修改 web.xml 中的过滤器配置,这样才能支持 forward,否 则默认只支持 request 方式的请求。 springSecurityFilterChain /* FORWARD REQUEST 45.2. 自定义登录页面 使用 JSF 写一个登陆页面。 192




然后创建对应的 loginBean。 public String doLogin() throws IOException, ServletException { ExternalContext context = FacesContext.getCurrentInstance() .getExternalContext(); RequestDispatcher dispatcher = ((ServletRequest) context.getRequest()) .getRequestDispatcher("/j_spring_security_check"); dispatcher.forward((ServletRequest) context.getRequest(), (ServletResponse) context.getResponse()); FacesContext.getCurrentInstance().responseComplete(); // It's OK to return null here because Faces is just going to exit. return null; 193 } 它主要负责在进行 doLogin 时,将请求转发到/j_spring_security 进行用 户登录认证。 45.3. 显示密码错误信息 为了在自定义页面回显密码错误的信息,需要定义一个监听器。 package com.family168.springsecuritybook.ch213; import javax.faces.application.FacesMessage; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import org.springframework.security.BadCredentialsException; import org.springframework.security.ui.AbstractProcessingFilter; public class LoginErrorPhaseListener implements PhaseListener { private static final long serialVersionUID = -1216620620302322995L; public void beforePhase(final PhaseEvent arg0) { Exception e = (Exception) FacesContext.getCurrentInstance() .getExternalContext() .getSessionMap() .get(AbstractProcessingFilter.SPRING_ SECURITY_LAST_EXCEPTION_KEY); if (e instanceof BadCredentialsException) { FacesContext.getCurrentInstance().getExternalContext() .getSessionMap() .put(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTIO N_KEY, null); FacesContext.getCurrentInstance() .addMessage(null, 194 new FacesMessage(FacesMessage.SEVERITY_ERROR, "Username or password not valid.", null)); } } public void afterPhase(final PhaseEvent arg0) { } public PhaseId getPhaseId() { return PhaseId.RENDER_RESPONSE; } } 在解析之前判断是否存在 BadCredentialsException 异常,如果存在,就 添加一条 message,这条 message 会显示在登录页面上。 在 faces-config.xml 添加这个监听器即可生效。 com.family168.springsecuritybook.ch213.LoginErrorPhaseListener 实例在 ch213 中。 195 第 46 章 自定义会话管理 注意:concurrent-session 存在一个问题,如果登录时选择了 rememberMe,然后 关闭浏览器,再打开浏览器访问应用就会出现 sessionId 为空的问题,暂时无法 解决。 46.1. 默认策略的缺陷 默认提供的 concurrent-session-control 只支持两种策略: • 一是允许后登陆的用户将之前登陆的用户踢出系统,先登录的用 户会看到“会话已失效”的提示。 这种策略实际上没办法阻止其他人在我登陆到系统之后,再使用我的账 号登陆系统。最后会形成双方争先恐后的进行登录,不断将对方踢出系 统的情况。 • 另一个是禁止用户使用已登录到系统的账户进行登录,后登陆的 用户会在登录时看到“会话数量超过允许范围”的提示。 这种策略会导致一个比较麻烦的问题,如果用户不慎关闭了浏览器,没 有通过 logout 退出系统,那么必须等到系统中用户的会话失效之后才能 再次登录。如果用户不留神关了浏览器,就要再等半个小时才能再登录 系统,这显然是没有道理的。 为了保证用户使用系统时不会受到其他人的影响,同时也保证不会同时 有多个人使用同一个账号登陆系统,我们系统默认提供的第二种策略上 进行一些修改。 196 简单来说,就是当用户已经登陆到系统时,如果又有人使用同一账号尝 试登录系统,系统会判断当前用户的 ip 地址,如果 ip 地址与上次登录 的 ip 地址相同,则注销上次登录的会话,允许当前用户登录系统。如 果 ip 地址与上次登录的 ip 地址不同,而抛出异常,禁止用户登录系统。 这样既保证了同一时间只能有一个用户登录系统,又可以在用户操作失 误关闭浏览器后可以再次登录系统。 46.2. 记录用户名与ip 默认情况下,系统使用 SessionRegistry 中只保存登录的用户名,为了比 对 ip 地址,我们需要在登录时将用户的远程 ip 也保存到 SessionRegistry 中。 为此我们创建了 SmartPrincipal 类,它包含 username 和 ip 两个字段,并 定义了 equals()和 hashCode(),这样可以保证它在 HashMap 中的操作不 会出现问题。 package com.family168.springsecuritybook.ch214; import org.springframework.security.Authentication; import org.springframework.security.ui.WebAuthenticationDetails; import org.springframework.security.userdetails.UserDetails; import org.springframework.util.Assert; public class SmartPrincipal { private String username; private String ip; 197 public SmartPrincipal(String username, String ip) { Assert.notNull(username, "username cannot be null (violation of interface contract)"); Assert.notNull(ip, "username cannot be null (violation of interface contract)"); this.username = username; this.ip = ip; } public boolean equalsIp(SmartPrincipal smartPrincipal) { return this.ip.equals(smartPrincipal.ip); } @Override public boolean equals(Object obj) { if (obj instanceof SmartPrincipal) { SmartPrincipal smartPrincipal = (SmartPrincipal) obj; return username.equals(smartPrincipal.username); } return false; } @Override public int hashCode() { return username.hashCode(); } } 46.3. 改造控制类 下一步要改造 ConcurrentSessionControllerImpl 和 SessionRegistryImpl。 改造 SessionRegistryImpl 的原因比较复杂,因为除了 ProviderManager 会通过 ConcurrentSessionController 调用 SessoinRegistry 来注册登录用户 198 之外,AuthenticationProcessFilter 和 SessionFixationProtectionFilter 也会 在处理会话伪造时直接调用 SessionRegistry。 默认情况下,这些类都会把用户名直接注册到 SessionRegistry 中,因为 我们现在需要获得用户 ip,就需要把 SmartPrincipal 保存到 SessionRegistry 中,这里需要的就是做一步转换工作。 public class SmartSessionRegistry extends SessionRegistryImpl { public synchronized void registerNewSession(String sessionId, Object principal) { // // convert for SmartPrincipal // if (!(principal instanceof SmartPrincipal)) { principal = new SmartPrincipal(SecurityContextHolder.getContext() .getAuthentication( )); } super.registerNewSession(sessionId, principal); } } 对于 ConcurrentSessionController 来说,就是为了实现在登录时判断用户 的 ip 是否与之前登陆是保存的一样,本打算继承 ConcurrentSessionControllerImpl,但是因为其中的 exceptionIfMaximumExceeded 属性未暴露给子类,所以只好重写了一个 类,其中主要修改了 allowableSessionsExceeded(), checkAuthenticationAllowed(), registerSuccessfulAuthentication()方法,具 体代码请参考实例中的 SmartConcurrentSessionController。 199 46.4. 修改配置文件 因为 concurrent-session-controller 标签不支持自定义的 ConcurrentSessionController,我们只好使用其他办法将我们自定义的组 件放入 Spring Security 中。 我们在 authentication-manager 中使用 session-controller-ref 将自定义的 SmartConcurrentController 引入命名空间中,它会自动将 SmartSessionRegistry 交给 AuthenticationProcessFilter 与 SessionFixationProtectionFilter 使用。 完成这些工作之后,我们需要找两台电脑来测试上述的策略是否可以正 常运行。当有用户登录之后,其他人是无法在另外的电脑上使用同一账 200 号登陆系统的,但是如果是当前登录的用户关闭了浏览器,这时再次登 录系统应该是被允许的。 实例在 ch214 中。 201 第 47 章 匹配URL地址 恐怕很多同志都有一个误解,就是 Spring Security 中配置 URL 的时候, 要么配置成一个特定的 URL,比如/admin/index.jsp,要么配置为某个路 径下所有的 URL,比如/admin/**。如果你真的这么认为的话,那就太 小看 Spring Security 了,/**只是默认提供的 AntUrlPathMatcher 所支持 功能的一部分而已,下面我们就来破除迷信,深入讨论一下匹配 URL 地址这方面的功能。 47.1. AntUrlPathMatcher 我们默认使用的 URL 匹配器就是这个 AntUrlPathMatcher,它来自于 http://ant.apache.org/项目,是一种简单易懂的路径匹配策略。 它为我们提供了三种通配符。 • 通配符:? 示例:/admin/g?t.jsp 匹配任意一个字符,/admin/g?t.jsp 可以匹配/admin/get.jsp 和 /admin/got.jsp 或是/admin/gxt.do。不能匹配/admin/xxx.jsp。 • 通配符:* 示例:/admin/*.jsp 202 匹配任意多个字符,但不能跨越目录。/*/index.jsp 可以匹配 /admin/index.jsp 和/user/index.jsp,但是不能匹配/index.jsp 和 /user/test/index.jsp。 • 通配符:** 示例:/**/index.jsp 可以匹配任意多个字符,可以跨越目录,可以匹配/index.jsp, /admin/index.jsp,/user/admin/index.jsp 和/a/b/c/d/index.jsp 47.2. RegexUrlPathMatcher 如果默认的 AntUrlPathMatcher 无法满足需求,还可以选择使用更强大 的 RegexUrlPathMatcher,它支持使用正则表达式对 URL 地址进行匹配。 如果希望使用 RegexUrlPathMatcher,就需要在配置文件中添加如下配 置: 上面例子中就使用了正则表达式进行 URL 匹配,^/.*$与之前使用的/** 意义相同,可以匹配任意一个请求。^(/.*)*/admin\.jsp$表示任意一个目 录下的 admin.jsp 请求。 203 有关正则表达式的更多信息请去网上找一下吧,与它相关的资源太多 了。 实例在 ch215。 47.3. lowercase-comparisons 与 URL 匹配相关的配置还有一个 lowercase-comparisons,它的功能是在 进行 URL 匹配前是否需要自动将 URL 中的字符都转换成小写。 如果我们没有设置 lowercase-comparisons 的值,那么在默认情况下 AntUrlPathMatcher 会在每次匹配 URL 之前都将 URL 中的字符转换为小 写,而 RegexUrlPathMatcher 默认不会将 URL 中的字符转换为小写。也 就是说默认情况下 AntUrlPathMatcher 的默认值是 lowercase-comparisons="true",而 RegexUrlPathMatcher 的默认值是 lowercase-comparisons="false"。 如果需要修改 lowercase-comparisons 参数的值,可以像下面这样修改配 置。 204 第 48 章 配置过滤器 48.1. 标准过滤器 下面是命名空间所支持的所有过滤器名称,类型,位置,以及在命名空 间中对应的配置。 表 48.1. 标准过滤器别名和顺序 别名 过滤器类 命名空间元素或属性 CHANNEL_FILTER ChannelProcessingFilter http/intercept-url CONCURRENT_SESSION_FI LTER ConcurrentSessionFilter http/concurrent-ses sion-control SESSION_CONTEXT_INTEG RATION_FILTER HttpSessionContextIntegr ationFilter http LOGOUT_FILTER LogoutFilter http/logout X509_FILTER X509PreAuthenticatedProc essigFilter http/x509 PRE_AUTH_FILTER ses N/A CAS_PROCESSING_FILTER CasProcessingFilter N/A AUTHENTICATION_PROCES SING_FILTER AuthenticationProcessing Filter http/form-login OPENID_PROCESSING_FIL TER OpenIDAuthenticationProc essingFilter N/A BASIC_PROCESSING_FILT ER BasicProcessingFilter http/http-basic SERVLET_API_SUPPORT_F ILTER SecurityContextHolderAwa reRequestFilter http/@servlet-api-p rovision REMEMBER_ME_FILTER RememberMeProcessingFilt er http/remember-me ANONYMOUS_FILTER AnonymousProcessingFilte r http/anonymous 205 别名 过滤器类 命名空间元素或属性 EXCEPTION_TRANSLATION _FILTER ExceptionTranslationFilt er http NTLM_FILTER NtlmProcessingFilter N/A FILTER_SECURITY_INTER CEPTOR FilterSecurityIntercepto r http SWITCH_USER_FILTER SwitchUserProcessingFilt er N/A 上面的表格中列出了命名空间中支持的所有标准过滤器以及它们对应 的顺序,这些过滤器的顺序是至关重要的,只有按照正确的顺序进行配 置才能让这些过滤器正常发挥作用,在 ACEGI 的年代有太多太多的问 题都是由于过滤器摆放的位置不正确造成的。幸好我们现在可以使用命 名空间,它会帮我们将使用到的过滤器按照正确的方式摆放在一起。 48.2. 在 http 中启用标准过滤器 如果我们什么也不配置,只使用标签的话,也会有五个默认的过 滤器被创建出来,并放到过滤器链中。它们是: HttpSessionContextIntegrationFilter SecurityContextHolderAwareRequestFilter ExceptionTranslationFilter SessionFixationProtectionFilter FilterSecurityInterceptor 如果追求最小的过滤器链,可以为标签加上 servlet-api-provision="false"。 这样就会禁用 SecurityContextHolderAwareRequestFilter 和 SessionFixationProtectionFilter 两个过滤器,只剩下三个过滤器会被自动 创建了。 这三个过滤器在系统中拥有十分特殊的含义,它们三个是命名空间限定 的最核心过滤器,如果我们使用了命名空间的配置方式,那么这三个过 滤器就是不可替换的,如果使用了妄图替换其中任何一 个过滤器,都会抛出异常,从而导致系统无法启动。 如果在中设置了 auto-config="true"就会默认启用多个常用的过滤 器,实际上就相当于如下配置: 使用auto-config="true"就等于在中添加了五个标签,同时会启用 “用户注销”,“基于表单认证”,“http basic认证”,“记忆登录信 息”,“匿名认证”五个功能。这五个功能的实际效果可以参考 第 11 章 图 解过滤器。 207 48.3. 为自定义过滤器设置位置 如果我们要自定义一个过滤器,就需要使用 custom-filter 标签,将过滤 器加入过滤器链中才能实际起作用。在 custom-filter 中可以使用以下设 置好的位置: "FIRST" "CHANNEL_FILTER" "CONCURRENT_SESSION_FILTER" "SESSION_CONTEXT_INTEGRATION_FILTER" "LOGOUT_FILTER" "X509_FILTER" "PRE_AUTH_FILTER" "CAS_PROCESSING_FILTER" "AUTHENTICATION_PROCESSING_FILTER" "OPENID_PROCESSING_FILTER" "BASIC_PROCESSING_FILTER" "SERVLET_API_SUPPORT_FILTER" "REMEMBER_ME_FILTER" "ANONYMOUS_FILTER" "EXCEPTION_TRANSLATION_FILTER" "NTLM_FILTER" "FILTER_SECURITY_INTERCEPTOR" "SWITCH_USER_FILTER" "LAST" 在 custom-filter 中可以使用 before|position|after 三种方式,将自定义过滤 器放在对应名称的位置上,或者位置之前,或者位置之后。除了表示最 前面的 FIRST 和表示最后面的 LAST 之外,这里的每个名称都对应着一 个标准过滤器,我们可以在上面的章节中找到其对应的位置,要记住 SESSION_CONTEXT_INTEGRATION_FILTER, EXCEPTION_TRANSLATION_FILTER 和 208 FILTER_SECURITY_INTERCEPTOR 三个过滤器是不可替换的,不能 对它们使用 position。 209 第 49 章 监控会话过期 此功能来自于官方JIRA,http://jira.springsource.org/browse/SEC-1142, 如果这个功能被官方接受,应该可以在Spring Security-3.0.0.M2 中提供。 49.1. 实现原理 官方给出的思路是根据 HttpServletRequest 的 getRequestedSessionId()和 isRequestedSessionIdValid()来判断当前用户的会话是否过期。 马上想到的就是直接通过 isRequestedSessionIdValid()来判断当前用户的 会话是否有效,如果会话失效就提示会话过期,并跳转到相应的页面。 可惜这样做的结果是无法区分用户第一次访问系统与真实的会话过期, 造成的结果是用户每次登陆系统首先看到的都是会话过期的的提示。 下一步考虑将 getRequestedSessionId()与 isRequestedSessionIdValid()结 合使用,先通过 getRequestedSessionId()判断用户当前的请求是否带有 jsessionid,如果请求中并没有包含 jsessionid,说明用户是第一次访问系 统,不需要理会。如果请求中包含了 jsessionid,再去判断对应的会话是 否失效,这样就可以区分用户第一次访问和会话失效两种情况。 这种方式还存在的一个漏洞,就是当已经登录的用户使用 logout 从系统 中注销时,LogoutFilter 中只会执行 session.invalidate()将会话销毁,浏 览器的 cookie 中依然保存着已销毁的 session 的 jsessionid,这样就造成 用户一旦进行注销就会提示会话已失效。为了解决这个问题,我们特地 210 编写了一个 LogoutHandler 用于在注销时删除浏览器端 cookie 中的 jsessionid。 49.2. 代码实现 最主要的代码就是 SessionTimeoutFilter 这个过滤器,它负责判断当前用 户的状态,如果当前用户不是第一次访问系统,并且他的请求所对应的 会话已经失效了,那么就发出会话已失效的信息。 public class SessionTimeoutFilter extends SpringSecurityFilter { private PortResolver portResolver = new PortResolverImpl(); private String sessionTimeoutUrl; public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (this.isSessionExpired(request)) { this.processRequest(request, response); } else { chain.doFilter(request, response); } } protected String determineSessionTimeoutUrl(HttpServletRequest request) { return (sessionTimeoutUrl != null) ? sessionTimeoutUrl : "/spring_security_login?login_error"; } protected boolean isSessionExpired(HttpServletRequest request) { return (request.getRequestedSessionId() != null) && !request.isRequestedSessionIdValid(); } protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpSession session = request.getSession(); SavedRequest savedRequest = new SavedRequest(request, portResolver); 211 session.setAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY, new SessionTimeoutException()); session.setAttribute(AbstractProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY, savedRequest); String targetUrl = determineSessionTimeoutUrl(request); targetUrl = request.getContextPath() + targetUrl; response.sendRedirect(response.encodeRedirectURL(targetUrl)); } } 为了演示方便,我们设置在用户会话失效时跳转到默认的登录页面,并 自定义了一个 SessionTimeoutException 异常,它负责在默认的登录页面 中显示会话失效的提示信息。 对应配置如下所示,为了避免其他浏览器新建 session,我们要将 SessionTimeoutException 放在过滤器链的最前面。 下一步需要处理注销时浏览器 cookie 中的 jsessionid,我们创建一个 SessionIdLogoutHandler 用于清空 cookie 中的 jsessionid。 public class SessionIdLogoutHandler implements LogoutHandler { public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { Cookie cookie = new Cookie("JSESSIONID", null); cookie.setMaxAge(0); 212 cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/"); response.addCookie(cookie); } } 配置的时候稍微有一点儿麻烦,因为 namespace 不支持添加自定义的 LogoutHandler,所以只好将整个 LogoutFilter 的配置都提取出来重新配 置一遍,再覆盖原来的 这样就实现了在用户会话失效时提示对应的信息,我们可以启动 ch217 测试一下实际的效果。 首先我们使用 user/user 或 admin/admin 登录系统,然后静待 1 分钟,为 了演示方便我们在 web.xml 中将 session 失效时间设置为 1 分钟,只要 登陆后 1 分钟以上不进行操作,session 就会自动失效。如下图所示: 213 图 49.1. 会话失效 49.3. 目前实现的缺陷 最大的缺陷就是不支持 remember-me,因 为 SessionTimeoutFilter 位于所 有过滤器的前面,如果用户设置了 RememberMe,他在遇到会话超时的 情况下依然会被 SessionTimeoutFilter 拦截住,应该在目前实现的基础上 添加对 RememberMe 的判断才好。 为求简便,我们这里就不做更多讨论了,请朋友们自行发挥。 实例见 ch217。 214 第 50 章 多个登陆页面 实际就是想让不同的用户看到的是不同的登录页面,比如前台用户进入 的页面要多放广告和新闻,而后台管理登陆页面就可以朴素点儿。 50.1. 未登录自动跳转到对应的登录页面 因为 Spring Security 会使用 AuthenticationEntryPoint 在未登录用户访问 被保护资源时自动跳转到登录页面,所以我们这里需要的就是扩展 AuthenticaitonEntryPoint。 public class LoginPageEntryPoint implements AuthenticationEntryPoint, InitializingBean { private LoginPageStrategy loginPageStrategy; public void afterPropertiesSet() throws Exception { Assert.notNull(loginPageStrategy, "loginPageStrategy must be specified"); } public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException { loginPageStrategy.process((HttpServletRequest) request, (HttpServletResponse) response); } public void setLoginPageStrategy(LoginPageStrategy loginPageStrategy) { this.loginPageStrategy = loginPageStrategy; } } 215 这里的 LoginPageEntryPoint 所作的工作就是将 request 和 response 转换 为 HttpServletRequest 和 HttpServletResponse,然后交给 LoginPageStrategy 进行处理,实际的操作都写在 LoginPageStrategy 中。 这里我们提供了两个简单的实现类,用来演示根据 ip 或者 url 来为用户 显示不同的登录页面的功能。 首先是 IpMappingLoginPageStrategy,它的作用是当请求来自 127.0.0.1 时,就会显示管理员的登陆页面,否则显示普通用户的登录页面。 public class IpMappingLoginPageStrategy implements LoginPageStrategy { public void process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String targetUrl = null; String ip = request.getRemoteAddr(); if ("127.0.0.1".equals(ip)) { targetUrl = "/login/admin.jsp"; } else { targetUrl = "/login/user.jsp"; } targetUrl = request.getContextPath() + targetUrl; response.sendRedirect(targetUrl); } } 然后是 UrlMappingLoginPageStrategy,它会判断用户当前请求的 URL, 如果 URL 中包含了“/admin”就跳转到管理员登陆页面,否则跳转到 用户登录页面。 public class UrlMappingLoginPageStrategy implements LoginPageStrategy { public void process(HttpServletRequest request, 216 HttpServletResponse response) throws IOException, ServletException { String targetUrl = null; String uri = request.getRequestURI(); if (uri.indexOf("admin") != -1) { targetUrl = "/login/admin.jsp"; } else { targetUrl = "/login/user.jsp"; } targetUrl = request.getContextPath() + targetUrl; response.sendRedirect(targetUrl); } } 实际配置时,我们演示使用 UrlMappingLoginPageStrategy 的情况。 访问 http://localhost:8080/user 就会显示用户登录页面。 图 50.1. 用户登录页面 217 访问 http://localhost:8080/admin 就会显示管理员登陆页面。 图 50.2. 管理员登录页面 50.2. 密码出错时返回对应页面 本来应该扩展 AuthenticationProcessFilter,但是因为现在比较懒,所以 直接写了一个 password-wrong.jsp 来替代,里边根据登录的类型决定跳 转到哪个页面,实现在对应页面中显示错误信息。 <% String loginType = (String) session.getAttribute("loginType"); if ("admin".equals(loginType)) { response.sendRedirect("admin.jsp?error=true"); } else { response.sendRedirect("user.jsp?error=true"); } %> 在配置文件中添加 password-wrong.jsp 的配置。 实例见 ch218。 218 第 51 章 角色继承 有两种方式可以用来实现角色继承,一种是使用 UserDetailsServiceWrapper 在读取用户信息时,使用角色继承为用户赋 予全部的权限,另一种是使用 RoleHierarchyVoter 在判断权限的时候, 使用角色继承为生成继承的全部权限。 使用 UserDetailsServiceWrapper 的情况,加载用户信息的同时用户会获 得所有权限,所以使用 Acl 的情况也不会出问题。使用 RoleHierarchyVoter 的情况,因为只会在每次校验权限时才会使用继承 获得所有权限,所以使用 Acl 的时候就会出现缺少权限的情况。 51.1. 使用RoleHierarchyVoter 把 RoleHierarchyVoter 加入配置文件中的 accessDecisionManager。 219 然后在 http 中引用这个 accessDecisionManager 即可。 51.2. 使用数据库实现RoleHierarchy 实现代码来自 http://jira.springframework.org/browse/SEC-1071。源代码 在 http://forum.springsource.org/showthread.php?t=65515。 使用基于 Dao 的 RoleHierarchy 需要两部设置。 第一是在数据库中建立对应的表结构,并设置初始数据,数据库表结构 如下所示: CREATE MEMORY TABLE ROLE_HIERARCHIES( INCLUDINGROLE VARCHAR(50), INCLUDEDROLE VARCHAR(50) ); 220 includingrole 表示继承了其他角色的角色。includedrole 表示被继承的角 色。如果我们希望设置 ROLE_ADMIN 继承 ROLE_USER,只需要进行 如下设置。 INSERT INTO ROLE_HIERARCHIES(INCLUDINGROLE,INCLUDEDROLE) VALUES('ROLE_ADMIN','ROLE_USER') 这样 RoleHierarchyEntryDaoJdbc 会读取数据库中的数据,将其组装成 ROLE_ADMIN > ROLE_USER 格式,再交给 RoleHierarchyImpl 进行分 析,最终用于 RoleHierarchyVoter 完成角色继承的判断。 下面修改配置文件,将 daoRoleHierarchy 的 bean 设置到配置文件中。 这样就完成了使用数据库保存角色继承信息的功能。 221 实例见 ch219。 222 第 52 章 设置方法拦截器 我们不一定必须使用 namespace 提供的 global-method-security,直接把 MethodInterceptor 放到 aop 里就可以。 com.family168.springsecuritybook.ch220.MessageService.userMessage=ROLE_ADMIN 实例见 ch220。 223 部分 IV ACL 篇 Access Control List 是一个很容易被人们提起的功能,比如业务员甲只能 查看自己签的合同信息,不能看到业务员乙签的合同信息。这个功能在 Spring Security 中也有支持,但是配置比较,namespace 方式只对 afterInvocation 做了一些支持,而且暂时没有找到虎牙子很好的解决方 案。 224 第 53 章 ACL基本操作 ACL 即访问控制列表(Access Controller List),它是用来做细粒度权限控 制所用的一种权限模型。对 ACL 最简单的描述就是两个业务员,每个 人只能查看操作自己签的合同,而不能看到对方的合同信息。 下面我们会介绍 Spring Security 中是如何实现 ACL 的。 53.1. 准备数据库和aclService ACL所需的四张表,表结构见附录:附录 E, 数据库表结构。 然后我们需要配置 aclService,它负责与数据库进行交互。 53.1.1. 为acl配置cache 默认使用 ehcache,spring security 提供了一些默认的实现类。 在 ehcache.xml 中配置对应的 aclCache 缓存策略。 53.1.2. 配置lookupStrategy 简单来说,lookupStrategy 的作用就是从数据库中读取信息,把这些信 息提供给 aclService 使用,所以我们要为它配置一个 dataSource,配置 中还可以看到一个 aclCache,这就是上面我们配置的缓存,它会把资源 最大限度的利用起来。 226 中间一部分可能会让人感到困惑,为何一次定义了三个 adminRole 呢? 这是因为一旦 acl 信息被保存到数据库中,无论是修改它的从属者,还 是变更授权,抑或是修改其他的 ace 信息,都需要控制操作者的权限, 这里配置的三个权限将对应于上述的三种修改操作,我们把它配置成只 有 ROLE_ADMIN 才能执行这三种修改操作。 53.1.3. 配置aclService 当我们已经拥有了 dataSource, lookupStrategy 和 aclCache 的时候,就可 以用它们来组装 aclService 了,之后所有的 acl 操作都是基于 aclService 展开的。 53.2. 使用aclService管理acl信息 当我们添加了一条信息,要在 acl 中记录这条信息的 ID,所有者,以及 对应的授权信息。下列代码在添加信息后执行,用于添加对应的 acl 信 息。 ObjectIdentity oid = new ObjectIdentityImpl(Message.class, message.getId()); MutableAcl acl = mutableAclService.createAcl(oid); acl.insertAce(0, BasePermission.ADMINISTRATION, 227 new PrincipalSid(owner), true); acl.insertAce(1, BasePermission.DELETE, new GrantedAuthoritySid("ROLE_ADMIN"), true); acl.insertAce(2, BasePermission.READ, new GrantedAuthoritySid("ROLE_USER"), true); mutableAclService.updateAcl(acl); 第一步,根据 class 和 id 生成 object 的唯一标示。 第二步,根据 object 的唯一标示,创建一个 acl。 第三步,为 acl 增加 ace,这里我们让对象的所有者拥有对这个对象的“管 理”权限,让“ROLE_ADMIN”拥有对这个对象的“删除”权限,让 “ROLE_USER”拥有对这个对象的“读取”权限。 最后,更新 acl 信息。 当我们删除对象时,也要删除对应的 acl 信息。下列代码在删除信息后 执行,用于删除对应的 acl 信息。 ObjectIdentity oid = new ObjectIdentityImpl(Message.class, id); mutableAclService.deleteAcl(oid, false); 使用 class 和 id 可以唯一标示一个对象,然后使用 deleteAcl()方法将对 象对应的 acl 信息删除。 53.3. 使用acl控制delete操作 228 上述代码中,除了对象的拥有者之外,我们还允许“ROLE_ADMIN” 也可以删除对象,但是我们不会允许除此之外的其他用户拥有删除对象 的权限,为了限制对象的删除操作,我们需要修改 Spring Security 的默 认配置。 首先要增加一个对 delete 操作起作用的表决器。 它只对 Message 这个类起作用,而且可以限制只有管理和删除权限的用 户可以执行删除操作。 然后要将这个表决器添加到 AccessDecisionManager 中。 229 现在 AccessDecisionManager 中有两个表决器了,除了默认的 RoleVoter 之外,又多了一个我们刚刚添加的 aclMessageDeleteVoter。 现在可以把新的 AccessDecisionManager 赋予全局方法权限管理器了。 然后我们就可以在 MessageService.java 中使用 Secured 注解,控制删除 操作了。 @Transactional @Secured("ACL_MESSAGE_DELETE") public void remove(Message message) { list.remove(message); ObjectIdentity oid = new ObjectIdentityImpl(Message.class, id); mutableAclService.deleteAcl(oid, false); } 实际上,我们最好不要让没有权限的操作者看到 remove 这个链接,可 以使用 taglib 隐藏当前用户无权看到的信息。 | Remove 230 8, 16 是 acl 默认使用的掩码,8 表示 DELETE,16 表示 ADMINISTRATOR,当用户不具有这些权限的时候,他在页面上就看 不到 remove 链接,也就无法执行操作了。 这比让用户可以执行 remove 操作,然后跑出异常,警告访问被拒绝要 友好得多。 53.4. 控制用户可以看到哪些信息 当用户无权查看一些信息时,我们需要配置 afterInvocation,使用后置 判断的方式,将用户无权查看的信息,从 MessageService 返回的结果集 中过滤掉。 后置判断有两种形式,一种用来控制单个对象,另一种可以过滤集合。 231 afterAclRead可以控制单个对象是否可以显示,afterAclCollectionRead则 用来过滤集合中哪些对象可以显示。[6] 对这两个 bean 都是用了 custom-after-invocation-provider 标签,将它们加 入的后置判断的行列,下面我们为 MessageService.java 中的对应方法添 加 Secured 注解,之后它们就可以发挥效果了。 @Secured({"ROLE_USER", "AFTER_ACL_READ"}) public Message get(Long id) { for (Message message : list) { if (message.getId().equals(id)) { return message; } } return null; } @Secured({"ROLE_USER", "AFTER_ACL_COLLECTION_READ"}) public List getAll() { return list; } 232 以上就是 Spring Security 支持的 ACL,这里用到了数据库,方法拦截器, 注解,taglib,基本可以说 ACL 就是囊括了我们之前所有讨论过功能的 集合体,下一步我们会继续研究 ACL 的一些高级特性。 [6] 这个地方会引发一个经典的问题,虎牙子,一般的思路是使用动态SQL的方式,在查询的时候 就过滤掉无权显示的信息,但随着查询条件的复杂化,当出现SQL语句长度超过DBMS最大限制 时,咱们就可以去撞墙了。 233 第 54 章 管理acl 这章介绍一些有关管理 acl 的内容,包括管理多个 domain 类和动态授权 与收回授权。 54.1. 管理多个domain类 上一章中我们演示了如何使用自定义 Voter 对单个 domain 类进行权限 控制,如果我们需要对多个 domain 类实现 acl 权限控制时,不必给每一 个类都配置一个 Voter,只需要定义一个接口,让所有需要进行 acl 权限 控制的 domain 类实现这个接口,然后在 Voter 中配置这个统一接口就 可以了。 示例中我们定义了一个 AclDomainClass 接口。 package com.family168.springsecuritybook.ch302; public interface AclDomainClass { } 然后让其他 domain 都实现这个接口。 public class Account implements Serializable, AclDomainClass { // .... } 这样就可以在配置文件中配置统一的 Voter。 234 这样,aclDeleteVoter 就可以处理所有实现了 AclDomainClass 接口的类, 而在数据库中依然会保存实现类的具体类型,不会因为在 Voter 中使用 了同一个接口类而造成影响。 54.2. 动态授权与收回授权 现在 acl 的好处是可以通过 aclService 自由管理 acl 和 ace 的信息,比如 我们可以为其他人授权,让其他人可以查看自己的消息,也可以收回这 些授权。 54.2.1. 获得对象的acl权限 可以通过 domain 类的类型和 id,获得对应 acl 中的所有 ace 信息。 public void list(HttpServletRequest request, HttpServletResponse response) throws Exception { Long id = Long.valueOf(request.getParameter("id")); String clz = request.getParameter("clz"); Acl acl = null; if (clz.equals("account")) { 235 Account account = new Account(); account.setId(id); acl = getAclService().readAclById(new ObjectIdentityImpl(account)); } else if (clz.equals("contract")) { Contract contract = new Contract(); contract.setId(id); acl = getAclService().readAclById(new ObjectIdentityImpl(contract)); } else if (clz.equals("message")) { Message message = new Message(); message.setId(id); acl = getAclService().readAclById(new ObjectIdentityImpl(message)); } request.setAttribute("acl", acl); request.getRequestDispatcher("/permission-list.jsp").forward(request, response); } 在 jsp 中使用如下方式显示 ace 中的权限实体以及对应的权限。 ${item.sid.principal} | ${item.permission.mask}  delete  ace 列表如下图所示: 图 54.1. 实现 domain 对象对应的所有 ace 信息 236 user 表示权限实体的名称,16 表示 administration 权限。这里表示当前 的对应已经赋予了 user 用户管理权限。 54.2.2. 添加授权 可以将一个 domain 对象的 acl 权限授予其他人。 可以选择授权的人或角色,然后选择授权的具体权限。 图 54.2. 添加授权 提交后可以看到 domain 对象对应的 acl 权限增加了 admin 用户的 read 权限,这时 admin 用户就可以查看这个 domain 对象的而信息了。 对应代码如下所示: @Transactional public void addPermission(long id, String clz, String recipient, int mask) { PrincipalSid sid = new PrincipalSid(recipient); Permission permission = BasePermission.buildFromMask(mask); ObjectIdentity oid = null; 237 if (clz.equals("account")) { oid = new ObjectIdentityImpl(Account.class, id); } else if (clz.equals("contract")) { oid = new ObjectIdentityImpl(Contract.class, id); } else if (clz.equals("message")) { oid = new ObjectIdentityImpl(Message.class, id); } MutableAcl acl; try { acl = (MutableAcl) mutableAclService.readAclById(oid); } catch (NotFoundException nfe) { acl = mutableAclService.createAcl(oid); } acl.insertAce(acl.getEntries().length, permission, sid, true); mutableAclService.updateAcl(acl); } 54.2.3. 收回授权 可以将已经授权的权限删除。 对应代码如下: @Transactional public void deletePermission(long id, String clz, String recipient, int mask) { PrincipalSid sid = new PrincipalSid(recipient); Permission permission = BasePermission.buildFromMask(mask); ObjectIdentity oid = null; if (clz.equals("account")) { oid = new ObjectIdentityImpl(Account.class, id); } else if (clz.equals("contract")) { oid = new ObjectIdentityImpl(Contract.class, id); } else if (clz.equals("message")) { oid = new ObjectIdentityImpl(Message.class, id); } MutableAcl acl = (MutableAcl) mutableAclService.readAclById(oid); 238 AccessControlEntry[] entries = acl.getEntries(); for (int i = 0; i < entries.length; i++) { if (entries[i].getSid().equals(sid) && entries[i].getPermission().equals(permission)) { acl.deleteAce(i); } } mutableAclService.updateAcl(acl); } 介于权限主体和权限可能有多种组合,所以我们只删除两者都完全匹配 的 ace 信息,操作成功后可以看到用户的 acl 权限已经被收回了。 实例在 ch302。 239 第 55 章 acl自动提醒 自定义 AfterInvocationProvider,在创建实体类时自动创建对应的 acl 权 限,在删除实体类的时候,自动删除 acl 权限。 55.1. 自动创建acl 首先一个目标是监听实体类的创建,在创建后生成对应的 acl 信息。 根据实体类创建 acl 信息的代码如下所示: ObjectIdentity oid = new ObjectIdentityImpl(object); MutableAcl acl = mutableAclService.createAcl(oid); acl.insertAce(0, BasePermission.ADMINISTRATION, new PrincipalSid(getUsername()), true); 需要创建一个 CreateAclEntryAfterInvocationProvider,将它注册到 Spring Security 中就可以在方法调用后进行对应的操作了。 public Object decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Object returnedObject) throws AccessDeniedException { Iterator iter = config.getConfigAttributes().iterator(); while (iter.hasNext()) { ConfigAttribute attr = (ConfigAttribute) iter.next(); if (this.supports(attr)) { Object domainObject = getDomainObjectInstance(object, processDomainObjectClass); Iterator cit = this.handlers.iterator(); while (cit.hasNext()) { AclHandler handler = (AclHandler) cit.next(); if (handler.supports(domainObject, returnedObject)) { 240 handler.create(authentication, domainObject, config, returnedObject); break; } } } } return returnedObject; } xml 文件中的配置如下: 为了指定在哪些方法调用后执行 CreateAclEntryAfterInvocationProvider, 我们在方法上使用 AFTER_ACL_CREATE 注解。 @Transactional 241 @Secured({"ROLE_USER", "AFTER_ACL_CREATE"}) public void save(Account account) { list.add(account); } 这样在 save(account)方法执行后,就会自动根据 account 这个对象生成 对应的 acl 信息。 55.2. 自动删除acl 自动删除 acl 的步骤与创建 acl 的大致相同。 首先来看一下删除 acl 信息的代码: ObjectIdentity oid = new ObjectIdentityImpl(object); mutableAclService.deleteAcl(oid, false); 其次是创建 DeleteAclEntryAfterInvocationProvider。 public Object decide(Authentication authentication, Object object, ConfigAttributeDefinition config, Object returnedObject) throws AccessDeniedException { Iterator iter = config.getConfigAttributes().iterator(); while (iter.hasNext()) { ConfigAttribute attr = (ConfigAttribute) iter.next(); if (this.supports(attr)) { Object domainObject = getDomainObjectInstance(object, processDomainObjectClass); Iterator cit = this.handlers.iterator(); while (cit.hasNext()) { AclHandler handler = (AclHandler) cit.next(); if (handler.supports(domainObject, returnedObject)) { 242 handler.delete(authentication, domainObject, config, returnedObject); break; } } } } return returnedObject; } xml 中的配置如下所示: 最后记得使用 AFTER_ACL_DELETE 对方法进行注解。 @Transactional @Secured({"ROLE_USER", "AFTER_ACL_DELETE"}) public void remove(Account account) { 243 list.remove(account); } 55.3. 根据id删除acl 上面演示的 remove()方法中需要传入 account 对象的实例才能起作用, 有时我们希望使用 removeById(id)的方式,只通过对象的 id 就可以删除 这个对象的实例。这时 AfterInvocation 就无法判断当前的 id 实际对应的 是哪个类,因此也就无法删除对应的 acl 信息了。 为了解决这个问题,我们引入了 AclDomainAware 注解。 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface AclDomainAware { Class value(); } 可以为它设置一个 Class 值,使用它标示当前方法中 id 所对应的类型。 @Transactional @Secured({"ACL_DELETE", "AFTER_ACL_DELETE"}) @AclDomainAware(Account.class) public void removeById(Long id) { Account account = this.get(id); list.remove(account); } 这样 DeleteAclEntryAfterInvocationProvider 就可以通过 AclDomainAware 中设置的类型和 id 的值找到对应实例的信息并删除 之。 244 对 AclDomainAware 进行扩展后,我们也可以让它负责处理那些参数中 包含 id 的情况,扩展 AclEntryVoter,让它可以使用 AclDomainAware 中的类型和 id 值生成的 acl 信息,以此进行 acl 的权限控制。 IdParameterAclEntryVoter 代码如下所示: public class IdParameterAclEntryVoter extends AclEntryVoter { public IdParameterAclEntryVoter(AclService aclService, String processConfigAttribute, Permission[] requirePermission) { super(aclService,processConfigAttribute,requirePermission); } @Override protected Object getDomainObjectInstance(Object secureObject) { Object[] args; Class[] params; if (secureObject instanceof MethodInvocation) { MethodInvocation invocation = (MethodInvocation) secureObject; params = invocation.getMethod().getParameterTypes(); args = invocation.getArguments(); } else { JoinPoint jp = (JoinPoint) secureObject; params = ((CodeSignature) jp.getStaticPart().getSignature()).getParameterTypes(); args = jp.getArgs(); } for (int i = 0; i < params.length; i++) { if (getProcessDomainObjectClass().isAssignableFrom(params[i])) { return args[i]; } } MethodInvocation invocation = (MethodInvocation) secureObject; Method method = invocation.getMethod(); Serializable id = null; for (int i = 0; i < params.length; i++) { 245 if (Serializable.class.isAssignableFrom(params[i])) { id = (Serializable) invocation.getArguments()[i]; break; } } if (id == null) { throw new AuthorizationServiceException("MethodInvocation: " + invocation + " did not provide any ID argument."); } if(method.isAnnotationPresent(AclDomainAware.class)) { try{ Class domainClass= method.getAnnotation(AclDomainAware.class) .value(); Object instance = domainClass.newInstance(); Method setter = domainClass.getDeclaredMethod("setId", new Class[]{id.getClass()}); setter.invoke(instance, new Object[]{id}); return instance; }catch(Exception ex) { ex.printStackTrace(); } } throw new AuthorizationServiceException("Secure object: " + secureObject + " did not provide any argument of type: " + getProcessDomainObjectClass()); } } 实例在 ch303。 246 部分 V 最佳实践篇 根据不同的权限模型,实现多种完整的权限管理系统。 • default: 使用 Spring Security 默认提供的数据库结构,实现用户信 息和权限管理后台。 • rbac0: RBAC0 模型。 • rbac1: RBAC1 模型。 • rbac2: RBAC2 模型。 • rbac3: RBAC3 模型。 • acl: 行级细粒度权限控制。 • ajax: 全站式 ajax 的 web 2.0 系统中应用权限管理。 • web: 在默认数据库结构的基础上,实现 web 所需的“用户注册”, “用户激活”,“安全提问”,“找回密码”功能。 • group: 分级组织管理模型。 • adapter: 权限适配器。 247 第 56 章 最简控制台 所谓的最简,实际上就是尽量利用现有资源,实现一个可管理的权限后 台。 我们将使用 Spring Security 提供的 filter 实现 URL 级的权限控制,使用 Spring Security 提供的 UserDetailsManager 实现用户管理,其中会包含 用户密码加密和用户信息缓存。麻雀虽小,五脏俱全,如果想为自己的 系统添加最简的权限后台,这一章将是不二之选。 我们将在这个简易的控制台中实现如下功能:浏览用户,新增用户,修 改用户,删除用户,修改密码,用户授权。 56.1. 平台搭建 选择 maven2 作为主要的构建工具,以便更加方便的管理第三方依赖, 这章使用的依赖如下所示: [INFO] [dependency:tree {execution: default-cli}] [INFO] com.family168.springsecuritybook:ch401:war:0.1 [INFO] +- org.springframework.security:spring-security-taglibs:jar:2.0.5.RELEASE:compile [INFO] | +- org.springframework.security:spring-security-core:jar:2.0.5.RELEASE:compile [INFO] | | +- org.springframework:spring-core:jar:2.0.8:compile [INFO] | | +- org.springframework:spring-context:jar:2.0.8:compile [INFO] | | | \- aopalliance:aopalliance:jar:1.0:compile [INFO] | | +- org.springframework:spring-aop:jar:2.0.8:compile [INFO] | | +- org.springframework:spring-support:jar:2.0.8:runtime [INFO] | | +- commons-logging:commons-logging:jar:1.1.1:compile [INFO] | | +- commons-codec:commons-codec:jar:1.3:compile [INFO] | | \- commons-collections:commons-collections:jar:3.2:compile 248 [INFO] | +- org.springframework.security:spring-security-acl:jar:2.0.5.RELEASE:compile [INFO] | | \- org.springframework:spring-jdbc:jar:2.0.8:compile [INFO] | | \- org.springframework:spring-dao:jar:2.0.8:compile [INFO] | \- org.springframework:spring-web:jar:2.0.8:compile [INFO] | \- org.springframework:spring-beans:jar:2.0.8:compile [INFO] +- org.hsqldb:hsqldb:jar:1.8.0.10:compile [INFO] +- javax.servlet:servlet-api:jar:2.4:provided [INFO] +- taglibs:standard:jar:1.1.2:compile [INFO] +- javax.servlet:jstl:jar:1.1.2:compile [INFO] \- net.sf.ehcache:ehcache:jar:1.6.0:compile 项目的目录结构如下: + ch401/ + src/ + main/ + java/ + com/ + family168/ + springsecuritybook/ + ch401 * UserBean.java * UserManager.java * UserServlet.java + resources/ + hsqldb/ * test.properties * test.scripts * applicationContext-security.xml * applicatoinContext-service.xml + webapp/ + includes/ * error.jsp * header.jsp * message.jsp * meta.jsp * taglibs.jsp + scripts/ * jquery.min.js * jquery.validate.pack.js * messages_cn.js 249 + WEB-INF/ * web.xml * index.jsp * login.jsp * user-changePassword.jsp * user-create.jsp * user-edit.jsp * user-list.jsp * user-view.jsp + test/ + resources/ * pom.xml src/main/java/目录下放着所有的 java 源代码。 src/main/resources/hsqldb/目录下放置着 hsqldb 数据库表结构和演示数 据。 权限控制相关的配置文件。 进行用户管理和权限管理所需的配置文件。 src/main/webapp/目录下放着 web 应用所需的 JavaScript 脚本与 jsp 文件。 56.2. 用户登录 用户需要登录系统才能进入系统进行操作。有关自定义登录页面的介 绍,请参考之前的章节。第 4 章 自定义登陆页面 图 56.1. 用户登录 250 我们为了演示的需要预设了两个用户 admin/admin 和 user/user,打开演 示用的数据库文件 test.scripts 可以看到用户信息以及加密过的用户密 码。 INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE) INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE) INSERT INTO AUTHORITIES VALUES('admin','ROLE_ADMIN') INSERT INTO AUTHORITIES VALUES('admin','ROLE_USER') INSERT INTO AUTHORITIES VALUES('user','ROLE_USER') 为了提升安全等级,我们对密码使用了 md5 和 saltValue 进行加密,对 应的配置文件在 applicationContext-securit.xml 中。 56.3. 用户信息列表 用户登录成功之后即进入用户信息列表。 图 56.2. 用户信息列表 251 显示所有用户信息的请求地址为/user.do?action=list,这个请求将交由 UserServlet.java 处理,在 list()方法中调用 UserManager.java 的 getAll() 方法获得数据库中所有的用户信息。 /** * get all of user. */ public List getAll() { String sql = "select username,password,enabled,authority" + " from users u inner join authorities a on u.username=a.username"; List list = jdbcTemplate.queryForList(sql); List userList = new ArrayList(); UserBean ub = null; for (Map map : list) { if (ub == null) { ub = new UserBean((String) map.get("username"), (String) map.get("password"), (Boolean) map.get("enabled")); ub.addAuthority((String) map.get("authority")); } else if (ub.getUsername().equals(map.get("username"))) { ub.addAuthority((String) map.get("authority")); } else { userList.add(ub); // ub = new UserBean((String) map.get("username"), (String) map.get("password"), (Boolean) map.get("enabled")); ub.addAuthority((String) map.get("authority")); } } if (!list.isEmpty()) { userList.add(ub); } return userList; } 252 getAll()方法中将数据库中所有的用户信息取出来,并将用户信息和对应 的权限组装在一起,并将获得的数据提交给 user-list.jsp 进行显示。出于 安全性的考虑,即使密码已经经过了加密,我们还是选择在页面上不显 示用户的密码。 56.4. 添加用户 点击页面左上角的 Create User 可以进入添加用户的界面。 图 56.3. 添加用户 如果操作成功,会向数据库中添加一条用户记录,以及对应的权限信息, 并在用户列表页面中显示成功提示。 253 图 56.4. 操作成功 如果操作失败,会跳转到添加页面,并显示错误信息。 图 56.5. 操作错误 254 添加用户操作过程中,UserServlet.java 中的 save()方法负责请求跳转与 数据校验,UserManager.java 中的 save()方法负责将提交的保存入数据 库。 /** * create a new user and insert he to database. */ public void save(String username, String password, boolean enabled, String[] authorities) { GrantedAuthority[] gas = new GrantedAuthority[authorities.length]; for (int i = 0; i < authorities.length; i++) { gas[i] = new GrantedAuthorityImpl(authorities[i].trim()); } String encodedPassword = passwordEncoder.encodePassword(password, username); UserDetails ud = new User(username, encodedPassword, enabled, true, true, true, gas); userDetailsManager.createUser(ud); } 这里我们需要注意两个部分。 第一部分,需要将用户拥有的权限转换为 GrantedAuthority 数组,并赋 予添加的用户。 第二部分,我们在保存用户密码之前,要将提交的明文密码使用 passwordEncoder 进行加密,encodePassword()方法会使用我们设置的加 密算法,并使用 username 作为 saltValue 对密码进行加密。 最终,我们将转化后的数据组装为一个 UserDetails 对象,并保存到数据 库中,以此完成整个添加用户的操作。 56.5. 修改用户信息 255 在用户列表页面选择一条用户信息进行修改。 图 56.6. 修改用户 修改操作与 UserManager.java 中的 update()方法对应。 /** * update a user information, includes username, enabled or authorities. */ public void update(String username, boolean enabled, String[] authorities) { GrantedAuthority[] gas = new GrantedAuthority[authorities.length]; for (int i = 0; i < authorities.length; i++) { gas[i] = new GrantedAuthorityImpl(authorities[i].trim()); } UserDetails oldUserDetails = userDetailsManager.loadUserByUsername(username); UserDetails ud = new User(username, oldUserDetails.getPassword(), enabled, oldUserDetails.isAccountNonExpired(), oldUserDetails.isAccountNonLocked(), oldUserDetails.isCredentialsNonExpired(), gas); userDetailsManager.updateUser(ud); } 256 因为修改用户信息不包含修改用户密码,所以此处不需要进行密码加 密,这里可以放心调用 UserDetailsManager 中提供的 updateUser()方法, 它会帮我们维护用户缓存。 可以查看指定用户的详细信息。 图 56.7. 浏览用户信息 删除用户操作与上述操作基本类似,UserDetailsManager 会帮我们处理 用户缓存的问题,不用担心出现脏数据。 56.6. 修改自己的密码 用户列表右上角有一个 Change Password 的链接,它用来修改当前登录 系统用户的密码。 257 图 56.8. 修改密码 在 UserManager.java 内部,我们会调用一个名为 changePassword()的方 法,这个方法需要两个参数 oldPassword 和 newPassword,这时因为修 改密码之前需要先对当前用户进行认证,所以可以在代码中看到,我们 为 oldPassword 和 newPassword 都进行了加密操作。 /** * let current user change password. */ public void changePassword(String oldPassword, String newPassword) { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); String username = userDetails.getUsername(); String encodedOldPassword = passwordEncoder.encodePassword(oldPassword, username); String encodedNewPassword = passwordEncoder.encodePassword(newPassword, username); userDetailsManager.changePassword(encodedOldPassword, encodedNewPassword); } 258 这个方法可以看做是使用编程方式获得当前登录用户信息的一个典型 范例,在系统的任何部分,我们都可以使用这样的方式直接获得 SecurityContext 中保存的登录用户信息。 至此,我们基于 Spring Security 完成了一个完整的权限管理系统后台, 实例代码在 ch401。 259 第 57 章 用户组控制台 在最简权限控制台的基础上添加对用户组的支持。 用户组控制台中将添加如下功能:浏览用户组,新增用户组,修改用户 组,删除用户组,为用户组授权,将用户添加到用户组中。 57.1. 添加对用户组的支持 默认情况是不会启用用户组功能的,我们需要为 userDetailsManager 设 置 enableGroups 属性。 这样就在系统中开启了用户组功能,与用户组有关的数据库表结构参 考:第 44 章 使用用户组。 57.2. 浏览用户组 这一步通过 GroupManager 接口中定义的 findAllGroups()方法获得数据 库中所有用户组的名称,再根据用户组名称获得对应的成员和权限。 public List getAll() { List list = new ArrayList(); String[] groups = groupManager.findAllGroups(); 260 for (String groupName : groups) { String[] members = groupManager.findUsersInGroup(groupName); GrantedAuthority[] authorities = groupManager .findGroupAuthorities(groupName); UserGroupBean bean = new UserGroupBean(); bean.setName(groupName); bean.setMembers(members); bean.setAuthorities(authorities); list.add(bean); } return list; } 页面上将显示用户组名称,拥有的成员和分配的权限。 图 57.1. 显示所有用户组 57.3. 创建用户组 创建用户组需要指定用户组的名称和用户组的权限。 public void save(String groupName, String[] authorities) { GrantedAuthority[] gas = new GrantedAuthority[authorities.length]; for (int i = 0; i < authorities.length; i++) { gas[i] = new GrantedAuthorityImpl(authorities[i].trim()); } 261 groupManager.createGroup(groupName, gas); } 图 57.2. 填写用户组信息 图 57.3. 创建成功 57.4. 修改用户组 可以修改用户组的名称,用户组的成员和用户组的权限。 public void update(String oldName, String groupName, String[] members, String[] authorities) { String[] usernames = groupManager.findUsersInGroup(oldName); 262 for (String username : usernames) { groupManager.removeUserFromGroup(username, oldName); } for (String member : members) { if ((member != null) && !member.equals("")) { groupManager.addUserToGroup(member, oldName); } } GrantedAuthority[] gas = groupManager.findGroupAuthorities(groupName); for (GrantedAuthority ga : gas) { groupManager.removeGroupAuthority(oldName, ga); } for (int i = 0; i < authorities.length; i++) { String auth = authorities[i]; if ((auth != null) && !auth.trim().equals("")) { GrantedAuthority ga = new GrantedAuthorityImpl(auth.trim()); groupManager.addGroupAuthority(oldName, ga); } } groupManager.renameGroup(oldName, groupName); } 图 57.4. 修改用户组 263 实例代码在 ch402。 264 附录: A:常见问题解答 A.1 Q: 如何获得源代码 A: 在 SpringSecurity 的发布包中的 dist 目录下,包含很多“.jar”文 件,名称中包含“sources”的文件中就是源文件了,比如:可以在 spring-security-core-2.0.5.RELEASE-sources.jar 中找到 core 模块 的所有文件。 A.2 Q: 为何登录时出现 There is no Action mapped for namespace / and action name j_spring_security_check. A: 这是因为登陆所发送的请求先被 struts2 的过滤器拦截了,为了试登 陆请求可以被 Spring Security 正常处理,需要在 web.xml 中将 Spring Security 的过滤器放在 struts2 之前。 A.3 Q: 用户登陆之后没有进入设置的 default-target-url 页面。 A: Spring Security 登陆成功后的策略是,先判断用户登录前是否尝试 访问过受保护的页面,如果有,则跳转到用户登录前访问的受保护页面, 否则跳转到 default-target-url。如果希望登陆后一直跳转到 default-target-url,可以使用 always-use-default-target="true"。 A.4 Q: 如何实现国际化。 A: 在 xml 中添加如下配置: A.5 Q: 如何监听 Spring Security 的事件日志。 A: 在 xml 中添加如下配置: 265 A.6 Q: 如何启用 group。 A: 设置 enableGroups="true"才能在 JdbcDaoImpl 中启用 group,默认 是禁用的。 namespace 中没有支持这个参数,如果想使用 group,只要设置 group-authorities-by-username-query 这个属性,就会自动打开 enableGroups。 A.7 Q: 如何判断用户是否登录到系统中。 A: 有三种方式判断用户是否登录进了系统。 如果没有使用 anonymous, SecurityContextHolder.getContext().getAuthenticaiton() == null 时用户就没有登录。 如果使用了 anonymous, SecurityContextHolder.getContext().getAuthentication() instanceof AnonymousAuthenticationToken 时用户就没有登录。 如果可以获得 HttpSession,当 session.getAttribute(HttpSessionContextIntegrationFilter.SPRIN G_SECURITY_CONTEXT_KEY) == null 时,用户就没有登录。 A.8 Q: 为什么在过滤器链里多加了一个 FilterSecurityInterceptor,原来 的 FilterSecurityInterceptor 就不控制请求了。 A: 因为 FilterSecurityInterceptor 会向 request 中写入一个标记,用 于标记是否已经控制了当前请求,这样可以避免对同一个请求多次处理。 因为前一个 FilterSecurityInterceptor 会在 request 中做一个标记, 这就会导致第二个 FilterSecurityInterceptor 不会再次执行了。 A.9 Q: 如何记录登录 ip 和登录时间。 A: 参考 第 43 章 自定义过滤器中的内容,自定义一个过滤器,将用户 登录ip和登录时间记录到session中。 A.10 Q: 如何在新建用户时,使用 saltValue 对密码进行配置? A: 可以参考 第 56 章 最简控制台中的第四节 添加用户,使用String encodedPassword = passwordEncoder.encodePassword(password, username);获得加密后的密码。 A.11 Q: 如何让没配置的资源也要通过认证才能访问? A: 需要为 AccessDecisionManager 设置 allowIfAllAbstainDecisions="false",这样当遇到没有显式定义的被 保护资源都保护起来,实际上是都投反对票,所以那些没定义的资源谁 266 也无法访问。 A.12 Q: Spring Security 2 与 Acegi 相比有哪些功能提升? 简化的 namespace 配置语法 集成 OpenID 支持 NTLM 支持 JSR250 支持 AspectJ 语法 提升 ACL 的性能 支持 RESTful 系统 添加对用户组的支持 加强了基于数据库的 remember-me 实现 支持 portlet 支持其他语言 提供了更多的代码加强,文档和新实例 提供对 spring web flow 2.0 的支持 支持在 Spring IDE 下进行可视化配置 加强了对 Spring Web Service 的支持 更新了对 CAS 的支持,现在支持 CAS3 A.13 Q: 不要过滤图片等静态资源。 267 B:数据库表结构 B1. User 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); B2. Group create table groups ( id bigint generated by default as identity(start with 0) primary key, group_name varchar_ignorecase(50) not null ); create table group_authorities ( group_id bigint not null, authority varchar(50) not null, constraint fk_group_authorities_group foreign key(group_id) references groups(id) ); create table group_members ( id bigint generated by default as identity(start with 0) primary key, username varchar(50) not null, group_id bigint not null, constraint fk_group_members_group foreign key(group_id) references groups(id) ); B3. RememberMe create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, 268 token varchar(64) not null, last_used timestamp not null ); B4. ACL create table acl_sid ( id bigint generated by default as identity(start with 100) not null primary key, principal boolean not null, sid varchar_ignorecase(100) not null, constraint unique_uk_1 unique(sid,principal) ); create table acl_class ( id bigint generated by default as identity(start with 100) not null primary key, class varchar_ignorecase(100) not null, constraint unique_uk_2 unique(class) ); create table acl_object_identity ( id bigint generated by default as identity(start with 100) not null primary key, object_id_class bigint not null, object_id_identity bigint not null, parent_object bigint, owner_sid bigint not null, entries_inheriting boolean not null, constraint unique_uk_3 unique(object_id_class,object_id_identity), constraint foreign_fk_1 foreign key(parent_object) references acl_object_identity(id), constraint foreign_fk_2 foreign key(object_id_class) references acl_class(id), constraint foreign_fk_3 foreign key(owner_sid) references acl_sid(id) ); create table acl_entry ( id bigint generated by default as identity(start with 100) not null primary key, acl_object_identity bigint not null, ace_order int not null, sid bigint not null, mask integer not null, granting boolean not null, audit_success boolean not null, audit_failure boolean not null, constraint unique_uk_4 unique(acl_object_identity,ace_order), 269 constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id), constraint foreign_fk_5 foreign key(sid) references acl_sid(id) ); 270 C:异常 org.springframework.security AccessDeniedException,当用户无权访问被保护资源时抛出。 AccountExpiredException,当用户过期时抛出。 AccountStatusException,当用户状态不正常时抛出。 AuthenticationCredentialsNotFoundException,当找不到验证凭证时抛出。 AuthenticationException,验证异常。 AuthenticationServiceException,验证服务异常。 AuthorizationServiceException,认证服务异常。 BadCredentialsException,凭证(密码)错误。 CredentialsExpiredException,凭证过期异常。 DisabledException,无效异常。 InsufficientAuthenticationException,不满足验证异常。 LockedException,锁定异常。 SpringSecurityException,安全异常。 org.springframework.security.concurrent ConcurrentLoginException,同步登陆异常。 SessionAlreadyUsedException,会话已存在异常。 org.springframework.security.config SecurityConfigurationException,安全配置异常。 org.springframework.security.ldap LdapDataAccessException,ldap 数据访问异常。 org.springframework.security.provider ProviderNotFoundException,找不到 provider 异常。 org.springframework.security.provider.rcp RemoteAuthenticationException,远程认证异常。 org.springframework.security.userdetails UsernameNotFoundException,找不到用户名异常。 org.springframework.security.userdetails.hierarchicalroles CycleInRoleHierarchyException,角色循环继承异常。 org.springframework.security.ui.digestauth NonceExpiredException,nonce 过期异常。 org.springframework.security.ui.preauth PreAuthenticatedCredentialsNotFoundException,未找到预验证凭证异常。 org.springframework.security.ui.rememberme CookieTheftException,cookie 被盗异常 InvalidCookieException,非法 cookie 异常。 RememberMeAuthenticationException,rememberme 验证异常。 271 D:事件 认证事件 AuthenticationFailureConcurrentLoginEvent 验证失败,同时登陆。 AuthenticationFailureCredentialsExpiredEvent 验证失败,凭证失效。 AuthenticationFailureDisabledEvent 验证失败,禁用。 AuthenticationFailureExpiredEvent 验证失败,失效。 AuthenticationFailureLockedEvent 验证失败,锁定。 AuthenticationFailureProviderNotFoundEvent 验证失败,找不到 provider。 AuthenticationFailureProxyUntrustedEvent 验证失败,不可信任的代理。 AuthenticationFailureServiceExceptionEvent 验证失败,服务异常 AuthenticationSuccessEvent 认证成功。 AuthenticationSwitchUserEvent 切换用户。 InteractiveAuthenticationSuccessEvent 内部验证成功。 验证事件 AuthenticationCredentialsNotFoundEvent 找不到凭证。 AuthorizationFailureEvent 认证失败。 AuthorizedEvent 认证成功 PublicInvocationEvent 公用调用。 272 F:RBAC模型 访问控制策略一般有以下几种方式: • 自主型访问控制(Discretionary Access Control-DAC):用户 /对象来决定访问权限。信息的所有者来设定谁有权限来访问信 息以及操作类型(读、写、执行。。。)。是一种基于身份的 访问控制。例如 UNIX 权限管理。 • 强制性访问控制(Mandatory Access Control-MAC):系统来 决定访问权限。安全属性是强制型的规定,它由安全管理员或 操作系统根据限定的规则确定的,是一种规则的访问控制。 • 基于角色的访问控制(格/角色/任务):角色决定访问权限。 用组织角色来同意或拒绝访问。比 MAC、DAC 更灵活,适合作 为大多数公司的安全策略,但对一些机密性高的政府系统部适 用。 • 规则驱动的基于角色的访问控制:提供了一种基于约束的访 问控制,用一种灵活的规则描述语言和一种 ixn 的信任规则执行 机制来实现。 • 基于属性证书的访问控制:访问权限信息存放在用户属性证 书的权限属性中,每个权限属性描述了一个或多个用户的访问 权限。但用户对某一资源提出访问请求时,系统根据用户的属 性证书中的权限来判断是否允许或拒绝。 模型的主要元素: 273 • 可视化授权策略生成器 • 授权语言控制台 • 用户、组、角色管理模块 • API 接口 • 授权决策引擎 • 授权语言解释器 F1. RBAC模型介绍 RBAC(Role-Based Access Control - 基于角色的访问控制)模型是 20 世 纪 90 年代研究出来的一种新模型,但从本质上讲,这种模型是对前面 描述的访问矩阵模型的扩展。这种模型的基本概念是把许可权 (Permission)与角色(Role)联系在一起,用户通过充当合适角色的 成员而获得该角色的许可权。 这种思想世纪上早在 20 世纪 70 年代的多用户计算时期就被提出来了, 但直到 20 世纪 90 年代中后期,RBAC 才在研究团体中得到一些重视。 本章将重点介绍美国 George Mason 大学的 RBAC96 模型。 F2. 有关概念 在实际的组织中,为了完成组织的业务工作,需要在组织内部设置不同 的职位,职位既表示一种业务分工,又表示一种责任与权利。根据业务 分工的需要,支援被划分为不同群体,各个群体的人根据其工作任务的 需要被赋予不同的职责和权利,每个人有权了解与使用与自己任务相关 274 的信息与资源,对于那些不应该被知道的信息则应该限制他们访问。这 就产生了访问控制的需求。 例如,在一个大学中,有校长、副校长、训练部长、组织处长、科研处 长、教保处长等不同的职位,在通常情况下,职位所赋予的权利是不变 的,但在某个职位上工作的人可以根据需要调整。RBAC 模型对组织内 部的这些关系与访问控制要求给出了非常恰当的描述。 F2.1. 什么是角色 在 RBAC 模型中,工作职位被描述为“角色”,职位所具有的权利称为 许可权。角色是 RBAC 模型中的核心概念,围绕这一概念实现了访问控 制策略的形式化。特殊的用户集合和许可权的集合通过角色这一媒介在 某个特定的时间内联系在一起。而角色确实相对稳定的,因为任何组织 的分工、活动或功能一般是很少经常改变的。 可以有不同的动机去构造一个角色。角色可以表示完成特殊任务的资 格,例如,是一个医师还是一个药师;橘色也可以表示一种权利与责任, 如工程监理。权利与责任不同于资格,例如,Alice 可能有资格领导几 个部门,但他只能被分配负责一个部门的领导。通过多个用户的轮转, 角色可以映射特殊责任的分配,例如,医师可以转换为管理者。RBAC 的模式及其实现可以方便的适应这种角色概念的多种表现。 在实际的计算机信息系统中,角色由系统管理员定义,角色的增加与删 除、角色权利的增加与减少等 uanli 工作都是由系统管理员完成的。根 275 据 RBAC 的要求,用户被分配为某个特定角色后,就被赋予了该角色所 拥有的权利和责任,这种授权方式是强制性的,用户只能被动的接受, 不能自主的决定为角色增加或减少权力,也不能把自己角色的权利转首 给用户,显然,这是一种非自主型的访问控制模式。 F2.2. 角色与用户组 角色与用户组有何区别? 两者的主要区别是:用户组是用户的集合,但不可许可权的集合;而角 色却同时具有用户集合和许可权集合的概念,角色的作用是把这两个集 合联系在一起的中间媒介。 在一个系统中,如果用户组的许可权和成员仅可以被系统安全员修改的 话,在这种机制下,用户组的机制是非常接近于角色的概念的。角色也 可以在用户组的基础上实现,这有利于保持原有系统中的控制关系。在 这种情况下,角色相当于一个策略不见,与用户组的授权及责任关系相 联系,而用户组是实现角色的机制,因此,两者之间是策略与实现机制 之间的关系。 虽然 RBAC 是一种无确定性质策略的模型,但它支持公认的安全原则: 最小特权原则、责任分离原则和数据抽象原则。最小特权原则得到支持, 是因为在 RBAC 模型中可以通过限制分配给角色权限的多少和大小来 实现,分配给与某用户对应的角色的权限只要不超过该用户完成其任务 的需要就可以了。 276 责任分离原则的实现,是因为在 RBAC 模型中可以通过在完成敏感任务 过程中分配两个责任上互相约束的两个角色来实现,例如在清查账目 时,只需要设置财务管理员和会计连个角色参加就可以了。 数据抽象是借助于抽象许可权这样的概念实现的,如在账目管理活动 中,可以使用信用,借方等抽象许可权,而不是使用操作系统提供的读、 写、执行等具体的许可权。但 RBAC 并不强迫实现这些原则,安全管理 员可以允许配置 RBAC 模型使它不支持这些原则。因此,RBAC 支持 数据抽象的程度与 RBAC 模型的实现细节有关。 在 20 世纪 90 年代期间,大量的专家学者和专门研究单位对 RBAC 的概 念进行了深入研究,先后提出了许多类型的 RBAC 模型,其中以美国 George Mason 大学信息安全技术实验室(LIST)提出的 RBAC96 模型 最具有系统性,得到普遍的公认。 RBAC96 是一个模型族,其中包括 RBAC0~RBAC3 四个概念性模型。基 本模型 RBAC0 定义了完全支持 RBAC 概念的任何系统的最低需求。 RBAC1 和 RBAC2 两者都包含 RBAC0,但各自都增加了独立的特点,它 们被成为高级模型。在 RBAC1 中增加了角色分级的概念,一个角色可 以从另一个角色继承许可权。RBAC2 增加了一些限制,强调在 RBAC 的不同组件中在配置方面的一些限制。 RBAC1 和 RBAC2 之间是不可比的。RBAC3 被成为统一模型,它包含了 RBAC1 和 RBAC2,利用传递性,也把 RBAC0 包括在内。这些模型构成 277 了 RBAC96 模型族。图 ap08-01 表示了族内各模型间的关系,图 ap08-02 是 RBAC3 模型的概念示意图。 图 H.1. RBAC96 内各模型间的关系 图 H.2. RBAC96 模型族 F3. 基本模型RBAC0 278 RBAC0 的模型结构可以参看图 ap08-02,但需要把途中的限制和角色等 级两部分不包含在 RBAC0 模型中。该模型中包括用户(U)、角色(R) 和许可权(P)的那个三类实体集合,此外还有一个会话集合(S)。 其中用户代表一个组织的职员;角色表示该组织内部的一项任务的功能 或某个工作职务,它也表示该角色成员所拥有的权利和职责;许可权是 用户对系统中各课题访问或操作的权利,客体是指系统中的数据客体和 资源客体,例如,目录、文件、记录、端口、设备、内存或子网都是客 体。 许可权因客体不同而不同,例如,对于目录、文件、设备、端口等类客 体的操作权是读、写、执行等;对应数据库管理系统的客体是关系、元 素、属性、记录、库文件、视图等,相应的操作权是 Select、Update、 Delete、Insert 等;在会计应用中,相应的操作权是预算、信用、转移、 创建和删除一个账目等。 图 ap08-02 说明了关系用户指派 UA(User Assignment)与许可权指派 PA(Permission Assignment)的含义,两者都是多对多的关系。RBAC 的关键就在于这两个关系,通过它们,一个用户将最终获得某些许可权 并执行的权力。从图中角色的位置可以看粗,它是用户能够获取许可权 的中间媒介。 279 会话集中的每个会话表示一个用户可以对应多个角色(指向角色有两个 箭头)。在某个会话的持续期间,一个用户可以同时激活多个角色,而 该用户所获得的许可权是所有这些角色的所拥有许可权的并集。 每个用户可以同时打开多个回话,每个会话都可以在工作站屏幕上用一 个窗口显示。每个会话可以有不同活动角色的组合。RBAC0 的这一特点 将受到最小特权原则的限制。如果一个用户在一次会话中激活所有角色 的权利超过该用户被允许的权利,将受到最小权利原则的限制。 F3.1. RBAC0 模型的形式定义如下 定义 1 RBAC0 模型由以下描述确定: U、R、P、S 分别表示用户集合、角色集合、许可权集合和会话集合。 PA P×R 表示许可权与角色之间多对多的指派关系。 UA U×R 表示用户与角色之间多对多的指派关系。 用户:S→U 每个会话 si 到单个用户 user(si)的映射函数(常量代表会话 的声明周期)。 角色:S→2R 每个会话 si 到角色子集 roles(si) {r|user(si, r')∈UA}(能随 时间改变)的映射函数,会话 si 有许可权 Ur∈roles(si){p|(p,r')∈PA}。 在使用 RBAC0 模型时,应该要求每个许可权和每个用户至少应该被分 配给一个角色。两个角色被分配的许可权完全一样是可能的,但仍是两 280 个完全独立的角色,用户也有类似情况。角色可以适当的被看做是一种 语义结构,是访问控制策略形式化的基础。 RBAC0 把许可权处理未非解释符号,因为其精确含义只能由实现确定且 与系统有关。RBAC0 中的许可权只能应用于数据和资源类客体,但不能 应用于模型本身的组件。修改集合 U、R、P 和关系 PA 和 UA 的权限称 为管理权限,后面将介绍 RBAC 的管理模型。因此,在 RBAC0 中假定 只有安全管理员才能修改这些组件。 会话是由单个用户控制的,在模型中,用户可以创建会话,并有选择的 激活用户角色的某些子集。在一个会话中的角色的激活是由用户来决断 的,会话的终止也是由用户初始化的。RBAC0 不允许由一个会话去创建 另一个会话,会话只能由用户创建。 F4. 角色分级模型RBAC1 RBAC1 模型的特色是模型中的角色是分级的,不同级别的角色由不同的 职责与权力,橘色的级别形成偏序关系。图 ap08-03 说明了角色等级的 概念。在途中位置处于较高处的角色的等级高于较低位置角色的等级。 利用角色的分级概念可以限制继承的范围(scope)。 281 图 H.3. 角色分级的概念 图中项目成员的等级最低,角色程序员和测试员的等级都高于角色项目 成员,并都可以继承项目成员的权利;角色管理员具有最高的等级,它 可以继承测试员和程序员的权利。为了满足实际组织中一个角色不完全 继承另一个角色所有权利与责任的需求,模型中引入了私有角色的概 念,如图中的测试员'和程序员'分别是测试员和程序员的私有 uese,它 们可以分别继承测试员和程序员的某些专用权利。 显然,角色的等级关系具有自反性(自己可以继承自己)、传递性(A 继承 B,B 继承 C,则 A 继承 C)和反对称性(A 继承 B,B 继承 A, 则 A=B),因此是偏序关系,下面是 RBAC1 的形式定义。 F4.1. 定义 2:RBAC1 由以下内容确定 U、R、P、S 分别表示用户集合、角色集合、许可权集合和会话集合。 PA P×R 表示许可权与角色之间多对多的指派关系。 282 UA U×R 表示用户与角色之间多对多的指派关系。 RH R×R 是对 R 的偏序关系,称为角色等级或角色支配关系,也可用 ≥符号表示。 用户:S→U 每个会话 si 到单个用户 user(si)的映射函数(常量代表会话 的声明周期)。 角色:S→2R 每个会话 si 到角色子集 roles(si) {r|(r'≥r)[user(si, r')∈UA]} (能随时间改变)的映射函数,会话 si 有许可权 Ur∈roles(si){p|(r''≤ r)[(p,r'')∈PA]}。 5. 限制模型RBAC2 RBAC2 模型是在RBAC0 模型增加限制后形成的,它与RBAC1 并不兼容。 RBAC2 定义如下: F5.1. 定义 3: 除了在 RBAC0 中增加了一些限制因素外,RBAC2 未加改变的来自于 RBAC0,这些限制是用于确定 RBAC0 中各个组件的值是否是可接受的, 只有那些可接受的值才是允许的。 RBAC2 中引入的限制可以施加到 RBAC0 模型中的所有关系和组件上。 RBAC2 中的一个基本限制时互斥角色的限制,互斥角色是指各自权限尅 283 一互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被 分配其中的一个角色,不能同时获得两个角色的使用权。 例如,在审计活动中,一个角色不能同时被指派给会计角色和审计员角 色。又如,在公司中,经理和副经理的角色也是互斥的,合同或支票只 能由经理签字,不能由副经理签字。在为公司建立的 RBAC2 模型中, 一个用户不能同时兼得经理和副经理两个角色。模型汇总的互斥限制可 以支持权责分离原则的实现。 更一般化而言,互斥限制可以控制在不同的角色组合中用户的成员关系 是否是可接受的。例如,一个用户可以既是项目 A 的程序言,也可以是 项目 B 的测试员和项目 C 的验收员,但他不能同时成为同一个项目中 的这 3 个角色。RBAC2 模型可以对这种情况进行限制。 另一个用户指派限制的例子是一个角色限制其最大成员数,这被称为角 色的基数限制。例如,一个单位的最高领导只能为 1 人,中层干部的数 量也是有限的,一旦分配给这些角色的用户数超过了角色基数的限制, 就不再接受新配给的用户了。 限制角色的最小基数实现起来有些困难。例如,如果规定占用某个角色 的最小用户数,问题是系统如何在任何时刻都能知道这些占用者中的某 个人没有消失,如果消失的话,系统又应该如何去做。 在为用户指派某个角色 A 时,在有的情况下要求该用户必须是角色 B 的一个成员,B 角色成为角色 A 的先决角色。先决角色(Prerequisite 284 Roles)的概念来自于能力和适应性。对先决绝对的限制成为先决限制。 一个通俗的例子是,一个数学副教授应该从数学讲师中提拔,讲师是任 副教授的先决角色。但在实际系统中,不兼容角色之间的先决限制的情 况也会发生。 在图 ap08-03 中,可以限制只有本项目的成员才有资格担任程序员的角 色,通常在一个系统中,先决角色比新指派的角色的级别要低一些。但 有的情况下,却要求只有当用户不是某个特殊角色时,才能担任另一个 角色 A。如,需要执行回避策略时需要这样做,例如,本课题组成员不 应当是本项目成果鉴定委员会的成员。这类限制也可以推广到许可权方 面。 由于用户与角色的作用会与会话联系在一起,因此对会话也可以施加限 制。例如,可以允许一个用户被指派给两个角色,但不允许在同一时间 内把该用户在两个角色中都激活。另外,还可以限制一个用户在同一时 间内可以激活的会话的数量,相应的,对该用户所激活的会话中所分配 许可权的数量也可以施加限制。 前面提到的继承概念也可以视为一种限制。被分配给低级别角色的权 限,也必须分配给该角色的所有上级角色。或等价的,一个指派给较高 级别的角色的用户必须指派给该角色的所有下级角色。因此从某种角度 上讲,RBAC1 模型是冗余的,它被包含在 RBAC2 中。但 RBAC1 模型比 较简洁,用继承代替限制可使概念更清晰。 285 实现时可以用函数来实现限制,当为用户指定角色或为角色分配权限时 就调用这些函数进行检查,根据函数返回的结果决定分配是否满足限制 的要求,通常只对那些可被有效检查和那些惯例性的一些简单限制给与 实现,因为这些限制可以保持较长的时间。 模型中的限制机制的有效性建立在每个用户只有唯一标识符的基础上, 如果一个实际系统支持用户拥有多标识符,限制将会失效。同样,如果 同一个操作可以有两个以上的许可权来比准,那么,RBAC 系统也无法 实施加强的基本限制和责任分离饿限制。因此要求用户与其标识符,许 可与对应的操作之间一一对应。 F6. 统一模型RBAC3 RBAC3 把RBAC1 和RBAC2 组合在一起,提供角色的分级和继承的能力。 但把这两种概念组合在一起也引起一些新问题。 限制也可以应用于角色等级本身,由于角色间的等级关系满足偏序关 系,这种限制对模型而言是本质性的,可能会影响这种偏序关系。例如, 附加的限制可能会限制一个给定角色的应有的下级角色的数量。 两个或多个角色由可能被限制成没有公共的上级角色或下级角色。这些 类型的限制在概念角色等级的权力已经被分散化的情况下是有用哦,但 是安全主管却希望对所有允许这些改变的方法加以限制。 286 在限制和角色的等级之间也会产生敏感的相互影响。在图 ap08-03 的环 境中,一个项目成员不允许同时担任程序言与测试员的角色,但项目管 理员所处的位置显然是违反了该限制。在某种情况 i 下由高等级的角色 违反这种限制是可接受的,但在其他情况下又不允许这种违反现象发 生。 从严格性的角度来讲,模型的规则不应该是一些情况下不允许而在另一 情况下是允许的。类似的情况也会发生在对基数的限制上。假定限制一 个用户之多能分配给一个橘色,那么对图中的测试员的一个指派能够未 被这种限制吗?换句话说,基数限制是不是只能用于直接成员,是否也 能应用于继承成员上? 私有角色的概念可以说明这些限制是有用的。同样在图 ap08-03 的环境 中,可以把测试员',程序员'和项目管理员 3 个角色说明为互斥的,它 们处于同一等级,没有共同的上级角色,所以管理员角色没有违反互斥 限制。通常私有角色和其他角色之间没有公共上级角色,因为它们是这 个等级的最大元素,所以私有角色之间互斥关系可以无冲突的定义。 诸私有角色之间的相同部分可以被说明为具有 0 成员的最大技术限制。 根据这种方法,测试员必须被指派给测试员'这个角色,而测试员角色就 作为与管理员角色共享许可权的一种工具。 在前面的讨论中,我们都假设 RBAC 的所有组件都是由单个的安全员来 管理里。但是,对于一个大系统而言,系统中的角色可能成百上千,再 287 加上它们之间的复杂关系,使得集中式的管理任务成为非常可怕的工 作,因此通常由几个管理员小组来完成。能否用 RBAC 管理自己本身 呢? RBAC 的管理模型示于图 ap08-04。该图的上半部分本质上与图 ap08-02 相同,图中的限制时针对所有成分的,图的下半部分是对上半部分关于 管理角色 AR 和管理许可权 AP 与正规角色集 R 和许可权集 P 是分别不 可相交的。这个模型显示,正规许可权只能分配给正规角色(RBAC 模 型中定义的角色),管理许可权只能分配给管理角色。 F7. 定义 4 管理许可权 AP 有权改变组成 RBAC0、RBAC1、RBAC2 或 RBAC3 的所 有成分,但正规许可权 P 不能。管理许可权与正规许可权不相交,即 AP∩P=。管理许可权和正规许可权只能分别分配给管理角色 AR 和正 规角色 R,并且 AR∩R=。 288 图 H.4. 管理模型示意图 在图 ap08-04 的上半部可以对应 RBAC0、RBAC1、RBAC2 和 RBAC3 模 型,类似地下半部可以对应 ARBAC0、ARBAC1、ARBAC2 和 ARBAC3 模型,此处的 A 表示“管理”。ARBAC0~ARBAC3 形成了 RBAC 的管 理模型族,成为 ARBAC97。通常我们期望管理模型比 RBAC 模型本身 简单一些,因此可以利用 ARBAC0 管理 RBAC3,而不是用 ARBAC3 去 管理 RBAC0 模型。 F8. 在ARBAC97 中,包括三种组件 URA87:用户-角色指派。该组件涉及用户-指派关系 UA 的管理,该关 系把用户与角色关联在一起。对该关系的修改权由管理角色控制,这样, 289 管理角色中的成员有权管理正规角色中的成员关系。把一个用户指定为 管理角色是在 URA97 以外完成的,并假定是由安全员完成的。 PRA97:许可权-角色指派。该组件涉及角色-许可权的指派与撤销。从 角色观点来看,用户和许可权有类似的特点,它们都是由角色联系在一 起的实在实体。因此,可以把 PRA97 看做是 URA97 的对偶组件。 RRA97:角色-角色指派。为了便于对角色的管理,对角色又进行了分 类。该组件涉及 3 类角色,它们是: 1. 能力(Abilities)角色——进以许可权和其他能力做成成员的角色。 2. 组(Groups)角色——仅以用户和其他组为成员的一类角色。 3. UP-角色——表示用户与许可权的角色,这类角色对其成员没有 限制,成员可以使用户、角色、许可权、能力、组或其他 UP-角 色。 区别这三类模型的主要原因是可以应用不同的管理模型去建立不同类 型角色之间的关系。区分的动因首先是对能力的考虑,能力是许可权的 集合,可以把该集合中所有许可权作为一个单位指派给一个角色。类似 的,组是用户的集合,可以把该集合中所有许可权作为一个单位指派给 一个角色。组和能力角色都似乎可以划分等级的。 在一个 UP-角色中,一个能力是否是其的一个成员是由 UP-角色是否支 配该能力决定的,如果支配就是,否则就不是。相反的,如果一个 UP- 角色被一个组角色支配,则这个组就是该 UP-角色的成员。 290 对 ARBAC97 管理模型的研究还在继续之中,对能力-指派与组-指派的 形式化已基本完成,对 UP-角色概念的研究成果还未形式化。 F9. RBAC模型的特点 符合各类组织机构的安全管理需求。RBAC 模型支持最小特权原则、责 任分离原则,这些原则是任何组织的管理工作都需要的。这就使得 RBAC 模型由广泛的应用前景。 RBAC 模型支持数据抽象原则和继承概念。由于目前主流程序设计语言 都支持面向对象技术,RBAC 的这一特性便于在实际系统中应用实现。 模型中概念与实际系统紧密对应。RBAC 模型中的角色、用户和许可权 等概念都是实际系统实际存在的实体,便于设计者建立现存的或待建系 统的 RBAC 模型。 RBAC 模型仍素具访问控制类模型,本质是对访问矩阵模型的扩充,能 够很好的解决系统中主体对客气的访问控制访问权力的分配与控制问 题,但模型没有提供信息流控制机制,还不能完全满足信息系统的全部 安全需求。 虽然也有人认为可以用 RBAC 去仿真基于格的访问控制系统(LBAC), 但 RBAC 对系统内部信息流的控制不是直观的,需要模型外的功能支 持。有关信息流控制的作用域原理将在第四章介绍,届时读者可以进一 步理解 RBAC 模型的这种缺陷。 291 RBAC 模型没有提供操作顺序控制机制。这一缺陷使得 RBAC 模型很难 应用关于那些要求有严格操作次序的实体系统,例如,在购物控制系统 中要求系统对购买步骤的控制,在客户未付款之前不应让他把商品拿 走。RBAC 模型要求把这种控制机制放到模型外去实现。 RBAC96 模型和 RBAC97uanli 模型都故意回避了一些问题,如是否允许 一个正在会话的用户再创建一个新会话,管理模型不支持用户和许可权 的增加与删除等管理工作灯,都是需要解决而未提供支持的问题,这些 问题都还在研究中,但是如果缺少这些能力的支持,模型的而应用也将 受到影响。相反,访问绝阵模型提供了用户和权限修改功能,因此,不 能说 RBAC 模型能够完全取代访问矩阵模型。 F10. 基于party的模型 谈到用户和用户组,以及组织结构之间的关系,就可以延伸到基于 party 的模型。大略可以看做是一个直线的结构。 user -> group -> department -> organization 最大范围上是 organize,然后 department, group, user 依次被上一级包含 在其中。实际中的公司组织结构基本如此,略为复杂的就是可能出现一 个用户身兼数职,属于多个组织部门的情况。 组织结构在实际使用需要考虑与其他系统进行结合使用,通常提到的就 叫做组织机构适配器,大多直接以 SQL 的形式来实现。只是涉及到权 292 限的部分,为了操作上的简便,总会倾向于将属于某一组织的所有用户 进行统一授权。而且也会遇到诸如层级权限继承,角色权限以父子方式 自动分配等方式。可以看出基于 party 的模型并没有直接加大权限模型 的复杂程度,而是大大的将权限管理模型复杂化了。 F11. 有关operation RBAC 的数据模型只涉及到 User - Role - Permission。在 Permission 之下 一般还要分割为 Resource 与 Operation。这里 Resource 与 Operation 都与 Permission 对应,Resource 中保存车辆、书籍等资源,而 Operation 中表 示对这些资源的操作,如新增,删除等。 目前见到的操作方法,多数是将 Resource, Operation 集中在 Permission 中。可能将 Permission, Resource, Operation 各自放在单独的表中,也可 能将 Operation 和 Resource 直接以字段的形式放入 Permission 表中。 在此,我对 Operation 的意义和它保存的具体内容提出很大的困惑,如 果根据权限模型中的定义来理解,Resource 中应该保存的内容是“car”、 “book”,Operation 保存的则是“Create”、“Update”。那么如下的 权限定义该如何在系统中使用呢? (Permission)添加图书 (Resource)book (Operation)create 293 因为数据模型中定义的只是抽象的定义,create 无法直接映射到实际的 BookManager.create()方法,我们需要依赖映射才能使 Operation 的含义 具象化,因此下一步我们需要为 Permission 填入具体的内容。 (Permission)添加图书 (Resource)book (Operation)create (Content)com.family168.springsecurity.BookManager.create() (Type)METHOD (Permission)添加图书 (Resource)book (Operation)create (Content)/book/create.do (Type)URL (Permission)添加图书 (Resource)book (Operation)create (Content)/book/create (Type)MENU 只是这样一来,权限内容与 Resource 和 Operation 实际是已经发生了重 复,现在我们完全可以只保留 Content 和 Type 连个字段,而将 Resource 与 Operation 的定义删除,虽然这样一来数据模型的表意性会被损失, 但其后可以通过对 Permission 的命名和描述进行弥补。 事实上,我更倾向于将 Resource 中的内容细化,多个 Resource 归于一 个 Permission,使用 Permission 进行授权,只是这样做的话完全忽略了 294 Operation 的存在,不清楚是否正确。或者说 Operation 的存在到底是为 了什么呢?为了实现或是为了简化何种情况才需要 Operation 呢? 从 ACL 角度来看,我可以理解 Operation 的用户,使用 mask 掩码来实 现对基本操作的权限校验,或是在权限管理时,通过对操作的分类来定 义限制规则,但是在 RBAC 模型中,实在无法想出 Operation 会为我们 带来任何益处。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享文档获得金币 ] 24 人已下载

下载文档

相关文档