基于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有效性
相关推荐
大时光1 小时前
gsap 配置解读 --2
前端
万岳科技程序员小金2 小时前
AI数字人小程序源码开发全流程实战:前端交互+后端算法部署指南
前端·人工智能·软件开发·ai数字人小程序·ai数字人系统源码·ai数字人软件开发·ai数字人平台搭建
白开水丶2 小时前
vue3源码学习(五)ref 、toRef、toRefs、proxyRefs 源码学习
前端·vue.js·学习
广州华水科技2 小时前
单北斗GNSS在变形监测中的应用与优势分析
前端
用泥种荷花2 小时前
【LangChain.js学习】大模型分类
前端
掘金安东尼2 小时前
Angular 中的增量水合:构建“秒开且可交互”的 SSR 应用
前端·angular.js
大龄程序员2 小时前
TypeScript 类型体操:如何为 SDK 编写优雅的类型定义
前端·aigc
大龄程序员2 小时前
别再用 ID 定位了!教你用"语义指纹"实现 99% 的元素定位成功率
前端·aigc
RaidenLiu2 小时前
拒绝重写!Flutter Add-to-App 全攻略:让原生应用“渐进式”拥抱跨平台
前端·flutter·前端框架