開發與維運

軟件測試實踐乾貨 | 測試登錄功能的思路與原理解析(基於 Spring Security)

本文為霍格沃茲測試學院優秀學員測試開發學習筆記,進階學習文末加群。

登錄功能對軟件測試工程師可能是最常見卻是最重要,也是最容易被忽視的測試場景。本文整理一些經驗豐富的測試工程師總結的測試用例,並結合 Java Spring Security 框架來簡單說下登錄的測試方向思路和部分原理,供大家交流探討。

登錄測試方向

功能測試(基礎)

  1. 輸入已註冊的用戶名和正確的密碼,驗證是否登錄成功;
  2. 輸入已註冊的用戶名和不正確的密碼,驗證是否登錄失敗,並且提示信息正確;
  3. 輸入未註冊的用戶名和任意密碼,驗證是否登錄失敗,並且提示信息正確;
  4. 用戶名和密碼兩者都為空,驗證是否登錄失敗,並且提示信息正確;
  5. 用戶名和密碼兩者之一為空,驗證是否登錄失敗,並且提示信息正確;
  6. 如果登錄功能啟用了驗證碼功能,在用戶名和密碼正確的前提下,輸入正確的驗證碼,驗證是否登錄成功;
  7. 如果登錄功能啟用了驗證碼功能,在用戶名和密碼正確的前提下,輸入錯誤的驗證碼,驗證是否登錄失敗,並且提示信息正確。

功能測試(深入)

1.用戶名和密碼是否大小寫敏感;
2.頁面上的密碼框是否加密顯示;
3.後臺系統創建的用戶第一次登錄成功時,是否提示修改密碼;
4.忘記用戶名和忘記密碼的功能是否可用;
5.前端頁面是否根據設計要求限制用戶名和密碼長度;
6.如果登錄功能需要驗證碼,點擊驗證碼圖片是否可以更換驗證碼,更換後的驗證碼是否可用;
7.刷新頁面是否會刷新驗證碼;
8.如果驗證碼具有時效性,需要分別驗證時效內和時效外驗證碼的有效性;
9.用戶登錄成功但是會話超時後,繼續操作是否會重定向到用戶登錄界面;
10.不同級別的用戶,比如管理員用戶和普通用戶,登錄系統後的權限是否正確;
11.頁面默認焦點是否定位在用戶名的輸入框中;
12.快捷鍵 Tab 和 Enter 等,是否可以正常使用。

安全測試

1.用戶密碼後臺存儲是否加密;
2.用戶密碼在網絡傳輸過程中是否加密;
3.密碼是否具有有效期,密碼有效期到期後,是否提示需要修改密碼;
4.不登錄的情況下,在瀏覽器中直接輸入登錄後的 URL 地址,驗證是否會重新定向到用戶登錄界面;
5.密碼輸入框是否不支持複製和粘貼;
6.密碼輸入框內輸入的密碼是否都可以在頁面源碼模式下被查看;
7.用戶名和密碼的輸入框中分別輸入典型的“SQL 注入攻擊”字符串,驗證系統的返回頁面;
8.用戶名和密碼的輸入框中分別輸入典型的“XSS 跨站腳本攻擊”字符串,驗證系統行為是否被篡改;
9.連續多次登錄失敗情況下,系統是否會阻止後續的嘗試以應對暴力破解;
10.同一用戶在同一終端的多種瀏覽器上登錄,驗證登錄功能的互斥性是否符合設計預期;
11.同一用戶先後在多臺終端的瀏覽器上登錄,驗證登錄是否具有互斥性。

性能壓力測試

1.單用戶登錄的響應時間是否小於 3 秒;
2.單用戶登錄時,後臺請求數量是否過多;
3.高併發場景下用戶登錄的響應時間是否小於 5 秒;
4.高併發場景下服務端的監控指標是否符合預期;
5.高集合點併發場景下,是否存在資源死鎖和不合理的資源等待;
6.長時間大量用戶連續登錄和登出,服務器端是否存在內存洩漏。

兼容性測試

1.不同瀏覽器下,驗證登錄頁面的顯示以及功能正確性;
2.相同瀏覽器的不同版本下,驗證登錄頁面的顯示以及功能正確性;
3.不同移動設備終端的不同瀏覽器下,驗證登錄頁面的顯示以及功能正確性;
4.不同分辨率的界面下,驗證登錄頁面的顯示以及功能正確性。

Spring Security簡介

Spring Security 是一個能夠為基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC,DI(控制反轉 Inversion of Control ,DI:Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複代碼的工作。

Java Web工程——登錄

配置文件

1、在 Maven 工程的 Pom.xml 文件中添加 Spring Security 的依賴

   <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>4.1.0.RELEASE</version>
  </dependency>
  <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>4.1.0.RELEASE</version>
  </dependency>

2、配置 web.xml 文件

     <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-security.xml</param-value>
   </context-param>
   <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>

3、創建 index.html
4、創建 spring 配置文件 spring-security.xml

    <!-- 以下頁面不被攔截 -->
    <http pattern="/login.html" security="none"></http>
    <http pattern="/css/**" security="none"></http>
    <http pattern="/img/**" security="none"></http>
    <http pattern="/js/**" security="none"></http>
    <http pattern="/plugins/**" security="none"></http>

    <!-- 頁面攔截規則 -->
    <http use-expressions="false">
        <intercept-url pattern="/*" access="ROLE_ADMIN" />
        <form-login login-page="/login.html"  default-target-url="/admin/index.html" authentication-failure-url="/login.html" always-use-default-target="true"/>    
        <csrf disabled="true"/>
        <headers>
            <frame-options policy="SAMEORIGIN"/>
        </headers>
    </http>

    <beans:bean id="bcryptEncoder"  
    class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

    <!-- 認證管理器 -->
<authentication-manager>
    <authentication-provider user-service-ref="userDetailService">          
    </authentication-provider>  
</authentication-manager>
<beans:bean id="userDetailService"
   class="com.pinyougou.service.UserDetailServiceImpl">
 </beans:bean>     

測試點提取:

測試點(1)

這裡設置了不被攔截的頁面,就表示在所有這些頁面的訪問過程中使不需要攜帶登錄信息的,直接輸入URL即可;所以在測試的過程中要注意頁面的區分,分別測試。

參考測試用例:不登錄的情況下,在瀏覽器中直接輸入登錄後的 URL 地址,驗證是否會重新定向到用戶登錄界面;

測試點(2)

這隻設置了用戶登錄的權限攔截規則,規定了登錄後跳轉的頁面

參考測試用例:不同級別的用戶,比如管理員用戶和普通用戶,登錄系統後的權限是否正確

測試點(3)

這裡使用了 Spring Security 的一個認證類,使用認證類調用服務UserDetails,對登錄的用戶進行認證校驗,判斷用戶是否為合法登錄用戶;結合後端代碼來看:

  public class UserDetailsServiceImpl implements UserDetailsService {
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();  
          grantedAuths.add(new SimpleGrantedAuthority("ROLE_SELLER"));          
          return new User(username,"123456", grantedAuths);
      }
  }

如果按照上述的寫法和配置,則用戶在輸入密碼123456時就會通過,無論用戶名為什麼,在開發階段可能為了某些功能的方便測試驗證而使用這種寫法,為防止測試環境或生產環境的代碼忘記修改,此場景也需要測試。(具體的通用密碼、賬號或驗證碼之類的可諮詢相關開發人員)

參考測試用例:參考上述功能測試用例

若後端代碼和配置這樣寫:

/**
 * 認證類
 * @author Administrator
 *
 */
public class UserDetailsServiceImpl implements UserDetailsService {
    private SellerService sellerService;
    public void setSellerService(SellerService sellerService) {
        this.sellerService = sellerService;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("經過了UserDetailsServiceImpl");
        //構建角色列表
        List<GrantedAuthority> grantAuths=new ArrayList();
        grantAuths.add(new SimpleGrantedAuthority("ROLE_SELLER"));
        //得到商家對象
        TbSeller seller = sellerService.findOne(username);
        if(seller!=null){
            if(seller.getStatus().equals("1")){
                return new User(username,seller.getPassword(),grantAuths);
            }else{
                return null;
            }           
        }else{
            return null;
        }
    }
}

修改spring-security.xml ,添加如下配置

  <!-- 引用dubbo 服務 -->    
    <dubbo:application name="shop-web" />
    <dubbo:registry address="zookeeper://192.168.25.129:2181"/>
    <dubbo:reference id="sellerService"  interface="com.pinyougou.sellergoods.service.SellerService" >
    </dubbo:reference>
    <beans:bean id="userDetailService" class="com.pinyougou.service.UserDetailsServiceImpl">
        <beans:property name="sellerService" ref="sellerService"></bean:property>
    </beans:bean>

經過上述修改後,在登陸頁輸入用戶名和密碼與數據庫一致即可登陸

參考測試用例:參考上述功能測試用例

密碼加密

用戶表的密碼通常使用 MD5 等不可逆算法加密後存儲,為防止彩虹表破解更會先使用一個特定的字符串(如域名)加密,然後再使用一個隨機的 salt(鹽值)加密。特定字符串是程序代碼中固定的,salt 是每個密碼單獨隨機,一般給用戶表加一個字段單獨存儲,比較麻煩。BCrypt 算法將 salt 隨機並混入最終加密後的密碼,驗證時也無需單獨提供之前的 salt,從而無需單獨處理 salt 問題。

我們在日常測試中除了要關注功能外,還要關注軟件的安全性,可能我們很多人並不是專業的安全測試工程師,但是一般的測試點還是要保證覆蓋的

後端部分代碼和配置文件:

@RequestMapping("/add")
public Result add(@RequestBody TbSeller seller){
    //密碼加密
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String password = passwordEncoder.encode(seller.getPassword());
    seller.setPassword(password);

spring-security.xml ,添加如下配置

  <beans:bean id="bcryptEncoder"  
            class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

修改認證管理器的配置

<!-- 認證管理器 -->
<authentication-manager alias="authenticationManager">  
    <authentication-provider user-service-ref='userDetailService'>   
          <password-encoder ref="bcryptEncoder"></password-encoder>           
    </authentication-provider>  
</authentication-manager> 

這裡我們可以看一下密碼在數據庫的顯示結果:
image.png

我們可以看到很明顯的區別,未加密的密碼直接暴露,會帶來賬戶安全隱患;而使用MD5和BCrypt加密的密碼要更為安全;理論上MD5也是不可逆的密碼,無法被破解,但是因為MD5在相同的密碼下生成的加密字符串是固定的,所以在大數據技術下可以建立數據庫將常用密碼進行一一對應存儲的方法來進行破解;相對比BCrypt加鹽的方式,BCrypt加密就更為安全的多了。

參考測試用例:參考上述安全測試

最後,準備好登錄界面 login.html。

以上,本文主要用作自己的工作總結,有不對和不足的地方請大家多指正。

更多技術文章分享及測試資料點此獲取

Leave a Reply

Your email address will not be published. Required fields are marked *