概述
目的:Spring 生态为 Java 后端开发提供了强大支持,但将分散的技术点整合成完整解决方案往往令人困惑。本文将以登录接口为切入点,系统演示如何将 IOC/DI、MyBatis 数据持久化、MD5 加密、Session/Cookie 管理、JWT 令牌和拦截器机制融合运用,打造企业级认证方案。技术栈:前端:HTML + CSS + JavaScript + jQuery;后端:SpringBoot + Mybatis + JWT
搭建环境:数据库:MySQL8.4.0;项目结构:Maven;前端框架:jQuery;后端框架:SpringBoot;JDK:17;编译器:IDEA
项目搭建及配置
- 创建 SpringBoot3.0.0+ 项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok
- 初始化数据库:
CREATE DATABASE spring_blog_login CHARACTER SET 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 前端实现
效果演示:
4. 登录成功
3. 密码错误
2. 用户不存在
1. 用户或密码为空
2. Cookie/Session
HTTP(超文本传输协议) 设计为无状态协议,指服务器默认不保留客户端请求之间的任何状态信息。每个请求独立处理,服务器不会记忆之前的交互内容 (如下图)
优点:请求独立性:每次请求被视为新请求,服务器不依赖历史请求数据;简单高效:无状态设计降低服务器资源消耗,简化实现逻辑
缺点:身份识别困难:需通过额外机制 (如 Cookies、Session) 跟踪用户状态;重复传输数据:每次请求需携带完整信息,可能增加冗余 (如认证信息)
cookie:是存储在客户端 (浏览器) 的小型文本数据,由服务器通过 HTTP 响应头 Set-Cookie 发送给客户端,并在后续请求中自动携带
session:是存储在服务器端的用户状态信息,通常通过一个唯一的 Session ID 标识,该 ID 可能通过 Cookie 或 URL 传递如上图片引用自我的博客:Java EE(13)——网络原理——应用层 HTTP 协议,服务器内部实际上专门开辟了一个 session 空间用于存储用户信息,每当新用户发送第一次请求时服务器会将用户信息存储在 session 中并生成一个 session id 通过 Set-Cookie 方法返回给客户端,即 cookie
session 结构如下:
修改 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("userInfo", map);
log.info("登录成功");
}
return result;
}
}
修改前端代码:
function login() {
$.ajax({
url: '/user/login',
type: 'POST',
data: {
userName: $('#username').val(),
password: $('#password').val(),
},
success: function(result) {
alert(result);
}
});
}
Fiddler 抓包结果: 前端/浏览器按住
Ctrl + Shift + i打开控制台点击应用程序/application,打开 Cookie:
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< T >类:主要用于规范服务端返回给客户端的响应数据格式。通过固定结构 (状态码、错误信息、数据) 确保前后端交互的一致性
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("userInfo", 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 === 200) {
alert(result.data);
} else {
alert(result.errorMessage);
}
}
});
}
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("captchaCode", lineCaptcha.getCode());
// 保存当前时间
session.setAttribute("captchaTime", System.currentTimeMillis());
// 关闭输出流
response.getOutputStream().close();
} catch (IOException e) {
throw new RuntimeException(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">
<div class="input-group">
<i class="fas fa-user"></i>
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名" required>
</div>
<div class="input-group">
<i class="fas fa-lock"></i>
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码" required>
</div>
<div class="input-group">
<div class="captcha-container">
<label for="inputCaptcha">验证码</label>
<input type="text" id="inputCaptcha" class="captcha-input" placeholder="输入验证码">
<img id="verificationCodeImg" src="/captcha/get" class="captcha-img" title="看不清?换一张" alt="验证码">
</div>
</div>
<div class="agreement">
<input type="checkbox" id="agreeCheck" checked>
<label for="agreeCheck">我已阅读并同意<a href="#">《服务条款》</a>和<a href="#">《隐私政策》</a></label>
</div>
<button type="submit" class="login-btn" onclick="login()">登录</button>
</form>
<div class="footer">
<p>版权所有 ©九转苍翎</p>
</div>
</div>
<!-- 引入 jQuery 依赖 -->
<script src="js/jquery.min.js"></script>
<script>
// 刷新验证码
$("#verificationCodeImg").click(function () {
// new Date().getTime()).fadeIn() 防止前端缓存
$(this).hide().attr('src', '/captcha/get?dt=' + new Date().getTime()).fadeIn();
});
// 登录
function login() {
$.ajax({
url: '/user/login',
type: 'POST',
data: {
userName: $('#username').val(),
password: $('#password').val(),
captcha: $('#inputCaptcha').val(),
},
success: function(result) {
console.log(result);
if (result.code === 200) {
alert(result.data);
} else {
alert(result.errorMessage);
}
}
});
}
</script>
</body>
</html>
- 在 UserController 类新增 captcha 形参接收来自 CaptchaController 类的请求,并传递给 UserService
import jakarta.servlet.http.HttpSession;
import org.example.springlogin.controller.CaptchaController;
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("captchaTime");
if (System.currentTimeMillis() - saveTime > CaptchaController.delay) {
return "验证码超时";
}
if (!captcha.equalsIgnoreCase((String) session.getAttribute("captchaCode"))) {
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());
// 判断数据库中储存的密码与输入密码是否一致
return (salt + finalPassword).equals(sqlPassword);
}
public static void main(String[] args) {
System.out.println(SecurityUtil.encrypt("123456"));
}
}
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("Received 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.interceptor.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 页面时
成功登陆时


