基于Spring Boot + Vue项目online_learn的用户登录认证全流程分析

🔐 用户登录认证全流程分析

📋 认证流程概览

前端登录请求 → 后端接收 → 密码验证 → JWT令牌生成 → 返回token → 前端存储token → 后续请求携带token → 权限校验

📁 关键文件清单(按执行顺序)

  1. 前端登录界面和请求
  • learn_front_mange-master/src/views/system/login/login.vue - 登录页面
xml 复制代码
<template>
  <div class="login-page">
    <!-- 装饰用渐变圆形背景 -->
    <div class="login"></div>

    <!-- Logo展示区域 -->
    <div class="content">
      <img
          style="border-radius:50%;width:249px;height:249px"
          src="../../../assets/image/logo.png"
          alt="系统logo"
      >
    </div>

    <!-- 登录卡片核心区域 -->
    <div class="login-card">
      <!-- 登录标题 -->
      <div class="login-title">
        <span>欢迎使用在线学习管理系统</span>
      </div>

      <!-- 用户名/密码输入框 -->
      <div class="login-input">
        <div class="input-item">
          <el-input
              prefix-icon="el-icon-user"
              v-model="username"
              placeholder="请输入用户名"
              clearable
          ></el-input>
        </div>
        <div class="input-item">
          <el-input
              prefix-icon="el-icon-lock"
              placeholder="请输入密码"
              v-model="password"
              show-password
              clearable
          ></el-input>
        </div>
      </div>

      <!-- 登录按钮 -->
      <el-button
          class="login-btn"
          type="primary"
          @click="login()"
          :loading="isLoading"
      >
        登录
      </el-button>

      <!-- 启用忘记密码,并绑定点击事件(可后续实现跳转/弹窗逻辑) -->
      <span class="forget" @click="handleForgetPwd">忘记密码?</span>
    </div>
  </div>
</template>

<script>
// 导入登录、获取用户信息接口
import { login, getUser } from '@/api/api'
// 导入锁状态工具函数
import { setLock } from '@/utils/lock'

export default {
  name: 'LoginPage', // 组件命名,便于调试
  data() {
    return {
      username: '', // 用户名绑定值
      password: '', // 密码绑定值
      isLoading: false // 登录按钮加载状态
    }
  },
  methods: {
    // 新增:忘记密码点击事件(可后续扩展弹窗/跳转逻辑)
    handleForgetPwd() {
      this.$message.info('忘记密码功能待实现,可跳转至密码重置页面');
      // 示例:跳转至忘记密码页面
      // this.$router.push("/forget-password");
    },
    // 登录核心方法
    async login() {
      // 1. 输入校验
      if (!this.username) {
        this.$message.warning('请输入用户名');
        return;
      }
      if (!this.password) {
        this.$message.warning('请输入密码');
        return;
      }

      // 2. 发起登录请求
      try {
        this.isLoading = true; // 开启加载状态
        const params = { username: this.username, password: this.password };
        const res = await login(params);

        // 3. 登录成功处理
        if (res.code === 1000) {
          this.$message.success('登录成功');
          // 存储token到Vuex
          this.$store.commit('user/setToken', res.data.token);
          // 获取并存储用户信息
          await this.getUserInfo();
          // 解锁状态
          setLock(false);
          // 延迟跳转首页(优化体验)
          setTimeout(() => {
            this.$router.push("/index");
          }, 500);
        } else {
          this.$message.error(res.message || '登录失败,请重试');
        }
      } catch (error) {
        // 网络/接口异常处理
        this.$message.error('网络异常,请检查后重试');
        console.error('登录请求失败:', error);
      } finally {
        this.isLoading = false; // 关闭加载状态
      }
    },

    // 获取用户信息方法
    async getUserInfo() {
      try {
        const res = await getUser();
        if (res.code === 1000) {
          // 存储用户信息到Vuex(转字符串便于存储)
          this.$store.commit('user/setUser', JSON.stringify(res.data));
        } else {
          this.$message.warning('用户信息获取失败');
        }
      } catch (error) {
        this.$message.error('获取用户信息异常');
        console.error('获取用户信息失败:', error);
      }
    }
  },
  created() {},
  mounted() {}
}
  • learn_front_mange-master/src/api/api.js - API接口封装
dart 复制代码
//-------------------------------登陆---------------------------------------
// 登陆
export const login = (params) => post("/login",params)
//登出
export const logout = () => get("/login/logout")
//修改密码
export const changePassword = (params) => post("/user/changePassword",params)
  • learn_front_mange-master/src/utils/request.js - HTTP请求拦截器 (1)核心:Axios 实例创建 + 拦截器核心逻辑
javascript 复制代码
import axios from 'axios'
import store from '@/store'
import { MessageBox } from 'element-ui'

// 1. 创建axios实例(基础配置)
const instance = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 5000,
  headers: { 'Content-Type': 'application/json;charset=UTF-8' }
})

// 2. 登出刷新工具方法(核心)
const handleLogoutAndReload = async () => {
  try {
    await store.dispatch('user/logout')
    store.commit('menu/setMenus', [])
    store.commit('menu/setRoutes', [])
    location.reload()
  } catch (error) {
    location.reload()
  }
}

// 3. 请求拦截器(token + 防缓存)
instance.interceptors.request.use(
  (config) => {
    // 防缓存
    if (config.method?.toLowerCase() === 'post') {
      config.data = { ...config.data, _t: Date.parse(new Date()) / 1000 }
    } else if (config.method?.toLowerCase() === 'get') {
      config.params = { random: Math.random(), ...config.params }
    }
    // 携带token
    const token = localStorage.getItem('token')
    token && (config.headers['x_access_token'] = token)
    return config
  },
  (error) => Promise.reject(error)
)

// 4. 响应拦截器(异常统一处理)
instance.interceptors.response.use(
  (response) => {
    const { data, status } = response
    // HTTP状态校验
    if (status < 200 || status >= 300) {
      MessageBox.alert('系统内部错误', '错误', { type: 'error' })
      return Promise.reject(new Error('请求异常'))
    }
    // 登录失效/账号锁定
    const loginErrorCodes = [1006, 1008, 1011]
    if (loginErrorCodes.includes(data.code) || data.code === 1009) {
      !document.querySelector('.el-message-box__title') && MessageBox.alert(
        data.code === 1009 ? '账号已被锁定' : '登录已过期,请重新登录',
        '错误',
        { confirmButtonText: '确定', type: 'error', closeOnClickModal: false }
      ).then(handleLogoutAndReload)
      return Promise.reject(new Error(data.code === 1009 ? '账号锁定' : '登录过期'))
    }
    return data
  },
  (error) => {
    !document.querySelector('.el-message-box__title') && MessageBox.alert(
      error.message.includes('timeout') ? '请求超时' : '网络异常',
      '错误', { type: 'error' }
    )
    return Promise.reject(error)
  }
)

// 5. 封装GET/POST(核心调用方法)
export const get = (url, params = {}) => instance.get(url, { params })
export const post = (url, data = {}) => instance.post(url, data)
export default instance

(2)关键代码块拆解(核心功能)

代码块类型 核心作用
实例创建 统一配置接口前缀、超时时间、默认请求头,避免重复配置
请求拦截器 自动加防缓存参数(解决 GET/POST 缓存问题)+ 自动携带 token(身份校验)
响应拦截器 统一处理:1. HTTP 状态异常2. 登录失效 / 账号锁定3. 网络 / 超时异常
登出工具方法 登出时清空 Vuex 状态 + 刷新页面,保证状态一致性
GET/POST 封装 简化业务层调用,无需关心 axios 原生参数格式
  1. 后端登录控制器(仅展示部分核心代码)
  • learn_server-master/ape-admin/src/main/java/com/ape/apeadmin/controller/login/LoginController.java - 核心登录逻辑
typescript 复制代码
/**
 * 用户登录接口
 * @param request HTTP请求对象(用于获取IP、User-Agent等)
 * @param jsonObject 请求体(包含username、password)
 * @return 登录结果(成功返回token,失败返回错误信息)
 */
@PostMapping
public Result login(HttpServletRequest request, @RequestBody JSONObject jsonObject) {
    try {
        // 1. 获取请求参数和客户端信息
        String ipAddr = RequestUtils.getRemoteHost(request);
        String username = jsonObject.getString("username");
        String password = jsonObject.getString("password");

        // 参数非空校验
        if (username == null || username.trim().isEmpty()) {
            saveLoginLog(request, "用户名为空", "", ipAddr, 1);
            return Result.fail("用户名不能为空!");
        }
        if (password == null || password.trim().isEmpty()) {
            saveLoginLog(request, "密码为空", username, ipAddr, 1);
            return Result.fail("密码不能为空!");
        }

        // 2. 查询用户信息
        QueryWrapper<ApeUser> query = new QueryWrapper<>();
        query.lambda().eq(ApeUser::getLoginAccount, username);
        ApeUser apeUser = apeUserService.getOne(query);

        // 3. 用户名不存在校验
        if (apeUser == null) {
            saveLoginLog(request, "用户名不存在", username, ipAddr, 1);
            return Result.fail("用户名不存在!");
        }

        // 4. 密码校验(解密对比)
        boolean decrypt = PasswordUtils.decrypt(password, apeUser.getPassword() + "$" + apeUser.getSalt());
        if (!decrypt) {
            saveLoginLog(request, "用户名或密码错误", username, ipAddr, 1);
            return Result.fail("用户名或密码错误!");
        }

        // 5. 用户状态校验(1=禁用)
        if (apeUser.getStatus() == 1) {
            saveLoginLog(request, "用户被禁用", username, ipAddr, 1);
            return Result.fail("用户被禁用!");
        }

        // 6. 登录成功:生成JWT token并返回
        String token = JwtUtil.sign(apeUser.getId(), password);
        JSONObject resultJson = new JSONObject();
        resultJson.put("token", token);
        saveLoginLog(request, "登录成功", username, ipAddr, 0);
        logger.info("用户{}登录成功,IP:{}", username, ipAddr);
        return Result.success(resultJson);
    } catch (Exception e) {
        logger.error("登录接口异常", e);
        saveLoginLog(request, "登录系统异常", jsonObject.getString("username"), RequestUtils.getRemoteHost(request), 1);
        return Result.fail("登录失败,请联系管理员!");
    }
}

/**
 * 用户注册接口
 * @param apeUser 注册用户信息(包含loginAccount、password、userType等)
 * @return 注册结果
 */
@PostMapping("/register")
public Result register(@RequestBody ApeUser apeUser) {
    try {
        // 1. 校验登录账号是否已存在
        boolean isAccountValid = checkAccount(apeUser);
        if (!isAccountValid) {
            logger.warn("注册失败:登录账号{}已存在", apeUser.getLoginAccount());
            return Result.fail("登录账号已存在,不可重复注册!");
        }

        // 2. 密码加盐加密处理
        String encrypt = PasswordUtils.encrypt(apeUser.getPassword());
        String[] split = encrypt.split("\$");
        if (split.length != 2) {
            logger.error("密码加密异常,加密结果:{}", encrypt);
            return Result.fail("密码加密失败,请重试!");
        }
        apeUser.setPassword(split[0]); // 加密后的密码
        apeUser.setSalt(split[1]);     // 盐值
        apeUser.setAvatar("/img/avatar.jpg"); // 默认头像
        apeUser.setPwdUpdateDate(new Date()); // 密码更新时间
        apeUser.setStatus(0); // 默认启用状态(0=启用,1=禁用)

        // 3. 保存用户信息
        boolean save = apeUserService.save(apeUser);
        if (save) {
            logger.info("用户{}注册成功", apeUser.getLoginAccount());
            return Result.success("注册成功!");
        } else {
            logger.warn("用户{}注册失败:保存用户信息失败", apeUser.getLoginAccount());
            return Result.fail("注册失败,请重试!");
        }
    } catch (Exception e) {
        logger.error("注册接口异常", e);
        return Result.fail("注册失败,请联系管理员!");
    }
}
  1. 密码处理工具
  • learn_server-master/ape-common/src/main/java/com/ape/apecommon/utils/PasswordUtils.java - 密码加密/解密工具
typescript 复制代码
// 静态加密方法:无盐值入参,自动生成随机盐值
public static String encrypt(String password) {
    // 1. 生成32位随机盐值:UUID去除横线(UUID原始格式含4个横线,移除后为32位)
    String salt = UUID.randomUUID().toString().replace("-", "");
    // 2. 加密核心逻辑:盐值+明文密码拼接后,通过MD5生成16进制哈希字符串
    String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
    // 3. 拼接盐值和加密密码:用$分隔,便于后续解密时拆分盐值
    String dbPassword = salt + "$" + finalPassword;
    // 返回最终加密结果(存入数据库的密码格式)
    return dbPassword;
}
/**
     * @description: 加盐加密(指定盐值)
     * @param: password 明文密码
     * @param: salt 自定义盐值(通常为数据库中存储的盐值)
     * @return: String 加密结果,格式为「盐值$32位MD5加密密码」
     * @author shaozhujie
     * @date: 2023/9/1 10:21
     */
    // 重载加密方法:手动传入盐值,用于解密验证时生成对比密码
    public static String encrypt(String password, String salt) {
        // 1. 加密核心逻辑:指定盐值+明文密码拼接后,生成MD5哈希字符串
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        // 2. 拼接盐值和加密密码:保持与自动生成盐值的格式一致
        String dbPassword = salt + "$" + finalPassword;
        // 返回拼接后的加密结果
        return dbPassword;
    }

    /**
     * @description: 验证加盐加密密码
     * @param: password 待验证的明文密码
     * @param: dbPassword 数据库存储的加密密码(格式:盐值$MD5加密密码)
     * @return: boolean 验证结果(true=密码正确,false=密码错误/参数异常)
     * @author shaozhujie
     * @date: 2023/9/1 10:21
     */
    // 静态解密验证方法:对比明文密码+盐值加密后是否与库中密码一致
    public static boolean decrypt(String password, String dbPassword) {
        // 初始化验证结果为false(默认密码错误)
        boolean result = false;
        // 参数合法性校验:
        // 1. 明文密码非空且有长度 2. 库中密码非空且有长度 
        // 3. 库中密码长度必须为65(32位盐+1位$+32位MD5) 4. 库中密码包含$分隔符
        if (StringUtils.hasLength(password) && StringUtils.hasLength(dbPassword) &&
                dbPassword.length() == 65 && dbPassword.contains("$")) {
            // 1. 按$拆分库中密码($需转义,避免正则匹配),得到盐值和加密密码数组
            String[] passwrodArr = dbPassword.split("\$");
            // 1.1 提取拆分后的盐值(数组第一个元素)
            String salt = passwrodArr[0];
            // 2. 用待验证的明文密码+库中盐值重新加密,生成对比密码
            String checkPassword = encrypt(password, salt);
            // 对比:重新加密的密码是否与库中密码完全一致
            if (dbPassword.equals(checkPassword)) {
                // 一致则验证成功,修改结果为true
                result = true;
            }
        }
        // 返回最终验证结果(参数异常/密码错误均返回false)
        return result;
    }
}
  • learn_server-master/ape-common/src/main/java/com/ape/apecommon/utils/JwtUtil.java - JWT令牌生成工具
java 复制代码
/**
 * JWT工具类
 * 核心功能:
 * 1. 生成JWT token(附带userId,基于HMAC256签名,缓存到Redis)
 * 2. 验证token有效性(签名+userId声明校验)
 * 3. 解析token中的userId
 * 4. 从HTTP请求头中提取token/解析userId
 *
 * @author shaozhujie
 * @version 1.0
 * @date 2023/8/11 10:00
 */
public class JwtUtil {
    // 日志记录器(便于排查token生成/验证异常)
    private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    /**
     * Redis中token过期天数(实际生效的过期时间,JWT本身无过期)
     */
    public static final int REDIS_TOKEN_EXPIRE_DAYS = 3;

    // 从Spring容器获取Redis模板(懒加载,避免容器未初始化时获取失败)
    private static volatile StringRedisTemplate stringRedisTemplate;

    /**
     * 懒加载获取StringRedisTemplate Bean
     */
    private static StringRedisTemplate getStringRedisTemplate() {
        if (stringRedisTemplate == null) {
            synchronized (JwtUtil.class) {
                if (stringRedisTemplate == null) {
                    stringRedisTemplate = SpringUtils.getBean(StringRedisTemplate.class);
                }
            }
        }
        return stringRedisTemplate;
    }

    /**
     * 校验token是否正确
     *
     * @param token     待验证的JWT token
     * @param userId    预期的用户ID(校验token中的userId声明)
     * @param userPhone 签名秘钥(用户手机号,HMAC256加密用)
     * @return boolean 验证结果(true=有效,false=无效/异常)
     */
    public static boolean verify(String token, String userId, String userPhone) {
        // 前置参数校验
        if (!hasText(token) || !hasText(userId) || !hasText(userPhone)) {
            logger.warn("token验证失败:参数为空(token:{},userId:{})", token, userId);
            return false;
        }

        try {
            // 生成HMAC256算法(手机号作为秘钥)
            Algorithm algorithm = Algorithm.HMAC256(userPhone);
            // 创建验证器:校验签名 + userId声明匹配
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("userId", userId)
                    .build();
            // 执行验证
            verifier.verify(token);
            logger.info("token验证成功,userId:{}", userId);
            return true;
        } catch (Exception e) {
            logger.error("token验证失败(userId:{})", userId, e);
            return false;
        }
    }

    /**
     * 解析token中的userId(仅解析,不验证签名)
     *
     * @param token 待解析的JWT token
     * @return String userId(解析失败返回null)
     */
    public static String getUserId(String token) {
        if (!hasText(token)) {
            logger.warn("解析userId失败:token为空");
            return null;
        }

        try {
            DecodedJWT jwt = JWT.decode(token);
            String userId = jwt.getClaim("userId").asString();
            if (!hasText(userId)) {
                logger.warn("解析userId失败:token中无有效userId声明");
                return null;
            }
            return userId;
        } catch (Exception e) {
            logger.error("解析token中的userId失败", e);
            return null;
        }
    }

    /**
     * 生成JWT token(并缓存到Redis)
     *
     * @param userId    要存入token的用户ID
     * @param userPhone 签名秘钥(用户手机号)
     * @return String 生成的token(生成失败返回null)
     */
    public static String sign(String userId, String userPhone) {
        // 前置参数校验
        if (!hasText(userId) || !hasText(userPhone)) {
            logger.warn("生成token失败:userId或userPhone为空(userId:{})", userId);
            return null;
        }

        try {
            // 生成HMAC256加密算法
            Algorithm algorithm = Algorithm.HMAC256(userPhone);
            // 生成token(附带userId声明)
            String token = JWT.create()
                    .withClaim("userId", userId)
                    .sign(algorithm);

            // 缓存到Redis
            String redisKey = Constants.PREFIX_USER_TOKEN + userId;
            getStringRedisTemplate().opsForValue()
                    .set(redisKey, token, REDIS_TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);

            logger.info("生成token成功并缓存到Redis,userId:{},过期天数:{}", userId, REDIS_TOKEN_EXPIRE_DAYS);
            return token;
        } catch (Exception e) {
            logger.error("生成token失败(userId:{})", userId, e);
            return null;
        }
    }
  1. 用户服务层
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/service/ApeUserService.java - 用户服务接口
markdown 复制代码
/**
 * 用户模块业务层接口
 * <p>
 * 核心职责:
 * 1. 继承MyBatis-Plus的IService,复用通用CRUD方法(save/delete/update/getById等);
 * 2. 定义用户模块专属的自定义业务方法(分页查询、账号校验、密码重置等)。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/28
 */
public interface ApeUserService extends IService<ApeUser> {

    /**
     * 分页查询用户列表(支持多条件动态筛选)
     * <p>
     * 筛选规则:根据ApeUser对象中非空字段作为查询条件,例如:
     * - apeUser.setLoginAccount("admin") → 按登录账号模糊查询
     * - apeUser.setStatus(0) → 按启用状态查询
     * - apeUser.setUserType("admin") → 按用户类型查询
     * 字段为空则不参与筛选,默认按创建时间降序排序。
     * </p>
     *
     * @param apeUser 分页查询筛选条件(需包含分页参数:pageNum-当前页、pageSize-每页条数)
     * @return Page<ApeUser> 分页结果对象,包含:
     *         - getTotal():总记录数
     *         - getPages():总页数
     *         - getCurrent():当前页码
     *         - getSize():每页条数
     *         - getRecords():当前页用户数据列表
     * @author shaozhujie
     * @date 2023/8/28 10:49
     */
    Page<ApeUser> getUserPage(ApeUser apeUser);
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/service/impl/ApeUserServiceImpl.java - 用户服务实现
scala 复制代码
/**
 * 用户服务实现类
 * <p>
 * 核心职责:
 * 1. 继承ServiceImpl,复用MyBatis-Plus基础CRUD实现;
 * 2. 实现ApeUserService接口的自定义方法,落地业务逻辑;
 * 3. 对接Mapper层,完成数据库交互。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/28
 */
@Service
public class ApeUserServiceImpl extends ServiceImpl<ApeUserMapper, ApeUser> implements ApeUserService {

    // 日志记录器(用于记录业务操作日志和异常)
    private static final Logger logger = LoggerFactory.getLogger(ApeUserServiceImpl.class);

    /**
     * 分页查询用户列表(支持多条件动态筛选)
     *
     * @param apeUser 分页参数+筛选条件(pageNumber-当前页、pageSize-页大小、loginAccount-账号、status-状态等)
     * @return Page<ApeUser> 分页结果(包含总条数、总页数、当前页数据)
     */
    @Override
    public Page<ApeUser> getUserPage(ApeUser apeUser) {
        try {
            // 1. 参数校验:防止分页参数为空/非法
            Assert.notNull(apeUser, "分页查询参数不能为空");
            Assert.isTrue(apeUser.getPageNumber() >= 1, "当前页码必须大于等于1");
            Assert.isTrue(apeUser.getPageSize() >= 1 && apeUser.getPageSize() <= 100, "每页条数必须在1-100之间");

            // 2. 构建分页对象
            Page<ApeUser> page = new Page<>(apeUser.getPageNumber(), apeUser.getPageSize());
            logger.info("开始分页查询用户列表:页码={},页大小={},筛选条件={}",
                         apeUser.getPageNumber(), apeUser.getPageSize(), apeUser);

            // 3. 调用Mapper层执行分页查询
            Page<ApeUser> userPage = baseMapper.getUserPage(page, apeUser);

            logger.info("分页查询用户列表成功:总条数={},总页数={}",
                         userPage.getTotal(), userPage.getPages());
            return userPage;
        } catch (IllegalArgumentException e) {
            logger.error("分页查询用户列表失败:参数非法 - {}", e.getMessage());
            throw e; // 抛出参数异常,上层统一处理
        } catch (Exception e) {
            logger.error("分页查询用户列表失败:数据库交互异常", e);
            throw new RuntimeException("查询用户列表失败,请联系管理员", e); // 封装通用异常
        }
    }
  1. 数据访问层
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/mapper/ApeUserMapper.java - MyBatis Mapper接口
markdown 复制代码
/**
 * 用户Mapper接口(数据库操作层)
 * <p>
 * 核心职责:
 * 1. 继承BaseMapper<ApeUser>,复用MyBatis-Plus内置的基础CRUD SQL映射(无需编写XML);
 * 2. 定义自定义SQL映射方法(如分页查询),对接XML/注解SQL完成复杂查询。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/28
 */
public interface ApeUserMapper extends BaseMapper<ApeUser> {

    /**
     * 分页查询用户列表(支持多条件动态筛选)
     * <p>
     * MyBatis-Plus分页插件核心逻辑:
     * 1. Page对象会自动拦截SQL,添加LIMIT/OFFSET分页条件;
     * 2. 自动执行COUNT查询,封装总条数、总页数到Page对象;
     * 3. 筛选条件通过ApeUser实体的非空字段动态拼接。
     * </p>
     *
     * @param page     分页参数对象(必填,包含pageNum-当前页码、pageSize-每页条数)
     * @param apeUser  筛选条件对象(可选,如loginAccount-账号、status-状态、userType-用户类型等)
     *                 @Param("ew") 命名为"ew",兼容MyBatis-Plus条件构造器的参数命名规范
     * @return Page<ApeUser> 分页结果对象,包含:
     *         - getTotal():总记录数
     *         - getPages():总页数
     *         - getRecords():当前页用户数据列表
     *         - getCurrent():当前页码
     *         - getSize():每页条数
     */
    Page<ApeUser> getUserPage(Page<ApeUser> page, @Param("ew") ApeUser apeUser);
  • learn_server-master/ape-system/src/main/resources/mapper/user/ApeUserMapper.xml - SQL映射文件
xml 复制代码
<!--
  分页查询用户列表(关联部门表)
  id:必须与Mapper接口的getUserPage方法名一致
  resultType:查询结果映射的实体类(建议写全类名,避免别名配置问题)
  入参:
    - page:MyBatis-Plus分页参数(自动处理分页,无需手动写LIMIT)
    - ew:ApeUser筛选条件(@Param("ew")命名)
-->
<select id="getUserPage" resultType="com.ape.apesystem.domain.ApeUser">
    select
        <include refid="userBaseColumns"/>,
        <include refid="deptJoinColumns"/>
    from ape_user u
    <!-- 左关联部门表:保证无部门的用户也能被查询到 -->
    left join ape_dept d on u.dept_id = d.id
    <!-- <where>标签:智能处理AND/OR,自动去除多余的前缀关键字 -->
    <where>
        <!-- 固定条件:只查询未逻辑删除的用户 -->
        u.del_flag = 0
        <!-- 动态条件:用户名模糊查询 -->
        <if test="ew.userName != null and ew.userName != ''">
            and u.user_name like concat('%', #{ew.userName}, '%')
        </if>
        <!-- 动态条件:专业模糊查询 -->
        <if test="ew.major != null and ew.major != ''">
            and u.major like concat('%', #{ew.major}, '%')
        </if>
        <!-- 动态条件:学校模糊查询 -->
        <if test="ew.school != null and ew.school != ''">
            and u.school like concat('%', #{ew.school}, '%')
        </if>
        <!-- 动态条件:手机号模糊查询 -->
        <if test="ew.tel != null and ew.tel != ''">
            and u.tel like concat('%', #{ew.tel}, '%')
        </if>
        <!-- 动态条件:用户状态精准查询(0=启用,1=禁用) -->
        <if test="ew.status != null">
            and u.status = #{ew.status}
        </if>
        <!-- 动态条件:用户类型精准查询(如admin/student/teacher) -->
        <if test="ew.userType != null and ew.userType != ''">
            and u.user_type = #{ew.userType}
        </if>
        <!-- 动态条件:部门ID精准查询 -->
        <if test="ew.deptId != null">
            and u.dept_id = #{ew.deptId}
        </if>
    </where>
    <!-- 排序:按创建时间降序,保证分页结果有序 -->
    order by u.create_time desc
</select>
  1. 安全框架集成(展示部分代码)
  • learn_server-master/ape-framework/src/main/java/com/ape/apeframework/utils/ShiroUtils.java - Shiro工具类
typescript 复制代码
public class ShiroUtils {

    /**
    * @description: 获取当前登陆用户
    * @param:
    * @return:
    * @author shaozhujie
    * @date: 2023/9/12 10:54
    */
    public static ApeUser getUserInfo(){
        return (ApeUser) SecurityUtils.getSubject().getPrincipal();
    }

}
  • learn_server-master/ape-framework/src/main/java/com/ape/apeframework/config/ShiroConfig.java - Shiro配置类
typescript 复制代码
/**
 * Shiro核心配置类
 * <p>
 * 核心设计:
 * 1. 基于JWT实现无状态认证(关闭Shiro默认Session);
 * 2. 基于Redis实现权限缓存(替换内存缓存,支持分布式部署);
 * 3. 配置URL过滤链(匿名访问/需要认证的URL);
 * 4. 支持Shiro注解(@RequiresPermissions/@RequiresRoles)。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/11
 */
@Configuration
public class ShiroConfig {
    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 配置Shiro过滤器工厂(核心:URL访问规则+自定义JWT过滤器)
     *
     * @param securityManager Shiro核心安全管理器(自动注入)
     * @return ShiroFilterFactoryBean 过滤器工厂
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        // ========== 1. 配置URL过滤链(LinkedHashMap保证顺序,先匹配优先) ==========
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        // 匿名访问(无需登录)的URL
        filterChainMap.put("/login", "anon");                      // 登录接口
        filterChainMap.put("/login/register", "anon");             // 注册接口
        // 公共数据接口
        filterChainMap.put("/classification/getApeClassificationList", "anon");
        filterChainMap.put("/school/getApeSchoolList", "anon");
        filterChainMap.put("/major/getApeMajorList", "anon");
        // 静态资源/文件接口
        filterChainMap.put("/user/setUserAvatar/**", "anon");      // 头像设置
        filterChainMap.put("/common/**", "anon");                  // 通用接口
        filterChainMap.put("/img/**", "anon");                     // 图片资源
        filterChainMap.put("/video/**", "anon");                   // 视频资源
        filterChainMap.put("/file/**", "anon");                    // 文件资源
        // Swagger/API文档接口(补充:防止文档被拦截)
        filterChainMap.put("/swagger-ui/**", "anon");
        filterChainMap.put("/v3/api-docs/**", "anon");
        filterChainMap.put("/doc.html", "anon");
        // 所有其他URL:必须通过JWT认证
        filterChainMap.put("/**", "jwt");

        // ========== 2. 配置自定义过滤器 ==========
        Map<String, Filter> filters = new HashMap<>(1);
        // 注册JWT过滤器,名称与过滤链中的"jwt"对应
        filters.put("jwt", new JwtFilter());
        shiroFilter.setFilters(filters);

        // ========== 3. 绑定配置 ==========
        shiroFilter.setFilterChainDefinitionMap(filterChainMap);

        logger.info("Shiro过滤链配置完成,匿名URL数量:{}", filterChainMap.entrySet().stream()
                .filter(entry -> "anon".equals(entry.getValue())).count());
        return shiroFilter;
    }
  1. 权限和角色管理(展示部分代码)
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/domain/ApeUser.java - 用户实体类
less 复制代码
/**
 * 用户实体类(对应数据库ape_user表)
 * <p>
 * 字段说明:
 * 1. 基础字段:继承BaseEntity(create_time/update_time/create_by/update_by);
 * 2. 核心字段:用户账号、密码、状态等核心信息;
 * 3. 扩展字段:学校、专业、职称等业务扩展信息;
 * 4. 传输字段:deptName/roleIds等非数据库字段,仅用于业务传输。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/11
 */
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "ape_user", autoResultMap = true) // autoResultMap:自动映射复杂类型
public class ApeUser extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID(雪花算法生成)
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/domain/ApeRole.java - 角色实体类
less 复制代码
/**
 * 角色实体类(对应数据库ape_role表)
 * <p>
 * 核心用途:
 * 1. 映射数据库角色表字段,支撑角色的CRUD操作;
 * 2. 封装分页参数、菜单ID列表等业务传输数据;
 * 3. 继承BaseEntity复用通用审计字段(创建时间/修改人等)。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/31
 */
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "ape_role", autoResultMap = true) // autoResultMap:支持复杂类型自动映射
public class ApeRole extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 角色主键ID(雪花算法生成)
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
  • learn_server-master/ape-system/src/main/java/com/ape/apesystem/domain/ApeMenu.java - 菜单实体类
less 复制代码
/**
 * 菜单实体类(对应数据库ape_menu表)
 * <p>
 * 核心用途:
 * 1. 映射数据库菜单表字段,支撑菜单的CRUD操作;
 * 2. 封装树形菜单结构(children字段),适配前端树形展示;
 * 3. 存储权限标识(perms),支撑Shiro权限校验。
 * </p>
 *
 * @author shaozhujie
 * @version 1.0
 * @since 2023/8/30
 */
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "ape_menu", autoResultMap = true) // 自动映射复杂类型
public class ApeMenu extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 菜单主键ID(雪花算法生成)
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

📊 用户登录认证流程详细解析

🔁 完整流程图

📋 详细步骤分析

第1步:前端发起登录请求

  • 文件: login.vue (第62-84行)
  • 关键代码: login(params).then(res => {...})
  • 功能: 收集用户名密码,调用API发送POST请求到 /login

第2步:后端接收请求

  • 文件: LoginController.java (第50-93行)
  • 关键代码: @PostMapping() 注解的 login() 方法
  • 功能:
    1. 获取客户端IP地址
    2. 查询用户信息
    3. 调用密码验证

第3步:密码验证

  • 文件: PasswordUtils.java (第45-70行)
  • 关键方法: decrypt(String password, String dbPassword)
  • 验证逻辑:
    1. 检查参数有效性(长度、格式)
    2. 分割数据库存储的 salt$encrypted_password
    3. 使用相同盐值重新加密输入密码
    4. 比较加密结果是否一致

第4步:生成JWT令牌

  • 文件: JwtUtil.java (第74-87行)
  • 关键方法: sign(String userId, String userPhone)
  • 生成逻辑:
    1. 使用用户手机号作为HMAC256密钥
    2. 将userId作为claim嵌入token
    3. 将token存入Redis缓存(3天过期)

第5步:返回响应

  • 文件: LoginController.java (第85-92行)
  • 返回格式: {code: 1000, data: {token: "xxx"}}
  • 前端处理: 存储token到Vuex,并跳转到首页

第6步:后续请求认证

  • 文件: ShiroUtils.java 和拦截器配置
  • 认证机制: 请求头携带 X-Access-Token,后端验证JWT有效性
相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端