From 45b2c3073f2998b56f34d60e883094089ad3b4c2 Mon Sep 17 00:00:00 2001 From: zk Date: Fri, 10 Jul 2026 14:18:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/constant/AdminRedisKeyName.java | 18 ++ .../controller/AdminLoginController.java | 29 ++++ .../pojo/param/login/AdminLoginParam.java | 23 +++ .../admin/service/AdminLoginService.java | 158 ++++++++++++++++++ admin/src/main/resources/static/img/logo.png | Bin 0 -> 9391 bytes .../src/main/resources/static/js/api/auth.js | 9 + admin/src/main/resources/static/js/request.js | 54 ++++++ admin/src/main/resources/templates/login.html | 117 ++++++++++++- .../org/jiayunet/mapper/AdminUserMapper.java | 11 ++ .../java/org/jiayunet/pojo/po/AdminUser.java | 54 ++++++ 10 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 admin/src/main/java/org/jiayunet/admin/constant/AdminRedisKeyName.java create mode 100644 admin/src/main/java/org/jiayunet/admin/controller/AdminLoginController.java create mode 100644 admin/src/main/java/org/jiayunet/admin/pojo/param/login/AdminLoginParam.java create mode 100644 admin/src/main/java/org/jiayunet/admin/service/AdminLoginService.java create mode 100644 admin/src/main/resources/static/img/logo.png create mode 100644 admin/src/main/resources/static/js/api/auth.js create mode 100644 admin/src/main/resources/static/js/request.js create mode 100644 manager/src/main/java/org/jiayunet/mapper/AdminUserMapper.java create mode 100644 manager/src/main/java/org/jiayunet/pojo/po/AdminUser.java diff --git a/admin/src/main/java/org/jiayunet/admin/constant/AdminRedisKeyName.java b/admin/src/main/java/org/jiayunet/admin/constant/AdminRedisKeyName.java new file mode 100644 index 0000000..898eaff --- /dev/null +++ b/admin/src/main/java/org/jiayunet/admin/constant/AdminRedisKeyName.java @@ -0,0 +1,18 @@ +package org.jiayunet.admin.constant; + +/** + * B 端管理后台 Redis Key 常量 + * + * @author zk + */ +public interface AdminRedisKeyName { + + /** 管理员登录 token 前缀,完整 key: admin:login:token:{adminId} */ + String LOGIN_TOKEN = "login:token:"; + + /** 登录失败次数计数,完整 key: admin:login:fail:{username} */ + String LOGIN_FAIL_COUNT = "login:fail:"; + + /** 账号锁定标记,完整 key: admin:login:lock:{username} */ + String LOGIN_LOCK = "login:lock:"; +} diff --git a/admin/src/main/java/org/jiayunet/admin/controller/AdminLoginController.java b/admin/src/main/java/org/jiayunet/admin/controller/AdminLoginController.java new file mode 100644 index 0000000..22ba64e --- /dev/null +++ b/admin/src/main/java/org/jiayunet/admin/controller/AdminLoginController.java @@ -0,0 +1,29 @@ +package org.jiayunet.admin.controller; + +import lombok.AllArgsConstructor; +import org.jiayunet.admin.pojo.param.login.AdminLoginParam; +import org.jiayunet.admin.service.AdminLoginService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 管理员登录控制类 + * + * @author zk + */ +@RestController +@RequestMapping("/public") +@AllArgsConstructor +@Validated +public class AdminLoginController { + + private AdminLoginService adminLoginService; + + @PostMapping("/login") + public Long login(@Validated @RequestBody AdminLoginParam param, HttpServletRequest request, HttpServletResponse response) { + return adminLoginService.login(param.getUsername(), param.getPassword(), request, response); + } +} diff --git a/admin/src/main/java/org/jiayunet/admin/pojo/param/login/AdminLoginParam.java b/admin/src/main/java/org/jiayunet/admin/pojo/param/login/AdminLoginParam.java new file mode 100644 index 0000000..d0dc47f --- /dev/null +++ b/admin/src/main/java/org/jiayunet/admin/pojo/param/login/AdminLoginParam.java @@ -0,0 +1,23 @@ +package org.jiayunet.admin.pojo.param.login; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 管理员登录入参 + * + * @author zk + */ +@Data +public class AdminLoginParam { + + @NotBlank(message = "账号不能为空") + @Size(max = 32, message = "账号长度不能超过32位") + private String username; + + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 32, message = "密码长度6-32位") + private String password; +} diff --git a/admin/src/main/java/org/jiayunet/admin/service/AdminLoginService.java b/admin/src/main/java/org/jiayunet/admin/service/AdminLoginService.java new file mode 100644 index 0000000..f047c38 --- /dev/null +++ b/admin/src/main/java/org/jiayunet/admin/service/AdminLoginService.java @@ -0,0 +1,158 @@ +package org.jiayunet.admin.service; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.admin.constant.AdminRedisKeyName; +import org.jiayunet.exception.BusinessException; +import org.jiayunet.exception.BusinessExpCodeEnum; +import org.jiayunet.mapper.AdminUserMapper; +import org.jiayunet.pojo.login.RedisLoginTokenInfo; +import org.jiayunet.pojo.po.AdminUser; +import org.jiayunet.tool.HttpIpTool; +import org.jiayunet.tool.server.RedisServerTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 管理员登录服务 + *

依赖:AdminUserMapper(管理员查询)、RedisServerTool(token管理与登录失败计数)

+ *

使用表:bg_admin_user(查询管理员账号)

+ *

使用Redis:login:token:{adminId}(登录令牌)、login:fail:{username}(失败计数)、login:lock:{username}(账号锁定)

+ * + * @author zk + */ +@Service +@Slf4j +public class AdminLoginService { + + /** 密码错误允许最大次数 */ + private static final int MAX_FAIL_COUNT = 3; + /** 账号锁定时长(小时) */ + private static final int LOCK_HOURS = 24; + + @Value("${app.secret.token:youweiqingnian123}") + private String secret; + @Value("${app.login.token.exceed_time:43200}") + private int tokenExceedTime; + @Value("${app.login.device_online_quantity:5}") + private int deviceOnlineQuantity; + @Autowired + private AdminUserMapper adminUserMapper; + @Autowired + private RedisServerTool redisServerTool; + + /** + * 账号密码登录 + *

1. 校验账号锁定状态 2. 查询管理员 3. 校验密码(失败累计,3次锁定24小时) 4. 生成JWT 5. 写入Redis管理多设备 6. 设置Cookie

+ */ + public Long login(String username, String password, HttpServletRequest request, HttpServletResponse response) { + Assert.hasText(username, "账号不能为空"); + Assert.hasText(password, "密码不能为空"); + + // 检查账号是否被锁定 + Assert.isTrue(!redisServerTool.hasKey(AdminRedisKeyName.LOGIN_LOCK + username), "账号已被锁定,请24小时后再试"); + + // 查询管理员 + AdminUser admin = adminUserMapper.selectOne(new LambdaQueryWrapper().eq(AdminUser::getUsername, username).eq(AdminUser::getStatus, 0)); + Assert.notNull(admin, "账号或密码错误"); + + // 校验密码:MD5(明文 + salt) + if (!md5(password + secret).equals(admin.getPassword())) { + handleLoginFail(username); + throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号或密码错误"); + } + + // 登录成功,清除失败计数 + clearLoginFail(username); + + // 生成JWT + String uuId = UUID.randomUUID().toString().replace("-", ""); + String token = JWT.create().withClaim("userId", admin.getId()).withClaim("uuId", uuId).sign(Algorithm.HMAC256(secret)); + + // 构建Redis登录信息 + String redisKey = AdminRedisKeyName.LOGIN_TOKEN + admin.getId(); + RedisLoginTokenInfo info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class); + if (info == null) { + info = new RedisLoginTokenInfo(); + info.setUserId(admin.getId()); + info.setAuthority(new ArrayList<>()); + info.setRole(new ArrayList<>()); + } + + // 过滤过期设备 + List devices = info.getLoginDevices() == null ? new ArrayList<>() : info.getLoginDevices(); + long expireMillis = System.currentTimeMillis() - tokenExceedTime * 1000L; + devices = devices.stream().filter(d -> d.getLastLoginTime().isAfter(Instant.ofEpochMilli(expireMillis))).collect(Collectors.toList()); + + // 超过设备上限,移除最早的 + while (devices.size() >= deviceOnlineQuantity) { + devices.stream().min(Comparator.comparing(RedisLoginTokenInfo.LoginDevice::getLastLoginTime)).ifPresent(devices::remove); + } + + // 添加当前设备 + RedisLoginTokenInfo.LoginDevice device = new RedisLoginTokenInfo.LoginDevice(); + device.setUuId(uuId); + device.setLastLoginTime(Instant.now()); + device.setLoginIp(HttpIpTool.gteRealIP(request)); + devices.add(device); + + info.setLoginDevices(devices); + redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS); + + // 设置Cookie + Cookie cookie = new Cookie("Token", token); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(tokenExceedTime); + response.addCookie(cookie); + + return admin.getId(); + } + + /** 处理登录失败:累计失败次数,达到上限则锁定账号24小时 */ + private void handleLoginFail(String username) { + String failKey = AdminRedisKeyName.LOGIN_FAIL_COUNT + username; + Integer failCount = redisServerTool.get(failKey, Integer.class); + int currentCount = (failCount == null ? 0 : failCount) + 1; + if (currentCount >= MAX_FAIL_COUNT) { + redisServerTool.set(AdminRedisKeyName.LOGIN_LOCK + username, true, LOCK_HOURS, TimeUnit.HOURS); + redisServerTool.delete(failKey); + log.warn("管理员账号 [{}] 密码错误{}次,已锁定{}小时", username, MAX_FAIL_COUNT, LOCK_HOURS); + } else { + redisServerTool.set(failKey, currentCount, LOCK_HOURS, TimeUnit.HOURS); + } + } + + /** 登录成功后清除失败计数 */ + private void clearLoginFail(String username) { + redisServerTool.delete(AdminRedisKeyName.LOGIN_FAIL_COUNT + username); + } + + /** MD5 加密 */ + private String md5(String input) { + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { sb.append(String.format("%02x", b)); } + return sb.toString(); + } catch (java.security.NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/admin/src/main/resources/static/img/logo.png b/admin/src/main/resources/static/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..772ed7f4ebfe977271953a2bad0ed28af5c1db9a GIT binary patch literal 9391 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 literal 0 HcmV?d00001 diff --git a/admin/src/main/resources/static/js/api/auth.js b/admin/src/main/resources/static/js/api/auth.js new file mode 100644 index 0000000..ebcbd86 --- /dev/null +++ b/admin/src/main/resources/static/js/api/auth.js @@ -0,0 +1,9 @@ +/** + * 认证相关 API + */ +const authApi = { + /** 登录 */ + login(username, password) { + return request.post('/public/login', { username, password }); + } +}; diff --git a/admin/src/main/resources/static/js/request.js b/admin/src/main/resources/static/js/request.js new file mode 100644 index 0000000..5be5d4c --- /dev/null +++ b/admin/src/main/resources/static/js/request.js @@ -0,0 +1,54 @@ +/** + * 统一请求封装 + */ +const request = (() => { + const BASE_URL = '/admin'; + + /** 从 cookie 中获取 Token */ + function getToken() { + const match = document.cookie.match(/(?:^|;\s*)Token=([^;]*)/); + return match ? match[1] : ''; + } + + /** 统一请求方法 */ + async function http(url, options = {}) { + const config = { + headers: { 'Content-Type': 'application/json', 'Token': getToken(), ...options.headers }, + ...options + }; + const response = await fetch(BASE_URL + url, config); + + // 401 未登录,跳转登录页 + if (response.status === 401) { + window.location.href = BASE_URL + '/page/login'; + return Promise.reject('未登录'); + } + + const result = await response.json(); + + // 业务异常 + if (result.status !== 200) { + const msg = result.message || '请求失败'; + alert(msg); + return Promise.reject(msg); + } + + return result.data; + } + + return { + get(url, params) { + const query = params ? '?' + new URLSearchParams(params).toString() : ''; + return http(url + query, { method: 'GET' }); + }, + post(url, data) { + return http(url, { method: 'POST', body: JSON.stringify(data) }); + }, + put(url, data) { + return http(url, { method: 'PUT', body: JSON.stringify(data) }); + }, + del(url, data) { + return http(url, { method: 'DELETE', body: data ? JSON.stringify(data) : undefined }); + } + }; +})(); diff --git a/admin/src/main/resources/templates/login.html b/admin/src/main/resources/templates/login.html index d58ffa5..7729ed5 100644 --- a/admin/src/main/resources/templates/login.html +++ b/admin/src/main/resources/templates/login.html @@ -3,10 +3,121 @@ - OfferPie 管理后台 - 登录 + OfferPie 管理后台 + + -

OfferPie 管理后台

-

登录页面占位 - 待实现

+
+
+ +
运营管理后台
+ +
+
+ +
+
+ + + + + + + diff --git a/manager/src/main/java/org/jiayunet/mapper/AdminUserMapper.java b/manager/src/main/java/org/jiayunet/mapper/AdminUserMapper.java new file mode 100644 index 0000000..3285362 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/AdminUserMapper.java @@ -0,0 +1,11 @@ +package org.jiayunet.mapper; + +import org.jiayunet.pojo.po.AdminUser; + +/** + * 管理员用户 Mapper + * + * @author zk + */ +public interface AdminUserMapper extends CommonMapper { +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/AdminUser.java b/manager/src/main/java/org/jiayunet/pojo/po/AdminUser.java new file mode 100644 index 0000000..2610b05 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/AdminUser.java @@ -0,0 +1,54 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 管理员用户表(bg_admin_user) + * + * @author zk + */ +@Data +@TableName("bg_admin_user") +public class AdminUser { + + /** 主键 */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 登录账号 */ + private String username; + + /** 密码(BCrypt) */ + private String password; + + /** 真实姓名 */ + private String realName; + + /** 昵称 */ + private String nickName; + + /** 手机号码 */ + private String mobileNumber; + + /** 头像URL */ + private String avatar; + + /** 状态 0=正常 1=禁用 */ + private Integer status; + + /** 创建时间 */ + private Instant createTime; + + /** 更新时间 */ + private Instant updateTime; + + /** 逻辑删除 0=正常 */ + @TableLogic(value = "0", delval = "UNIX_TIMESTAMP()") + private Long isDelete; +}