SpringMVC + security模块 框架整合详解

来源:互联网 发布:优化人员结构 编辑:程序博客网 时间:2024/06/05 18:22
      最近整理一个二手后台管理项目,整体大体可分为 : Springmvc + mybatis + tiles + easyui + security 这几个模块,再用maven做管理。刚拿到手里面东西实在太多,所以老大让重新整一个,只挑出骨架。不可否认,架构整体规划得还是很好的,尤其是前端界面用tiles+easyui ,开发很高效!其实,之前虽然听说过security,但做过的项目都没用过security,所以也算是一个新手!整合过程还是很烧脑的。

    1、首先是springmvc部分,也是框架的最核心骨架部分。springmvc也是基于servlet框架完成的,所以我们都可以从web.xml入手,然后顺藤摸瓜,整理出整个架构。web.xml中主要部分是放置spring的各种配置:

<context-param>        <param-name>contextConfigLocation</param-name>        <param-value>        /WEB-INF/spring/appServlet/servlet-context.xml        </param-value>            </context-param>

<servlet>        <servlet-name>appServlet</servlet-name>        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>        <init-param>            <param-name>contextConfigLocation</param-name>            <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>        </init-param>        <load-on-startup>1</load-on-startup>        <async-supported>true</async-supported>    </servlet>        <servlet-mapping>        <servlet-name>appServlet</servlet-name>        <url-pattern>/</url-pattern>    </servlet-mapping>



这两部分指定了配置文件地址和web请求地址拦截规则。需要注意的是,然后配置文件里就是常用的注解、数据源、之类的,不一而足。


    2、然后是security,本文实现了自定义的用户信息登录认证,主要部分:spring的security主要注意几个地方:

       1)web.xml中配置security必须的过滤器,注意名字必须是springSecurityFilterChain,这是内置的过滤器;然后将过滤规则设置为/*,表示对所有请求都拦截。

       2)  关于security的配置文件必须在<context-param>中加入,因为ContextLoaderListener在加载的时候就会去加载security相关的东西,而此时springmvc(servlet)模块还没有初始化。

      二者配置如下:

       

<context-param>        <param-name>contextConfigLocation</param-name>        <param-value>        /WEB-INF/spring/appServlet/servlet-context.xml        </param-value>            </context-param>    <!-- Creates the Spring Container shared by all Servlets and Filters -->    <listener>        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>    </listener><!-- 用户权限模块 -->    <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>
本文中security配置文件再servlet-context.xml中被引入,形如:

<beans:import resource="spring-security.xml"/>

     3)spring-security.xml配置文件:

          现在来看这个xml文件:

          首先 <http>标签部分声明了访问拦截规则,这是security的关键。在这部分,我们先声明了一些不需要验证的资源和访问路径;然后是地址拦截规则部分:该部分拦截路径是/**,**表示可以跨目录结构,因此此处拦截该站点所有请求(除之前声明security=“none”的以外),然后拦截规则access="isAuthenticated()",这是SecurityExpressionRoot中的判断是否认证过的方法之一,跟hasRole()类似,官方描述是Returns true if the user is not anonymous ,也就是用户认证后返回true。

<?xml version="1.0" encoding="UTF-8"?><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"xmlns:security="http://www.springframework.org/schema/security"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd                        http://www.springframework.org/schema/security                         http://www.springframework.org/schema/security/spring-security-3.2.xsd"><!-- For Web security --><http pattern="/js/**" security="none"/>    <http pattern="/images/**" security="none"/>  <http pattern="/mgmt/**" security="none"/>  <http pattern="/image_sys/**" security="none"/>  <http pattern="/style/**" security="none"/>  <http pattern="/view/login.jsp" security="none"/>  <http pattern="/view/login/forgotPassword.jsp" security="none"/>  <http pattern="/securityCodeImage.html" security="none"/>  <http pattern="/checkSecurityCode.html" security="none"/>  <http pattern="/admin/validateMobile.html" security="none"/>  <http pattern="/verifycode/sendVerifyCode.html" security="none"/>  <http pattern="/admin/resetPassword.html" security="none"/>          <http use-expressions="true" entry-point-ref="authenticationProcessingFilterEntryPoint" access-denied-page="/view/403.jsp">        <intercept-url pattern="/**" access="isAuthenticated()" />        <remember-me />       <!--  <expression-handler ref="webSecurityExpressionHandler"/> -->        <custom-filter ref="loginFilter"  position="FORM_LOGIN_FILTER" />        <logout logout-url="/j_spring_security_logout" logout-success-url="/view/login.jsp" delete-cookies="JSESSIONID"/>    </http>    <!-- 未登录的切入点 -->      <beans:bean id="authenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">          <beans:property name="loginFormUrl" value="/view/login.jsp"></beans:property>      </beans:bean>         <!-- 登录体系loginFilter -->  <beans:bean id="loginFilter"        class="com.security.AdminUsernamePasswordAuthenticationFilter">        <beans:property name="filterProcessesUrl" value="/j_spring_security_check"></beans:property>        <beans:property name="authenticationSuccessHandler" ref="myAuthenticationSuccessHandler"></beans:property>        <beans:property name="authenticationFailureHandler" ref="myAuthenticationFailureHandler"></beans:property>        <beans:property name="authenticationManager" ref="authenticationManager"></beans:property>    </beans:bean>        <!-- 验证失败显示页面 -->    <beans:bean id="myAuthenticationFailureHandler"        class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">        <beans:property name="defaultFailureUrl" value="/view/login.jsp?error=true" />    </beans:bean><!-- 验证成功默认显示页面  -->    <beans:bean id="myAuthenticationSuccessHandler"        class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">        <beans:property name="alwaysUseDefaultTargetUrl" value="true" />        <!--此处可以请求登录首页的action地址  -->        <beans:property name="defaultTargetUrl" value="/index/homepage" />    </beans:bean>    <!-- authentication体系 -->    <authentication-manager alias="authenticationManager" erase-credentials="false">        <authentication-provider ref="authenticationProvider" />    </authentication-manager>    <beans:bean id="authenticationProvider"        class="com.security.AdminAuthenticationProvider">        <beans:property name="userDetailsService" ref="userDetailsService" />    </beans:bean>    <beans:bean id="userDetailsService"        class="com.security.AdminUserDetailsService">        <beans:property name="adminService" ref="adminServiceImpl"></beans:property>    </beans:bean>
</beans:beans>

      然后声明了自定义的用户登录的拦截器loginFilter,用于在登录时实现security认证。spring的登录认证模块执行顺序为:

1、UsernamePasswordAuthenticationFilter的attemptAuthentication()方法,此部分可以做用户名密码的判断,正确后继续执行;

2、接下来调用AuthenticationManager的authenticate()方法(中途具体怎么调用此处不深究),一个Mannager对应了多个authenticationProvider,其实最终是通过调用provider的authenticate()方法来进行认证的,只要provider的supports方法返回true即可声明该provider或可进行认证,最后将被manager调用。在authenticate()方法中,调用UserDetails的loadUserByUserName()方法来加载登录用户信息,包括权限信息,最后封装成AbstractAuthenticationToken,这个对象就是security模块认证后的用户完整信息。

自定义认证主要类如下:
/** * 用户登录验证步骤一:attemptAutentication() */public class AdminUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {    public static final String VALIDATE_CODE = "validateCode";    public static final String USERNAME = "j_username";    public static final String PASSWORD = "j_password";    public static final String EMPLOYEENO = "j_employeeNo";    @Resource    private AdminService adminService;    @Override    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)            throws AuthenticationException {        if (!request.getMethod().equals("POST")) {            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());        }        // 判断用户信息               // UsernamePasswordAuthenticationToken实现 Authentication        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(                adminModel.getAdminId(), password);        // 允许子类设置详细属性        setDetails(request, authRequest);        // 运行UserDetailsService的loadUserByUsername 再次封装Authentication        return this.getAuthenticationManager().authenticate(authRequest);    }    @Override    protected String obtainUsername(HttpServletRequest request) {        Object obj = request.getParameter(USERNAME);        return null == obj ? "" : obj.toString();    }    @Override    protected String obtainPassword(HttpServletRequest request) {        Object obj = request.getParameter(PASSWORD);        return null == obj ? "" : obj.toString();    }}

/** * <span style="font-family: Arial, Helvetica, sans-serif;">用户登录验证步骤二:</span><span style="font-family: Arial, Helvetica, sans-serif;">authenticate</span><span style="font-family: Arial, Helvetica, sans-serif;">()</span><span style="font-family: Arial, Helvetica, sans-serif;"></span> */public class AdminAuthenticationProvider implements AuthenticationProvider {    protected Logger logger = LoggerFactory.getLogger(this.getClass());      private UserDetailsService userDetailsService = null;    public AdminAuthenticationProvider() {        super();    }    /**     * @param userDetailsService     */    public AdminAuthenticationProvider(UserDetailsService userDetailsService) {        super();        this.userDetailsService = userDetailsService;    }    public UserDetailsService getUserDetailsService() {        return userDetailsService;    }    public void setUserDetailsService(UserDetailsService userDetailsService) {        this.userDetailsService = userDetailsService;    }    /**     * provider的authenticate()方法,用于登录验证     */    @SuppressWarnings("unchecked")    public Authentication authenticate(Authentication authentication) throws AuthenticationException {        // 1. Check username and password        try {            doLogin(authentication);        } catch (Exception e) {            if (e instanceof AuthenticationException) {                throw (AuthenticationException) e;            }            logger.error("failure to doLogin", e);        }        // 2. Get UserDetails        UserDetails userDetails = null;        try {            userDetails = this.userDetailsService.loadUserByUsername(authentication.getName());        } catch (Exception e) {            if (e instanceof AuthenticationException) {                throw (AuthenticationException) e;            }            logger.error("failure to get user detail", e);        }
        // 3. Check and get all of admin roles and contexts.        Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) userDetails.getAuthorities();        if (authorities != null && !authorities.isEmpty()) {            AdminAuthenticationToken token = new AdminAuthenticationToken(authentication.getName(),                    authentication.getCredentials(), authorities);            token.setDetails(userDetails);            return token;        }        throw new BadCredentialsException("没有分配权限");    }    protected void doLogin(Authentication authentication) throws AuthenticationException {            }    @Override    public boolean supports(Class<?> authentication) {        // TODO Auto-generated method stub        return true;    }}

/**用户详细信息获取 */public class AdminUserDetailsService implements UserDetailsService {    private Logger logger = LoggerFactory.getLogger(AdminUserDetailsService.class);    private AdminService adminService;        @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {                AdminUserDetails userDetails = new AdminUserDetails();        userDetails.setAdminId(username);                ///加载用户基本信息        AdminModel adminModel = adminService.getAdminByAdminId(username);        try {            PropertyUtils.copyProperties(userDetails, adminModel);        } catch (IllegalAccessException e) {            logger.error("用户信息复制到userDetails出错",e);        } catch (InvocationTargetException e) {            logger.error("用户信息复制到userDetails出错",e);        } catch (NoSuchMethodException e) {            logger.error("用户信息复制到userDetails出错",e);        }        //加载权限信息        List<AdminRoleGrantedAuthority> authorities = this.adminService.getAuthorityByUserId(username);        if (authorities == null || authorities.size() == 0) {////如果为普通用户            if (isCommonUserRequest()) {          AdminRoleGrantedAuthority authority =                 new AdminRoleGrantedAuthority(AdminRoleGrantedAuthority.ADMIN_ROLE_TYPE_COMMON_USER);          userDetails.getAuthorities().add(authority);            } else {                logger.warn("person authorities is empty, personId is [{}]", username);            }        }        //加载用户权限        userDetails.getAuthorities().addAll(authorities);                ///这个就是权限系统最后的用户信息        return userDetails;    }    private boolean isCommonUserRequest() {        // TODO Auto-generated method stub        return true;    }    public AdminService getAdminService() {        return adminService;    }    public void setAdminService(AdminService adminService) {        this.adminService = adminService;    }}


public class AdminAuthenticationToken extends AbstractAuthenticationToken {   /**   *    */  private static final long serialVersionUID = 5976309306377973996L;  private final Object principal;  private Object credentials;  public AdminAuthenticationToken(Object principal, Object credentials) {    super(null);    this.principal = principal;    this.credentials = credentials;    setAuthenticated(false);  }c AdminAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {    super(authorities);    this.principal = principal;    this.credentials = credentials;    super.setAuthenticated(true); //注意,这里设置为true了! must use super, as we override  }  public Object getCredentials() {    return this.credentials;  }  public Object getPrincipal() {    return this.principal;  }  public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {    if (isAuthenticated) {      throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");    }    super.setAuthenticated(false);  }  @Override  public void eraseCredentials() {    super.eraseCredentials();    credentials = null;  }}

到这里基本就完成了spring security的设置,


         4)最后就是jsp页面部分,页面主要是表单的action地址必须为:path+/j_spring_security_check,其中path是项目路径,然后登录的时候就可以使用security模块了!

         至于文章中可能涉及到的一些bean或者service,这里就不贴代码了,毕竟项目较为完善,涉及东西太多,另外每个项目业务逻辑也不一样。


         在整个框架整合过程中,遇到了很多奇怪的问题,很多是因为版本不一致引起的,建议用maven进行jar等依赖包的统一管理。

        下面是spring security3.2的reference 手册:http://docs.spring.io/spring-security/site/docs/3.2.9.RELEASE/reference/htmlsingle/



















0 1
原创粉丝点击