12.1 前端代码
前端代码包括login.jsp文件提取的HTML代码和index.js,由于index.css没有改动,不做评审。
12.1.1申请前端代码评审
请对下面前端代码进行评审
1)login.jsp文件提取的HTML代码
代码语言:javascript
复制
<!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
代码语言:javascript
复制
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代码
优点:
结构清晰:HTML 结构简洁明了,表单字段和错误提示清晰可见,便于用户理解。
使用了 CSRF 令牌:通过隐藏字段传递 CSRF 令牌,增强了安全性。
表单验证:通过 JavaScript 进行前端验证,提升了用户体验。
改进建议:
CSS和JS文件路径:相对路径可能在不同环境下导致加载失败,建议使用绝对路径或确保路径正确。
表单提交:表单的action属性指向login.jsp,这可能导致表单提交后页面重新加载。可以考虑将其指向一个处理登录请求的Servlet。
错误信息展示:当前错误信息的显示方式较为简单,可以考虑使用更友好的提示框或样式来增强用户体验。
2)index.js 文件评审
优点:
输入验证:提供了多种输入验证,确保用户输入符合预期格式,增强了安全性。
使用CryptoJS进行密码加密:在客户端对密码进行SHA-256加密,增加了密码的安全性。
改进建议:
密码哈希的安全性:虽然在客户端进行 SHA-256 加密可以提供一定的安全性,但仍然建议在服务器端进行密码的加密和验证。客户端的加密可能会被攻击者截获,因此应考虑使用 HTTPS。
重复代码:validateForm和validateLoginForm函数中有许多重复的逻辑(如用户名和密码的验证)。可以考虑将这些逻辑提取为一个共享的验证函数,以减少代码重复。
未使用的变量:在validateForm中,confirmPassword、phone 和 email 的验证逻辑在登录表单中并不需要,建议将这些代码移除以减少混乱。
日志信息:在控制台中输出的成功信息(如“注册成功!”)可能会引起误解,建议更改为“登录成功!”以匹配实际操作。
index.js做如下修改
代码语言:javascript
复制
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"); } }
总体评审
用户体验:整体上,前端代码在用户体验方面表现良好,表单清晰且易于理解,但在错误处理和提示方面可以进一步优化。
安全性:在密码处理上使用了加密,增强了安全性,但仍需注意客户端加密的局限性,建议在服务器端进行更严格的处理。
代码可维护性:代码结构合理,但存在重复逻辑,建议提取公共方法以提高可维护性。
结论
整体来看,前端代码功能良好,结构清晰,但在安全性、代码重复性和用户体验方面仍有提升空间。通过优化这些方面,可以提升代码的可维护性和用户体验。
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
代码语言:javascript
复制
<%@ 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
代码语言:javascript
复制
<%@ 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
代码语言:javascript
复制
<%@ 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
代码语言:javascript
复制
<% Cookie expiredCookie = new Cookie("username", ""); expiredCookie.setMaxAge(0); // 设置为立即过期 expiredCookie.setPath("/"); // 确保路径一致,以便被正确清除 response.addCookie(expiredCookie); response.sendRedirect("login.jsp"); %>
2) jsp文件进行代码评审回复
对这四个 JSP 文件的评审主要集中在代码的可读性、安全性、结构性和维护性。以下是对每个文件的具体评审意见:
① register.jsp
优点:
使用了CSRF令牌来防止跨站请求伪造攻击,增强了安全性。
表单验证通过JavaScript实现,提升了用户体验。
改进建议:
代码结构:尽量将业务逻辑(如用户注册处理)移到 Servlet 中,而不是在 JSP 中处理。JSP 应该主要负责视图层,而不是业务逻辑。
避免重复代码:在register.jsp和login.jsp中,获取CSRF令牌和Cookie的逻辑重复。可以考虑将这些逻辑提取到一个公共的Java类中。
错误处理:如果注册失败,应该在页面上提供用户友好的错误信息,而不仅仅是返回 myregister.myRegister()的结果。可以使用一个更友好的方式来展示错误信息。
输入验证:虽然有JavaScript验证,但也应该在服务器端进行验证,以防止恶意用户绕过客户端验证。
② login.jsp
优点:
类似于register.jsp,使用了CSRF令牌。
表单结构清晰,易于理解。
改进建议:
同样的代码重复:与register.jsp中的建议相同,考虑提取公共逻辑。
错误信息展示:如果登录失败,应该提供具体的错误信息,而不仅仅是调用 mylogin.myLogin()。可以考虑使用一个状态变量来显示错误信息。
安全性:确保密码在传输过程中是安全的,考虑使用 HTTPS。
③ welcome.jsp
优点:
简洁明了,能够有效地检查用户是否已登录。
改进建议:
重定向逻辑:可以考虑使用HttpServletResponse的sendRedirect方法更好地处理重定向逻辑,避免在JSP中直接使用response.sendRedirect。
用户体验:可以考虑在用户未登录时,提供一个更友好的提示信息,而不仅仅是重定向。
④ logout.jsp
优点:
逻辑简单明了,能够有效地清除 Cookie 并重定向。
改进建议:
安全性:在注销时,除了清除Cookie之外,还可以考虑使CSRF令牌失效,以增强安全性。
用户反馈:可以考虑在注销后提供一个用户反馈,例如“您已成功注销”。
总体评审
安全性:整体上,代码中考虑了一些安全措施(如CSRF令牌),但仍需确保在服务器端进行充分的输入验证和错误处理。
代码重用性:建议将重复的逻辑提取到公共类或方法中,以提高代码的可维护性。
用户体验:在表单处理和错误反馈方面,可以进一步增强用户体验,提供更友好的提示和反馈信息。
综上所述,这些 JSP 文件在功能上是有效的,但在代码结构和安全性方面还有改进的空间。希望这些建议能帮助你提升代码的质量和可维护性!
12.2.2 Java文件进行代码评审
1)申请Java文件进行代码评审
对下面Java文件进行评审。
①CookiesManager.java
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
优点:
提供了setCookie和getCookie方法,封装了 Cookie 操作,方便调用。
使用了HttpOnly和Secure属性,提高了 Cookie 的安全性,防止JavaScript访问和在非 HTTPS 连接下传输。
改进建议:
cookies_value的空检查:在getCookie方法中,对于value != null和value.equals("")的判断逻辑不太清晰,可以改为if (value == null || value.isEmpty())来明确判断空值情况。
代码语言:javascript
复制
if (name.equals(this.cookies_name) &&(value!=null)||(value.equals(""))){
改为
代码语言:javascript
复制
if (name.equals(this.cookies_name) && (value == null || value.isEmpty())){
日志记录:System.out.println用于记录Cookie操作的异常信息,这在生产环境中不适用。建议使用日志框架(如Log4j、SLF4J来记录错误信息。
询问ChatGPT如何为使用Log4j,ChatGPT回答:
log4j 是一个常用的 Java 日志记录库,它可以帮助你在应用程序中记录日志、输出信息和进行调试。以下是如何使用 log4j 记录日志的基本步骤:
1. 添加 log4j 依赖
首先,你需要将 log4j 添加到项目的依赖中。假设你使用的是 Maven,可以在 pom.xml 文件中添加以下依赖:
代码语言:javascript
复制
<dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <!-- 或者使用其他版本 --> </dependency> </dependencies>
如果你使用 Gradle,则在 build.gradle 中添加:
代码语言:javascript
复制
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 文件,内容示例:
代码语言:javascript
复制
# 设置日志级别 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 目录下:
代码语言:javascript
复制
<?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 记录日志。
示例代码:
代码语言:javascript
复制
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 来实现日志文件的滚动。
代码语言:javascript
复制
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配置
代码语言:javascript
复制
<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
在资源目录(如src/main/resources)下创建Log4j配置文件log4j.properties。
代码语言:javascript
复制
# 设置日志级别 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记录日志:
代码语言:javascript
复制
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。结果与上面基本类似,在这里不再做描述。
多余的构造函数:构造函数CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request) 没有设置cookies_value,建议在没有值的情况下将其设为 null,或在构造函数中明确设定默认值。
修改构造函数CookiesManager(String cookies_name, HttpServletResponse response,HttpServletRequest request),在里面加入:this.cookies_value = null;,即:
代码语言:javascript
复制
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
优点:
提供了连接数据库、插入数据、检查用户是否存在等功能,封装了数据库操作,功能模块清晰。
使用了PreparedStatement来避免 SQL 注入,确保查询安全。
改进建议:
资源管理:stmt和rs是静态变量,可能导致在多线程环境下资源共享问题。建议将它们的声明修改为方法级变量,避免静态持有连接。
改为:
代码语言:javascript
复制
private Statement stmt = null; private ResultSet rs;
SQL异常处理:在捕获 SQL 异常时,可以将异常信息记录到日志中,而不仅仅是 e.printStackTrace(),这样可以帮助开发者在生产环境中进行调试。
在DBuser.java中加入
代码语言:javascript
复制
… 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()); } …
数据库连接关闭:mysql.disconnect(conn) 关闭连接的代码应该放在finally块中,确保即使在数据库操作过程中发生异常时也能正确释放资源。
在register.java和login.java中做如下修改
代码语言:javascript
复制
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。
代码语言:javascript
复制
@Test public void testSuccessfulLogin() throws IOException, SQLException { … } @Test public void testFailedLogin() throws SQLException { … }
和
RegisterTest.java
代码语言:javascript
复制
@Test public void testSuccessfulRegistration() throws IOException, SQLException { … } @Test public void testFailedRegistration() throws SQLException { … }
③ login.java
优点:
清晰的流程控制,使用了validateUser类进行用户验证。
登录成功后,使用CookiesManager设置用户的登录信息(如用户名 Cookie),并跳转到 welcome.jsp。
改进建议:
返回值处理:myLogin() 方法返回的字符串用于显示错误信息。可以考虑使用 response 直接返回状态码或重定向,避免在业务逻辑中处理视图显示逻辑。
异常处理:在catch块中仅调用e.printStackTrace(),应当记录详细日志并返回适当的错误信息,而不是简单打印堆栈信息。
重复代码:建议将数据库连接部分提取为公共方法,以减少代码重复,提高可维护性。
④ register.java
优点:
逻辑清晰,注册成功后跳转到登录页面。
使用了validateUser对用户输入进行验证。
改进建议:
返回值处理:myRegister() 方法的返回值是一个字符串。可以考虑通过抛出异常来处理错误,并在外层捕获异常以返回用户友好的错误消息。
数据库连接和断开:和login.java类似,数据库连接的管理应当在finally块中进行,确保连接的关闭即使发生异常也能执行。
前面已处理。
异常处理:对数据库错误的捕获有些简单,建议提供更详细的错误信息或者记录错误日志。
⑤ User.java
优点:
提供了getters和setters,简洁明了,便于后续扩展。
改进建议:
构造函数默认值:在构造函数中为phone和email提供默认值 13611112222 和 jerry@126.com 可能是不合适的,特别是在创建新用户时,应该确保这些信息被传入并且是有效的。
ID 属性:id 是User类的一个字段,但它没有在构造函数中初始化,应该在构造时传入或通过数据库自动生成。
总体评审
代码结构和模块化:整体上,代码结构清晰,将数据库操作、用户验证和 Cookie 管理进行了良好的分离。每个类都有明确的职责。
异常处理和日志记录:代码中的异常处理相对简单,建议增强异常捕获、日志记录和错误反馈机制,以便在生产环境中能更好地调试和追踪问题。
安全性:数据库操作和 Cookie 管理考虑到了安全性,但建议对密码的存储和验证(例如使用加密算法)做更多安全措施。
性能优化:数据库连接在每个请求中都被创建和关闭,考虑使用连接池来优化性能,避免频繁的连接创建与销毁。
总结:
代码结构良好,功能清晰,但在异常处理、安全性、日志记录等方面有进一步优化的空间。可以通过将数据库连接和其他重复逻辑提取到服务层、使用日志框架来提高代码的健壮性和可维护性。
注意:修改所有代码后,请运行所有测试用例,以保证没有引入新的缺陷。 |
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()两个方法,合并为:
代码语言:javascript
复制
#为登录初始化数据 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中,修改
代码语言:javascript
复制
self.db.init_login()
为
代码语言:javascript
复制
self.db.init_login(username="",password="")
在test_Login.py中,修改
代码语言:javascript
复制
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")
为
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
代码语言:javascript
复制
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
优点:
结构清晰:使用unittest框架,组织良好,便于理解。
覆盖面广:测试了有效和无效的登录情况,包括用户名和密码的边界条件。
安全性测试:包含了XSS、CSRF 和SQL注入的测试用例,关注安全性。
建议:
URL 配置:将 BASE_URL 和 WELCOME_URL 提取到配置文件或环境变量中,以便于在不同环境中使用。
数据库操作:在setUp和tearDown中的数据库初始化和清理逻辑可以考虑使用上下文管理器,以确保数据库连接的正确关闭。
测试数据:可以考虑使用工厂模式或假数据生成库(如 Faker)来生成测试数据,以提高测试的灵活性和可维护性。
状态码验证:在一些测试用例中,response.status_code 的验证可以根据 API 的实际设计进行调整,例如,可能需要验证 401 或 403 状态码。
2)Test_register_GUI.py
优点:
使用 pytest:利用 pytest 的参数化特性,简化了测试用例的编写。
清晰的测试逻辑:每个测试用例都有明确的目的,便于理解。
建议:
teardown 方法:建议将teardown方法重命名为teardown_class,以便更好地遵循 pytest的命名约定。
将teardown方法重命名为teardown_class。
测试用例的描述:在参数化测试中,考虑使用更具描述性的测试用例名称,以便在测试失败时更容易理解问题。
错误信息验证:在验证错误信息时,可以考虑使用更具体的选择器,确保测试的稳定性。
3)Test_login_GUI.py
优点:
结构清晰:与Test_register_GUI.py 类似,使用pytest和参数化测试。
涵盖基本用例:测试了有效和无效的登录情况。
建议:
setup_class和teardown_class:同样,建议将teardown方法重命名为 teardown_class 以符合pytest的约定。
页面等待:在测试中使用 wait_for_selector 是个好主意,但可以考虑使用更具体的等待条件,以提高测试的可靠性。
重复代码:test_login 方法中存在一些重复代码,考虑将登录逻辑提取到一个单独的方法中,以提高代码的复用性。
4)Util.py
优点:
功能模块化:将数据库操作、CSRF 处理和密码哈希功能分开,增强了代码的可读性和可维护性。
建议:
SQL注入风险:在init_login方法中,使用字符串拼接构建 SQL 查询可能导致 SQL 注入风险。建议使用参数化查询来避免此问题。
测试代码,不考虑SQL注入。
异常处理:在数据库操作中,考虑添加异常处理,以便在数据库连接失败或查询出错时能够捕获并处理异常。
代码注释:尽管代码相对清晰,但适当的注释可以帮助其他开发者更快理解代码逻辑,尤其是在复杂的逻辑部分。
总结
整体来看,您的测试代码结构清晰,覆盖面广,关注了安全性测试。通过一些小的改进,可以提高代码的可维护性和安全性。继续保持良好的测试习惯,并考虑使用一些现代化的工具和库来简化测试工作。