Java 登录注册系统完整指南
1. 系统架构概览
采用三层架构模式:表现层 (Controller) → 业务逻辑层 (Service) → 数据访问层 (DAO) → 数据库 (MySQL/Oracle)
| 组件 | 职责 | 主要类 |
|---|
| Controller | 处理 HTTP 请求,参数校验 | UserController |
| Service | 业务逻辑处理,事务管理 | UserService |
| DAO | 数据库操作 | UserDAO |
| Entity | 数据模型 | User |
| Security | 安全验证,加密 | SecurityUtil |
2. 数据库设计
用户表设计 (users)
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
salt VARCHAR(50) NOT NULL,
status TINYINT DEFAULT 1,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login_time TIMESTAMP NULL,
login_attempts INT DEFAULT 0,
locked_until TIMESTAMP NULL
);
设计要点:
- 密码使用 BCrypt 或 SHA-256 + 盐值加密存储
- 增加登录失败次数控制,防止暴力破解
- 记录最后登录时间,便于安全审计
- 支持账户锁定机制
3. 核心实体类
User 实体类
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
private String password;
private String salt;
private Integer status;
private Date createTime;
private Date updateTime;
private Date lastLoginTime;
private Integer loginAttempts;
private Date lockedUntil;
public boolean isAccountLocked() {
return lockedUntil != null && lockedUntil.after(new Date());
}
public boolean isEnabled() {
return status == ;
}
String {
+ id + + username + ;
}
}
4. 安全工具类
密码加密与验证
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
public class SecurityUtil {
public static String generateSalt() {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
public static String hashPassword(String password, String salt) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt.getBytes());
byte[] hashedPassword = md.digest(password.getBytes());
return Base64.getEncoder().encodeToString(hashedPassword);
} catch (Exception e) {
throw new RuntimeException("密码加密失败", e);
}
}
{
hashPassword(inputPassword, salt);
hashedInput.equals(storedPassword);
}
String {
();
[] token = [];
random.nextBytes(token);
Base64.getEncoder().encodeToString(token);
}
}
安全提示:
- 永远不要明文存储密码
- 每个密码使用独立的盐值
- 考虑使用更强的加密算法如 BCrypt
- 实施密码强度策略
5. 数据访问层 (DAO)
UserDAO 接口与实现
UserDAO.java
import java.util.Date;
public interface UserDAO {
User findByUsername(String username);
User findByEmail(String email);
boolean save(User user);
boolean updateLastLoginTime(Long userId);
boolean updateLoginAttempts(Long userId, int attempts);
boolean lockAccount(Long userId, Date lockUntil);
}
UserDAOImpl.java
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
public class UserDAOImpl implements UserDAO {
private final Connection connection;
public UserDAOImpl(Connection connection) {
this.connection = connection;
}
@Override
public User findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return mapResultSetToUser(rs);
}
} catch (SQLException e) {
throw new RuntimeException("根据用户名查询用户失败:" + username, e);
}
return null;
}
@Override
User {
;
( connection.prepareStatement(sql)) {
stmt.setString(, email);
stmt.executeQuery();
(rs.next()) {
mapResultSetToUser(rs);
}
} (SQLException e) {
( + email, e);
}
;
}
{
;
( connection.prepareStatement(sql)) {
stmt.setString(, user.getUsername());
stmt.setString(, user.getEmail());
stmt.setString(, user.getPassword());
stmt.setString(, user.getSalt());
stmt.executeUpdate() > ;
} (SQLException e) {
( + user.getUsername(), e);
}
}
{
;
( connection.prepareStatement(sql)) {
stmt.setLong(, userId);
stmt.executeUpdate() > ;
} (SQLException e) {
( + userId, e);
}
}
{
;
( connection.prepareStatement(sql)) {
stmt.setInt(, attempts);
stmt.setLong(, userId);
stmt.executeUpdate() > ;
} (SQLException e) {
( + userId, e);
}
}
{
;
( connection.prepareStatement(sql)) {
stmt.setTimestamp(, (lockUntil.getTime()));
stmt.setLong(, userId);
stmt.executeUpdate() > ;
} (SQLException e) {
( + userId, e);
}
}
User SQLException {
();
user.setId(rs.getLong());
user.setUsername(rs.getString());
user.setEmail(rs.getString());
user.setPassword(rs.getString());
user.setSalt(rs.getString());
user.setStatus(rs.getInt());
user.setCreateTime(rs.getTimestamp());
user.setUpdateTime(rs.getTimestamp());
user.setLastLoginTime(rs.getTimestamp());
user.setLoginAttempts(rs.getInt());
user.setLockedUntil(rs.getTimestamp());
user;
}
}
6. 业务逻辑层 (Service)
UserService 核心业务逻辑
import java.util.Date;
public class UserService {
private final UserDAO userDAO;
private static final int MAX_LOGIN_ATTEMPTS = 5;
private static final long LOCK_DURATION = 30 * 60 * 1000;
public UserService(UserDAO userDAO) {
this.userDAO = userDAO;
}
public boolean register(String username, String email, String password) {
if (!isValidUsername(username) || !isValidEmail(email) || !isValidPassword(password)) {
throw new IllegalArgumentException("参数格式不正确:用户名 3-50 位,邮箱格式正确,密码至少 6 位");
}
if (userDAO.findByUsername(username) != ) {
();
}
(userDAO.findByEmail(email) != ) {
();
}
SecurityUtil.generateSalt();
SecurityUtil.hashPassword(password, salt);
();
user.setUsername(username);
user.setEmail(email);
user.setPassword(hashedPassword);
user.setSalt(salt);
userDAO.save(user);
}
User {
userDAO.findByUsername(username);
(user == ) {
();
}
(!user.isEnabled()) {
();
}
(user.isAccountLocked()) {
();
}
(!SecurityUtil.verifyPassword(password, user.getPassword(), user.getSalt())) {
handleLoginFailure(user);
}
handleLoginSuccess(user);
user;
}
{
user.getLoginAttempts() + ;
userDAO.updateLoginAttempts(user.getId(), attempts);
(attempts >= MAX_LOGIN_ATTEMPTS) {
(System.currentTimeMillis() + LOCK_DURATION);
userDAO.lockAccount(user.getId(), lockUntil);
();
}
( + (MAX_LOGIN_ATTEMPTS - attempts) + );
}
{
userDAO.updateLoginAttempts(user.getId(), );
userDAO.updateLastLoginTime(user.getId());
}
{
username != && username.length() >= && username.length() <= ;
}
{
(email == ) {
;
}
email.matches();
}
{
password != && password.length() >= ;
}
}
7. 控制器层 (Controller)
UserController - Web 层处理
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(@RequestBody RegisterRequest request, HttpServletRequest httpRequest) {
Map<String, Object> response = new HashMap<>();
try {
if (request.getUsername() == null || request.getPassword() == || request.getEmail() == ) {
response.put(, );
response.put(, );
ResponseEntity.badRequest().body(response);
}
userService.register(request.getUsername(), request.getEmail(), request.getPassword());
response.put(, success);
response.put(, success ? : );
ResponseEntity.ok(response);
} (Exception e) {
response.put(, );
response.put(, e.getMessage());
ResponseEntity.badRequest().body(response);
}
}
ResponseEntity<Map<String, Object>> {
Map<String, Object> response = <>();
{
userService.login(request.getUsername(), request.getPassword());
SecurityUtil.generateToken();
(, token);
cookie.setHttpOnly();
cookie.setSecure();
cookie.setMaxAge( * * );
cookie.setPath();
httpResponse.addCookie(cookie);
httpRequest.getSession();
session.setAttribute(, user.getId());
session.setAttribute(, user.getUsername());
response.put(, );
response.put(, );
response.put(, Map.of(, user.getId(), , user.getUsername(), , user.getEmail()));
ResponseEntity.ok(response);
} (Exception e) {
response.put(, );
response.put(, e.getMessage());
ResponseEntity.badRequest().body(response);
}
}
ResponseEntity<Map<String, Object>> {
request.getSession();
(session != ) {
session.invalidate();
}
(, );
cookie.setMaxAge();
cookie.setPath();
response.addCookie(cookie);
Map<String, Object> result = <>();
result.put(, );
result.put(, );
ResponseEntity.ok(result);
}
}
{
String username;
String email;
String password;
String { username; }
{ .username = username; }
String { email; }
{ .email = email; }
String { password; }
{ .password = password; }
}
{
String username;
String password;
String { username; }
{ .username = username; }
String { password; }
{ .password = password; }
}
配置文件
# Spring Boot 主配置文件
spring:
# 应用基本信息
application:
name: bushuo-users # 应用名称
# 环境配置 (dev/test/prod,当前激活开发环境)
profiles:
active: dev
# 数据源配置(MySQL)
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.x 驱动类
url: jdbc:mysql://localhost:3306/bushuo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root # 数据库用户名
password: 123456 # 数据库密码
# Hikari 连接池配置(Spring Boot 默认连接池)
hikari:
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
auto-commit: true # 自动提交事务
idle-timeout: 30000 # 空闲连接超时时间(30 秒)
pool-name: UserAuthHikariCP # 连接池名称
max-lifetime: 1800000 # 连接最大生命周期(30 分钟)
connection-timeout: 30000 # 连接获取超时时间(30 秒)
# JPA/Hibernate 配置(ORM 框架)
jpa:
hibernate:
ddl-auto: update # 表结构自动更新(开发环境用,生产环境建议改为 validate)
show-sql: true # 控制台打印 SQL 语句
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect # MySQL 8.x 方言
format_sql: true # 格式化 SQL 语句(便于调试)
# Redis 配置(用于会话存储、缓存)
redis:
host: localhost # Redis 服务地址
port: 6379 # Redis 端口
database: 0 # Redis 数据库索引(默认 0)
timeout: 2000ms # 连接超时时间(2 秒)
# Jedis 连接池配置
jedis:
pool:
max-active: 20 # 最大活跃连接数
max-idle: 10 # 最大空闲连接数
min-idle: 5 # 最小空闲连接数
max-wait: 2000ms # 连接获取最大等待时间(2 秒)
# 会话配置(基于 Redis 存储会话)
session:
store-type: redis # 会话存储类型(redis/memory/jdbc)
timeout: 1800s # 会话超时时间(30 分钟)
redis:
namespace: "user:session" # Redis 会话键前缀(避免键冲突)
# Web 配置
mvc:
pathmatch:
matching-strategy: ant_path_matcher # 路径匹配策略(兼容老版本 Ant 风格)
web:
resources:
static-locations: classpath:/static/ # 静态资源路径(CSS/JS/图片等)
cache-period: 3600 # 静态资源缓存时间(1 小时)
# 邮件配置(用于注册验证、密码找回)
mail:
# host: smtp.qq.com # 邮件服务器地址(QQ 邮箱示例)
# port: 587 # 邮件服务器端口(TLS 协议默认 587)
# username: ${MAIL_USERNAME:[email protected]} # 发件人邮箱(支持环境变量注入)
# password: ${MAIL_PASSWORD:your-auth-code} # 邮箱授权码(非登录密码,支持环境变量注入)
# properties:
# mail:
# smtp:
# auth: true # 启用 SMTP 认证
# starttls:
# enable: true # 启用 STARTTLS 加密(安全传输)
# 服务器配置
server:
port: 8080 # 应用端口
servlet:
context-path: / # 应用上下文路径(默认 /,如需前缀可改为 /api 等)
session:
timeout: 30m # 服务器会话超时时间(与 Spring 会话超时保持一致)
cookie:
http-only: true # 启用 HttpOnly(防止 JS 读取 Cookie,提升安全性)
secure: false # 启用 Secure(仅 HTTPS 传输 Cookie,生产环境建议设为 true)
max-age: 1800 # Cookie 最大存活时间(30 分钟,与会话超时保持一致)
# 错误页面配置
error:
whitelabel:
enabled: false # 禁用默认错误页面(自定义错误页面时需关闭)
include-message: always # 错误响应中包含错误信息
include-binding-errors: always # 错误响应中包含参数绑定错误
# 日志配置
logging:
level:
# 日志级别(DEBUG < INFO < WARN < ERROR)
com.example.demo: DEBUG # 自定义包日志级别(调试用)
org.springframework.security: DEBUG # Spring Security 日志级别(调试权限问题用)
org.springframework.web: INFO # Spring Web 日志级别
org.hibernate.SQL: DEBUG # Hibernate SQL 日志级别(打印执行的 SQL)
org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 打印 SQL 参数(调试参数绑定用)
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" # 控制台日志格式(简洁版)
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" # 日志文件格式(含线程、日志器)
file:
name: logs/user-auth.log # 日志文件路径及名称(项目根目录下 logs 文件夹)
max-size: 10MB # 单个日志文件最大大小(超过后切割)
max-history: 30 # 日志文件保留历史天数(30 天)
controller → service → mapper → 数据库
(前端请求) (业务处理) (数据访问)
model 贯穿各层,用于数据传递;utils 为各层提供通用支持。