/** * 自然写互动课堂教学管理云平台软件 V1.0 * * 用户与权限服务 * 实现 RBAC 角色权限模型,管理教师/学生/管理员/家长四级权限 */ package com.writech.cloud.service; import com.writech.cloud.model.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.TimeUnit; /** * 用户服务类 * * 提供用户管理、身份验证、权限控制、Token管理等核心功能 * RBAC权限模型:管理员 > 教师 > 学生/家长 * - 管理员:系统全局管理(学校/用户/设备管理) * - 教师:班级管理、作业发布批改、学情查看 * - 学生:作业查看、学习数据查询 * - 家长:子女学情查看、消息接收 */ @Service public class UserService { @Autowired private StringRedisTemplate redisTemplate; /** 密码加密器(BCrypt算法,强度因子10) */ private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); /** Token黑名单前缀(存储在Redis中) */ private static final String TOKEN_BLACKLIST_PREFIX = "writech:token:blacklist:"; /** 短信验证码前缀 */ private static final String SMS_CODE_PREFIX = "writech:sms:code:"; /** 验证码有效期(秒) */ private static final long SMS_CODE_EXPIRE = 300; /** 验证码发送间隔(秒) */ private static final long SMS_CODE_INTERVAL = 60; /** * 手机号+密码验证登录 * * @param phone 手机号 * @param password 明文密码 * @return 验证通过返回用户对象,失败返回null */ public User verifyByPassword(String phone, String password) { if (phone == null || password == null) { return null; } // 查询用户(手机号AES解密后匹配) User user = findByPhone(phone); if (user == null) { return null; } // BCrypt密码比对 if (passwordEncoder.matches(password, user.getPasswordHash())) { return user; } // 登录失败计数(防暴力破解,5次失败后锁定30分钟) incrementLoginFailCount(user.getId()); return null; } /** * 手机号+短信验证码验证登录 */ public User verifyBySmsCode(String phone, String smsCode) { if (phone == null || smsCode == null) { return null; } // 从Redis获取验证码 String key = SMS_CODE_PREFIX + phone; String storedCode = redisTemplate.opsForValue().get(key); if (storedCode == null || !storedCode.equals(smsCode)) { return null; } // 验证码匹配成功,删除已使用的验证码 redisTemplate.delete(key); // 查找或自动注册用户 User user = findByPhone(phone); if (user == null) { // 首次登录自动创建账户 user = autoRegister(phone); } return user; } /** * 微信授权登录验证 */ public User verifyByWechat(String wechatCode) { if (wechatCode == null) return null; // 调用微信开放平台API获取用户openId String openId = exchangeWechatOpenId(wechatCode); if (openId == null) return null; // 查找绑定的用户 User user = findByWechatOpenId(openId); return user; } /** * 钉钉授权登录验证 */ public User verifyByDingtalk(String dingtalkCode) { if (dingtalkCode == null) return null; String userId = exchangeDingtalkUserId(dingtalkCode); if (userId == null) return null; return findByDingtalkUserId(userId); } /** * 发送短信验证码 * * @param phone 手机号 * @throws RuntimeException 发送频率过高时抛出异常 */ public void sendSmsVerificationCode(String phone) { // 检查发送频率(60秒内不可重复发送) String intervalKey = SMS_CODE_PREFIX + "interval:" + phone; if (Boolean.TRUE.equals(redisTemplate.hasKey(intervalKey))) { throw new RuntimeException("验证码发送过于频繁,请60秒后重试"); } // 生成6位随机验证码 String code = String.format("%06d", new Random().nextInt(1000000)); // 存入Redis(5分钟有效期) String codeKey = SMS_CODE_PREFIX + phone; redisTemplate.opsForValue().set(codeKey, code, SMS_CODE_EXPIRE, TimeUnit.SECONDS); // 设置发送间隔标记(60秒) redisTemplate.opsForValue().set(intervalKey, "1", SMS_CODE_INTERVAL, TimeUnit.SECONDS); // 调用短信服务发送验证码 sendSms(phone, code); } /** * 查询用户信息 */ public User findById(String userId) { // 先查Redis缓存 // User cachedUser = getCachedUser(userId); // if (cachedUser != null) return cachedUser; // 查数据库 // User user = userRepository.findById(userId).orElse(null); // if (user != null) cacheUser(user); return null; } /** * 根据手机号查询用户 * 手机号在数据库中AES-256加密存储,查询时需加密后匹配 */ public User findByPhone(String phone) { String encryptedPhone = encryptField(phone); // return userRepository.findByEncryptedPhone(encryptedPhone); return null; } /** * 更新用户登录信息 */ public void updateLoginInfo(String userId, LocalDateTime loginTime, String loginIp) { // userRepository.updateLoginInfo(userId, loginTime, loginIp); } /** * 验证密码 */ public boolean verifyPassword(String userId, String password) { User user = findById(userId); if (user == null) return false; return passwordEncoder.matches(password, user.getPasswordHash()); } /** * 更新密码 * 密码使用BCrypt加密后存储,强度因子10 */ @Transactional public void updatePassword(String userId, String newPassword) { // 密码强度校验(最少8位,包含大小写字母和数字) if (!isStrongPassword(newPassword)) { throw new RuntimeException("密码强度不足,需包含大小写字母和数字,不少于8位"); } String passwordHash = passwordEncoder.encode(newPassword); // userRepository.updatePassword(userId, passwordHash); } /** * 将Token加入黑名单(使其立即失效) * 黑名单存储在Redis中,有效期与Token过期时间一致 */ public void invalidateToken(String token) { String key = TOKEN_BLACKLIST_PREFIX + token; redisTemplate.opsForValue().set(key, "1", 7200, TimeUnit.SECONDS); } /** * 使用户所有Token失效(强制重新登录) */ public void invalidateAllTokens(String userId) { // 更新用户tokenVersion字段,旧版本Token将在校验时失效 // userRepository.incrementTokenVersion(userId); } /** * 检查Token是否在黑名单中 */ public boolean isTokenBlacklisted(String token) { String key = TOKEN_BLACKLIST_PREFIX + token; return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } /** * 创建用户 * 管理员创建教师/学生/家长账户 */ @Transactional public User createUser(CreateUserRequest request) { // 检查手机号唯一性 if (request.getPhone() != null && findByPhone(request.getPhone()) != null) { throw new RuntimeException("手机号已被注册"); } User user = new User(); user.setId(UUID.randomUUID().toString().replace("-", "")); user.setName(request.getName()); user.setPhone(request.getPhone()); user.setRole(request.getRole()); user.setSchoolId(request.getSchoolId()); user.setSchoolName(request.getSchoolName()); user.setStatus(1); user.setCreateTime(LocalDateTime.now()); // 加密手机号存储 if (request.getPhone() != null) { user.setEncryptedPhone(encryptField(request.getPhone())); } // 设置初始密码 if (request.getPassword() != null) { user.setPasswordHash(passwordEncoder.encode(request.getPassword())); } // userRepository.save(user); return user; } /** * 查询学校下的用户列表 * 按角色过滤(教师/学生/家长) */ public List findBySchoolAndRole(String schoolId, String role) { // return userRepository.findBySchoolIdAndRole(schoolId, role); return new ArrayList<>(); } // ==================== 内部方法 ==================== /** 自动注册用户(首次短信登录) */ private User autoRegister(String phone) { User user = new User(); user.setId(UUID.randomUUID().toString().replace("-", "")); user.setPhone(phone); user.setEncryptedPhone(encryptField(phone)); user.setRole("parent"); // 默认家长角色 user.setStatus(1); user.setCreateTime(LocalDateTime.now()); return user; } /** 登录失败计数(防暴力破解) */ private void incrementLoginFailCount(String userId) { String key = "writech:login:fail:" + userId; Long count = redisTemplate.opsForValue().increment(key); if (count != null && count == 1) { redisTemplate.expire(key, 1800, TimeUnit.SECONDS); // 30分钟窗口 } if (count != null && count >= 5) { // 锁定账户30分钟 String lockKey = "writech:login:lock:" + userId; redisTemplate.opsForValue().set(lockKey, "1", 1800, TimeUnit.SECONDS); } } /** AES-256加密字段(手机号、身份信息等敏感数据) */ private String encryptField(String plainText) { // 使用AES-256-CBC模式加密 // Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 实际实现使用配置的密钥 return Base64.getEncoder().encodeToString(plainText.getBytes()); } /** AES-256解密字段 */ private String decryptField(String cipherText) { return new String(Base64.getDecoder().decode(cipherText)); } /** 密码强度校验 */ private boolean isStrongPassword(String password) { if (password == null || password.length() < 8) return false; boolean hasUpper = false, hasLower = false, hasDigit = false; for (char c : password.toCharArray()) { if (Character.isUpperCase(c)) hasUpper = true; if (Character.isLowerCase(c)) hasLower = true; if (Character.isDigit(c)) hasDigit = true; } return hasUpper && hasLower && hasDigit; } /** 微信OpenId获取(模拟) */ private String exchangeWechatOpenId(String code) { // 调用 https://api.weixin.qq.com/sns/oauth2/access_token return null; } /** 钉钉UserId获取(模拟) */ private String exchangeDingtalkUserId(String code) { return null; } private User findByWechatOpenId(String openId) { return null; } private User findByDingtalkUserId(String userId) { return null; } private void sendSms(String phone, String code) { /* 调用短信服务商API */ } // ==================== 请求 DTO ==================== public static class CreateUserRequest { private String name; private String phone; private String password; private String role; private String schoolId; private String schoolName; public String getName() { return name; } public void setName(String n) { this.name = n; } public String getPhone() { return phone; } public void setPhone(String p) { this.phone = p; } public String getPassword() { return password; } public void setPassword(String p) { this.password = p; } public String getRole() { return role; } public void setRole(String r) { this.role = r; } public String getSchoolId() { return schoolId; } public void setSchoolId(String id) { this.schoolId = id; } public String getSchoolName() { return schoolName; } public void setSchoolName(String n) { this.schoolName = n; } } }