Java外功实战(4)——SpringBoot登录认证全栈实现:Session、统一结果封装、MD5加密与拦截器

Java外功实战(4)——SpringBoot登录认证全栈实现:Session、统一结果封装、MD5加密与拦截器

本文简介

目的: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

目录结构

项目搭建及配置

1.创建SpringBoot3.0.0+项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok
2.初始化数据库:

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');

3.将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: ***.mysql.cj.jdbc.Driver
mybatis:
  configuration:
    map-underscore-to-camel-case: true #自动驼峰转换
server:
  port: 8080 #不显式设置默认为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 前端实现

Gitee:项目前端代码,Gitee上的前端代码是最新提交的,如下效果图仅作参考
效果演示:

  • 1.用户或密码为空
  • 2.用户不存在
  • 3.密码错误
  • 4.登录成功

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("cookie", map);
            log.info("登录成功");
        }
        return result;
    }

修改前端代码:

    function login() {
      $.ajax({
        url: '/user/login',
        type: "post",
        data:{
          userName:$('#username').val(),
          password:$('#password').val(),
        },
        su***ess: function(result) {
          alert(result);
        },
      })
    }

Fiddler抓包结果
前端/浏览器按住Ctrl + Shift + i打开控制台点击应用程序/application,打开Cookie

3.统一返回结果封装

统一返回结果封装是后端开发中的重要设计模式,能够保持API响应格式的一致性,便于前端处理

1.创建枚举类:统一管理接口或方法的返回状态码和描述信息,标准化业务逻辑中的成功或失败状态

import lombok.Getter;
@Getter
public enum ResultStatus {
    SU***ESS(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> su***ess(T data) {
        Result<T> result = new Result<>();
        result.setCode(ResultStatus.SU***ESS);
        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.su***ess(result);
    }
}

4.修改前端代码

    function login() {
      $.ajax({
        url: '/user/login',
        type: "post",
        data:{
          userName:$('#username').val(),
          password:$('#password').val(),
        },
        su***ess: function(result) {
          if (result.code === "SU***ESS") {
            alert(result.data)
          }else {
            alert(result.error)
          }
        },
      })
    }

4.图形验证码

图形验证码(captcha)是一种区分用户是人类还是自动化程序的技术,主要通过视觉或交互任务实现。其核心意义体现在以下方面:

  • 防止自动化攻击:通过复杂图形或扭曲文字,阻止爬虫、暴力破解工具等自动化程序批量注册或登录,降低服务器压力
  • 提升安全性:在敏感操作(如支付、修改密码)前增加验证步骤,减少数据泄露或恶意操作风险

Hutool提供了CaptchaUtil类用于快速生成验证码,支持图形验证码和GIF动态验证码。在pom.xml文件中添加图下配置:

<dependency>
    <groupId>***.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <!-- 版本号应与springboot版本兼容 -->
    <version>5.8.40</version>
</dependency>

1.创建CaptchaController类,用于生成验证码并返回给前端

import ***.hutool.captcha.CaptchaUtil;
import ***.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();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2.修改前端代码:最终版

<!DOCTYPE html>
<html lang="zh-***">
<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.***/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(),
        },
        su***ess: function(result) {
          console.log(result);
          if (result.code === "SU***ESS") {
            alert(result.data)
          }else {
            alert(result.error)
          }
        },
      })
    }
  </script>
</body>
</html>

3.在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("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());
        //判断数据库中储存的密码与输入密码是否一致
        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方法执行后、视图渲染前调用
  • after***pletion:请求完成、视图渲染完毕后调用
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.***ponent;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@***ponent
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 after***pletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        log.info("after***pletion");
    }
}

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.WebMv***onfigurer;
import java.util.Arrays;
import java.util.List;

@Configuration
public class Config implements WebMv***onfigurer {
    private final Interceptor Interceptor;

    @Autowired
    public Config(Interceptor interceptor) {
        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页面时
转载请说明出处内容投诉
CSS教程网 » Java外功实战(4)——SpringBoot登录认证全栈实现:Session、统一结果封装、MD5加密与拦截器

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买