spring中的Acegi
Acegi作为web项目的一种资源保护安全机制,已经得到广泛应用。配置也十分简单,受先在web.xml加入下段代码,为其工作提供入口
<!-- Acegi Filters-->
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>
org.acegisecurity.util.FilterChainProxy
</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
只要url已.do结尾的都将进入org.acegisecurity.util.FilterToBeanProxy这个类里,进入acegi认证体系。
首先看一下doInit()方法
if ((targetBean != null) && (ctx.containsBean(targetBean))) {
beanName = targetBean;
} else {
if (targetBean != null) {
throw new ServletException("targetBean '" + targetBean + "' not found in context");
}
String targetClassString = this.filterConfig.getInitParameter("targetClass");
if ((targetClassString == null) || ("".equals(targetClassString))) {
throw new ServletException("targetClass or targetBean must be specified");
}
Class targetClass;
try
{
targetClass = Thread.currentThread().getContextClassLoader().loadClass(targetClassString);
} catch (ClassNotFoundException ex) {
throw new ServletException("Class of type " + targetClassString + " not found in classloader");
}
Map beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(ctx, targetClass, true, true);
if (beans.size() == 0) {
throw new ServletException("Bean context must contain at least one bean of type " + targetClassString);
}
beanName = (String)beans.keySet().iterator().next();
}
Object object = ctx.getBean(beanName);
if (!(object instanceof Filter)) {
throw new ServletException("Bean '" + beanName + "' does not implement javax.servlet.Filter");
}
this.delegate = ((Filter)object);
由上面代码可知,我们使用参数targetClass来指定我们的delegate对象。
由web.xml配置可知delegate为org.acegisecurity.util.FilterChainProxy对象,而正是这个bean是在我们的spring中所配置的。
<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>
其中filterInvocationDefinitionSource是主要进行认证的类,组合在FilterChainProxy中,这些bean也配在spring中。
来看一下FilterChainProxy的dofilter方法
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
FilterInvocation fi = new FilterInvocation(request, response, chain);
ConfigAttributeDefinition cad = this.filterInvocationDefinitionSource.getAttributes(fi);
if (cad == null) {
if (logger.isDebugEnabled()) {
logger.debug(fi.getRequestUrl() + " has no matching filters");
}
chain.doFilter(request, response);
return;
}
Filter[] filters = obtainAllDefinedFilters(cad);
if (filters.length == 0) {
if (logger.isDebugEnabled()) {
logger.debug(fi.getRequestUrl() + " has an empty filter list");
}
chain.doFilter(request, response);
return;
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi, filters);
virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
}
obtainAllDefinedFilters(cad)将所有的filter加载到一个chain里,然后每个filter进行验证,下面是filter的spring配置
<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" />
<bean id="logoutFilter"
class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/pages/jsp/loginH.jsp" />
<constructor-arg>
<list>
<bean
class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/j_acegi_logout"/>
</bean>
<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationFailureUrl" value="/pages/jsp/loginH.jsp?loginerror=1" />
<property name="defaultTargetUrl" value="/index.do" />
<property name="filterProcessesUrl" value="/j_acegi_security_check" />
</bean>
<bean id="securityContextHolderAwareRequestFilter"
class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter" />
<bean id="anonymousProcessingFilter"
class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="changeThis" />
<property name="userAttribute"
value="anonymousUser,ROLE_ANONYMOUS" />
</bean>
<bean id="anonymousAuthenticationProvider" class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="changeThis"/>
</bean>
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/pages/jsp/loginH.jsp" />
</bean>
</property>
<property name="accessDeniedHandler">
<bean
class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/pages/jsp/accessDenied.jsp" />
</bean>
</property>
</bean>
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="accessDecisionManager"
ref="accessDecisionManager" />
<!--property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=ROLE_ANONYMOUS,ROLE_1
</value>
</property-->
<property name="objectDefinitionSource"
ref="filterDefinitionSource" />
</bean>
这里先讲一下用户登录授权的filte(rauthenticationProcessingFilter),他的授权行为由已成员authenticationManager来完成。来看其认证方法。
public Authentication attemptAuthentication(HttpServletRequest request)
throws AuthenticationException
{
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
request.getSession().setAttribute("ACEGI_SECURITY_LAST_USERNAME", username);
setDetails(request, authRequest);
return getAuthenticationManager().authenticate(authRequest);
}
authenticationManager完整配置如下
<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list> <!-- 一般用户认证 -->
<ref local="anonymousAuthenticationProvider"/>
<ref local="daoAuthenticationProvider" />
</list>
</property>
</bean>
<bean id="anonymousAuthenticationProvider" class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="changeThis"/>
</bean>
<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService" />
<!-- UserCache property will activate the cache, it is not
mandatory but increases performance by cacheing the user
details retrieved from user-base -->
<property name="userCache" ref="userCache" />
<property name="passwordEncoder" ref="passwordEncoder" />
</bean>
<!-- 使用Md5算法加密 -->
<bean id="passwordEncoder"
class="org.acegisecurity.providers.encoding.Md5PasswordEncoder" />
<bean id="userDetailsService"
class="com.impl.CustomUserDetailsServiceImpl">
<!-- property name="userIdIncluded" value="true" /-->
<property name="dataSource" ref="dataSource" />
<property name="usersByUsernameQuery">
<value>
SELECT USER_LOGINNAME,USER_LOGINPWD,1 FROM dfl_user WHERE USER_LOGINNAME=?
AND USER_STATUS='1'
</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>
SELECT u.USER_LOGINNAME,r.CODE from dfl_user u, SYS_ROLES r,
SYS_USER_ROLE ur WHERE u.user_id=ur.USER_ID and r.id=ur.ROLE_ID
and u.USER_LOGINNAME = ?
</value>
</property>
</bean>
<bean id="userCache"
class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
<property name="cache">
<bean
class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />
</property>
<property name="cacheName" value="userCache" />
</bean>
</property>
</bean>
可见,authenticationManager也只是所有认证方法的聚合,具体使用哪种认证方式由providers决定,这里配置了两种(一种是普遍的查数据库方式还有一种是匿名方式)
说一下数据库操作的认证方式
使用的是org.acegisecurity.providers.dao.DaoAuthenticationProvider,其方法为
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException notFound) {
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw notFound;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
this.preAuthenticationChecks.check(user);
try {
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} else {
throw exception;
}
}
this.postAuthenticationChecks.check(user);
if (!(cacheWasUsed)) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
该方法分别从cache和数据库里拿user信息,之后比对password,成功返回认证成功,失败则抛出异常。
在这里retrieveUser方法变得十分重要,他负责获取数据库的user信息
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException
{
UserDetails loadedUser;
try {
loadedUser = getUserDetailsService().loadUserByUsername(username);
} catch (DataAccessException repositoryProblem) {
throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new AuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
Acegi提供UserDetails接口给我们实现,所有与数据库的交互将交给UserDetails的实现类去完成。上诉方法通过调用loadUserByUsername获得user
下面是UserDetailsServiceImpl的部分代码
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException
{
List users = this.usersByUsernameMapping.execute(username);
if (users.size() == 0) {
throw new UsernameNotFoundException("User not found");
}
User user = (User)users.get(0);
List dbAuths = this.authoritiesByUsernameMapping.execute(user.getUsername());
addCustomAuthorities(user.getUsername(), dbAuths);
if (dbAuths.size() == 0) {
throw new UsernameNotFoundException("User has no GrantedAuthority");
}
GrantedAuthority[] arrayAuths = (GrantedAuthority[])dbAuths.toArray(new GrantedAuthority[dbAuths.size()]);
User retUser = null;
if (!(this.userIdIncluded))
retUser = new User(user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, arrayAuths);
else
retUser = new User(user.getUserId(), user.getUsername(), user.getPassword(), user.isEnabled(), true, true, true, arrayAuths);
Map attributes = new HashMap();
addCustomAttributes(user.getUsername(), attributes);
if (attributes.size() > 0) {
retUser.setAttributes(attributes);
}
return retUser;
}
这里比较简单使用jdbc方式,MappingSqlQuery来获取数据库记录,之后返回org.acegisecurity.userdetails.User即可。
filterInvocationInterceptor使用来保证url反问的安全性
url访问的可行性由accessDecisionManager来处理,其实accessDecisionManager也是各类策略的集合,真正决定url是否可以访问由单个决策类来完成。
<bean id="accessDecisionManager"
class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<bean class="org.acegisecurity.vote.RoleVoter" />
<bean class="org.acegisecurity.vote.AuthenticatedVoter" />
</list>
</property>
</bean>
可以先看一下org.acegisecurity.vote.AffirmativeBased的部分代码,代码遍历每个voter类,调用他们的vote方法。返回1则成功,-1则表示拒绝访问。
public void decide(Authentication authentication, Object object, ConfigAttributeDefinition config)
throws AccessDeniedException
{
Iterator iter = getDecisionVoters().iterator();
int deny = 0;
while (iter.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)iter.next();
int result = voter.vote(authentication, object, config);
switch (result) {
case 1:
return;
case -1:
++deny;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
checkAllowIfAllAbstainDecisions();
}
filterInvocationInterceptor权限资源的获取
ConfigAttributeDefinition attr = obtainObjectDefinitionSource().getAttributes(object);
而obtainObjectDefinitionSource正是我们需要实现从数据库获取权限信息的类,先看一下acegi提供的父类
public abstract class AbstractFilterInvocationDefinitionSource
implements FilterInvocationDefinitionSource
{
public ConfigAttributeDefinition getAttributes(Object object)
throws IllegalArgumentException
{
if ((object == null) || (!(supports(object.getClass())))) {
throw new IllegalArgumentException("Object must be a FilterInvocation");
}
String url = ((FilterInvocation)object).getRequestUrl();
return lookupAttributes(url);
}
public abstract ConfigAttributeDefinition lookupAttributes(String paramString);
public boolean supports(Class clazz)
{
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
由此可见只需实现lookupAttributes方法即可,这里有使用者自行实现,在最后返回ConfigAttributeDefinition的类型即可。