Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
后,進入金三銀四招聘旺季,相信不少參加測試相關崗位面試的時候,會遇到設計測試的題目。用戶登錄測試屬于老生常談的問題了,如果面試時能先概述一下你的設計思路、考慮的維度,再開始逐個寫出用例的設計,應該會更有條理。
給你一個“用戶登錄”功能你會如何測試它?
設計測試用例,通常主要有 2 個大類:功能性需求和非功能性需求。
一、功能需求
功能需求其實就是軟件本身需要實現的具體功能,通常它們是這個功能的最直接體現。
在設計這種用例的時候,我們基本會用【等價類】和【邊界值】這兩種方法。
1. 用戶名和密碼都為空,驗證是否登錄失敗,并提示信息(“用戶名為空”)
2. 用戶名為空或密碼為空,驗證是否登錄失敗,并提示信息(“用戶名/密碼為空”)
3. 輸入已注冊的用戶名和正確的密碼,驗證是否登錄成功
4. 登錄成功后,鏈接能否跳轉至正確的頁面
5. 輸入已注冊的用戶名和錯誤的密碼,驗證是否登錄失敗,并提示信息(“密碼錯誤”)
6. 輸入未注冊的用戶名和隨意密碼,驗證是否登錄失敗,并提示信息(“用戶名錯誤”)
7. 如果登錄功能啟用驗證碼功能,在用戶名和密碼正確的前提下,輸入正確的驗證碼,驗證是否登錄成功
8. 如果登錄功能啟用驗證碼功能,在用戶名和密碼正確的前提下,輸入錯誤的驗證碼,驗證是否登錄失敗,并提示信息正確(“驗證碼錯誤”)
9. 如果登錄功能啟用驗證碼功能,考慮驗證碼的辨認難易程度,是否可以點擊驗證碼圖片(“換一個”),更換驗證碼
10. 記住用戶名和密碼的功能是否正確,且登錄失敗后不記錄密碼
11. 必填項為空時,功能是否正確
不合法的用戶名:空白的用戶名,不正確的用戶名,使用了字符大于用戶名的限制;正常用戶名不允許的特殊字符;系統的保留字段;
不合法的密碼:空密碼;錯誤的密碼;字符大于密碼的限制;正常密碼不允許的特殊字符;系統的保留字段。
二、非功能性需求
很多時候,僅做了功能性需求的測試覆蓋還是不夠的,因為還存在一些其他"隱藏"的需求,比如:界面UI、安全性、性能、兼容性。這些往往是決定軟件質量的關鍵因素。
這些需求往往不容易優先想到,需要仔細深入場景、設身處地的考慮才能很好地構思出來。
1、界面測試(UI Test):
1. 布局是否美觀、合理
2. 控件是否美觀、對齊
3. 界面設計風格是否統一
4. 界面文字無錯別字
2、安全性測試
考慮登錄的安全性,可能需要用到以下測試用例:
1. 用戶名和密碼的后臺存儲是否加密
2. 用戶名和密碼是否加密后,發給web服務器
3. 用戶名和密碼的驗證,應該通過服務器驗證,而不單單在客戶端用javascript驗證
4. 登錄成功后生成的cookie,是否是httponly(否則容易被腳本盜取)
5. 密碼框內輸入的密碼是否可以在頁面源碼模式下被查看
6. 密碼是否具有有效期,有效期到期后,是否提示修改密碼
7. 未登錄的情況下,在瀏覽器中直接輸入登錄后的URL地址,是否會重定向到登錄界面
8. 用戶名和密碼輸入框中分別輸入典型的“SQL注入攻擊”字符串,驗證系統的返回頁面(防止SQL注入攻擊)
9. 用戶名和密碼輸入框中分別輸入典型的“XSS跨站腳本攻擊”字符串,驗證系統行為是否被篡改(防止XSS攻擊)
10. 連續多次登錄失敗后,系統是否會阻止后續的嘗試,錯誤登錄的次數限制,以應對暴力破解(防止暴力破解)
11. 考慮很多用戶在同一終端上的登陸
12. 考慮同一用戶在不同終端登錄的互斥性
13. 同一用戶在同一終端的多種瀏覽器上登錄,驗證登錄功能的互斥性
3、 性能壓力測試
考慮到性能,可能還需要增加以下的測試用例:
1. 單用戶登錄的響應時間是否小于 3 秒
2. 單用戶登錄時,后臺請求數量是否過多
3. 高并發場景下用戶登錄的響應時間是否小于 5 秒
4. 高并發場景下服務端的監控指標是否符合預期
5. 高集合點并發場景下,是否存在資源死鎖和不合理的資源等待
6. 長時間大量用戶連續登錄和登出,服務器端是否存在內存泄漏
4、兼容性測試
1. 不同瀏覽器下,驗證登錄頁面的顯示以及功能正確性
2. 相同瀏覽器的不同版本下,驗證登錄頁面的顯示以及功能正確性
3. 不同移動終端的不同瀏覽器下,驗證登錄頁面的顯示以及功能正確性
4. 不同分辨率的界面下,驗證登錄頁面的顯示以及功能正確性
5. 不同平臺(Windows、Mac)、不同移動設備(iPhone、Andriod),登錄界面的顯示及功能是否正確
6. 不同語言環境下,登錄界面的顯示及功能是否正確 (本地化測試 Localization Test)
5、弱網測試
1、網絡延遲或者弱網或者切換網絡、斷網是否登錄正確
2、未激活或者凍結的用戶登陸
3、登錄的日志是否記錄正確
4、密碼強弱進行校驗
5、有沒有對登陸設備和地區進行檢測
三、測試的不可窮盡性
通常在實際工作中,測試由于受限于時間成本和經濟成本,是不可能去窮盡所有可能的組合的,而是采用基于風險驅動的模式,有所側重地選擇測試范圍和設計測試用例,以尋求缺陷風險和研發成本之間的平衡。
對于高質量的軟件測試,用例設計不僅需要考慮明確的功能性需求,還要涉及兼容性、安全性和性能等一系列的非功能性需求,這些非功能性需求對軟件系統的質量有著舉足輕重的作用。
載自: java大師
博客系統訪問:
采用的是layui-admin框架,文中的驗證碼內容,請參考作者之前的驗證碼功能
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<title>ds博客</title>
<div th:replace="common/link::header"></div>
<link rel="stylesheet" th:href="@{/static/layuiadmin/style/login.css}" media="all">
</head>
<body>
<div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login" style="display: none;">
<div class="layadmin-user-login-main">
<div class="layadmin-user-login-box layadmin-user-login-header">
<h2>ds博客</h2>
<p>后臺登錄</p>
</div>
<div class="layadmin-user-login-box layadmin-user-login-body layui-form">
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-username" for="LAY-user-login-username"></label>
<input type="text" name="userName" value="test" id="LAY-user-login-username" lay-verify="required" placeholder="用戶名" class="layui-input">
</div>
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-password" for="LAY-user-login-password"></label>
<input type="password" name="passWord" value="test" id="LAY-user-login-password" lay-verify="required" placeholder="密碼" class="layui-input">
</div>
<div class="layui-form-item">
<div class="layui-row">
<div class="layui-col-xs7">
<label class="layadmin-user-login-icon layui-icon layui-icon-vercode"></label>
<input type="text" name="code" lay-verify="required" placeholder="圖形驗證碼" class="layui-input">
</div>
<div class="layui-col-xs5">
<div style="margin-left: 10px;">
<img id="codeImg" class="layadmin-user-login-codeimg">
</div>
</div>
</div>
</div>
<div class="layui-form-item" style="margin-bottom: 20px;">
<input type="checkbox" name="remember-me" lay-skin="primary" title="記住密碼">
</div>
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid layui-bg-blue" lay-submit lay-filter="login">登 錄</button>
</div>
</div>
</div>
<!-- <div class="layui-trans layadmin-user-login-footer">-->
<!-- <p>版權所有 ? 2022 <a href="#" target="_blank">濟南高新開發區微本地軟件開發工作室</a> 魯ICP備20002306號-1</p>-->
<!-- </div>-->
</div>
<div th:replace="common/script::footer"></div>
<script th:inline="javascript">
layui.config({
base: '/static/layuiadmin/' //靜態資源所在路徑
}).extend({
index: 'lib/index' //主入口模塊
}).use(['index', 'user'], function(){
let $=layui.$,
form=layui.form;
// 初始化
getImgCode();
form.render();
//提交
form.on('submit(login)', function(obj) {
// 打開loading
let loading=layer.load(0, {
shade: false,
time: 2 * 1000
});
// 禁止重復點擊按鈕
$('.layui-btn').attr("disabled",true);
//請求登入接口
$.ajax({
type: 'POST',
url: ctx + '/login',
data: obj.field,
dataType: 'json',
success: function(result) {
if (result.code===200) {
layer.msg('登入成功', {
icon: 1
,time: 1000
}, function(){
window.location.href='/';
});
} else {
layer.msg(result.message);
// 刷新驗證碼
getImgCode();
// 關閉loading
layer.close(loading);
// 開啟點擊事件
$('.layui-btn').attr("disabled", false);
}
}
});
});
$("#codeImg").on('click', function() {
// 添加驗證碼
getImgCode();
});
$(document).keydown(function (e) {
if (e.keyCode===13) {
$('.layui-btn').click();
}
});
// 解決session過期跳轉到登錄頁并跳出iframe框架
$(document).ready(function () {
if (window !=top) {
top.location.href=location.href;
}
});
});
/**
* 獲取驗證碼
*/
function getImgCode() {
let url=ctx + '/getImgCode';
let xhr=new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType="blob";
xhr.onload=function() {
if (this.status===200) {
let blob=this.response;
document.getElementById("codeImg").src=window.URL.createObjectURL(blob);
}
}
xhr.send();
}
</script>
</body>
</html>
通過springsecurity的.loginProcessingUrl(“/login”)處理,處理邏輯如下:
.loginProcessingUrl("/login") 用于指定處理登錄操作的URL地址,而具體的驗證邏輯是由 Spring Security 提供的認證過濾器鏈負責的。在Spring Security中,主要的認證過程是由UsernamePasswordAuthenticationFilter來完成的。
當用戶提交登錄表單,請求到達.loginProcessingUrl("/login")配置的URL時,UsernamePasswordAuthenticationFilter會攔截這個請求,然后進行以下主要步驟:
這個整個過程是由 Spring Security 提供的默認配置完成的,通常情況下,開發者只需要配置好認證管理器、用戶信息服務(UserDetailsService),以及成功和失敗的處理器,Spring Security 就會負責處理登錄驗證的整個流程。
package com.ds.core.config;
import com.ds.blog.system.service.SysUserService;
import com.ds.core.security.CustomAccessDeniedHandler;
import com.ds.core.security.DefaultAuthenticationFailureHandler;
import com.ds.core.security.DefaultAuthenticationSuccessHandler;
import com.ds.core.security.filter.ValidateCodeFilter;
import net.bytebuddy.asm.Advice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 放過
.antMatchers("/loginPage", "/getImgCode").permitAll()
.antMatchers("/**/*.jpg", "/**/*.png", "/**/*.gif", "/**/*.jpeg").permitAll()
// 剩下的所有的地址都是需要在認證狀態下才可以訪問
.anyRequest().authenticated()
.and()
// 過濾登錄驗證碼
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 配置登錄功能
.formLogin()
.usernameParameter("userName")
.passwordParameter("passWord")
// 指定指定要的登錄頁面
.loginPage("/loginPage")
// 處理認證路徑的請求
.loginProcessingUrl("/login")
.successHandler(defaultAuthenticationSuccessHandler)
.failureHandler(defaultAuthenticationFailureHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.and()
// 登出
.logout()
.invalidateHttpSession(true)
.deleteCookies("remember-me")
.logoutUrl("/logout")
.logoutSuccessUrl("/loginPage")
.and()
.rememberMe()
// 有效期7天
.tokenValiditySeconds(3600 * 24 * 7)
// 開啟記住我功能
.rememberMeParameter("remember-me")
.and()
//禁用csrf
.csrf().disable()
// header response的X-Frame-Options屬性設置為SAMEORIGIN
.headers().frameOptions().sameOrigin()
.and()
// 配置session管理
.sessionManagement()
//session失效默認的跳轉地址
.invalidSessionUrl("/loginPage");
}
}
創建監聽器,在登錄成功的時候記錄登錄日志。
總的來說,這段代碼的作用是在用戶成功登錄后,通過監聽 Spring Security 的認證成功事件,記錄用戶的登錄日志信息,包括登錄賬號、登錄IP和登錄成功的備注。這樣可以實現登錄成功后的自定義操作,例如記錄登錄日志等。
@Slf4j
@Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {
@Autowired
private SysLoginLogService sysLoginLogService;
@Override
public void onApplicationEvent(AuthenticationSuccessEvent authenticationSuccessEvent) {
// 登錄賬號
User user=(User) authenticationSuccessEvent.getAuthentication().getPrincipal();
// 請求IP
String ip=ServletUtil.getClientIP(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(), "");
SysLoginLog sysLoginLog=new SysLoginLog();
sysLoginLog.setAccount(user.getUsername());
sysLoginLog.setLoginIp(ip);
sysLoginLog.setRemark("登錄成功");
sysLoginLogService.save(sysLoginLog);
}
}
創建監聽器,在登錄失敗的時候記錄異常登錄日志。
@Slf4j
@Component
public class AuthenticationFailureListener implements ApplicationListener<AbstractAuthenticationFailureEvent> {
@Autowired
private SysLoginLogService sysLoginLogService;
@Override
public void onApplicationEvent(AbstractAuthenticationFailureEvent abstractAuthenticationFailureEvent) {
// 登錄賬號
String username=abstractAuthenticationFailureEvent.getAuthentication().getPrincipal().toString();
// 登錄失敗原因
String message ;
if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureBadCredentialsEvent) {
//提供的憑據是錯誤的,用戶名或者密碼錯誤
message="提供的憑據是錯誤的,用戶名或者密碼錯誤";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureCredentialsExpiredEvent) {
//驗證通過,但是密碼過期
message="驗證通過,但是密碼過期";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureDisabledEvent) {
//驗證過了但是賬戶被禁用
message="驗證過了但是賬戶被禁用";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureExpiredEvent) {
//驗證通過了,但是賬號已經過期
message="驗證通過了,但是賬號已經過期";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureLockedEvent) {
//賬戶被鎖定
message="賬戶被鎖定";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureProviderNotFoundEvent) {
//配置錯誤,沒有合適的AuthenticationProvider來處理登錄驗證
message="配置錯誤";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureProxyUntrustedEvent) {
// 代理不受信任,用于Oauth、CAS這類三方驗證的情形,多屬于配置錯誤
message="代理不受信任";
} else if (abstractAuthenticationFailureEvent instanceof AuthenticationFailureServiceExceptionEvent) {
// 其他任何在AuthenticationManager中內部發生的異常都會被封裝成此類
message="內部發生的異常";
} else {
message="其他未知錯誤";
}
// 請求IP
String ip=ServletUtil.getClientIP(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(), "");
SysLoginLog sysLoginLog=new SysLoginLog();
sysLoginLog.setAccount(username);
sysLoginLog.setLoginIp(ip);
sysLoginLog.setRemark(message);
sysLoginLogService.save(sysLoginLog);
}
}
下面是一個認證成功處理器,登錄成功后,會返回響應的信息給前端
@Component
@Slf4j
public class DefaultAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("-----login in success----");
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer=response.getWriter();
writer.write(JSON.toJSONString(Result.success()));
writer.flush();
}
}
.successHandler(defaultAuthenticationSuccessHandler)
.failureHandler(defaultAuthenticationFailureHandler)
下面是一個認證成功處理器,登錄成功后,會返回響應的信息給前端
@Component
@Slf4j
public class DefaultAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("login in failure : " + exception.getMessage());
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer=response.getWriter();
String message;
if (exception instanceof BadCredentialsException) {
message="用戶名或密碼錯誤,請重試。";
writer.write(JSON.toJSONString(Result.failure(message)));
}else{
writer.write(JSON.toJSONString(Result.failure(exception.getMessage())));
}
writer.flush();
}
.successHandler(defaultAuthenticationSuccessHandler)
.failureHandler(defaultAuthenticationFailureHandler)
返回200,就代表成功,跳轉到/請求,進去index或者main頁面
if (result.code===200) {
layer.msg('登入成功', {
icon: 1
,time: 1000
}, function(){
window.location.href='/';
});
} else {
AuthenticationSuccessEvent 是 Spring Security 中用于表示用戶認證成功的事件。判斷登錄成功的主要依據是在認證過程中,用戶提供的憑據(通常是用戶名和密碼)與系統中存儲的憑據匹配。以下是判斷登錄成功的基本流程:
在上述流程中,認證成功的判斷主要是在 AuthenticationProvider 中完成的。DaoAuthenticationProvider 會檢查用戶提供的密碼與數據庫中存儲的密碼是否匹配。如果匹配,就認為認證成功。當認證成功后,后續的處理流程包括 AuthenticationSuccessHandler 的執行和 AuthenticationSuccessEvent 的發布。你可以通過監聽 AuthenticationSuccessEvent 事件來執行一些額外的自定義邏輯,例如記錄登錄日志。
解Spring Security的formLogin登錄認證模式
在本專欄之前的文章中,已經給大家介紹過Spring Security的HttpBasic模式,該模式比較簡單,只是進行了通過攜帶Http的Header進行簡單的登錄驗證,而且沒有定制的登錄頁面,所以使用場景比較窄。
對于一個完整的應用系統,與登錄驗證相關的頁面都是高度定制化的,非常美觀而且提供多種登錄方式。這就需要Spring Security支持我們自己定制登錄頁面,也就是本文給大家介紹的formLogin模式登錄認證模式。
準備工作
需求
以上就是本文介紹formLogin模式需要進行的準備工作及需求,下面我們就來實現其中的核心的登錄驗證邏輯,準備工作非常簡單請自行實現。(新建spring boot應用,登錄頁面、首頁、四個業務頁面都寫成非常簡單的html即可,不用寫實際業務和樣式。)
formLogin模式的三要素:
一般來說,使用權限認證框架的的業務系統登錄驗證邏輯是固定的,而資源訪問控制規則和用戶信息是從數據庫或其他存儲介質靈活加載的。但本文所有的用戶、資源、權限信息都是代碼配置寫死的,旨在為大家介紹formLogin認證模式,如何從數據庫加載權限認證相關信息我還會結合RBAC權限模型再寫文章的。
首先,我們要繼承WebSecurityConfigurerAdapter ,重寫configure(HttpSecurity http) 方法,該方法用來配置登錄驗證邏輯。請注意看下文代碼中的注釋信息。
實現formLogin模式基礎配置(點擊可放大)
上面的代碼分為兩部分:
這時候,我們通過瀏覽器訪問,隨便測試一個沒有訪問權限的資源,都會跳轉到login.html頁面。
在上文中,我們配置了登錄驗證及資源訪問的權限規則,我們還沒有具體的用戶,下面我們就來配置具體的用戶。重寫WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法
實現資源訪問限制的需求(點擊可放大)
這樣,我們就實現了文首提出的普通用戶只能訪問biz1(業務一)和biz2(業務二)資源的需求。那么管理員用戶可以訪問所有的資源的配置方式,你會不會呢?同樣的配方、同樣的方式、自己可以嘗試一下哦!
在我們的實際開發中,登錄頁面login.html和控制層Controller登錄驗證'/login'都必須無條件的開放。除此之外,一些靜態資源如css、js文件通常也都不需要驗證權限,我們需要將它們的訪問權限也開放出來。下面就是實現的方法:重寫WebSecurityConfigurerAdapter類的configure(WebSecurity web) 方法
靜態資源訪問(點擊可放大)
*請認真填寫需求信息,我們會在24小時內與您取得聯系。