项目搭建及配置
- 创建 SpringBoot3.0.0+ 项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok
- 初始化数据库:
create database spring_blog_login charset utf8mb4;
use spring_blog_login;
create table user_info (
id int primary key auto_increment,
user_name varchar(128) unique,
password varchar(128) not null,
delete_flag int default 0,
create_time datetime default now(),
update_time datetime default now()
);
insert into user_info (user_name, password)
values ('张三', '123456'), ('李四', '123456'), ('王五', '123456');
- 将 application.properties 修改为 application.yml 并添加如下配置:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/spring_blog_login?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
server:
port: 8080
按住 Ctrl + F5,如果程序能运行成功则说明搭建及配置都没问题 (MySQL 服务器必须要处于运行状态)
1. 登录认证全栈实现 -> 基础版
1.1 后端实现
1.1.1 架构设计
本次登录功能采用 Controller、Service、Mapper 三层架构:Controller 层依赖于 Service 层来执行业务逻辑并获取处理结果,而 Service 层又依赖于 Mapper 层来进行数据持久化操作
1.1.2 实体类
实体类用于封装业务数据,需要与数据库表结构一一对应
import lombok.Data;
import java.util.Date;
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
1.1.3 Controller
处理 HTTP 请求、参数校验、返回响应
import org.example.springlogin.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/login")
public String login(String userName, String password) {
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return "用户或密码为空";
}
return userService.getUserInfoByUserName(userName, password);
}
}
1.1.4 Service
业务逻辑处理
import org.example.springlogin.mapper.UserMapper;
import org.example.springlogin.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public String getUserInfoByUserName(String userName, String password) {
UserInfo userInfo = userMapper.getUserInfoByUserName(userName);
if (userInfo == null) {
return "用户不存在";
}
if (!password.equals(userInfo.getPassword())) {
return "密码错误";
}
return "登录成功";
}
}
1.1.5 Mapper
数据持久化操作
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.example.springlogin.model.UserInfo;
@Mapper
public interface UserMapper {
@Select("select * from user_info where user_name = #{userName}")
UserInfo getUserInfoByUserName(String userName);
}
1.2 前端实现
效果演示:
1. 用户或密码为空
2. 用户不存在
3. 密码错误
4. 登录成功
2. Cookie/Session
HTTP(超文本传输协议) 设计为无状态协议,指服务器默认不保留客户端请求之间的任何状态信息。每个请求独立处理,服务器不会记忆之前的交互内容。
优点:请求独立性:每次请求被视为新请求,服务器不依赖历史请求数据;简单高效:无状态设计降低服务器资源消耗,简化实现逻辑。缺点:身份识别困难:需通过额外机制 (如 Cookies、Session) 跟踪用户状态;重复传输数据:每次请求需携带完整信息,可能增加冗余 (如认证信息)。
cookie:是存储在客户端 (浏览器) 的小型文本数据,由服务器通过 HTTP 响应头 Set-Cookie 发送给客户端,并在后续请求中自动携带。session:是存储在服务器端的用户状态信息,通常通过一个唯一的 Session ID 标识,该 ID 可能通过 Cookie 或 URL 传递。 服务器内部实际上专门开辟了一个 session 空间用于存储用户信息,每当新用户发送第一次请求时服务器会将用户信息存储在 session 中并生成一个 session id 通过 Set-Cookie 方法返回给客户端,即 cookie。
修改 Controller 类代码:
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.example.springlogin.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/login")
public String login(String userName, String password, HttpSession session) {
log.info("接收到参数,userName:{},password:{}", userName, password);
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return "用户或密码为空";
}
String result = userService.getUserInfoByUserName(userName, password);
if (result.equals("登录成功")) {
HashMap<String, String> map = new HashMap<>();
map.put("userName", userName);
map.put("password", password);
// 将 map 作为用户信息存储到 session/会话中
session.setAttribute("cookie", map);
log.info();
}
result;
}
}
修改前端代码:
function login() {
$.ajax({
url: '/user/login',
type: "post",
data: {
userName: $('#username').val(),
password: $('#password').val(),
},
success: function(result) {
alert(result);
},
})
}
3. 统一返回结果封装
统一返回结果封装是后端开发中的重要设计模式,能够保持 API 响应格式的一致性,便于前端处理
1. 创建枚举类:统一管理接口或方法的返回状态码和描述信息,标准化业务逻辑中的成功或失败状态
import lombok.Getter;
@Getter
public enum ResultStatus {
SUCCESS(200, "成功"),
FAIL(-1, "失败");
private final Integer code;
private final String message;
ResultStatus(Integer code, String message) {
this.code = code;
this.message = message;
}
}
2. 创建 Result 类:主要用于规范服务端返回给客户端的响应数据格式。通过固定结构 (状态码、错误信息、数据) 确保前后端交互的一致性
import lombok.Data;
@Data
// 通过泛型<T>设计,可以灵活封装任意类型的数据对象到 data 字段
public class Result<T> {
// 业务码
private ResultStatus code;
// 错误信息
private String errorMessage;
// 数据
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultStatus.SUCCESS);
result.setErrorMessage(null);
result.setData(data);
return result;
}
public static <T> Result<T> fail(String errorMessage) {
Result<T> result = new Result<>();
result.setCode(ResultStatus.FAIL);
result.setErrorMessage(errorMessage);
result.setData(null);
return result;
}
}
3. 修改 Controller 代码:
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/login")
public Result<String> login(String userName, String password, HttpSession session) {
log.info("接收到参数,userName:{},password:{}", userName, password);
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return Result.fail("用户或密码为空");
}
String result = userService.getUserInfoByUserName(userName, password);
if (!result.equals("登录成功")) {
return Result.fail(result);
}
HashMap<String, String> map = new HashMap<>();
map.put("userName", userName);
map.put("password", password);
// 将 map 作为用户信息存储到 session/会话中
session.setAttribute("cookie", map);
log.info("登录成功");
return Result.success(result);
}
}
4. 修改前端代码:
function login() {
$.ajax({
url: '/user/login',
type: "post",
data: {
userName: $('#username').val(),
password: $('#password').val(),
},
success: function(result) {
if (result.code === "SUCCESS") {
alert(result.data)
} else {
alert(result.error)
}
},
})
}
4. 图形验证码
图形验证码 (captcha)是一种区分用户是人类还是自动化程序的技术,主要通过视觉或交互任务实现。其核心意义体现在以下方面:防止自动化攻击:通过复杂图形或扭曲文字,阻止爬虫、暴力破解工具等自动化程序批量注册或登录,降低服务器压力;提升安全性:在敏感操作 (如支付、修改密码) 前增加验证步骤,减少数据泄露或恶意操作风险。
Hutool 提供了 CaptchaUtil 类用于快速生成验证码,支持图形验证码和 GIF 动态验证码。在 pom.xml 文件中添加如下配置:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<!-- 版本号应与 springboot 版本兼容 -->
<version>5.8.40</version>
</dependency>
1. 创建 CaptchaController 类,用于生成验证码并返回给前端
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/captcha")
@Slf4j
public class CaptchaController {
// 设置过期时间
public final static long delay = 60_000L;
@RequestMapping("/get")
public void getCaptcha(HttpSession session, HttpServletResponse response) {
log.info("getCaptcha");
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
// 设置返回类型
response.setContentType("image/jpeg");
// 禁止缓存
response.setHeader("Pragma", "No-cache");
try {
// 通过响应输出生成的图形验证码
lineCaptcha.write(response.getOutputStream());
// 保存 code
session.setAttribute("CAPTCHA_SESSION_CODE", lineCaptcha.getCode());
// 保存当前时间
session.setAttribute("CAPTCHA_SESSION_DATE", System.currentTimeMillis());
response.getOutputStream().close();
} (IOException e) {
(e);
}
}
}
2. 修改前端代码:最终版
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="css/login.css">
</head>
<body>
<div class="login-container">
<div class="logo"><i class="fab fa-weixin"></i></div>
<h2>用户登录</h2>
<form id="loginForm">
我已阅读并同意《服务条款》和《隐私政策》
登录
- 在 UserController 类新增 captcha 形参接收来自 CaptchaController 类的请求,并传递给 UserService
import jakarta.servlet.http.HttpSession;
import org.example.springlogin.mapper.UserMapper;
import org.example.springlogin.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public String getUserInfoByUserName(String userName, String password, String captcha, HttpSession session) {
UserInfo userInfo = userMapper.getUserInfoByUserName(userName);
if (userInfo == null) {
return "用户不存在";
}
if (!password.equals(userInfo.getPassword())) {
return "密码错误";
}
long saveTime = (long) session.getAttribute("CAPTCHA_SESSION_DATE");
if (System.currentTimeMillis() - saveTime > CaptchaController.delay) {
return "验证码超时";
}
if (!captcha.equalsIgnoreCase((String) session.getAttribute("CAPTCHA_SESSION_CODE"))) {
return "验证码错误";
}
return "登录成功";
}
}
5. MD5 加密
MD5(Message-Digest Algorithm 5)是一种广泛使用的哈希函数,可将任意长度数据生成固定长度 (128 位,16 字节) 的哈希值,通常表示为 32 位十六进制字符串,常用于校验数据完整性或存储密码。但因其安全性不足,通常结合盐值 (Salt) 配合使用。 不可逆性:无法通过哈希值反推原始数据;唯一性:理论上不同输入产生相同哈希值的概率极低 (哈希碰撞);固定长度:无论输入数据大小,输出均为 32 位十六进制字符串。
1. 创建 SecurityUtil 类用于生成和验证密文
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.UUID;
public class SecurityUtil {
// 加密
public static String encrypt(String inputPassword) {
// 生成随机盐值
String salt = UUID.randomUUID().toString().replaceAll("-", "");
// (密码 + 盐值) 进行加密
String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
return salt + finalPassword;
}
// 验证
public static boolean verify(String inputPassword, String sqlPassword) {
if (!StringUtils.hasLength(inputPassword)) {
return false;
}
if (sqlPassword == null || sqlPassword.length() != 64) {
return false;
}
// 取出盐值
String salt = sqlPassword.substring(0, 32);
// (输入密码 + 盐值) 重新生成 加密密码
String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
// 判断数据库中储存的密码与输入密码是否一致
(salt + finalPassword).equals(sqlPassword);
}
{
System.out.println(SecurityUtil.encrypt());
}
}
2. 将数据库中的密码替换为加密后的值 3. 修改验证密码的逻辑 (UserService 类)
if (!SecurityUtil.verify(password, userInfo.getPassword())) {
return "密码错误";
}
6. 拦截器
Spring 拦截器 (Interceptor)是一种基于 AOP 的机制,用于在请求处理的不同阶段插入自定义逻辑。常用于权限校验、日志记录、参数预处理等场景。
1. 创建拦截器类并实现 HandlerInterceptor 接口,该接口提供了三种方法:
- preHandle:在 Controller 方法执行前调用
- postHandle:Controller 方法执行后、视图渲染前调用
- afterCompletion:请求完成、视图渲染完毕后调用
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 获取 token
String cookie = request.getHeader("cookie");
if (cookie == null) {
response.setStatus(401);
return false;
}
log.info("接收到 cookie:{}", cookie);
// 2. 校验 token
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
log.info("postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("afterCompletion");
}
}
2. 注册拦截器
import org.example.springlogin.intercepter.Interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class Config implements WebMvcConfigurer {
private final Interceptor interceptor;
@Autowired
public Config(Interceptor interceptor) {
this.interceptor = interceptor;
}
// 排除不需要拦截的路径
private static final List<String> excludes = Arrays.asList("/**/login.html", "/user/login", "/captcha/get");
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器
registry.addInterceptor(interceptor)
// 拦截所有路径
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
3. 创建 home.html 文件,并且在登录成功后跳转到该页面 (在 login.html 中添加 location.href="/home.html")
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>home</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
实现效果:
未登录直接访问 home.html 页面时
成功登陆时


