🔐 用户登录认证全流程分析
📋 认证流程概览
前端登录请求 → 后端接收 → 密码验证 → JWT令牌生成 → 返回token → 前端存储token → 后续请求携带token → 权限校验
📁 关键文件清单(按执行顺序)
- 前端登录界面和请求
- 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 原生参数格式 |
- 后端登录控制器(仅展示部分核心代码)
- 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("注册失败,请联系管理员!");
}
}
- 密码处理工具
- 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;
}
}
- 用户服务层
- 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); // 封装通用异常
}
}
- 数据访问层
- 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>
- 安全框架集成(展示部分代码)
- 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;
}
- 权限和角色管理(展示部分代码)
- 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() 方法
- 功能:
- 获取客户端IP地址
- 查询用户信息
- 调用密码验证
第3步:密码验证
- 文件: PasswordUtils.java (第45-70行)
- 关键方法: decrypt(String password, String dbPassword)
- 验证逻辑:
- 检查参数有效性(长度、格式)
- 分割数据库存储的 salt$encrypted_password
- 使用相同盐值重新加密输入密码
- 比较加密结果是否一致
第4步:生成JWT令牌
- 文件: JwtUtil.java (第74-87行)
- 关键方法: sign(String userId, String userPhone)
- 生成逻辑:
- 使用用户手机号作为HMAC256密钥
- 将userId作为claim嵌入token
- 将token存入Redis缓存(3天过期)
第5步:返回响应
- 文件: LoginController.java (第85-92行)
- 返回格式: {code: 1000, data: {token: "xxx"}}
- 前端处理: 存储token到Vuex,并跳转到首页
第6步:后续请求认证
- 文件: ShiroUtils.java 和拦截器配置
- 认证机制: 请求头携带 X-Access-Token,后端验证JWT有效性