系统登录

使用Java代码开发用户名、密码+腾讯云短信验证码的双因子认证登录模块

项目概述

本项目实现了一个基于双因子认证的系统登录模块,结合用户名密码验证和短信验证码验证,提高系统安全性。使用腾讯云短信API发送验证码,JWT生成令牌,Swing构建图形界面。

1

技术栈

  • Java 11+
  • Swing (GUI)
  • JDBC (数据库连接)
  • JWT (Token生成)
  • 腾讯云短信API
2

功能特性

  • 双因子认证:用户名、密码 + 短信验证码双重验证
  • 腾讯云短信集成:支持腾讯云短信API发送验证码
  • JWT Token:登录成功后生成JWT Token
  • 防短信轰炸:1分钟内同一手机号仅能发送1次验证码
  • 验证码过期:验证码5分钟内有效
  • Java窗体界面:Swing图形界面,操作简单
3

项目结构

项目目录结构:login/src/

language-bash
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                  # 数据库初始化脚本
4

环境要求

  • JDK 11 或更高版本
  • MySQL 8.0版本
  • MySQL JDBC驱动(mysql-connector-j-9.6.0.jar)
5

安装步骤

1. 数据库配置

执行 sql/init.sql 创建表和测试数据

数据库连接信息:

  • 地址:127.0.0.1:3306
  • 账号:root
  • 密码:root123
  • 数据库:smartshop

init.sql中建库建表代码如下:

language-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;

2. 下载MySQL驱动

从MySQL官网下载JDBC驱动,或使用本站已下载文件:

mysql-connector-j-9.6.0.jar

放置在 lib 目录下

3. 配置腾讯云短信

申请过程见腾讯云官网文档,大家可使用已申请好的API接口

填写腾讯云短信配置代码格式如下:

language-properties
tencent.secretId=你的SecretId
tencent.secretKey=你的SecretKey
tencent.sms.appId=你的AppId
tencent.sms.signName=你的短信签名
tencent.sms.templateId=你的模板ID

编辑"src/config.properties"文件,代码如下:

language-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

如果不配置腾讯云短信或配置失败,系统会以模拟模式运行,验证码会输出到控制台。

6

逻辑结构

1. 应用配置类

login/src/config/AppConfig.java,代码如下:

language-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);
    }
}

2. 数据库配置类

login/src/config/DatabaseConfig.java,代码如下:

language-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;
        }
    }
}

3. 用户数据访问层

login/src/dao/UserDao.java,代码如下:

language-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;
    }
}

4. 用户实体类

login/src/entity/User.java,代码如下:

language-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;
    }
}

5. 验证码生成器

login/src/util/CodeGenerator.java,代码如下:

language-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("---");
        }
    }
}

6. JWT工具类

login/src/util/JwtUtil.java,代码如下:

language-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;
    }
}

7. 验证码缓存管理

login/src/util/SmsCodeCache.java,代码如下:

language-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());
    }
}

8. 腾讯云短信工具类

login/src/util/TencentSmsUtil.java,代码如下:

language-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();
    }
}

9. 登录业务服务类

login/src/service/LoginService.java,代码如下:

language-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();
                }
            }
        }
    }
}

10. 登录窗体界面

login/src/ui/LoginFrame.java,代码如下:

language-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);
            }
        });
    }
}

11. 程序入口

login/src/Main.java,代码如下:

language-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);
            }
        });
    }
}

12. 运行结果

程序运行后的登录界面如下:

登录界面
7

运行说明

1. 编译项目

language-bash
cd loginavac -cp "mysql-connector-j-9.6.0.jar" -d bin src/**/*.java

2. 运行程序

language-bash
cd loginava -cp "bin;mysql-connector-j-9.6.0.jar" Main

3. 测试账号

用户名 密码 手机号
admin admin123 13800138000
test test123 13900139000
demo demo123 13700137000

4. 配置说明

配置项 说明 默认值
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分钟)

5. 安全说明

  • 密码存储:示例中使用明文存储,生产环境请使用BCrypt等加密算法
  • JWT密钥:请修改默认的JWT密钥为复杂字符串
  • 验证码防刷:实现了1分钟内同一手机号只能发送1次的限制
  • 验证码有效期:验证码5分钟后自动过期
  • 日志安全:生产环境避免在日志中输出完整的密钥信息
  • 网络安全:使用HTTPS协议进行API调用
  • 权限控制:为API密钥设置最小必要权限

6. 腾讯云短信模板示例

模板内容:您的验证码为:{1},{2}分钟内有效,请勿泄露给他人。

language-json
{
    "code": "${code}",
    "expire": "5"
}

参数说明:

  • {1}:验证码
  • {2}:有效期(分钟)

7. 常见问题

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" ***

8. 技术支持

如果遇到问题,请参考以下资源:

注意事项

9

扩展建议

  • 实现用户注册和登录功能
  • 添加订单管理功能
  • 实现商品评价功能
  • 优化推荐算法(考虑基于物品的协同过滤、矩阵分解等)
  • 添加商品图片显示功能
  • 实现商品分类浏览
  • 添加用户个人中心
  • 实现支付功能