12.1 前端代码
前端代码包括login.jsp文件提取的HTML代码和index.js,由于index.css没有改动,不做评审。
12.1.1申请前端代码评审
请对下面前端代码进行评审
1)login.jsp文件提取的HTML代码
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../css/register.css">
<script src="../js/crypto-js.min.js" type="text/javascript"></script>
<script src="../js/core.js" type="text/javascript"></script>
<script src="../js/sha256.js" type="text/javascript"></script>
<script src="../js/index.js" type="text/javascript"></script>
<title>登录页面</title>
</head>
<body>
<div class="container">
<h2>登录</h2>
<form id="loginForm" name="loginForm" method="POST" action="login.jsp" onsubmit="return validateLoginForm();">
<input type="hidden" id="csrftoken" name="csrftoken" value="ckK8E1SnIKqVj4No6odSVJyhDgwBHJopwBBvDrbfp6TkkwChw9CuhBrhZUBUtZ6hYroTuvpl0TkECQ5vqrIQ2SqtqLLTatzBk70d">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" placeholder="输入账号" required>
<div id="usernameError" class="error"></div>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="输入密码" required>
<div id="passwordError" class="error"></div>
</div>
<button type="submit">登录</button>
</form>
<div id="loginError" class="error">
</div>
</div>
</body>
</html>
2)index.js
function validateForm() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const phone = document.getElementById('phone').value;
const email = document.getElementById('email').value;
let isValid = true;
clearErrorMessagesRegister(); // 清空所有错误信息
const usernamePattern = /^[a-zA-Z0-9]{5,20}$/;
if (!usernamePattern.test(username)) {
showError('usernameError', '账号必须是5-20位的字母或数字');
isValid = false;
}
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/;
if (!passwordPattern.test(password)) {
showError('passwordError', '密码必须包含大小写字母、数字和特殊字符,长度在5-30之间');
isValid = false;
}
if (password !== confirmPassword) {
showError('confirmPasswordError', '密码确认不一致');
isValid = false;
}
const phonePattern = /^1[3-9]\d{9}$/;
if (!phonePattern.test(phone)) {
showError('phoneError', '请输入有效的中国手机号');
isValid = false;
}
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailPattern.test(email)) {
showError('emailError', '请输入有效的Email地址');
isValid = false;
}
if (isValid) {
// 获取密码并进行SHA-256哈希
let passwordField = document.forms["myForm"]["password"];
let hashedPassword = CryptoJS.SHA256(password).toString(CryptoJS.enc.Base64);
// 将哈希后的密码重新赋值给表单的密码字段
passwordField.value = hashedPassword;
console.log('加密后的密码:', hashedPassword);
console.log('注册成功!');
}
return isValid;
}
function validateLoginForm() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
let isValid = true;
clearErrorMessagesLogin(); // 清空所有错误信息
const usernamePattern = /^[a-zA-Z0-9]{5,20}$/;
if (!usernamePattern.test(username)) {
showError('usernameError', '账号必须是5-20位的字母或数字');
isValid = false;
}
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/;
if (!passwordPattern.test(password)) {
showError('passwordError', '密码必须包含大小写字母、数字和特殊字符,长度在5-30之间');
isValid = false;
}
if (isValid) {
// 获取密码并进行SHA-256哈希
let passwordField = document.forms["loginForm"]["password"];
let hashedPassword = CryptoJS.SHA256(password).toString(CryptoJS.enc.Base64);
// 将哈希后的密码重新赋值给表单的密码字段
passwordField.value = hashedPassword;
console.log('登录成功!');
}
return isValid;
}
//用于显示错误信息
function showError(elementId, message) {
document.getElementById(elementId).innerText = message;
}
//用于清空登录所有错误信息
function clearErrorMessagesRegister() {
document.getElementById('usernameError').innerText = '';
document.getElementById('passwordError').innerText = '';
document.getElementById('confirmPasswordError').innerText = '';
document.getElementById('phoneError').innerText = '';
document.getElementById('emailError').innerText = '';
}
//用于清空所有错误信息
function clearErrorMessagesLogin() {
document.getElementById('usernameError').innerText = '';
document.getElementById('passwordError').innerText = '';
}
12.1.2前端代码评审回复
1)login.jsp文件提取的HTML代码
优点:
l结构清晰:HTML 结构简洁明了,表单字段和错误提示清晰可见,便于用户理解。
l使用了 CSRF 令牌:通过隐藏字段传递 CSRF 令牌,增强了安全性。
l表单验证:通过 JavaScript 进行前端验证,提升了用户体验。
改进建议:
lCSS和JS文件路径:相对路径可能在不同环境下导致加载失败,建议使用绝对路径或确保路径正确。
l表单提交:表单的action属性指向login.jsp,这可能导致表单提交后页面重新加载。可以考虑将其指向一个处理登录请求的Servlet。
l错误信息展示:当前错误信息的显示方式较为简单,可以考虑使用更友好的提示框或样式来增强用户体验。
2)index.js 文件评审
优点:
l输入验证:提供了多种输入验证,确保用户输入符合预期格式,增强了安全性。
l使用CryptoJS进行密码加密:在客户端对密码进行SHA-256加密,增加了密码的安全性。
改进建议:
l密码哈希的安全性:虽然在客户端进行 SHA-256 加密可以提供一定的安全性,但仍然建议在服务器端进行密码的加密和验证。客户端的加密可能会被攻击者截获,因此应考虑使用 HTTPS。
l重复代码:validateForm和validateLoginForm函数中有许多重复的逻辑(如用户名和密码的验证)。可以考虑将这些逻辑提取为一个共享的验证函数,以减少代码重复。
l未使用的变量:在validateForm中,confirmPassword、phone 和 email 的验证逻辑在登录表单中并不需要,建议将这些代码移除以减少混乱。
l日志信息:在控制台中输出的成功信息(如“注册成功!”)可能会引起误解,建议更改为“登录成功!”以匹配实际操作。
index.js做如下修改
function CheckField(Pattern, value, field, message, isValid) {
if (!Pattern.test(value)) {
showError(field, message);
isValid = false; // 修改 isValid
}
return isValid; // 返回 isValid
}
function hash256(isValid, passwordField, message) {
if (isValid) {
let password = passwordField.value; // 从密码字段获取值
let hashedPassword = CryptoJS.SHA256(password).toString(CryptoJS.enc.Base64);
passwordField.value = hashedPassword;
}
return isValid;
}
function check(username, password, confirmPassword, phone, email) {
let isValid = true;
clearErrorMessages("reg"); // 清空所有错误信息
const usernamePattern = /^[a-zA-Z0-9]{5,20}$/;
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/;
const phonePattern = /^1[3-9]\d{9}$/;
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
isValid = CheckField(usernamePattern, username, 'usernameError', '账号必须是5-20位的字母或数字', isValid);
isValid = CheckField(passwordPattern, password, 'passwordError', '密码必须包含大小写字母、数字和特殊字符,长度在5-30之间', isValid);
isValid = CheckField(phonePattern, phone, 'phoneError', '请输入有效的中国手机号', isValid);
isValid = CheckField(emailPattern, email, 'emailError', '请输入有效的Email地址', isValid);
if (password !== confirmPassword) {
document.getElementById('confirmPasswordError').innerText = '密码确认不一致';
isValid = false;
}
return hash256(isValid, document.forms["myForm"]["password"], '注册成功!');
}
function checkLogin(username, password) {
let isValid = true;
clearErrorMessages("log"); // 清空所有错误信息
const usernamePattern = /^[a-zA-Z0-9]{5,20}$/;
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{5,30}$/;
isValid = CheckField(usernamePattern, username, 'usernameError', '账号必须是5-20位的字母或数字', isValid);
isValid = CheckField(passwordPattern, password, 'passwordError', '密码必须包含大小写字母、数字和特殊字符,长度在5-30之间', isValid);
return hash256(isValid, document.forms["loginForm"]["password"], '登录成功!');
}
function validateRegisterForm() {
const username = document.getElementById('username') ? document.getElementById('username').value : '';
const password = document.getElementById('password') ? document.getElementById('password').value : '';
const confirmPassword = document.getElementById('confirmPassword') ? document.getElementById('confirmPassword').value : '';
const phone = document.getElementById('phone') ? document.getElementById('phone').value : '';
const email = document.getElementById('email') ? document.getElementById('email').value : '';
return check(username, password, confirmPassword, phone, email);
}
function validateLoginForm() {
const username = document.getElementById('username') ? document.getElementById('username').value : '';
const password = document.getElementById('password') ? document.getElementById('password').value : '';
return checkLogin(username, password); // 调用正确的函数
}
//用于显示错误信息
function showError(elementId, message) {
document.getElementById(elementId).innerText = message;
}
//用于清空注册所有错误信息
function clearErrorMessages(sign) {
if (sign === "reg") {
document.getElementById('usernameError').innerText = '';
document.getElementById('passwordError').innerText = '';
document.getElementById('confirmPasswordError').innerText = '';
document.getElementById('phoneError').innerText = '';
document.getElementById('emailError').innerText = '';
} else if (sign === "log") {
document.getElementById('usernameError').innerText = '';
document.getElementById('passwordError').innerText = '';
} else {
console.log("sig for clearErrorMessages is error");
}
}
总体评审
l用户体验:整体上,前端代码在用户体验方面表现良好,表单清晰且易于理解,但在错误处理和提示方面可以进一步优化。
l安全性:在密码处理上使用了加密,增强了安全性,但仍需注意客户端加密的局限性,建议在服务器端进行更严格的处理。
l代码可维护性:代码结构合理,但存在重复逻辑,建议提取公共方法以提高可维护性。
结论
整体来看,前端代码功能良好,结构清晰,但在安全性、代码重复性和用户体验方面仍有提升空间。通过优化这些方面,可以提升代码的可维护性和用户体验。
12.2后端代码
到目前为止共有以下后端产品代码更改过:
register.jsp、login.jsp、welcome.jsp、logout.jsp以及CookiesManager.java、DBuser.java、login.java、register.java、User.java。validateUser.java和Util.javam没有发生变化。
12.2.1 jsp文件进行代码评审
1)申请jsp文件进行代码评审
对下面jsp文件进行评审。
① register.jsp
<%@ page contentType="text/html; charset=utf-8" %>
<%@ page import="java.sql.*, ebusiness.DBuser, ebusiness.validateUser, ebusiness.Util, ebusiness.User,ebusiness.CookiesManager,ebusiness.register" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册界面</title>
<link rel="stylesheet" type="text/css" href="../css/index.css"/>
<script src="../js/index.js" type="text/javascript"></script>
<script src="../js/crypto-js.min.js" type="text/javascript"></script>
<script src="../js/core.js" type="text/javascript"></script>
<script src="../js/sha256.js" type="text/javascript"></script>
<script src="../js/index.js" type="text/javascript"></script>
</head>
<body>
<%
Util util = new Util();
String csrftoken=util.generateRandomString(100);
%>
<div class="container">
<h2>用户注册</h2>
<form name="myForm" method="POST" action="register.jsp" onsubmit="return validateForm();">
<input type="hidden" id="csrftoken" name="csrftoken" value="<%=csrftoken%>">
<label for="username">账号 (5-20位字母或数字):</label>
<input type="text" id="username" name="username" placeholder="输入账号" required>
<div id="usernameError" class="error"></div>
<label for="password">密码 (5-30位,包含大小写字母、数字和特殊字符):</label>
<input type="password" id="password" name="password" placeholder="输入密码" required>
<div id="passwordError" class="error"></div>
<label for="confirmPassword">密码确认:</label>
<input type="password" id="confirmPassword" name="confirmPassword" placeholder="输入确认密码" required>
<div id="confirmPasswordError" class="error"></div>
<label for="phone">手机号 (中国):</label>
<input type="tel" id="phone" name="phone" placeholder="输入手机号" required>
<div id="phoneError" class="error"></div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" placeholder="输入邮箱" required>
<div id="emailError" class="error"></div>
<button type="submit">注册</button>
</form>
<div id="registerError" class="error">
<%
CookiesManager ct = new CookiesManager("csrftoken",csrftoken, response,request);
String method = request.getMethod();
if ("get".equalsIgnoreCase(method)) {
ct.setCookie();
}else if("post".equalsIgnoreCase(method)){
String csrftoken_post = request.getParameter("csrftoken");
String csrftoken_cookies = ct.getCookie();
ct.setCookie();
if((csrftoken_cookies.equals(csrftoken_post))&&(csrftoken_cookies!=null)&&(csrftoken_cookies.length() > 0)){
register myregister = new register(response,request);
%>
<%= myregister.myRegister()%>
<%
}
}
%>
</div>
</div>
</body>
</html>
②login.jsp
<%@ page contentType="text/html; charset=utf-8" %>
<%@ page import="java.sql.*, ebusiness.DBuser, ebusiness.validateUser, ebusiness.Util, ebusiness.User,ebusiness.CookiesManager,ebusiness.login" %>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../css/index.css">
<script src="../js/crypto-js.min.js" type="text/javascript"></script>
<script src="../js/core.js" type="text/javascript"></script>
<script src="../js/sha256.js" type="text/javascript"></script>
<script src="../js/index.js" type="text/javascript"></script>
<title>登录页面</title>
</head>
<body>
<%
Util util = new Util();
String csrftoken=util.generateRandomString(100);
%>
<div class="container">
<h2>登录</h2>
<form id="loginForm" name="loginForm" method="POST" action="login.jsp" onsubmit="return validateLoginForm();">
<input type="hidden" id="csrftoken" name="csrftoken" value="<%=csrftoken%>">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" placeholder="输入账号" required>
<div id="usernameError" class="error"></div>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" placeholder="输入密码" required>
<div id="passwordError" class="error"></div>
</div>
<button type="submit">登录</button>
</form>
<div id="loginError" class="error">
<%
CookiesManager ct = new CookiesManager("csrftoken",csrftoken, response,request);
String method = request.getMethod();
if ("get".equalsIgnoreCase(method)) {
ct.setCookie();
}else if("post".equalsIgnoreCase(method)){
String csrftoken_post = request.getParameter("csrftoken");
String csrftoken_cookies = ct.getCookie();
ct.setCookie();
if((csrftoken_cookies.equals(csrftoken_post))&&(csrftoken_cookies!=null)&&(csrftoken_cookies.length() > 0)){
login mylogin = new login(response,request);
%>
<%= mylogin.myLogin()%>
<%
}
}
%>
</div>
</div>
</body>
</html>
③ welcome.jsp
<%@ page contentType="text/html; charset=utf-8" %>
<%@ page import="ebusiness.CookiesManager" %>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../css/index.css">
<script src="../js/index.js" type="text/javascript"></script>
<title>系统欢迎您</title>
</head>
<body>
<%
CookiesManager myusername = new CookiesManager("username",response,request);
String currentusername=myusername.getCookie();
if (currentusername==null){
response.sendRedirect("login.jsp");
}else{
%>
<h1>系统欢迎您</h1>
欢迎<%=currentusername%>进入XXX系统
<a href="logout.jsp">登出</a>
<%
}
%>
</body>
</html>
④ logout.jsp
<%
Cookie expiredCookie = new Cookie("username", "");
expiredCookie.setMaxAge(0); // 设置为立即过期
expiredCookie.setPath("/"); // 确保路径一致,以便被正确清除
response.addCookie(expiredCookie);
response.sendRedirect("login.jsp");
%>
2) jsp文件进行代码评审回复
对这四个 JSP 文件的评审主要集中在代码的可读性、安全性、结构性和维护性。以下是对每个文件的具体评审意见:
① register.jsp
优点:
l使用了CSRF令牌来防止跨站请求伪造攻击,增强了安全性。
l表单验证通过JavaScript实现,提升了用户体验。
改进建议:
l代码结构:尽量将业务逻辑(如用户注册处理)移到 Servlet 中,而不是在 JSP 中处理。JSP 应该主要负责视图层,而不是业务逻辑。
l避免重复代码:在register.jsp和login.jsp中,获取CSRF令牌和Cookie的逻辑重复。可以考虑将这些逻辑提取到一个公共的Java类中。
l错误处理:如果注册失败,应该在页面上提供用户友好的错误信息,而不仅仅是返回 myregister.myRegister()的结果。可以使用一个更友好的方式来展示错误信息。
l输入验证:虽然有JavaScript验证,但也应该在服务器端进行验证,以防止恶意用户绕过客户端验证。
② login.jsp
优点:
l类似于register.jsp,使用了CSRF令牌。
l表单结构清晰,易于理解。
改进建议:
l同样的代码重复:与register.jsp中的建议相同,考虑提取公共逻辑。
l错误信息展示:如果登录失败,应该提供具体的错误信息,而不仅仅是调用 mylogin.myLogin()。可以考虑使用一个状态变量来显示错误信息。
l安全性:确保密码在传输过程中是安全的,考虑使用 HTTPS。
③ welcome.jsp
优点:
l简洁明了,能够有效地检查用户是否已登录。
改进建议:
l重定向逻辑:可以考虑使用HttpServletResponse的sendRedirect方法更好地处理重定向逻辑,避免在JSP中直接使用response.sendRedirect。
l用户体验:可以考虑在用户未登录时,提供一个更友好的提示信息,而不仅仅是重定向。
④ logout.jsp
优点:
l逻辑简单明了,能够有效地清除 Cookie 并重定向。
改进建议:
l安全性:在注销时,除了清除Cookie之外,还可以考虑使CSRF令牌失效,以增强安全性。
l用户反馈:可以考虑在注销后提供一个用户反馈,例如“您已成功注销”。
总体评审
l安全性:整体上,代码中考虑了一些安全措施(如CSRF令牌),但仍需确保在服务器端进行充分的输入验证和错误处理。
l代码重用性:建议将重复的逻辑提取到公共类或方法中,以提高代码的可维护性。
l用户体验:在表单处理和错误反馈方面,可以进一步增强用户体验,提供更友好的提示和反馈信息。
综上所述,这些 JSP 文件在功能上是有效的,但在代码结构和安全性方面还有改进的空间。希望这些建议能帮助你提升代码的质量和可维护性!
12.2.2 Java文件进行代码评审
1)申请Java文件进行代码评审
对下面Java文件进行评审。
①CookiesManager.java
package ebusiness;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
public class CookiesManager {
private HttpServletResponse response;
private HttpServletRequest request;
private String cookies_name;
private String cookies_value;
// 构造函数,接受 cookies 和 response 对象
public CookiesManager(String cookies_name,String cookies_value, HttpServletResponse response,HttpServletRequest request) {
this.cookies_name = cookies_name;
this.cookies_value = cookies_value;
this.response = response;
this.request = request;
}
public CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request) {
this.cookies_name = cookies_name;
this.response = response;
this.request = request;
}
// 设置cookies的方法
public void setCookie() {
if (this.cookies_value != null && !this.cookies_value.isEmpty()) {
// 创建一个新的 Cookie
Cookie myCookie = new Cookie(this.cookies_name, this.cookies_value);
// 设置 Cookie 的有效期为 1 天(单位是秒)
myCookie.setMaxAge(60 * 60 * 24);
// 设置 Cookie 的路径
myCookie.setPath("/");
// 如果使用 HTTPS,确保 Cookie 只通过安全连接发送
myCookie.setSecure(true);
// 防止客户端脚本访问 Cookie
myCookie.setHttpOnly(true);
// 将 Cookie 添加到响应中
response.addCookie(myCookie);
} else {
// 处理cookies为空的情况
System.out.println("cookie is null or empty.");
}
}
// 获取cookies的方法
public String getCookie() {
Cookie[] cookies = this.request.getCookies(); // 获取所有的cookies
if (cookies != null) {
for (Cookie cookie : cookies) {
String name = cookie.getName(); // 获取cookie的名称
String value = cookie.getValue(); // 获取cookie的值
if (name.equals(this.cookies_name) &&(value!=null)||(value.equals(""))){
this.cookies_value=value;
}
}
}
return this.cookies_value;
}
}
② DBuser.java
package ebusiness;
import java.sql.*;
public class DBuser {
private static final String url = "jdbc:mysql://localhost:3306/ebusiness";
private static final String user = "root";
private static final String password = "123456";
private static Statement stmt = null;
private static ResultSet rs;
public Connection connect() throws SQLException {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
return conn;
} catch (ClassNotFoundException e) {
System.out.println("JDBC Driver not found: " + e.getMessage());
} catch (SQLException e) {
System.out.println("Database connection failed: " + e.getMessage());
}
return null;
}
public void disconnect(Connection conn) {
try {
if (rs != null) {
rs.close();
}
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public String insert(Connection conn, User user) throws SQLException {
if (!userExists(conn, user.getUsername(), user.getEmail(), user.getPhone()).equals("0")) {
return userExists(conn, user.getUsername(), user.getEmail(), user.getPhone());
}
String insertString = "INSERT INTO user (username, password, phone, email) VALUES (?, ?, ?, ?)";
try {
PreparedStatement stmt = conn.prepareStatement(insertString);
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getPassword());
stmt.setString(3, user.getPhone());
stmt.setString(4, user.getEmail());
int affectedRows = stmt.executeUpdate();
return String.valueOf(affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
return "-1";
}
public String userExists(Connection conn, String username, String email, String phone) throws SQLException {
if (!judgeInfo(conn,"username",username).equals("0"))
return "注册用户的用户名必须唯一";
else if(!judgeInfo(conn,"phone",phone).equals("0"))
return "注册用户的手机必须唯一";
else if(!judgeInfo(conn,"email",email).equals("0"))
return "注册用户的邮箱必须唯一";
else
return "0";
}
public String judgeInfo(Connection conn, String field,String value) throws SQLException {
String queryString = "SELECT * FROM user WHERE "+field+" = ?";
try {
PreparedStatement stmt = conn.prepareStatement(queryString);
stmt.setString(1, value);
ResultSet rs = stmt.executeQuery();
if(rs.next())
return field;
} catch (SQLException e) {
e.printStackTrace();
}
return "0";
}
public String login(Connection conn, User user) throws SQLException {
String username=user.getUsername();
String password=user.getPassword();
PreparedStatement stmt = null;
ResultSet rs = null;
try {
String queryString = "SELECT * FROM user WHERE username=? AND password=?";
stmt = conn.prepareStatement(queryString);
stmt.setString(1, username);
stmt.setString(2, password);
rs = stmt.executeQuery();
if (!rs.next()) {
return "0";
} else {
return "1";
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return "-1";
}
}
③ login.java
package ebusiness;
import java.sql.Connection;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
public class login {
private HttpServletResponse response;
private HttpServletRequest request;
public login(HttpServletResponse response,HttpServletRequest request) {
this.response = response;
this.request = request;
}
public String myLogin() {
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = new User(username, password);
validateUser ValidateUser = new validateUser();
String validationResult = ValidateUser.validate(user);
if (validationResult.equals("验证成功")) {
try {
DBuser mysql = new DBuser();
Connection conn = mysql.connect();
if (mysql.login(conn, user).equals("0")){
return "用户名或密码错误";
}else{
CookiesManager myusername = new CookiesManager("username",username, response,request);
myusername.setCookie();
response.sendRedirect("welcome.jsp");
}
mysql.disconnect(conn);
} catch (Exception e) {
e.printStackTrace();
}
}else{
return validationResult;
}
return "0";
}
}
④ register.java
package ebusiness;
import java.sql.Connection;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
public class register {
private HttpServletResponse response;
private HttpServletRequest request;
public register(HttpServletResponse response,HttpServletRequest request) {
this.response = response;
this.request = request;
}
public String myRegister() {
String username = this.request.getParameter("username");
String password = this.request.getParameter("password");
String phone = this.request.getParameter("phone");
String email = this.request.getParameter("email");
User user = new User(username,password,phone,email);
validateUser ValidateUser = new validateUser();
String validationResult = ValidateUser.validate(user);
if (!validationResult.equals("验证成功")) {
return validationResult;
}else {
try {
DBuser mysql = new DBuser();
Connection conn = mysql.connect();
String info = mysql.insert(conn, user);
if (info.equals("1")) {
this.response.sendRedirect("login.jsp");
} else {
return info;
}
mysql.disconnect(conn);
} catch (Exception e) {
return "数据库错误: " + e.getMessage();
}
}
return "0";
}
}
⑤ User.java
package ebusiness;
public class User {
private int id;
private String username;
private String password;
private String phone;
private String email;
public User(String username,String password) {
this.username = username;
this.password = password;
this.phone = "13611112222";
this.email = "jerry@126.com";
}
public User(String username,String password,String phone,String email) {
this.username = username;
this.password = password;
this.phone = phone;
this.email = email;
}
// Getters and Setters
public int getId() {
return id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
2)Java文件进行代码评审回复
①CookiesManager.java
优点:
l提供了setCookie和getCookie方法,封装了 Cookie 操作,方便调用。
l使用了HttpOnly和Secure属性,提高了 Cookie 的安全性,防止JavaScript访问和在非 HTTPS 连接下传输。
改进建议:
lcookies_value的空检查:在getCookie方法中,对于value != null和value.equals("")的判断逻辑不太清晰,可以改为if (value == null || value.isEmpty())来明确判断空值情况。
if (name.equals(this.cookies_name) &&(value!=null)||(value.equals(""))){
改为
if (name.equals(this.cookies_name) && (value == null || value.isEmpty())){
l日志记录:System.out.println用于记录Cookie操作的异常信息,这在生产环境中不适用。建议使用日志框架(如Log4j、SLF4J来记录错误信息。
询问ChatGPT如何为使用Log4j,ChatGPT回答:
log4j 是一个常用的 Java 日志记录库,它可以帮助你在应用程序中记录日志、输出信息和进行调试。以下是如何使用 log4j 记录日志的基本步骤:
1. 添加 log4j 依赖
首先,你需要将 log4j 添加到项目的依赖中。假设你使用的是 Maven,可以在 pom.xml 文件中添加以下依赖:
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version> <!-- 或者使用其他版本 -->
</dependency>
</dependencies>
如果你使用 Gradle,则在 build.gradle 中添加:
dependencies {
implementation 'log4j:log4j:1.2.17' // 或者其他版本
}
2. 配置 log4j(log4j.properties 或 log4j.xml)
log4j 需要一个配置文件来指定日志输出的格式、级别和输出位置。你可以选择使用 log4j.properties 或 log4j.xml 文件。
使用 log4j.properties 配置文件
在你的 src/main/resources 目录下创建一个 log4j.properties 文件,内容示例:
# 设置日志级别
log4j.rootLogger=DEBUG, console, file
# 设置控制台输出
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c - %m%n
# 设置文件输出
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=logs/app.log
log4j.appender.file.Append=true
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c - %m%n
使用 log4j.xml 配置文件
或者,你可以使用 XML 配置文件来设置日志。创建 log4j.xml 文件并放在 src/main/resources 目录下:
<?xml version="1.0" encoding="UTF-8"?>
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<!-- 设置根日志级别 -->
<root>
<level value="DEBUG" />
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
<!-- 设置控制台输出 -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{ISO8601} %-5p [%t] %c - %m%n" />
</layout>
</appender>
<!-- 设置文件输出 -->
<appender name="file" class="org.apache.log4j.FileAppender">
<param name="File" value="logs/app.log" />
<param name="Append" value="true" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{ISO8601} %-5p [%t] %c - %m%n" />
</layout>
</appender>
</log4j:configuration>
3. 在代码中使用 log4j 记录日志
一旦你添加了 log4j 配置文件并将依赖加入项目中,你可以在 Java 代码中使用 log4j 记录日志。
示例代码:
import org.apache.log4j.Logger;
public class MyApp {
// 创建日志记录器
private static final Logger logger = Logger.getLogger(MyApp.class);
public static void main(String[] args) {
// 记录不同级别的日志
logger.debug("这是调试日志");
logger.info("这是信息日志");
logger.warn("这是警告日志");
logger.error("这是错误日志");
logger.fatal("这是致命错误日志");
// 模拟一个异常
try {
int result = 10 / 0;
} catch (Exception e) {
logger.error("发生异常: ", e);
}
}
}
4. 日志输出
在控制台:日志会根据配置输出到控制台(console appender)。
在文件:日志也会被写入到指定的日志文件中(file appender),例如 logs/app.log。
5. 日志级别
log4j 提供了不同的日志级别,按优先级从低到高排列如下:
TRACE:最细粒度的日志记录,用于程序的微小事件。
DEBUG:调试信息,通常在开发过程中使用。
INFO:常规的信息日志,用于记录应用程序的运行情况。
WARN:警告日志,用于表示潜在的或非致命的问题。
ERROR:错误日志,用于记录运行时错误。
FATAL:严重错误日志,通常表示程序不能继续运行。
6. 其他配置选项
日志文件滚动:你可以配置日志文件自动滚动,避免文件过大。你可以使用 RollingFileAppender 来实现日志文件的滚动。
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=logs/app.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c - %m%n
以上配置会使日志文件的大小限制为 10MB,并且保留最多 5 个旧的日志文件。
总结
通过以上步骤,你可以使用 log4j 在 Java 应用中记录不同级别的日志。配置文件允许你灵活控制日志的输出位置、格式和级别,使得开发和维护过程更加高效。如果你使用的是 log4j 2.x,配置和用法会有所不同,但大体思路类似。
根据回复
在pom.xml配置
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
在资源目录(如src/main/resources)下创建Log4j配置文件log4j.properties。
# 设置日志级别
log4j.rootLogger=DEBUG, console, file
# 设置控制台输出
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c - %m%n
# 设置文件输出
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=logs/app.log
log4j.appender.file.Append=true
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c - %m%n
在CookiesManager.java中使用Log4j记录日志:
import org.apache.log4j.Logger;
public class CookiesManager {
private static final Logger logger = Logger.getLogger(CookiesManager.class);
…
// 处理cookies为空的情况
logger.info("cookie is null or empty.");
如果使用Tomcat作为WebServer,可以询问ChatGPT,如何在Tomcat中使用Log4j。结果与上面基本类似,在这里不再做描述。
l多余的构造函数:构造函数CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request) 没有设置cookies_value,建议在没有值的情况下将其设为 null,或在构造函数中明确设定默认值。
修改构造函数CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request),在里面加入:this.cookies_value = null;,即:
public CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request) {
this.cookies_name = cookies_name;
this.cookies_value = null;
this.response = response;
this.request = request;
}
② DBuser.java
优点:
l提供了连接数据库、插入数据、检查用户是否存在等功能,封装了数据库操作,功能模块清晰。
l使用了PreparedStatement来避免 SQL 注入,确保查询安全。
改进建议:
l资源管理:stmt和rs是静态变量,可能导致在多线程环境下资源共享问题。建议将它们的声明修改为方法级变量,避免静态持有连接。
改为:
private Statement stmt = null;
private ResultSet rs;
lSQL异常处理:在捕获 SQL 异常时,可以将异常信息记录到日志中,而不仅仅是 e.printStackTrace(),这样可以帮助开发者在生产环境中进行调试。
在DBuser.java中加入
…
import org.apache.log4j.Logger;
…
public Connection connect() throws SQLException {
…
logger.error("JDBC Driver not found: " + e.getMessage());
} catch (SQLException e) {
logger.error("JDBC Driver not found: " + e.getMessage());
}
…
l数据库连接关闭:mysql.disconnect(conn) 关闭连接的代码应该放在finally块中,确保即使在数据库操作过程中发生异常时也能正确释放资源。
在register.java和login.java中做如下修改
public String myRegister() throws SQLException {
…
if (!validationResult.equals("验证成功")) {
return validationResult;
}else {
DBuser mysql = new DBuser();
Connection conn = mysql.connect();
try {
String info = mysql.insert(conn, user);
if (info.equals("1")) {
this.response.sendRedirect("login.jsp");
} else {
return info;
}
} catch (Exception e) {
return "数据库错误: " + e.getMessage();
}finally {
mysql.disconnect(conn);
}
}
return "0";
}
public String myLogin() throws SQLException {
if (validationResult.equals("验证成功")) {
DBuser mysql = new DBuser();
Connection conn = mysql.connect();
try {
if (mysql.login(conn, user).equals("0")){
return "用户名或密码错误";
}else{
CookiesManager myusername = new CookiesManager("username",username, response,request);
myusername.setCookie();
response.sendRedirect("welcome.jsp");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
mysql.disconnect(conn);
}
}else{
return validationResult;
}
return "0";
}
修改后需要修改LoginTest.java。
@Test
public void testSuccessfulLogin() throws IOException, SQLException {
…
}
@Test
public void testFailedLogin() throws SQLException {
…
}
和
RegisterTest.java
@Test
public void testSuccessfulRegistration() throws IOException, SQLException {
…
}
@Test
public void testFailedRegistration() throws SQLException {
…
}
③ login.java
优点:
l清晰的流程控制,使用了validateUser类进行用户验证。
l登录成功后,使用CookiesManager设置用户的登录信息(如用户名 Cookie),并跳转到 welcome.jsp。
改进建议:
l返回值处理:myLogin() 方法返回的字符串用于显示错误信息。可以考虑使用 response 直接返回状态码或重定向,避免在业务逻辑中处理视图显示逻辑。
l异常处理:在catch块中仅调用e.printStackTrace(),应当记录详细日志并返回适当的错误信息,而不是简单打印堆栈信息。
l重复代码:建议将数据库连接部分提取为公共方法,以减少代码重复,提高可维护性。
④ register.java
优点:
l逻辑清晰,注册成功后跳转到登录页面。
l使用了validateUser对用户输入进行验证。
改进建议:
l返回值处理:myRegister() 方法的返回值是一个字符串。可以考虑通过抛出异常来处理错误,并在外层捕获异常以返回用户友好的错误消息。
l数据库连接和断开:和login.java类似,数据库连接的管理应当在finally块中进行,确保连接的关闭即使发生异常也能执行。
前面已处理。
l异常处理:对数据库错误的捕获有些简单,建议提供更详细的错误信息或者记录错误日志。
⑤ User.java
优点:
l提供了getters和setters,简洁明了,便于后续扩展。
改进建议:
l构造函数默认值:在构造函数中为phone和email提供默认值 13611112222 和 jerry@126.com 可能是不合适的,特别是在创建新用户时,应该确保这些信息被传入并且是有效的。
lID 属性:id 是User类的一个字段,但它没有在构造函数中初始化,应该在构造时传入或通过数据库自动生成。
总体评审
l代码结构和模块化:整体上,代码结构清晰,将数据库操作、用户验证和 Cookie 管理进行了良好的分离。每个类都有明确的职责。
l异常处理和日志记录:代码中的异常处理相对简单,建议增强异常捕获、日志记录和错误反馈机制,以便在生产环境中能更好地调试和追踪问题。
l安全性:数据库操作和 Cookie 管理考虑到了安全性,但建议对密码的存储和验证(例如使用加密算法)做更多安全措施。
l性能优化:数据库连接在每个请求中都被创建和关闭,考虑使用连接池来优化性能,避免频繁的连接创建与销毁。
总结:
代码结构良好,功能清晰,但在异常处理、安全性、日志记录等方面有进一步优化的空间。可以通过将数据库连接和其他重复逻辑提取到服务层、使用日志框架来提高代码的健壮性和可维护性。
注意:修改所有代码后,请运行所有测试用例,以保证没有引入新的缺陷。 |
---|
12.3 测试代码
测试代码Test_Register.py没有发生变动,Test_Login.py更新过,另外还产生了Test_register_GUI.py和Test_login_GUI.py 两个测试程序。还有一个作为测试用例用到的公共方法的文件Util.py。
在评审之前,先修改Util.py
去掉login_for_name()和login_for_password()两个方法,合并为:
#为登录初始化数据
def init_login(self,username,password):
connection = self.initDB()
try:
with connection.cursor() as cursor:
if username == "":
username = "validUser"
else:
username = username
if password == "":
password = HASH.hash_password("ValidPassword123!")
else:
password = HASH.hash_password(password)
# SQL语句,用于清空user表
sql = "INSERT INTO user (username, password, phone, email) VALUES (\""+username+"\",\""+password+"\", \"13611223344\", \"a@b.com\")"
# 执行SQL语句
cursor.execute(sql)
# 提交事务
connection.commit()
finally:
self.closedb(connection)
在test_Login.py和Test_login_GUI.py中,修改
self.db.init_login()
为
self.db.init_login(username="",password="")
在test_Login.py中,修改
self.db.login_for_name("validUser12345678900")
…
self.db.login_for_name("user1")
…
self.db. login_for_password ("ValidPassword1234567890123456!")
…
self.db. login_for_password("P@ss1")
为
self.db.init_login(username="validUser12345678900",password="")
…
self.db.init_login(username="user1",password="")
…
self.db.init_login(username="",password="ValidPassword1234567890123456!")
…
self.db.init_login(username="",password="P@ss1")
12.3.1申请测试代码评审
请对下面测试代码进行评审。
1)Test_Login.py
import requests
import unittest
from Util import ClassDB
from Util import CSRF
from Util import HASH
#全局URL
BASE_URL = "http://127.0.0.1:8080/CharGPTEbusiness/jsp/login.jsp" # 替换为实际的登录API URL
WELCOME_URL = "http://127.0.0.1:8080/CharGPTEbusiness/jsp/welcome.jsp" # 替换为实际的受保护页面URL
class TestLoginAPI(unittest.TestCase):
def setUp(self):
self.session = requests.Session()
response = self.session.get(BASE_URL)
self.csrf = CSRF.get_csrf(response.text)
self.token={"csrftoken":self.csrf}
self.db = ClassDB()
self.db.init_login(username="",password="")
def tearDown(self):
self.db.init_db()
def test_valid_login(self):
"""TC-001:验证有效用户名和密码登录"""
self.token['username'] = "validUser"
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("系统欢迎您", response.text)
def test_invalid_username(self):
"""TC-002:验证无效用户名登录"""
self.token['username'] = "invalidUser"
response = requests.post(BASE_URL,
data={
'csrftoken':self.csrf,
'username': "invalidUser",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("用户名或密码错误", response.text)
def test_invalid_password(self):
"""TC-003,验证无效密码登录"""
self.token['username'] = "validUser"
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser",
'password': HASH.hash_password("wrongPassword")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("用户名或密码错误", response.text)
def test_empty_username(self):
"""TC-004,验证空用户名登录"""
self.token['username'] = ""
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "",
'password':HASH. hash_password("validPassword")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn(" ", response.text)
def test_empty_password(self):
"""TC-005,验证空密码登录"""
self.token['username'] = "validUser"
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser",
'password': HASH.hash_password("")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("用户名或密码错误", response.text)
def test_username_max_length(self):
"""TC-008,验证用户名最大长度"""
self.token['username'] = "validUser12345678900"
self.db.init_login(username="validUser12345678900",password="")
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser12345678900",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("系统欢迎您", response.text)
def test_username_min_length(self):
"""TC-009,验证用户名最小长度"""
self.token['username'] = "user1"
self.db.init_login(username="user1",password="")
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "user1",
'password':HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("系统欢迎您", response.text)
def test_password_max_length(self):
"""TC-010,验证密码最大长度"""
self.token['username'] = "validUser"
self.db.init_login(username="",password="ValidPassword1234567890123456!")
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser",
'password': HASH.hash_password("ValidPassword1234567890123456!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("系统欢迎您", response.text)
def test_password_min_length(self):
"""TC-011,验证密码最小长度"""
self.token['username'] = "validUser"
self.db.init_login(username="",password="P@ss1")
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "validUser",
'password': HASH.hash_password("P@ss1")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("系统欢迎您", response.text)
def test_xss_injection(self):
"""TC-012,验证XSS注入"""
self.token['username'] = " <> alert('XSS') "
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': " <> alert('XSS1') ",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("账号必须是5-20位字母或数字", response.text)
def test_csrf_protection(self):
"""TC-013,验证CSRF保护"""
# 假设CSRF令牌是必需的,这里我们不提供CSRF令牌
self.token['username'] = "validUser"
response = requests.post(BASE_URL, data={
'username': "validUser",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn(" ", response.text)
def test_sql_injection(self):
"""TC-014,验证SQL注入"""
self.token['username'] = "admin' --"
response = requests.post(BASE_URL, data={
'csrftoken':self.csrf,
'username': "admin' --",
'password': HASH.hash_password("ValidPassword123!")
},cookies=self.token)
self.assertEqual(response.status_code, 200)
self.assertIn("账号必须是5-20位字母或数字", response.text)
if __name__ == "__main__":
unittest.main()
2)Test_register_GUI.py
import pytest
from playwright.sync_api import Page
from Util import ClassDB
import time
REGISTER_URL = 'http://127.0.0.1:8080/CharGPTEbusiness/jsp/register.jsp'
# 测试类
class TestUserRegistration:
def teardown(self):
self.db = ClassDB()
self.db.init_db()
# 测试用例
@pytest.mark.parametrize("username, password, repassword, phone, email, expected_message", [
("validUser123", "ValidPass123!", "ValidPass123!","13800138000","test@example.com","登录"), #GTC1-账号有效性测试
("testUser", "Aa1@", "Aa1@","13800138000","test@example.com","密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"), #GTC2 - 密码长度不足测试
("testUser", "iLongpassword12@Longpassword12@", "iLongpassword12@Longpassword12@","13800138000","test@example.com","密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"), #GTC3 - 密码长度超出测试
("testUser", "ValidPass123!", "validPass123!","13800138000","test@example.com","密码确认不一致"), #GTC4 - 输入密码与确认密码确保不一致性
("testUser", "ValidPass123!", "ValidPass123!","12345","test@example.com","请输入有效的中国手机号"), ##GTC5 - 手机号格式错误测试
])
def test_Login(self,page: Page,username, password, repassword, phone, email, expected_message):
page.goto(REGISTER_URL)
page.reload()
page.fill('input[name="username"]', username)
page.fill('input[name="password"]', password)
page.fill('input[name="confirmPassword"]', repassword)
page.fill('input[name="phone"]', phone)
page.fill('input[name="email"]', email)
page.click('button[type="submit"]')
# 检查注册是否成功(这里需要根据实际页面反馈调整)
if expected_message == "登录":
assert page.get_by_role("heading", name="登录")
else:
assert page.locator('text='+expected_message).is_visible()
# 测试用例
@pytest.mark.parametrize("username, password, repassword, phone, email, username_1,phone_1,email_1,expected_message", [
("duplicateUser", "ValidPass123!", "ValidPass123!","13800138000","duplicate@example.com","duplicateUser","13800138001","duplicate2@example.com","注册用户的用户名必须唯一"), #GTC6 – 一个用户重复注册两次。
("duplicateUser3", "ValidPass123!", "ValidPass123!","13800138003","duplicate1@example.com","duplicateUser4","13800138003","duplicate1@example.com","注册用户的手机必须唯一"), #GTC7 – – 不同用户注册,手机号重复
("duplicateUser1", "ValidPass123!", "ValidPass123!","13800138004","duplicate3@example.com","duplicateUser4","13800138005","duplicate3@example.com","注册用户的邮箱必须唯一"), #GTC8 – 不同用户注册,邮件重复
])
def test_duplicate(self,page: Page,username, password, repassword, phone, email, username_1,phone_1,email_1,expected_message):
page.goto(REGISTER_URL)
page.reload()
# 第一次注册
page.fill('input[name="username"]', username)
page.fill('input[name="password"]', password)
page.fill('input[name="confirmPassword"]', repassword)
page.fill('input[name="phone"]', phone)
page.fill('input[name="email"]', email)
page.click('button[type="submit"]')
# 检查注册成功
assert page.get_by_role("heading", name="登录")
# 第二次注册相同用户
page.goto(REGISTER_URL)
page.reload()
page.fill('input[name="username"]', username_1)
page.fill('input[name="password"]', password)
page.fill('input[name="confirmPassword"]', repassword)
page.fill('input[name="phone"]', phone_1)
page.fill('input[name="email"]', email_1)
page.click('button[type="submit"]')
# 检查错误信息
assert page.locator('text='+expected_message).is_visible()
if __name__ == '__main__':
pytest.main(["-sv", "Test_register_GUI.py"])
3)Test_login_GUI.py
import pytest
from playwright.sync_api import Page
from Util import ClassDB
import time
LOGIN_URL = 'http://127.0.0.1:8080/CharGPTEbusiness/jsp/login.jsp'
WELCOME_URL ='http://127.0.0.1:8080/CharGPTEbusiness/jsp/welcome.jsp'
# 测试类
class TestUserLogin:
def setup_class(self):
self.db = ClassDB()
self.db.init_login(username="",password="")
def teardown_class(self):
self.db.init_db()
# 测试用例
@pytest.mark.parametrize("username, password, expected_message", [
("validUser", "ValidPassword123!", "系统欢迎您"), # TC-001
("invalidUser", "ValidPassword123!", "用户名或密码错误"), # TC-002
("a", "ValidPassword123!", "账号必须是5-20位的字母或数字"), # TC-006
("validUser", "123", "密码必须包含大小写字母、数字和特殊字符,长度在5-30之间"), # TC-007
])
def test_login(self,page: Page,username, password, expected_message):
page.goto(LOGIN_URL)
page.reload()
# 填充用户名和密码
page.get_by_placeholder("输入账号").fill(username)
page.get_by_placeholder("输入密码").fill(password)
page.get_by_role("button", name="登录").click()
# 等待页面加载并验证消息
if expected_message == "系统欢迎您":
page.wait_for_selector("h1") # 等待成功消息
else:
page.wait_for_selector("h2") # 等待登录错误
if username == "validUser":
if password != "123":
assert page.locator("h1").inner_text() == expected_message
else:
assert expected_message in page.locator("#passwordError").inner_text()
elif username == "invalidUser":
assert expected_message in page.locator("#loginError").inner_text()
elif username == "a":
assert expected_message in page.locator("#usernameError").inner_text()
else:
assert 0==1
def test_access_protected_page_after_logout(self,page: Page):
# 登录
page.goto(LOGIN_URL)
page.reload()
page.get_by_placeholder("输入账号").fill("validUser")
page.get_by_placeholder("输入密码").fill("ValidPassword123!")
page.get_by_role("button", name="登录").click()
page.wait_for_selector("h1") # 等待成功消息
page.click("body > a") # 替换为实际的注销按钮选择器
# 尝试访问受保护的页面
page.goto(WELCOME_URL) # 替换为欢迎页面的真实路径
# 验证重定向回登录页面
assert page.url == LOGIN_URL # 替换为登录页面的真实路径
if __name__ == '__main__':
pytest.main(["-sv", "Test_login_GUI.py"])
4)Util.py
import pymysql
import re
import hashlib
class ClassDB:
#初始化数据库连接
def initDB(self):
host = 'localhost'
user = 'root'
password = '123456'
database = 'ebusiness'
# 创建数据库连接
connection = pymysql.connect(host=host, user=user, password=password, database=database)
return connection
# 关闭数据库连接
def closedb(self,connection):
connection.close()
# 清空数据库user表中数据,用于DearDown
def init_db(self):
connection = self.initDB()
try:
with connection.cursor() as cursor:
# SQL语句,用于清空user表
sql = "TRUNCATE TABLE user"
# 执行SQL语句
cursor.execute(sql)
# 提交事务
connection.commit()
finally:
self.closedb(connection)
#为登录初始化数据
def init_login(self,username,password):
connection = self.initDB()
try:
with connection.cursor() as cursor:
if username == "":
username = "validUser"
else:
username = username
if password == "":
password = HASH.hash_password("ValidPassword123!")
else:
password = HASH.hash_password(password)
# SQL语句,用于清空user表
sql = "INSERT INTO user (username, password, phone, email) VALUES (\""+username+"\",\""+password+"\", \"13611223344\", \"a@b.com\")"
# 执行SQL语句
cursor.execute(sql)
# 提交事务
connection.commit()
finally:
self.closedb(connection)
class CSRF:
def get_csrf(html):
pattern = r'input[^>]+value="([^"]+)"'
# 使用 re.search 查找匹配
match = re.search(pattern, html)
# 提取并打印 value 值
if match:
csrftoken_value = match.group(1)
return csrftoken_value
else:
return "未找到 csrftoken"
class HASH:
#对字符串仅更新SHA256散列
def hash_password(password):
"""对密码进行SHA256散列"""
return hashlib.sha256(password.encode('utf-8')).hexdigest()
12.3.2测试评审回复
下面是对您提供的测试代码的评审,包括 Test_Login.py、Test_register_GUI.py、Test_login_GUI.py 和 Util.py 的分析和建议。
1)Test_Login.py
优点:
l结构清晰:使用unittest框架,组织良好,便于理解。
l覆盖面广:测试了有效和无效的登录情况,包括用户名和密码的边界条件。
l安全性测试:包含了XSS、CSRF 和SQL注入的测试用例,关注安全性。
建议:
lURL 配置:将 BASE_URL 和 WELCOME_URL 提取到配置文件或环境变量中,以便于在不同环境中使用。
l数据库操作:在setUp和tearDown中的数据库初始化和清理逻辑可以考虑使用上下文管理器,以确保数据库连接的正确关闭。
l测试数据:可以考虑使用工厂模式或假数据生成库(如 Faker)来生成测试数据,以提高测试的灵活性和可维护性。
l状态码验证:在一些测试用例中,response.status_code 的验证可以根据 API 的实际设计进行调整,例如,可能需要验证 401 或 403 状态码。
2)Test_register_GUI.py
优点:
l使用 pytest:利用 pytest 的参数化特性,简化了测试用例的编写。
l清晰的测试逻辑:每个测试用例都有明确的目的,便于理解。
建议:
lteardown 方法:建议将teardown方法重命名为teardown_class,以便更好地遵循 pytest的命名约定。
将teardown方法重命名为teardown_class。
l测试用例的描述:在参数化测试中,考虑使用更具描述性的测试用例名称,以便在测试失败时更容易理解问题。
l错误信息验证:在验证错误信息时,可以考虑使用更具体的选择器,确保测试的稳定性。
3)Test_login_GUI.py
优点:
l结构清晰:与Test_register_GUI.py 类似,使用pytest和参数化测试。
l涵盖基本用例:测试了有效和无效的登录情况。
建议:
lsetup_class和teardown_class:同样,建议将teardown方法重命名为 teardown_class 以符合pytest的约定。
l页面等待:在测试中使用 wait_for_selector 是个好主意,但可以考虑使用更具体的等待条件,以提高测试的可靠性。
l重复代码:test_login 方法中存在一些重复代码,考虑将登录逻辑提取到一个单独的方法中,以提高代码的复用性。
4)Util.py
优点:
l功能模块化:将数据库操作、CSRF 处理和密码哈希功能分开,增强了代码的可读性和可维护性。
建议:
lSQL注入风险:在init_login方法中,使用字符串拼接构建 SQL 查询可能导致 SQL 注入风险。建议使用参数化查询来避免此问题。
测试代码,不考虑SQL注入。
l异常处理:在数据库操作中,考虑添加异常处理,以便在数据库连接失败或查询出错时能够捕获并处理异常。
l代码注释:尽管代码相对清晰,但适当的注释可以帮助其他开发者更快理解代码逻辑,尤其是在复杂的逻辑部分。
总结
整体来看,您的测试代码结构清晰,覆盖面广,关注了安全性测试。通过一些小的改进,可以提高代码的可维护性和安全性。继续保持良好的测试习惯,并考虑使用一些现代化的工具和库来简化测试工作。