前言:
Spring security是一个能够为基于Spring的企业应用系统提供描述性安全访问控制解决方案的安全框架。(Spring Security provides a comprehensive security solution for J2EE-based enterprise software applications)。本文目标为通过一个简单的例子介绍Spring security3的基本使用。
Spring security官网 :http://www.springsource.org/spring-security/。
Spring security百度百科:http://baike.baidu.com/view/2677773.htm (百科页首的图片很形象的描述了Spring security的过滤器链)
本文涉及Spring security3版本:3.0.5
友情提示:对于对此技术有兴趣的童鞋,本文中代码和配置文件的注释部分请细看,如有疏漏,请多提宝贵意见。
1,Spring security3简述
Spring security相对于之前学习的框架而言,比较的有难度,原因是学习它的时候,你需要明白的概念要多一些。Spring security从前身Spring ACEGI逐步演化,配置也逐渐的简化,某些目录结构和类都发生了变化,对于将Spring security2转换为Spring security3的时候还需要多多少的思考。下面,以官网官方下载包中的英文文档为参考,说明Spring security3的最为核心的两个概念。
As you probably know two major areas of application security are “authentication” and “authorization” (or “access-control”).即认证和授权。
Spring security对用户请求的处理过程,即是这两个过程的体现。
关于用户,角色,权限,资源的概念。用户:应用系统的使用者;角色:区分用户职能;权限:描述访问资源级别;用户和角色是多对多的关系,角色和权限也是多对多的关系。对于某个资源,可能会提供多个访问权限级别,那么只要是持有该权限的角色都是可访问的,最终一个用户是否能访问该资源的问题就演变为一个用户对应的角色所拥有的权限是否包含该资源提供的各种访问级别的权限中的一个。
首先,认证在前,授权在后。其次,认证时用户获得登录的口令,如果口令错误,用户将无法请求资源;口令正确,则用户需要等待授权,同时在认证的过程中,用户信息和用户所拥有的角色会被包装成为认证对象传递给授权过程使用。授权时会检查请求资源的权限并与用户所拥有角色的权限对比,如果能够能匹配,则认证通过,反之不通过。
2,构建基于Spring security的项目
按照文档步骤Part III. Web Application Security的步骤开始配置,首先使用Maven构建一个Web项目,从官方获取Spring security3.0.5坐标,添加到Pom文件中,并在Web.xml当中配置Spring容器的监听器和Spring security的过滤器链钩子。(Spring's DelegatingFilterProxy provides the link between web.xml and the application context. )
web.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <!-- 配置spring security 权限过滤器 --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 使用Spring中的过滤器解决在请求和应答中的中文乱码问题 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> <init-param> <!--强制转换编码(request和response均适用) --> <param-name>ForceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext*.xml </param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- 解决el表达式在页面中不生效的问题,事实上这是web-app_2_5标准中的 一个小小冲突--> <jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>false</el-ignored> </jsp-property-group> </jsp-config> </web-app>
3,Spring security3 授权认证及Url访问控制过滤器自定义
首先,参考 Part?I.?Getting Started ---->2.?Security Namespace Configuration--->2.2?Getting Started with Security Namespace Configuration,并按照2.x的目录阅读。下面我们关注三个方面:自定义用户认证,自定义用户授权,自定义过滤器。
自定义用户认证:Using other Authentication Providers 继承自UserDetailsService,创建CustomUserDetailsService和CustomUserDetails类,具体如下:
package org.wit.ff.authentication; import java.util.HashMap; import java.util.Map; import org.springframework.dao.DataAccessException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * <p>UserDetailService 给用户提供自定义的授权处理 * @author fangfan * @see UserDetailService */ public class CustomUserDetailsService implements UserDetailsService { /** * 模拟临时的用户信息存储,实际应用当中应该是从数据源加载 */ private Map<String,UserDetails> usersinfo = new HashMap<String,UserDetails>(); /* * <p>用户访问应用资源之前,将会调用此方法获取用户的登录信息及对应的权限范围(ROLE_USER,ROLE_ADMIN,ROLE_LOGIN) * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String) */ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { if(usersinfo.isEmpty()){ loadUsersinfo(); } return usersinfo.get(username); } /** * 模拟用户信息 */ private void loadUsersinfo(){ UserDetails detailsOne = new CustomUserDetails("ff","123","ROLE_ADMIN",true); usersinfo.put("ff", detailsOne); UserDetails detailsTwo = new CustomUserDetails("zcq","123456","ROLE_USER",true); usersinfo.put("zcq", detailsTwo); } } package org.wit.ff.authentication; import java.util.Arrays; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.UserDetails; /** * <p>自定义用户信息 * @author fangfan * @see UserDetails * */ public class CustomUserDetails implements UserDetails { /** * */ private static final long serialVersionUID = 1L; /** * 用戶名 */ private String username; /** * 密码 */ private String password; /** * 角色 */ private String role; private boolean enabled; private static final String ROLE_LOGIN="ROLE_LOGIN"; public CustomUserDetails(){ } public CustomUserDetails(String username, String password, String role, boolean enabled) { super(); this.username = username; this.password = password; this.role = role; this.enabled = enabled; } /** * 获取当前用户的权限 * 其实用户应该拥有多个角色,这里简单起见只用了一个String类型来表示 * 其实用户 角色权限 资源三者可以各自创建对象并关联能实现一个非常复杂的权限控制 */ public Collection<GrantedAuthority> getAuthorities() { GrantedAuthority[] gas = new GrantedAuthority[2]; gas[0] = new GrantedAuthorityImpl(role); //分配登录权限给所有通过登录验证的角色 gas[1] = new GrantedAuthorityImpl(ROLE_LOGIN); return Arrays.asList(gas); } public String getPassword() { return password; } public String getUsername() { return username; } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return enabled; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setEnabled(boolean enabled) { this.enabled = enabled; } }
自定义用户授权:参考2.5?The Default AccessDecisionManager 自定义实现CustomAccessDecisionManager 继承自AbstractAccessDecisionManager。
package org.wit.ff.authentication; import java.util.Collection; import java.util.Iterator; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.vote.AbstractAccessDecisionManager; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; public class CustomAccessDecisionManager extends AbstractAccessDecisionManager{ /** * 裁定当前用户对应权限authentication是否包含所请求资源所拥有的权限 * 如果成立 则通过裁定 否则发生异常 */ public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(configAttributes == null) { return; } //所请求的资源拥有的权限(一个资源对多个权限) Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while(iterator.hasNext()) { ConfigAttribute configAttribute = iterator.next(); //访问所请求资源所需要的权限 String needPermission = configAttribute.getAttribute(); System.out.println("needPermission is " + needPermission); //用户所拥有的权限authentication for(GrantedAuthority ga : authentication.getAuthorities()) { if(needPermission.equals(ga.getAuthority())) { return; } } } //没有权限 throw new AccessDeniedException(" No Access Dendied "); } }
自定义过滤器:自定义过滤器采用了系统自带的过滤器类实现,这里没有像网上的大神们一样自定义,因为使用系统已经实现的过滤器更方便。这里重定义了资源元数据类FilterInvocationSecurityMetadataSource。
package org.wit.ff.authentication; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; /** * <p>自定义 资源与权限对应关系 * @author fangfan * */ public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { /** * 原本资源(url)与角色的对应关系需要从数据源中获取,这里做了简化 */ private Map<String, Collection<ConfigAttribute>> resourceMap; public CustomFilterInvocationSecurityMetadataSource() { resourceMap = loadResourceMatchAuthority(); } public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String url = ((FilterInvocation) object).getRequestUrl(); System.out.println("requestUrl is " + url); if (resourceMap == null) { loadResourceMatchAuthority(); } return resourceMap.get(url); } public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } public boolean supports(Class<?> clazz) { return true; } private Map<String, Collection<ConfigAttribute>> loadResourceMatchAuthority() { Map<String, Collection<ConfigAttribute>> map = new HashMap<String, Collection<ConfigAttribute>>(); // admin页面 Map<String, String> configs = getResourcesConfig(); for (Entry<String, String> entry : configs.entrySet()) { Collection<ConfigAttribute> list = new ArrayList<ConfigAttribute>(); String[] vals = entry.getValue().split(","); for (String val : vals) { ConfigAttribute config = new SecurityConfig(val); list.add(config); } map.put(entry.getKey(), list); } return map; } /** * 定义简单url 与role的对应 * @return */ private Map<String, String> getResourcesConfig() { Map<String, String> map = new HashMap<String, String>(); map.put("/pages/admin.jsp", "ROLE_ADMIN"); map.put("/**", "ROLE_USER,ROLE_ADMIN"); return map; } }
最后,附上配置文件:
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> <!-- 定义上下文返回的消息的国际化。 --> <beans:bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <beans:property name="basename" value="classpath:org/springframework/security/messages_zh_CN" /> </beans:bean> <!-- Spring security provides two major areas of application security are “authentication” and “authorization” 授权和认证 --> <!-- http配置如何拦截用户请求。将auto-config设为'true'将自动配置几种常用的权限控制机制,包括form, anonymous, rememberMe。 --> <http auto-config="true"> <!-- 我们利用intercept-url来判断用户需要具有何种权限才能访问对应的url资源, 可以在pattern中指定一个特定的url资源,也可以使用通配符指定一组类似的url资源。 例子中定义的两个intercepter-url,第一个用来控制对/admin.jsp的访问, 第二个使用了通配符/**,说明它将控制对系统中所有url资源的访问。 在实际使用中,Spring Security采用的是一种就近原则, 就是说当用户访问的url资源满足多个intercepter-url时, 系统将使用第一个符合条件的intercept-url进行权限控制。 在我们这个例子中就是,当用户访问/admin.jsp时, 虽然两个intercept-url都满足要求, 但因为第一个intercept-url排在上面, 所以Spring Security会使用第一个intercept-url中的配置处理对/admin.jsp的请求, 也就是说,只有那些拥有了ROLE_ADMIN权限的用户才能访问/admin.jsp。 --> <!-- <intercept-url pattern="/pages/admin.jsp" access="ROLE_ADMIN" /> --> <intercept-url pattern="/pages/login.jsp" filters="none" /> <intercept-url pattern="/pages/css/**" filters="none" /> <intercept-url pattern="/pages/js/**" filters="none" /> <intercept-url pattern="/pages/images/**" filters="none" /> <!-- <intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" /> --> <form-login login-page="/pages/login.jsp" default-target-url="/pages/index.jsp" always-use-default-target="true" /> <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" /> </http> <!-- 认证管理:提供用户权限对应关系 --> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="customUserDatailsService"> <!-- user-service中定义了两个用户,admin和user, 重要的部分是authorities,这里定义了这个用户登陆之后将会拥有的权限, 它与上面intercept-url中定义的权限内容一一对应。每个用户可以同时拥有多个权限, 例子中的admin用户就拥有ROLE_ADMIN和ROLE_USER两种权限, 这使得admin用户在登陆之后可以访问ROLE_ADMIN和ROLE_USER允许访问的所有资源。 与之对应的是,user用户就只拥有ROLE_USER权限,所以他只能访问ROLE_USER允许访问的资源, 而不能访问ROLE_ADMIN允许访问的资源。 --> <!-- <user-service> <user name="admin" password="admin" authorities="ROLE_USER,ROLE_ADMIN" /> <user name="user" password="user" authorities="ROLE_USER" /> </user-service> --> </authentication-provider> </authentication-manager> <!-- 访问决策管理:通过资源所拥有的权限,对用户所具备的角色对应的权限进行裁定,决策是否用户具备访问资源的权限--> <beans:bean id="accessDecisionManager" class="org.wit.ff.authentication.CustomAccessDecisionManager"> <beans:property name="allowIfAllAbstainDecisions" value="false" /> <beans:property name="decisionVoters"> <beans:list> <beans:bean class="org.springframework.security.access.vote.RoleVoter" /> <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" /> </beans:list> </beans:property> </beans:bean> <!--使用Spring security3提供的过滤器,并将过滤器默认的认证和授权组件替换为我们已经定义的认证和授权组件 --> <beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="accessDecisionManager" ref="accessDecisionManager" /> <beans:property name="securityMetadataSource" ref="customSecurityMetadataSource" /> </beans:bean> <beans:bean id="customSecurityMetadataSource" class="org.wit.ff.authentication.CustomFilterInvocationSecurityMetadataSource" /> <!-- 自定义授权用户信息处理 --> <beans:bean id="customUserDatailsService" class="org.wit.ff.authentication.CustomUserDetailsService" /> </beans:beans>