1. 程式人生 > >SpringMVC + security模組 框架整合詳解

SpringMVC + security模組 框架整合詳解

      最近整理一個二手後臺管理專案,整體大體可分為 : 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等依賴包的統一管理。