使用Java代码开发用户名、密码+腾讯云短信验证码的双因子认证登录模块
本项目实现了一个基于双因子认证的系统登录模块,结合用户名密码验证和短信验证码验证,提高系统安全性。使用腾讯云短信API发送验证码,JWT生成令牌,Swing构建图形界面。
项目目录结构:login/src/
login/
├── src/
│ ├── config/
│ │ ├── AppConfig.java # 应用配置类
│ │ └── DatabaseConfig.java # 数据库配置类
│ ├── dao/
│ │ └── UserDao.java # 用户数据访问层
│ ├── entity/
│ │ └── User.java # 用户实体类
│ ├── service/
│ │ └── LoginService.java # 登录业务服务类
│ ├── ui/
│ │ └── LoginFrame.java # 登录窗体界面
│ ├── util/
│ │ ├── CodeGenerator.java # 验证码生成器
│ │ ├── JwtUtil.java # JWT工具类
│ │ ├── SmsCodeCache.java # 验证码缓存管理
│ │ └── TencentSmsUtil.java # 腾讯云短信工具类
│ ├── config.properties # 配置文件
│ └── Main.java # 程序入口
├── sql/
└── init.sql # 数据库初始化脚本
执行 sql/init.sql 创建表和测试数据
数据库连接信息:
init.sql中建库建表代码如下:
-- SmartShop 用户表结构
-- 如果表不存在则创建
CREATE TABLE IF NOT EXISTS s_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '密码',
phone VARCHAR(20) COMMENT '手机号',
nickname VARCHAR(50) COMMENT '昵称',
status INT DEFAULT 1 COMMENT '状态:1-正常 0-禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 插入测试用户数据
-- 密码为明文存储,实际生产环境应使用加密存储
INSERT INTO s_user (username, password, phone, nickname, status) VALUES
('admin', 'admin123', '13800138000', '管理员', 1),
('test', 'test123', '13900139000', '测试用户', 1),
('demo', 'demo123', '13700137000', '演示用户', 1)
ON DUPLICATE KEY UPDATE update_time = NOW();
-- 查询用户数据验证
SELECT * FROM s_user;
申请过程见腾讯云官网文档,大家可使用已申请好的API接口
填写腾讯云短信配置代码格式如下:
tencent.secretId=你的SecretId
tencent.secretKey=你的SecretKey
tencent.sms.appId=你的AppId
tencent.sms.signName=你的短信签名
tencent.sms.templateId=你的模板ID
编辑"src/config.properties"文件,代码如下:
db.url=jdbc:mysql://127.0.0.1:3306/smartshop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
db.username=root
db.password=root123
db.driver=com.mysql.cj.jdbc.Driver
tencent.secretId="AKID3yinhPxKTSFpDRSIUZR6lETURGtKYaR6"
tencent.secretKey="Wr1zluqb8UecP0OaaNxKVU4RcbM1f7OW"
tencent.sms.appId="1401049958"
tencent.sms.signName="河南创新源软件开发"
tencent.sms.templateId="2625625"
jwt.secret=SmartShopJWTSecretKey2024ForTwoFactorAuthentication
jwt.expiration=86400000
sms.code.expire=300000
sms.interval=60000
如果不配置腾讯云短信或配置失败,系统会以模拟模式运行,验证码会输出到控制台。
login/src/config/AppConfig.java,代码如下:
package config;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class AppConfig {
private static final Properties properties = new Properties();
private static final String CONFIG_FILE = "config.properties";
static {
loadConfig();
}
private static void loadConfig() {
try (InputStream input = AppConfig.class.getClassLoader().getResourceAsStream(CONFIG_FILE)) {
if (input == null) {
properties.setProperty("db.url", "jdbc:mysql://127.0.0.1:3306/smartshop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8");
properties.setProperty("db.username", "root");
properties.setProperty("db.password", "root123");
properties.setProperty("db.driver", "com.mysql.cj.jdbc.Driver");
properties.setProperty("tencent.secretId", "your-secret-id");
properties.setProperty("tencent.secretKey", "your-secret-key");
properties.setProperty("tencent.sms.appId", "your-app-id");
properties.setProperty("tencent.sms.signName", "your-sign-name");
properties.setProperty("tencent.sms.templateId", "your-template-id");
properties.setProperty("jwt.secret", "SmartShopJWTSecretKey2024ForTwoFactorAuthentication");
properties.setProperty("jwt.expiration", "86400000");
properties.setProperty("sms.code.expire", "300000");
properties.setProperty("sms.interval", "60000");
} else {
properties.load(input);
}
} catch (IOException e) {
throw new RuntimeException("加载配置文件失败: " + e.getMessage(), e);
}
}
public static String get(String key) {
return properties.getProperty(key);
}
public static String get(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
public static int getInt(String key, int defaultValue) {
String value = properties.getProperty(key);
if (value == null) {
return defaultValue;
}
return Integer.parseInt(value);
}
public static String getDbUrl() {
return get("db.url");
}
public static String getDbUsername() {
return get("db.username");
}
public static String getDbPassword() {
return get("db.password");
}
public static String getDbDriver() {
return get("db.driver");
}
public static String getTencentSecretId() {
return get("tencent.secretId");
}
public static String getTencentSecretKey() {
return get("tencent.secretKey");
}
public static String getTencentSmsAppId() {
return get("tencent.sms.appId");
}
public static String getTencentSmsSignName() {
return get("tencent.sms.signName");
}
public static String getTencentSmsTemplateId() {
return get("tencent.sms.templateId");
}
public static String getJwtSecret() {
return get("jwt.secret");
}
public static long getJwtExpiration() {
return getLong("jwt.expiration", 86400000L);
}
public static long getSmsCodeExpire() {
return getLong("sms.code.expire", 300000L);
}
public static long getSmsInterval() {
return getLong("sms.interval", 60000L);
}
private static long getLong(String key, long defaultValue) {
String value = properties.getProperty(key);
if (value == null) {
return defaultValue;
}
return Long.parseLong(value);
}
}
login/src/config/DatabaseConfig.java,代码如下:
package config;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConfig {
private static Connection connection = null;
static {
try {
Class.forName(AppConfig.getDbDriver());
} catch (ClassNotFoundException e) {
throw new RuntimeException("数据库驱动加载失败: " + e.getMessage(), e);
}
}
public static Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(
AppConfig.getDbUrl(),
AppConfig.getDbUsername(),
AppConfig.getDbPassword()
);
}
return connection;
}
public static void closeConnection() {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
System.err.println("关闭数据库连接失败: " + e.getMessage());
}
}
}
public static boolean testConnection() {
try (Connection conn = getConnection()) {
return conn != null && !conn.isClosed();
} catch (SQLException e) {
System.err.println("数据库连接测试失败: " + e.getMessage());
return false;
}
}
}
login/src/dao/UserDao.java,代码如下:
package dao;
import config.DatabaseConfig;
import entity.User;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserDao {
public User findByUsername(String username) {
String sql = "SELECT id, username, password, phone, nickname, status, create_time, update_time FROM s_user WHERE username = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return mapResultSetToUser(rs);
}
} catch (SQLException e) {
System.err.println("查询用户失败: " + e.getMessage());
}
return null;
}
public User findByPhone(String phone) {
String sql = "SELECT id, username, password, phone, nickname, status, create_time, update_time FROM s_user WHERE phone = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, phone);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return mapResultSetToUser(rs);
}
} catch (SQLException e) {
System.err.println("根据手机号查询用户失败: " + e.getMessage());
}
return null;
}
public User findById(Long id) {
String sql = "SELECT id, username, password, phone, nickname, status, create_time, update_time FROM s_user WHERE id = ?";
try (Connection conn = DatabaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setLong(1, id);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return mapResultSetToUser(rs);
}
} catch (SQLException e) {
System.err.println("根据ID查询用户失败: " + e.getMessage());
}
return null;
}
private User mapResultSetToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setPhone(rs.getString("phone"));
user.setNickname(rs.getString("nickname"));
user.setStatus(rs.getInt("status"));
user.setCreateTime(rs.getTimestamp("create_time"));
user.setUpdateTime(rs.getTimestamp("update_time"));
return user;
}
}
login/src/entity/User.java,代码如下:
package entity;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String password;
private String phone;
private String nickname;
private Integer status;
private Date createTime;
private Date updateTime;
public User() {}
public User(String username, String password, String phone) {
this.username = username;
this.password = password;
this.phone = phone;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = 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 getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
login/src/util/CodeGenerator.java,代码如下:
import java.util.Random;
/**
* 纯数字6位随机验证码生成(短信验证码首选)
*/
public class CodeGenerator {
private static final Random RANDOM = new Random();
public static String generateVerificationCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < 6; i++) {
int digit = (i == 0) ? random.nextInt(9) + 1 : random.nextInt(10);
code.append(digit);
}
return code.toString();
}
public static String generateVerificationCodeSimple() {
int code = (int) (Math.random() * 900000 + 100000);
return String.valueOf(code);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("Random生成:" + generateVerificationCode());
System.out.println("Math生成:" + generateVerificationCodeSimple());
System.out.println("---");
}
}
}
login/src/util/JwtUtil.java,代码如下:
package util;
import config.AppConfig;
import entity.User;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final String HEADER_ALG = "HS256";
private static final String HEADER_TYP = "JWT";
public static String generateToken(User user) {
Map header = new HashMap<>();
header.put("alg", HEADER_ALG);
header.put("typ", HEADER_TYP);
long now = System.currentTimeMillis();
long exp = now + AppConfig.getJwtExpiration();
Map payload = new HashMap<>();
payload.put("sub", user.getId());
payload.put("username", user.getUsername());
payload.put("phone", user.getPhone());
payload.put("iat", now / 1000);
payload.put("exp", exp / 1000);
String headerJson = mapToJson(header);
String payloadJson = mapToJson(payload);
String encodedHeader = base64UrlEncode(headerJson);
String encodedPayload = base64UrlEncode(payloadJson);
String signature = hmacSha256(encodedHeader + "." + encodedPayload, AppConfig.getJwtSecret());
return encodedHeader + "." + encodedPayload + "." + signature;
}
public static Map parseToken(String token) {
if (token == null || token.isEmpty()) {
return null;
}
String[] parts = token.split("\\.");
if (parts.length != 3) {
return null;
}
String expectedSignature = hmacSha256(parts[0] + "." + parts[1], AppConfig.getJwtSecret());
if (!expectedSignature.equals(parts[2])) {
return null;
}
String payloadJson = base64UrlDecode(parts[1]);
Map payload = jsonToMap(payloadJson);
if (payload == null) {
return null;
}
Long exp = getLong(payload, "exp");
if (exp != null && exp * 1000 < System.currentTimeMillis()) {
return null;
}
return payload;
}
public static boolean validateToken(String token) {
return parseToken(token) != null;
}
public static String getUsernameFromToken(String token) {
Map payload = parseToken(token);
if (payload != null) {
return (String) payload.get("username");
}
return null;
}
public static Long getUserIdFromToken(String token) {
Map payload = parseToken(token);
if (payload != null) {
Object sub = payload.get("sub");
if (sub instanceof Number) {
return ((Number) sub).longValue();
}
}
return null;
}
private static String mapToJson(Map map) {
StringBuilder json = new StringBuilder("{");
boolean first = true;
for (Map.Entry entry : map.entrySet()) {
if (!first) {
json.append(",");
}
first = false;
json.append("\"").append(entry.getKey()).append("\":");
Object value = entry.getValue();
if (value instanceof String) {
json.append("\"").append(value).append("\"");
} else if (value instanceof Number) {
json.append(value);
} else {
json.append("\"").append(value).append("\"");
}
}
json.append("}");
return json.toString();
}
private static Map jsonToMap(String json) {
Map map = new HashMap<>();
try {
json = json.trim();
if (json.startsWith("{") && json.endsWith("}")) {
json = json.substring(1, json.length() - 1);
String[] pairs = json.split(",");
for (String pair : pairs) {
String[] kv = pair.split(":");
if (kv.length == 2) {
String key = kv[0].trim().replace("\"", "");
String value = kv[1].trim();
if (value.startsWith("\"") && value.endsWith("\"")) {
map.put(key, value.substring(1, value.length() - 1));
} else {
try {
map.put(key, Long.parseLong(value));
} catch (NumberFormatException e) {
map.put(key, value);
}
}
}
}
}
return map;
} catch (Exception e) {
return null;
}
}
private static String base64UrlEncode(String data) {
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(data.getBytes(StandardCharsets.UTF_8));
}
private static String base64UrlDecode(String data) {
return new String(Base64.getUrlDecoder().decode(data), StandardCharsets.UTF_8);
}
private static String hmacSha256(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("HMAC-SHA256签名失败", e);
}
}
private static Long getLong(Map map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
}
return null;
}
}
login/src/util/SmsCodeCache.java,代码如下:
package util;
import config.AppConfig;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SmsCodeCache {
private static final Map codeMap = new ConcurrentHashMap<>();
private static final Map sendTimeMap = new ConcurrentHashMap<>();
public static class CodeInfo {
private final String code;
private final long createTime;
private final long expireTime;
private final String username;
public CodeInfo(String code, String username) {
this.code = code;
this.username = username;
this.createTime = System.currentTimeMillis();
this.expireTime = this.createTime + AppConfig.getSmsCodeExpire();
}
public String getCode() {
return code;
}
public long getCreateTime() {
return createTime;
}
public long getExpireTime() {
return expireTime;
}
public String getUsername() {
return username;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
public static boolean canSendCode(String phone) {
Long lastSendTime = sendTimeMap.get(phone);
if (lastSendTime == null) {
return true;
}
long interval = AppConfig.getSmsInterval();
return System.currentTimeMillis() - lastSendTime >= interval;
}
public static long getRemainingWaitTime(String phone) {
Long lastSendTime = sendTimeMap.get(phone);
if (lastSendTime == null) {
return 0;
}
long elapsed = System.currentTimeMillis() - lastSendTime;
long interval = AppConfig.getSmsInterval();
return Math.max(0, interval - elapsed);
}
public static void storeCode(String phone, String code, String username) {
codeMap.put(phone, new CodeInfo(code, username));
sendTimeMap.put(phone, System.currentTimeMillis());
}
public static CodeInfo getCodeInfo(String phone) {
return codeMap.get(phone);
}
public static boolean verifyCode(String phone, String code) {
CodeInfo codeInfo = codeMap.get(phone);
if (codeInfo == null) {
return false;
}
if (codeInfo.isExpired()) {
codeMap.remove(phone);
return false;
}
boolean valid = codeInfo.getCode().equals(code);
if (valid) {
codeMap.remove(phone);
}
return valid;
}
public static void removeCode(String phone) {
codeMap.remove(phone);
}
public static void clearExpired() {
codeMap.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
}
login/src/util/TencentSmsUtil.java,代码如下:
package util;
import config.AppConfig;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
public class TencentSmsUtil {
private static final String SERVICE = "sms";
private static final String HOST = "sms.tencentcloudapi.com";
private static final String REGION = "ap-guangzhou";
private static final String ACTION = "SendSms";
private static final String VERSION = "2021-01-11";
private static final String ALGORITHM = "TC3-HMAC-SHA256";
private static final String CONTENT_TYPE = "application/json";
public static boolean sendVerificationCode(String phoneNumber, String code) {
try {
String secretId = AppConfig.getTencentSecretId();
String secretKey = AppConfig.getTencentSecretKey();
String appId = AppConfig.getTencentSmsAppId();
String signName = AppConfig.getTencentSmsSignName();
String templateId = AppConfig.getTencentSmsTemplateId();
if ("your-secret-id".equals(secretId) || secretId == null || secretId.isEmpty()) {
System.out.println("[模拟短信] 手机号: " + phoneNumber + ", 验证码: " + code);
return true;
}
String formattedPhone = formatPhoneNumber(phoneNumber);
Map params = new HashMap();
params.put("PhoneNumberSet", new String[]{formattedPhone});
params.put("SmsSdkAppId", appId);
params.put("SignName", signName);
params.put("TemplateId", templateId);
params.put("TemplateParamSet", new String[]{code, "5"});
String requestBody = toJson(params);
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date(Long.parseLong(timestamp) * 1000));
String credentialScope = date + "/" + SERVICE + "/tc3_request";
String hashedRequestPayload = sha256Hex(requestBody);
String httpRequestMethod = "POST";
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:" + CONTENT_TYPE + "\n" +
"host:" + HOST + "\n";
String signedHeaders = "content-type;host";
String canonicalRequest = httpRequestMethod + "\n" +
canonicalUri + "\n" +
canonicalQueryString + "\n" +
canonicalHeaders + "\n" +
signedHeaders + "\n" +
hashedRequestPayload;
String hashedCanonicalRequest = sha256Hex(canonicalRequest);
String stringToSign = ALGORITHM + "\n" +
timestamp + "\n" +
credentialScope + "\n" +
hashedCanonicalRequest;
byte[] secretDate = hmacSha256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
byte[] secretService = hmacSha256(secretDate, SERVICE);
byte[] secretSigning = hmacSha256(secretService, "tc3_request");
String signature = bytesToHex(hmacSha256(secretSigning, stringToSign));
String authorization = ALGORITHM + " " +
"Credential=" + secretId + "/" + credentialScope + ", " +
"SignedHeaders=" + signedHeaders + ", " +
"Signature=" + signature;
HttpURLConnection conn = (HttpURLConnection) new java.net.URL("https://" + HOST).openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", CONTENT_TYPE);
conn.setRequestProperty("Host", HOST);
conn.setRequestProperty("X-TC-Action", ACTION);
conn.setRequestProperty("X-TC-Version", VERSION);
conn.setRequestProperty("X-TC-Timestamp", timestamp);
conn.setRequestProperty("X-TC-Region", REGION);
conn.setRequestProperty("Authorization", authorization);
conn.setDoOutput(true);
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
wr.write(requestBody.getBytes(StandardCharsets.UTF_8));
wr.flush();
wr.close();
int responseCode = conn.getResponseCode();
BufferedReader in;
if (responseCode >= 200 && responseCode < 300) {
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
} else {
in = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8));
}
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
String responseBody = response.toString();
System.out.println("腾讯云短信API响应: " + responseBody);
return responseBody.contains("\"SendStatusSet\"") && !responseBody.contains("\"Error\"");
} catch (Exception e) {
System.err.println("发送短信失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
private static String formatPhoneNumber(String phone) {
if (phone.startsWith("+86")) {
return phone;
}
return "+86" + phone;
}
private static String toJson(Map params) {
StringBuilder json = new StringBuilder("{");
boolean first = true;
for (Map.Entry entry : params.entrySet()) {
if (!first) {
json.append(",");
}
first = false;
json.append("\"").append(entry.getKey()).append("\":");
Object value = entry.getValue();
if (value instanceof String[]) {
json.append("[");
String[] arr = (String[]) value;
for (int i = 0; i < arr.length; i++) {
if (i > 0) json.append(",");
json.append("\"").append(arr[i]).append("\"");
}
json.append("]");
} else if (value instanceof String) {
json.append("\"").append(value).append("\"");
} else {
json.append(value);
}
}
json.append("}");
return json.toString();
}
private static String sha256Hex(String data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
}
private static byte[] hmacSha256(byte[] key, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
login/src/service/LoginService.java,代码如下:
package service;
import config.AppConfig;
import dao.UserDao;
import entity.User;
import util.CodeGenerator;
import util.JwtUtil;
import util.SmsCodeCache;
import util.TencentSmsUtil;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.regex.Pattern;
public class LoginService {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{4,20}$");
private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$");
private UserDao userDao;
public LoginService() {
this.userDao = new UserDao();
}
public static class LoginResult {
private final boolean success;
private final String message;
private final String token;
private final User user;
public LoginResult(boolean success, String message) {
this.success = success;
this.message = message;
this.token = null;
this.user = null;
}
public LoginResult(boolean success, String message, String token, User user) {
this.success = success;
this.message = message;
this.token = token;
this.user = user;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
public String getToken() {
return token;
}
public User getUser() {
return user;
}
}
public LoginResult register(String username, String password, String phone, String verificationCode) {
if (username == null || username.trim().isEmpty()) {
return new LoginResult(false, "用户名不能为空");
}
if (password == null || password.trim().isEmpty()) {
return new LoginResult(false, "密码不能为空");
}
if (phone == null || phone.trim().isEmpty()) {
return new LoginResult(false, "手机号不能为空");
}
if (verificationCode == null || verificationCode.trim().isEmpty()) {
return new LoginResult(false, "验证码不能为空");
}
if (!USERNAME_PATTERN.matcher(username).matches()) {
return new LoginResult(false, "用户名必须为4-20位字母、数字或下划线");
}
if (!PASSWORD_PATTERN.matcher(password).matches()) {
return new LoginResult(false, "密码必须至少8位,包含大小写字母和数字");
}
if (!PHONE_PATTERN.matcher(phone).matches()) {
return new LoginResult(false, "手机号格式不正确");
}
if (!SmsCodeCache.verifyCode(phone, verificationCode)) {
return new LoginResult(false, "验证码错误或已过期");
}
Connection conn = null;
try {
conn = DBUtil.getConnection();
if (conn == null) {
return new LoginResult(false, "数据库连接失败");
}
if (userDao.findByUsername(conn, username) != null) {
return new LoginResult(false, "用户名已存在");
}
if (userDao.findByPhone(conn, phone) != null) {
return new LoginResult(false, "该手机号已注册");
}
String hashedPassword = hashPassword(password);
User newUser = new User();
newUser.setUsername(username);
newUser.setPassword(hashedPassword);
newUser.setPhone(phone);
boolean success = userDao.insert(conn, newUser);
if (success) {
User user = userDao.findByUsername(conn, username);
String token = JwtUtil.generateToken(user);
return new LoginResult(true, "注册成功", token, user);
} else {
return new LoginResult(false, "注册失败");
}
} catch (Exception e) {
e.printStackTrace();
return new LoginResult(false, "注册异常: " + e.getMessage());
} finally {
DBUtil.closeConnection(conn);
}
}
public LoginResult login(String username, String password) {
if (username == null || username.trim().isEmpty()) {
return new LoginResult(false, "用户名不能为空");
}
if (password == null || password.trim().isEmpty()) {
return new LoginResult(false, "密码不能为空");
}
Connection conn = null;
try {
conn = DBUtil.getConnection();
if (conn == null) {
return new LoginResult(false, "数据库连接失败");
}
User user = userDao.findByUsername(conn, username);
if (user == null) {
return new LoginResult(false, "用户名或密码错误");
}
String hashedPassword = hashPassword(password);
if (!hashedPassword.equals(user.getPassword())) {
return new LoginResult(false, "用户名或密码错误");
}
String token = JwtUtil.generateToken(user);
return new LoginResult(true, "登录成功", token, user);
} catch (Exception e) {
e.printStackTrace();
return new LoginResult(false, "登录异常: " + e.getMessage());
} finally {
DBUtil.closeConnection(conn);
}
}
public LoginResult sendVerificationCode(String phone) {
if (phone == null || phone.trim().isEmpty()) {
return new LoginResult(false, "手机号不能为空");
}
if (!PHONE_PATTERN.matcher(phone).matches()) {
return new LoginResult(false, "手机号格式不正确");
}
if (!SmsCodeCache.canSendCode(phone)) {
long waitTime = SmsCodeCache.getRemainingWaitTime(phone) / 1000;
return new LoginResult(false, "请在" + waitTime + "秒后重试");
}
Connection conn = null;
try {
conn = DBUtil.getConnection();
if (conn == null) {
return new LoginResult(false, "数据库连接失败");
}
User user = userDao.findByPhone(conn, phone);
if (user == null) {
return new LoginResult(false, "该手机号未注册");
}
String code = CodeGenerator.generateVerificationCode();
SmsCodeCache.storeCode(phone, code, user.getUsername());
boolean sent = TencentSmsUtil.sendVerificationCode(phone, code);
if (sent) {
return new LoginResult(true, "验证码已发送");
} else {
SmsCodeCache.removeCode(phone);
return new LoginResult(false, "验证码发送失败");
}
} catch (Exception e) {
e.printStackTrace();
return new LoginResult(false, "发送异常: " + e.getMessage());
} finally {
DBUtil.closeConnection(conn);
}
}
public LoginResult smsLogin(String phone, String verificationCode) {
if (phone == null || phone.trim().isEmpty()) {
return new LoginResult(false, "手机号不能为空");
}
if (verificationCode == null || verificationCode.trim().isEmpty()) {
return new LoginResult(false, "验证码不能为空");
}
if (!PHONE_PATTERN.matcher(phone).matches()) {
return new LoginResult(false, "手机号格式不正确");
}
if (!SmsCodeCache.verifyCode(phone, verificationCode)) {
return new LoginResult(false, "验证码错误或已过期");
}
Connection conn = null;
try {
conn = DBUtil.getConnection();
if (conn == null) {
return new LoginResult(false, "数据库连接失败");
}
User user = userDao.findByPhone(conn, phone);
if (user == null) {
return new LoginResult(false, "该手机号未注册");
}
String token = JwtUtil.generateToken(user);
return new LoginResult(true, "登录成功", token, user);
} catch (Exception e) {
e.printStackTrace();
return new LoginResult(false, "登录异常: " + e.getMessage());
} finally {
DBUtil.closeConnection(conn);
}
}
public LoginResult changePassword(String phone, String verificationCode, String newPassword) {
if (phone == null || phone.trim().isEmpty()) {
return new LoginResult(false, "手机号不能为空");
}
if (verificationCode == null || verificationCode.trim().isEmpty()) {
return new LoginResult(false, "验证码不能为空");
}
if (newPassword == null || newPassword.trim().isEmpty()) {
return new LoginResult(false, "新密码不能为空");
}
if (!PASSWORD_PATTERN.matcher(newPassword).matches()) {
return new LoginResult(false, "新密码必须至少8位,包含大小写字母和数字");
}
if (!SmsCodeCache.verifyCode(phone, verificationCode)) {
return new LoginResult(false, "验证码错误或已过期");
}
Connection conn = null;
try {
conn = DBUtil.getConnection();
if (conn == null) {
return new LoginResult(false, "数据库连接失败");
}
User user = userDao.findByPhone(conn, phone);
if (user == null) {
return new LoginResult(false, "该手机号未注册");
}
String hashedPassword = hashPassword(newPassword);
user.setPassword(hashedPassword);
boolean success = userDao.updatePassword(conn, user);
if (success) {
return new LoginResult(true, "密码修改成功");
} else {
return new LoginResult(false, "密码修改失败");
}
} catch (Exception e) {
e.printStackTrace();
return new LoginResult(false, "修改异常: " + e.getMessage());
} finally {
DBUtil.closeConnection(conn);
}
}
private String hashPassword(String password) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(password.getBytes(java.nio.charset.StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("密码加密失败", e);
}
}
private static class DBUtil {
private static final String DB_URL = AppConfig.getDbUrl();
private static final String DB_USER = AppConfig.getDbUsername();
private static final String DB_PASSWORD = AppConfig.getDbPassword();
public static Connection getConnection() {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
return DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void closeConnection(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
login/src/ui/LoginFrame.java,代码如下:
package ui;
import service.LoginService;
import util.JwtUtil;
import entity.User;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Map;
public class LoginFrame extends JFrame {
private final LoginService loginService;
private JTextField usernameField;
private JPasswordField passwordField;
private JTextField phoneField;
private JTextField codeField;
private JButton loginButton;
private JButton verifyButton;
private JLabel statusLabel;
private String currentPhone;
private int countdown = 0;
private Timer countdownTimer;
public LoginFrame() {
this.loginService = new LoginService();
initComponents();
setupLayout();
setupEventListeners();
testDatabaseConnection();
}
private void initComponents() {
setTitle("SmartShop 双因子登录系统");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
usernameField = new JTextField(20);
passwordField = new JPasswordField(20);
phoneField = new JTextField(20);
phoneField.setEditable(false);
codeField = new JTextField(20);
loginButton = new JButton("登录并获取验证码");
verifyButton = new JButton("验证登录");
verifyButton.setEnabled(false);
statusLabel = new JLabel(" ");
statusLabel.setForeground(new Color(220, 53, 69));
Font labelFont = new Font("微软雅黑", Font.PLAIN, 14);
Font fieldFont = new Font("微软雅黑", Font.PLAIN, 14);
Font buttonFont = new Font("微软雅黑", Font.BOLD, 14);
usernameField.setFont(fieldFont);
passwordField.setFont(fieldFont);
phoneField.setFont(fieldFont);
codeField.setFont(fieldFont);
loginButton.setFont(buttonFont);
verifyButton.setFont(buttonFont);
statusLabel.setFont(labelFont);
loginButton.setPreferredSize(new Dimension(180, 35));
verifyButton.setPreferredSize(new Dimension(180, 35));
}
private void setupLayout() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
mainPanel.setBackground(Color.WHITE);
JPanel headerPanel = new JPanel(new BorderLayout());
headerPanel.setBackground(Color.WHITE);
JLabel titleLabel = new JLabel("SmartShop 登录", SwingConstants.CENTER);
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24));
titleLabel.setForeground(new Color(52, 58, 64));
headerPanel.add(titleLabel, BorderLayout.CENTER);
JLabel subtitleLabel = new JLabel("双因子认证登录系统", SwingConstants.CENTER);
subtitleLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
subtitleLabel.setForeground(new Color(108, 117, 125));
headerPanel.add(subtitleLabel, BorderLayout.SOUTH);
mainPanel.add(headerPanel, BorderLayout.NORTH);
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBackground(Color.WHITE);
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(8, 5, 8, 5);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font("微软雅黑", Font.PLAIN, 14);
gbc.gridx = 0;
gbc.gridy = 0;
JLabel usernameLabel = new JLabel("用户名:");
usernameLabel.setFont(labelFont);
formPanel.add(usernameLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
formPanel.add(usernameField, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0;
JLabel passwordLabel = new JLabel("密 码:");
passwordLabel.setFont(labelFont);
formPanel.add(passwordLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
formPanel.add(passwordField, gbc);
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0;
gbc.gridwidth = 2;
gbc.insets = new Insets(15, 5, 15, 5);
JPanel buttonPanel1 = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0));
buttonPanel1.setBackground(Color.WHITE);
loginButton.setBackground(new Color(0, 123, 255));
loginButton.setForeground(Color.WHITE);
loginButton.setFocusPainted(false);
loginButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
buttonPanel1.add(loginButton);
formPanel.add(buttonPanel1, gbc);
gbc.gridy = 3;
gbc.insets = new Insets(8, 5, 8, 5);
JLabel stepLabel = new JLabel("—— 第二步:验证码校验 ——");
stepLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
stepLabel.setForeground(new Color(108, 117, 125));
stepLabel.setHorizontalAlignment(SwingConstants.CENTER);
formPanel.add(stepLabel, gbc);
gbc.gridy = 4;
JLabel phoneLabel = new JLabel("手机号:");
phoneLabel.setFont(labelFont);
formPanel.add(phoneLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
phoneField.setBackground(new Color(248, 249, 250));
formPanel.add(phoneField, gbc);
gbc.gridx = 0;
gbc.gridy = 5;
gbc.weightx = 0;
JLabel codeLabel = new JLabel("验证码:");
codeLabel.setFont(labelFont);
formPanel.add(codeLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
formPanel.add(codeField, gbc);
gbc.gridx = 0;
gbc.gridy = 6;
gbc.gridwidth = 2;
gbc.insets = new Insets(15, 5, 15, 5);
JPanel buttonPanel2 = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0));
buttonPanel2.setBackground(Color.WHITE);
verifyButton.setBackground(new Color(40, 167, 69));
verifyButton.setForeground(Color.WHITE);
verifyButton.setFocusPainted(false);
verifyButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
buttonPanel2.add(verifyButton);
formPanel.add(buttonPanel2, gbc);
mainPanel.add(formPanel, BorderLayout.CENTER);
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.setBackground(Color.WHITE);
footerPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
footerPanel.add(statusLabel, BorderLayout.CENTER);
mainPanel.add(footerPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
setSize(450, 520);
setLocationRelativeTo(null);
}
private void setupEventListeners() {
loginButton.addActionListener(this::handleLogin);
verifyButton.addActionListener(this::handleVerify);
passwordField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
handleLogin(null);
}
}
});
codeField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
handleVerify(null);
}
}
});
}
private void testDatabaseConnection() {
SwingWorker worker = new SwingWorker() {
@Override
protected Boolean doInBackground() {
return DatabaseConfig.testConnection();
}
@Override
protected void done() {
try {
boolean connected = get();
if (!connected) {
showStatus("数据库连接失败,请检查配置", false);
}
} catch (Exception e) {
showStatus("数据库连接异常: " + e.getMessage(), false);
}
}
};
worker.execute();
}
private void handleLogin(ActionEvent e) {
String username = usernameField.getText().trim();
String password = new String(passwordField.getPassword());
if (username.isEmpty()) {
showStatus("请输入用户名", false);
usernameField.requestFocus();
return;
}
if (password.isEmpty()) {
showStatus("请输入密码", false);
passwordField.requestFocus();
return;
}
setButtonsEnabled(false);
showStatus("正在验证用户名密码...", true);
SwingWorker worker = new SwingWorker() {
@Override
protected LoginService.LoginResult doInBackground() {
return loginService.loginWithUsernamePassword(username, password);
}
@Override
protected void done() {
try {
LoginService.LoginResult result = get();
if (result.isSuccess()) {
currentPhone = result.getPhone();
phoneField.setText(currentPhone);
verifyButton.setEnabled(true);
showStatus(result.getMessage(), true);
startCountdown();
} else {
showStatus(result.getMessage(), false);
setButtonsEnabled(true);
}
} catch (Exception ex) {
showStatus("登录异常: " + ex.getMessage(), false);
setButtonsEnabled(true);
}
}
};
worker.execute();
}
private void handleVerify(ActionEvent e) {
String phone = phoneField.getText().trim();
String code = codeField.getText().trim();
if (phone.isEmpty()) {
showStatus("请先完成用户名密码登录", false);
return;
}
if (code.isEmpty()) {
showStatus("请输入验证码", false);
codeField.requestFocus();
return;
}
if (!code.matches("^\\d{6}$")) {
showStatus("验证码格式不正确,请输入6位数字", false);
codeField.requestFocus();
return;
}
setButtonsEnabled(false);
showStatus("正在验证...", true);
SwingWorker worker = new SwingWorker() {
@Override
protected LoginService.LoginResult doInBackground() {
return loginService.verifyCodeAndLogin(phone, code);
}
@Override
protected void done() {
try {
LoginService.LoginResult result = get();
if (result.isSuccess()) {
showLoginSuccess(result);
} else {
showStatus(result.getMessage(), false);
setButtonsEnabled(true);
}
} catch (Exception ex) {
showStatus("验证异常: " + ex.getMessage(), false);
setButtonsEnabled(true);
}
}
};
worker.execute();
}
private void showLoginSuccess(LoginService.LoginResult result) {
User user = result.getUser();
String token = result.getToken();
Map tokenInfo = JwtUtil.parseToken(token);
StringBuilder sb = new StringBuilder();
sb.append("登录成功!\n\n");
sb.append("用户信息:\n");
sb.append(" 用户ID:").append(user.getId()).append("\n");
sb.append(" 用户名:").append(user.getUsername()).append("\n");
sb.append(" 手机号:").append(user.getPhone()).append("\n");
sb.append(" 昵 称:").append(user.getNickname() != null ? user.getNickname() : "未设置").append("\n\n");
sb.append("Token信息:\n");
sb.append(" ").append(token.substring(0, Math.min(50, token.length()))).append("...\n\n");
sb.append("Token将在24小时后过期");
JOptionPane.showMessageDialog(this, sb.toString(), "登录成功", JOptionPane.INFORMATION_MESSAGE);
showStatus("登录成功!欢迎 " + user.getUsername(), true);
}
private void startCountdown() {
countdown = 60;
countdownTimer = new Timer(1000, new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
countdown--;
if (countdown > 0) {
loginButton.setText(countdown + "秒后可重发");
loginButton.setEnabled(false);
} else {
loginButton.setText("登录并获取验证码");
loginButton.setEnabled(true);
((Timer) e.getSource()).stop();
}
}
});
countdownTimer.start();
}
private void setButtonsEnabled(boolean enabled) {
if (countdown <= 0) {
loginButton.setEnabled(enabled);
}
verifyButton.setEnabled(enabled && currentPhone != null);
}
private void showStatus(String message, boolean success) {
statusLabel.setText(message);
statusLabel.setForeground(success ? new Color(40, 167, 69) : new Color(220, 53, 69));
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
LoginFrame frame = new LoginFrame();
frame.setVisible(true);
}
});
}
}
login/src/Main.java,代码如下:
import ui.LoginFrame;
import javax.swing.*;
public class Main {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
LoginFrame frame = new LoginFrame();
frame.setVisible(true);
}
});
}
}
程序运行后的登录界面如下:
cd loginavac -cp "mysql-connector-j-9.6.0.jar" -d bin src/**/*.java
cd loginava -cp "bin;mysql-connector-j-9.6.0.jar" Main
| 用户名 | 密码 | 手机号 |
|---|---|---|
| admin | admin123 | 13800138000 |
| test | test123 | 13900139000 |
| demo | demo123 | 13700137000 |
| 配置项 | 说明 | 默认值 |
|---|---|---|
| db.url | 数据库连接地址 | jdbc:mysql://127.0.0.1:3306/smartshop |
| db.username | 数据库用户名 | root |
| db.password | 数据库密码 | root123 |
| jwt.secret | JWT签名密钥 | SmartShopJWTSecretKey2024... |
| jwt.expiration | Token过期时间(毫秒) | 86400000 (24小时) |
| sms.code.expire | 验证码过期时间(毫秒) | 300000 (5分钟) |
| sms.interval | 短信发送间隔(毫秒) | 60000 (1分钟) |
模板内容:您的验证码为:{1},{2}分钟内有效,请勿泄露给他人。
{
"code": "${code}",
"expire": "5"
}
参数说明:
Q: 数据库连接失败?
A: 检查MySQL服务是否启动,用户名密码是否正确,数据库是否存在。
Q: 短信发送失败?
A: 检查腾讯云短信配置是否正确,签名和模板是否已审核通过。
Q: 验证码在哪里查看?
A: 如果未配置腾讯云短信,验证码会输出到控制台(命令行窗口)。
Q: 如何编译和运行程序?
A: 编译:javac -cp ".;okhttp-4.9.3.jar;okio-2.10.0.jar;kotlin-stdlib-1.9.20.jar;mysql-connector-j-9.6.0.jar" ***.java
运行:java -cp ".;okhttp-4.9.3.jar;okio-2.10.0.jar;kotlin-stdlib-1.9.20.jar;mysql-connector-j-9.6.0.jar" ***
如果遇到问题,请参考以下资源: