由于ss2的demo配置太过简单,要想在项目中应用的话必须进行相应扩展,这里简单写一下简单的扩展方法。
xml头中引入security命名空间
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd" default-lazy-init="true">
然后是启用ss2默认配置的一段代码
<!-- 2.0新增的命名空间,使得配置简化了很多 auto-config 自动使用默认的配置 access-denied-page 指定访问未授权页面时显示的页面 --> <security:http auto-config="true" access-denied-page="/accessDenied.html"> <security:anonymous granted-authority="BASIC" /> </security:http>
这段代码作用是以ss2的默认配置方式加入1.0时需要手工配置的AuthenticationProcessingFilter等多个必须配置的filter,详细可参考1.0配置和2.0参考手册。
auto-config="true" 表示使用ss2自动配置
access-denied-page="/accessDenied.html"表示拒绝访问时显示的页面
<security:anonymous granted-authority="BASIC" />表示匿名权限的authority为BASIC
ss对权限的管理分为认证和授权两部分,先看认证
<!-- 负责认证处理的filter --> <bean id="authenticationProcessingFilter" class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter"> <!-- 此行说明此filter会覆盖ss2默认配置的filter,before 被覆盖filter的别名 --> <security:custom-filter before="AUTHENTICATION_PROCESSING_FILTER" /> <!-- 认证管理器 --> <property name="authenticationManager" ref="authenticationManager" /> <!-- 认证失败后跳转到的页面,/spring_security_login是ss2默认的登录入口 --> <property name="authenticationFailureUrl" value="/spring_security_login" /> <!-- 认证成功后跳转到的页面 --> <property name="defaultTargetUrl" value="/index.html" /> </bean>
这是负责认证处理的filter,中间custom-filter一行意思是将filter放在默认配置中别名为AUTHENTICATION_PROCESSING_FILTER的filter前边,即负责认证的filter(别名列表参照参考手册)。
按官方的说法,如果需要用自定义的filter覆盖默认filter,则应该将security:http标签的auto-config属性改为false,这样的话就需要增加很多手动配置项。我试了下,不改false也可以,只是运行期间会出现一个warn信息“Possible error: Filters at position 2 and 3 are both instances of xxxx”,意思是filter串中有两个相同类型的filter。
另:在2.0.2中可以使用position代替before,真正的覆盖默认filter。但是有个bug,如果使用默认登录入口的话,还是会调用默认filter,必须连登录入口一并改掉。
其引用的authenticationManager
<!-- 认证管理器 根据用户名和密码,使用多个provider进行认证 认证成功会生成一个Authentication,否则抛出AuthenticationException --> <bean id="authenticationManager" class="org.springframework.security.providers.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider" /> </list> </property> </bean>
认证管理器通过多个provider实现基于用户名和密码的认证,多个provider中只要有一个认证成功,即成功。
这里只使用了一个daoPorvider
<!-- 认证的provider userDetailsService 根据用户名获取用户信息 userCache ehcache缓存user信息。 --> <bean id="daoAuthenticationProvider" class="org.springframework.security.providers.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="userDetailsService" /> <property name="userCache" ref="userCache" /> </bean>
userDetailsService:根据登录的用户名获取一个UserDetails,即代表一个用户的实体对象。
<!-- 通过dao查询用户信息 --> <bean id="userDetailsService" class="org.catspaw.ss2test1.security.UserDetailsSerivceImpl"> <property name="userDao" ref="userDao" /> </bean>
UserDetailsSerivceImpl代码
package org.catspaw.ss2test1.security;
import org.catspaw.ss2test1.dao.UserDao;
import org.springframework.dao.DataAccessException;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;
/**
* 获取UserDetails
* 使用UserDao查询User
*
* @author 孙宁振
*
*/
public class UserDetailsSerivceImpl implements UserDetailsService {
private UserDao userDao;
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
return userDao.findByUsername(username);
}
}
UserDao实现(接口省略)
package org.catspaw.ss2test1.dao.hibernate;
import java.util.List;
import org.catspaw.ss2test1.dao.UserDao;
import org.catspaw.ss2test1.model.User;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Expression;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
/**
* 查询User
*
* @author 孙宁振
*
*/
public class UserDaoHibernate extends HibernateDaoSupport implements UserDao {
public User get(String id) {
return (User) getHibernateTemplate().get(User.class, id);
}
@SuppressWarnings("unchecked")
public User findByUsername(String username) {
DetachedCriteria dc = DetachedCriteria.forClass(User.class);
dc.add(Expression.eq("username", username));
List<User> list = getHibernateTemplate().findByCriteria(dc);
if (!list.isEmpty()) {
return list.get(0);
}
return null;
}
}
User实体,直接实现了UserDetails
package org.catspaw.ss2test1.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Transient;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.GrantedAuthorityImpl;
import org.springframework.security.userdetails.UserDetails;
/**
* 用户
* 实现了UserDetails
* 与Resource多对多关联
*
* @author 孙宁振
*
*/
@Entity
public class User implements UserDetails {
private String id;
private String username;
private String password;
private Set<Resource> resources;
@Id
public String getId() {
return id;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
@ManyToMany(targetEntity = Resource.class)
@JoinTable(name = "user_resource", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "resource_id"))
public Set<Resource> getResources() {
return resources;
}
@Transient
public GrantedAuthority[] getAuthorities() {
Set<Resource> resources = getResources();
List<GrantedAuthority> grandtedAuthorities = new ArrayList<GrantedAuthority>(
resources.size());
for (Resource resource : resources) {
String authority = resource.getAuthority();
grandtedAuthorities.add(new GrantedAuthorityImpl(authority));
}
return grandtedAuthorities.toArray(new GrantedAuthority[0]);
}
@Transient
public boolean isAccountNonExpired() {
return true;
}
@Transient
public boolean isAccountNonLocked() {
return true;
}
@Transient
public boolean isCredentialsNonExpired() {
return true;
}
@Transient
public boolean isEnabled() {
return true;
}
public void setId(String id) {
this.id = id;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setResources(Set<Resource> resources) {
this.resources = resources;
}
}
userCache缓存,都是使用的spring提供的实现。
<bean id="userCache" class="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache"> <property name="cache" ref="userCacheBacked" /> </bean> <bean id="userCacheBacked" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager" ref="cacheManager" /> <property name="cacheName" value="myUserCache" /> </bean> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache-security.xml" /> </bean>
ehcache-security.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache> <diskStore path="java.io.tmpdir/ehcache-security" /> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <!-- security cache--> <cache name="myUserCache" maxElementsInMemory="10000" eternal="false" overflowToDisk="true" timeToIdleSeconds="1200" timeToLiveSeconds="7200" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> </ehcache>
至此认证部分结束,下面是授权部分。
filterSecurityInterceptor,虽然名叫interceptor,实际上是个filter,负责针对要访问的资源进行用户授权,同样也是覆盖默认实现。
<!-- 负责授权的filter,检查Authentication所授予的权限是否可以访问被访问的资源 --> <bean id="filterSecurityInterceptor" class="org.springframework.security.intercept.web.FilterSecurityInterceptor"> <security:custom-filter before="FILTER_SECURITY_INTERCEPTOR" /> <property name="authenticationManager" ref="authenticationManager" /> <property name="accessDecisionManager" ref="accessDecisionManager" /> <!-- 获取访问被访问的资源所需要的权限(authority) --> <property name="objectDefinitionSource" ref="databaseFilterInvocationDefinitionSource" /> </bean>
accessDecisionManager,授权管理器,通过多个voter投票判定是否授权,有多种决策机制实现(一票通过,半数通过,一票否决等),具体原理参见参考手册,这里使用的AffirmativeBased是一票通过制,即有一个voter投赞成票就授权。voter会取出当前登录用户的UserDetails的所有authority,与所访问url所对应的authority进行匹配,有相同就通过。
<!-- 经过投票机制来决定是否可以访问某一资源 allowIfAllAbstainDecisions为false时如果有一个或以上的decisionVoters投票通过,则授权通过 其他可选的决策机制: ConsensusBased UnanimousBased --> <bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased"> <property name="decisionVoters"> <list> <bean class="org.springframework.security.vote.RoleVoter"> <property name="rolePrefix" value="" /> </bean> </list> </property> </bean>
objectDefinitionSource,获得访问资源所必须具有的权限(authority)。这里和UserDetails的getAuthorities方法刚好相反。
<!-- 获取访问被访问的资源所需要的权限(authority),以ConfigAttributeDefinition形式返回 --> <bean id="databaseFilterInvocationDefinitionSource" class="org.springframework.security.intercept.web.DefaultFilterInvocationDefinitionSource"> <!-- 匹配url的matcher --> <constructor-arg type="org.springframework.security.util.UrlMatcher" ref="antUrlPathMatcher" /> <!-- url对应authority的map --> <constructor-arg type="java.util.LinkedHashMap" ref="requestMap" /> </bean> <!--ant path风格的匹配器--> <bean id="antUrlPathMatcher" class="org.springframework.security.util.AntUrlPathMatcher" />
requestMapFactoryBean,实现Spring的FactoryBean接口
这里在容器初始化时就查询所有url和authority的对应关系,保存为一个map,在认证操作时遍历这个map
package org.catspaw.ss2test1.security;
import java.util.LinkedHashMap;
import java.util.List;
import org.catspaw.ss2test1.dao.ResourceDao;
import org.catspaw.ss2test1.model.Resource;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.security.ConfigAttribute;
import org.springframework.security.ConfigAttributeDefinition;
import org.springframework.security.SecurityConfig;
import org.springframework.security.intercept.web.RequestKey;
/**
* 产生一个map,包含所有url:authority的映射
* key是resourceString(ant表达式,表示一个url集合)(如果是restful风格的应用,则key是url和method)
* value是访问这些url所需要的权限(authority)
* 用于设置databaseFilterInvocationDefinitionSource中的requestMap
*
* @author 孙宁振
*
*/
public class RequestMapFactoryBean implements FactoryBean {
private ResourceDao resourceDao;
private LinkedHashMap<RequestKey, ConfigAttributeDefinition> requestMap;
public void init() {
requestMap = new LinkedHashMap<RequestKey, ConfigAttributeDefinition>();
List<Resource> resources = resourceDao.findAll();
for (Resource resource : resources) {
RequestKey key = new RequestKey(resource.getResourceString());//如果是restful风格的应用,则构造方法的参数应该是url和method
ConfigAttribute attribute = new SecurityConfig(resource
.getAuthority());
ConfigAttributeDefinition definition = new ConfigAttributeDefinition(
attribute);
requestMap.put(key, definition);
}
}
public Object getObject() throws Exception {
if (requestMap == null) {
init();
}
return requestMap;
}
public Class getObjectType() {
return LinkedHashMap.class;
}
public boolean isSingleton() {
return true;
}
public void setResourceDao(ResourceDao resourceDao) {
this.resourceDao = resourceDao;
}
}
ResourceDao实现(接口省略)
package org.catspaw.ss2test1.dao.hibernate;
import java.util.List;
import org.catspaw.ss2test1.dao.ResourceDao;
import org.catspaw.ss2test1.model.Resource;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
public class ResourceDaoHibernate extends HibernateDaoSupport implements
ResourceDao {
@SuppressWarnings("unchecked")
public List<Resource> findAll() {
DetachedCriteria criteria = DetachedCriteria.forClass(Resource.class);
criteria.addOrder(Order.desc("resourceString"));
return getHibernateTemplate().findByCriteria(criteria);
}
}
Resource实体
package org.catspaw.ss2test1.model;
import java.io.Serializable;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
/**
* 资源
* 与User多对多关联
*
* @author 孙宁振
*
*/
@Entity
public class Resource implements Serializable {
private String id;
private String authority; //ss中权限的标识符
private String resourceString; //ant表达式,表示一个url集合
private Set<User> users;
@Id
public String getId() {
return id;
}
public String getAuthority() {
return authority;
}
public String getResourceString() {
return resourceString;
}
@ManyToMany(targetEntity = User.class, mappedBy = "resources")
public Set<User> getUsers() {
return users;
}
public void setId(String id) {
this.id = id;
}
public void setResourceString(String resourceString) {
this.resourceString = resourceString;
}
public void setUsers(Set<User> users) {
this.users = users;
}
public void setAuthority(String authority) {
this.authority = authority;
}
}
最后,dao配置
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/ss2test1" /> <property name="username" value="root" /> <property name="password" value="root" /> </bean> <bean id="abstractSessionFactory" abstract="true"> <property name="dataSource" ref="dataSource" /> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.MySQL5InnoDBDialect </prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.format_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">none</prop> </props> </property> </bean> <!-- 使用annotation方式描述的SessionFactory --> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" parent="abstractSessionFactory"> <property name="configLocation" value="classpath:hibernate.cfg.xml" /> </bean> <bean id="baseDao" abstract="true"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="userDao" class="org.catspaw.ss2test1.dao.hibernate.UserDaoHibernate" parent="baseDao"> </bean> <bean id="resourceDao" class="org.catspaw.ss2test1.dao.hibernate.ResourceDaoHibernate" parent="baseDao"> </bean>
hibernate配置文件 hibernate.cfg.xml
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <mapping class="org.catspaw.ss2test1.model.User"/> <mapping class="org.catspaw.ss2test1.model.Resource"/> </session-factory> </hibernate-configuration>
本文只是一个最简单的示例,没有遵循rbac规范,仅仅是将user和resource关联起来,一个resource对应一个authority。也没有密码加密,cookie等功能,这些功能配置请参见参考手册。
在ss中,授权是以authority为基本单位的,user拥有多个authority,resource也可以拥有多个authority,在授权时只需要将user和resource的authority一一匹配即可。在实际应用中,可以在user和resource增加一层role来解耦,user-role-resource都是多对多关系,authority可以和role对应,授权时就变成了“检查用户是否具有访问某个资源所必须的角色”这一问题。
ss只提供认证和授权两个功能的实现,中间权限分配的实现由程序员负责,不管中间权限分配多么复杂,最后只要能将user和resource用authority关联起来就可以。
ss2常用的扩展点有以下几个:
UserDetails,表示一个用户实体,可以通过他来获得用户所具有的authority
AuthenticationProvider,用来认证一个用户Authentication,可以添加多个认证方式,比如sso
accessDecisionManager和Voter,用来对是否允许用户访问进行投票,以及投票的通过策略
objectDefinitionSource,用来获取资源和authority的对应,为用户授权提供依据。
基本上针对这三个扩展点进行相应扩展就可以满足大部分的应用需求。
2.0的整体架构和1.0相比基本没有改变,1.0时代的功能和配置方式也都可以继续使用,如果对ss2的默认配置不太放心,大可以把原来1.0的配置代码copy过来继续使用。
附demo
所用到的jar:spring2.0.7,hibernate3.2.5&jpa,spring-security2.0.3,ehcache1.3
数据库:mysql5,建好相应数据库后,把<prop key="hibernate.hbm2ddl.auto">none</prop>的none改为create即可在运行时自动建表
登录入口为/spring_security_login
用户名 密码
admin admin
aaa aaa
bbb bbb