`
lujiawu12
  • 浏览: 126939 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

spring security

    博客分类:
  • java
阅读更多
转载:

Acegi提供了多种身份验证方式(表单验证,CAS等),但只允许一种用户登录,而就个人了解,有一些系统是需要多种用户登录的。比如企业的员工需要登录并使用系统,企业也允许客户登录系统并使用有限的功能。以下尝试剖析Acegi的表单验证过程,并给出一种允许多种用户登录的方案。本方案基本达到“能用”的目的,但不一定是最佳方案。希望这篇文章能起到抛砖引玉的作用,给各位朋友一点参考,也希望各位提出有益的建议。

  Acegi的表单验证方式简要分析

  一个使用Acegi的表单验证的登录页面通常需要在表单提交时request的j_username和j_password参数赋值,即用户名和密码,而表单则提交到Acegi设定到验证地址。例如:

<form method="post" id="loginForm" action="<c:url value='/j_security_check'/>" >
<input type="text" name="j_username" id="j_username" />

<input type="password" name="j_password" id="j_password" />

<input type="submit" name="login" value="Login" />
</form>

  服务器的Servlet容器收到请求后会传递给Acegi的FilterToBeanProxy,这需要在web.xml中进行配置。例如:

<filter>
<filter-name>securityFilter</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>securityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

  FilterToBeanProxy基本上只起到调用转发的作用。在它的doFilter方法中会找到类型为FilterChainProxy的bean,调用后者的doFilter方法,同时把request、response会chain参数都传递过去。代码如下:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!initialized) {
doInit();
}

delegate.doFilter(request, response, chain);
}

  上面的代码中的delegate就是找到的类型FilterChainProxy的bean。FilterChainProxy的典型配置如下:

<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,
</value>
</property>
</bean>

  对于上面的配置,引用一段Acegi联机帮助中的说明来帮助理解:

Internally Acegi Security will use a PropertyEditor to convert the string presented in the above XML fragment into a FilterInvocationDefinitionSource object. What's important to note at this stage is that a series of filters will be run - in the order specified by the declaration - and each of those filters are actually the <bean id> of another bean inside the application context.

  实际上,FilterChainProxy的doFilter方法会执行如下处理:

    1.读取配置,如果配置为空,则直接调用chain.doFilter,返回
    2.如果配置不为空,则根据配置找到各个bean,放入Filter数组中。如果配置中没有配置任何bean,则直接调用chain.doFilter,返回
    3.FilterChainProxy创建一个VirtualFilterChain对象,并将chain封装为一个FilterInvocation对象,将它和Filter数组一起传递给VirtualFilterChain的构造函数。VirtualFilterChain的构造函数初始化了一个指针currentPosition,指向Filter数组的第一个元素additionalFilters[0]
    4.FilterChainProxy调用VirtualFilterChain的doFilter方法,在该方法中将指针currentPosition前移,调用additionalFilters[0]的doFilter方法。注意这里VirtualFilterChain把自身作为参数传递给additionalFilters[0]的doFilter方法,这样additionalFilters[0]的doFilter方法最后会调用VirtualFilterChain的doFilter方法,这样控制就又回到了VirtualFilterChain!于是VirtualFilterChain又将currentPosition前移,调用additionalFilters[1]的doFilter方法......
    5.当additionalFilters中所有元素的doFilter都执行完毕,VirtualFilterChain执行fi.getChain().doFilter,而fi.getChain()的值就是FilterChainProxy的doFilter方法中的参数chain的值。这样我们就理解了FilterChainProxy是怎样让调用兜了个圈,又传递出去的。

  重新回到FilterChainProxy的配置,看到它调用了authenticationProcessingFilter这个Filter。让我们看看它的配置:

<bean id="authenticationProcessingFilter"
class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationFailureUrl" value="/login.jsp?error=true"/>
<property name="defaultTargetUrl" value="/"/>
<property name="filterProcessesUrl" value="/j_security_check"/>
<property name="rememberMeServices" ref="rememberMeServices"/>
</bean>

  authenticationProcessingFilter的其中一个作用就是获取客户端提交的用户名和密码,将它们封装为一个Token,传递给authenticationManager的authenticate方法,由后者负责验证。

  看看authenticationManager的配置:

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="rememberMeAuthenticationProvider"/>
</list>
</property>
</bean>

  authenticationManager依次调用每个provider的authenticate方法。如果某个provider验证成功则返回;如果所有的验证都不成功,则抛出异常。

  让我们看看daoAuthenticationProvider的配置:

<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDao"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

  daoAuthenticationProvider在authenticate方法中调用retrieveUser方法取得用户信息,执行基本的验证,然后调用additionalAuthenticationChecks执行附加的验证(比如验证密码是否正确)。在retrieveUser方法中调用userDetailsService的loadUserByUsername方法取得用户信息,而userDetailsService是一个名为userDao的bean。让我们看看userDao的配置:

<bean id="userDao" class="cn.net.cogent.summer.extension.appfuse.dao.hibernate.EmployeeDaoHibernate">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

  userDao实现了Acegi的UserDetailsService接口,该接口只有loadUserByUsername方法。loadUserByUsername方法根据传入的username取得相应的Employee对象(Employee实现了UserDetails接口),该对象返回给daoAuthenticationProvider,由它和authenticationManager联合完成验证的任务。

  以上对Acegi对表单验证过程进行了简单对分析,限于篇幅,无法深入分析源码。尽管Acegi调用了多个Filter来完成验证过程,关键点却在三处:

   1.在客户端输入身份验证信息,包括用户名和密码
   2.AuthenticationProcessingFilter取出用户名和密码,封装为一个Token往后传递
   3.DaoAuthenticationProvider从系统中找出用户资料,并和ProviderManager一起执行验证

  实现多种用户登录

  很明显,要让系统识别不同种类的用户,必须设立一个用户类型标志。问题就转化为:

   1.用户在客户端输入身份信息时系统就必须设立相应的标志
   2.该标志如何传递到DaoAuthenticationProvider
   3.DaoAuthenticationProvider如何识别该标志,并从相应类型的用户中找到指定用户

  我不打算改动Acegi的源码,只打算扩展出我需要的功能。

  首先在登录页面中加入用户类型标志j_userkind。在登录页面中加入如下代码:

<input type="hidden" name="j_userkind" id="j_userkind" value="0">

  其中0代码员工,1代码客户。可以考虑在登录页面中增加一个选项,如果用户要以员工身份登录,则把j_userkind置为0;如果用户要以客户身份登录,则把j_userkind置为1。也可以提供两个登录页面,其中一个员工专用(j_userkind被强制置为0),另一个客户专用(j_userkind被强制置为1)

  系统如何根据收到的用户类型标志去读取指定的用户呢?如果在代码中写死(比如当用户类型标志=0时,读取员工;当用户类型标志=1时,读取客户)非常不好,还是通过配置来确定比较灵活。首先编写UserKindComparisonAware接口:

package cn.net.cogent.summer.extension.acegisecurity.providers;

public interface UserKindComparisonAware {

public void setExpectedUserKind(String expectedUserKind);
public void setCurrentUserKind(String currentUserKind);

}

  该接口说明实现类需要实现两个方法,setExpectedUserKind用于接受一个期望的用户类型标志(通常该标志通过配置来设置),setCurrentUserKind用于接受当前登录用户的用户类型标志(系统在运行时捕获,并传递给实现类)

  编写MKUDaoAuthenticationProvider类:

package cn.net.cogent.summer.extension.acegisecurity.providers.dao;

import cn.net.cogent.summer.extension.acegisecurity.BadUserKindException;
import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.acegisecurity.AuthenticationException;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;

import cn.net.cogent.summer.util.LoggerUtil;

public class MKUDaoAuthenticationProvider extends DaoAuthenticationProvider implements
UserKindComparisonAware {

private String expectedUserKind;
private String currentUserKind;

public String getExpectedUserKind() {
return expectedUserKind;
}
public void setExpectedUserKind(String expectedUserKind) {
this.expectedUserKind = expectedUserKind;
}

public String getCurrentUserKind() {
return currentUserKind;
}
public void setCurrentUserKind(String currentUserKind) {
this.currentUserKind = currentUserKind;
}

protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
LoggerUtil.getLogger().debug("expectedUserKind = '" + expectedUserKind + "', currentUserKind = '" + currentUserKind + "'");
if (currentUserKind.equals(expectedUserKind))
super.additionalAuthenticationChecks(userDetails, authentication);
else
throw new BadUserKindException(
"Flag UserKind does not match");
}
}

  该类继承自DaoAuthenticationProvider并实现UserKindComparisonAware接口,在additionalAuthenticationChecks方法中判断当前登录用户的用户类型标志与期望的用户类型标志是否一致,如果一致则执行父类的additionalAuthenticationChecks,完成验证;否则抛出一个BadUserKindException异常,表明验证失败。BadUserKindException继承自org.acegisecurity.AuthenticationException,具体的代码略

  在applicationContext.xml中删除daoAuthenticationProvider相关的配置,增加如下配置:

<bean id="customerDaoAuthenticationProvider" class="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider">
<property name="userDetailsService" ref="customerDao"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
<property name="expectedUserKind" value="1"/>
</bean>

<bean id="userDaoAuthenticationProvider" class="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider">
<property name="userDetailsService" ref="userDao"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
<property name="expectedUserKind" value="0"/>
</bean>

  可以看出customerDaoAuthenticationProvider仅用于验证客户(其expectedUserKind被指定为1),而userDaoAuthenticationProvider仅用于验证员工(其expectedUserKind被指定为0)。customerDao的配置如下:

<bean id="customerDao" class="cn.net.cogent.summer.extension.appfuse.dao.hibernate.CustomerDaoHibernate">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

  CustomerDaoHibernate的代码如下:

package cn.net.cogent.summer.extension.appfuse.dao.hibernate;

import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;

import cn.net.cogent.summer.model.Customer;
import org.appfuse.dao.hibernate.GenericDaoHibernate;
import org.springframework.dao.DataAccessException;

import java.util.List;

public class CustomerDaoHibernate extends GenericDaoHibernate<Customer, Long> implements UserDetailsService {

public CustomerDaoHibernate() {
super(Customer.class);
}

public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
List<Customer> users = getHibernateTemplate().find("from Customer where username=?", username);
if (users == null || users.isEmpty()) {
throw new UsernameNotFoundException("Customer '" + username + "' not found");
} else {
return (UserDetails) users.get(0);
}
}
}

  可以看出CustomerDaoHibernate是取得一个Customer对象(实现了UserDetails接口),而不是Employee。

  修改authenticationManager的配置如下:

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="customerDaoAuthenticationProvider"/>
<ref local="userDaoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="rememberMeAuthenticationProvider"/>
</list>
</property>
</bean>

  在哪里捕获当前登录用户的用户类型标志,并传递给MKUDaoAuthenticationProvider呢?我决定增加一个名为PreAuthenticationProcessingFilter的Filter,放在AuthenticationProcessingFilter之前,代码如下:

package cn.net.cogent.summer.extension.acegisecurity.ui.webapp;

import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.io.IOException;

import java.util.Iterator;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class PreAuthenticationProcessingFilter implements Filter, ApplicationContextAware {

public static final String ACEGI_SECURITY_FORM_USERKIND = "j_userkind";

private FilterConfig filterConfig;
private boolean initialized = false;
private Map targetBeans;
private String targetClass;
private ApplicationContext applicationContext;

public String getTargetClass() {
return targetClass;
}
public void setTargetClass(String targetClass) {
this.targetClass = targetClass;
}

public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

public void destroy() {
}

public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Can only process HttpServletRequest");
}

if (!initialized) {
doInit();
}

String userKind = obtainUserKind((HttpServletRequest)request);
for (Iterator it = targetBeans.values().iterator(); it.hasNext(); ) {
UserKindComparisonAware comparison = (UserKindComparisonAware)it.next();
comparison.setCurrentUserKind(userKind);
}

chain.doFilter(request, response);
}

private synchronized void doInit() throws ServletException {
if ((targetClass == null) || "".equals(targetClass)) {
throw new ServletException("targetClass must be specified");
}

Class _targetClass;

try {
_targetClass = Thread.currentThread().getContextClassLoader().loadClass(targetClass);
} catch (ClassNotFoundException ex) {
throw new ServletException("Class of type " + targetClass + " not found in classloader");
}

targetBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, _targetClass, true, true);

if (targetBeans.size() == 0) {
throw new ServletException("Bean context must contain at least one bean of type " + targetClass);
}

for (Iterator it = targetBeans.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry)it.next();
if (!(entry.getValue() instanceof UserKindComparisonAware)) {
throw new ServletException("Bean '" + entry.getKey() +
"' does not implement cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware");
}
}

// Set initialized to true at the end of the synchronized method, so
// that invocations of doFilter() before this method has completed will not
// cause NullPointerException
initialized = true;
}

protected String obtainUserKind(HttpServletRequest request) {
return request.getParameter(ACEGI_SECURITY_FORM_USERKIND);
}
}

  PreAuthenticationProcessingFilter需要在初始化参数中指定targetClass,该参数的值是一个类,该类实现了UserKindComparisonAware接口。PreAuthenticationProcessingFilter找到容器中所有该类的实例,并把捕获的当前登录用户的用户类型标志赋值给它们。PreAuthenticationProcessingFilter的配置如下:

<bean id="preAuthenticationProcessingFilter"
class="cn.net.cogent.summer.extension.acegisecurity.ui.webapp.PreAuthenticationProcessingFilter">
<property name="targetClass"
value="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider"/>
</bean>

  还需要把preAuthenticationProcessingFilter加入到filterChainProxy的配置中:

<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=,preAuthenticationProcessingFilter,authenticationProcessingFilter,
</value>
</property>
</bean>

  注意把它放在authenticationProcessingFilter的前面

  至此我们初步实现了使用Acegi实现多种用户登录
分享到:
评论

相关推荐

    Spring Security in Action

    Spring Security 实践指南 Spring Security 是一个基于 Java 的安全框架,旨在提供身份验证、授权和访问控制等功能。下面是 Spring Security 的主要知识点: 一、身份验证(Authentication) 身份验证是指对用户...

    SpringSecurity.pdf

    Spring Security是一个功能强大、高度定制的安全框架,它专门用于为基于Spring的应用程序提供安全性解决方案。Spring Security架构的设计初衷是为了解决认证和授权的需求,确保应用程序的安全性。它提供了全面的安全...

    Spring Security 资料合集

    Spring Security 是一个强大的安全框架,主要用于Java应用的安全管理,它为Web应用和企业级应用提供了全面的安全服务。这个框架能够处理认证、授权以及各种安全相关的功能,帮助开发者构建安全、可扩展的应用。以下...

    SpringSecurity笔记,编程不良人笔记

    SpringSecurity是Java领域中一款强大的安全框架,主要用于Web应用程序的安全管理。它提供了全面的身份验证、授权、会话管理以及安全相关的功能,可以帮助开发者构建安全的Web应用。在本笔记中,我们将深入探讨Spring...

    spring security 完整项目实例

    Spring Security 是一个强大的安全框架,用于为Java应用提供身份验证和授权服务。在这个完整的项目实例中,我们将深入探讨Spring Security的核心概念以及如何将其应用于实际的Web应用程序开发。 首先,我们从用户、...

    Spring Cloud Gateway 整合 Spring Security 统一登录认证鉴权

    在压缩包文件`spring_gateway_security_webflux`中,可能包含了示例代码或配置文件,用于演示如何在Spring Cloud Gateway中集成Spring Security,实现统一登录认证鉴权。这些资源可以帮助开发者更快地理解和实践上述...

    springsecurity学习笔记

    在"springsecurity学习笔记"中,你可能会涉及以下主题: - Spring Security的基本配置,包括web安全配置和全局安全配置。 - 如何自定义认证和授权流程,比如实现自定义的AuthenticationProvider和...

    SpringBoot+SpringSecurity处理Ajax登录请求问题(推荐)

    SpringBoot+SpringSecurity处理Ajax登录请求问题 SpringBoot+SpringSecurity处理Ajax登录请求问题是SpringBoot开发中的一個常见问题,本文将详细介绍如何使用SpringBoot+SpringSecurity处理Ajax登录请求问题。 ...

    spring security 官方文档

    Spring Security 是一个强大的安全框架,用于为Java应用提供全面的安全管理解决方案。它是Spring生态系统的组成部分,专注于身份验证、授权和访问控制。Spring Security的核心特性包括: 1. **身份验证...

    SpringSecurity学习总结源代码

    SpringSecurity是Java开发中用于构建安全Web应用的框架,它提供了强大的身份验证、授权和访问控制功能。在本文中,我们将深入探讨SpringSecurity的核心概念、关键组件以及如何配置和使用这个框架。 首先,Spring...

    spring spring security2.5 jar

    Spring Security是Spring生态体系中的一个核心组件,主要负责应用程序的安全性,包括认证和授权。它为Web应用提供了全面的保护,防止未经授权的访问和操作。在版本2.5时,Spring Security已经是一个成熟且功能丰富的...

    最详细Spring Security学习资料(源码)

    Spring Security是一个功能强大且高度可定制的身份验证和授权框架,专门用于保护Java应用程序的安全性。它构建在Spring Framework基础之上,提供了全面的安全解决方案,包括身份验证、授权、攻击防护等功能。 Spring...

    spring security3 中文版本

    ### Spring Security 3.0.1 中文版知识点解析 #### 一、Spring Security 3.0.1 概览 ##### 1.1 Spring Security 是什么? Spring Security 是一个强大的、高度可定制的身份验证和访问控制框架。它提供了许多功能...

    安全框架Spring Security深入浅出视频教程

    视频详细讲解,需要的小伙伴自行网盘下载,链接见附件,永久有效。 首先,SSM环境中我们通过xml配置的...Springsecurity在两种不同的开发模式中使用,有经典的独立web后台管理系统,也有时下最流行的前后端分离场景。

    springSecurity 实现传参

    Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架,用于Java应用程序。在这个场景中,我们关注的是如何使用Spring Security实现登录验证以及在登录过程中传递参数,特别是记录并返回用户登录前的...

    springsecurity原理流程图.pdf

    Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是安全领域中Spring生态系统的一部分。Spring Security旨在为Java应用程序提供一个全面的安全解决方案,尤其适用于企业级应用场景。它主要...

    精彩:Spring Security 演讲PPT

    ### Spring Security 概述与应用实践 #### 一、引言 在当今互联网时代,网络安全问题日益凸显,尤其是Web应用程序的安全性受到了前所未有的关注。为了应对这些挑战,Spring Security 应运而生,成为了一个非常重要...

    Spring security认证授权

    Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架,用于Java应用程序。它提供了全面的安全解决方案,包括用户认证、权限授权、会话管理、CSRF防护以及基于HTTP的访问控制。在这个例子中,我们将...

    SpringSecurity素材.rar

    SpringSecurity是Java领域中一款强大的安全框架,专为Spring和Spring Boot应用设计,提供全面的安全管理解决方案。在SpringBoot Web开发中,SpringSecurity扮演着核心角色,负责处理身份验证、授权以及访问控制等...

    spring security实现动态授权

    Spring Security 是一个强大的安全框架,用于为Java应用提供安全控制。在传统的权限管理中,权限配置通常硬编码在应用程序中,这使得权限调整变得困难,每次变动都需要重新部署应用。然而,通过动态授权,我们可以将...

Global site tag (gtag.js) - Google Analytics