1 Commits

26 changed files with 75 additions and 1050 deletions

View File

@@ -1,9 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git mv:*)",
"Bash(./mvnw.cmd dependency:resolve:*)",
"Bash(findstr:*)"
"Bash(git mv:*)"
]
}
}

20
pom.xml
View File

@@ -46,7 +46,7 @@
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -68,24 +68,6 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-test</artifactId>

View File

@@ -1,69 +0,0 @@
package com.bicloud.Interceptor;
import com.bicloud.common.context.UserContext;
import com.bicloud.common.utils.JwtUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* JWT认证拦截器
*/
@Slf4j
@Component
public class JwtAuthInterceptor implements HandlerInterceptor {
@Resource
private JwtUtils jwtUtils;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头获取Token
String token = jwtUtils.extractToken(request);
// 2. 验证Token是否存在
if (!StringUtils.hasText(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录,请先登录!\"}");
return false;
}
// 3. 验证Token有效性
if (!jwtUtils.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
return false;
}
// 4. 解析Token获取用户信息
Long userId = jwtUtils.getUserIdFromToken(token);
String username = jwtUtils.getUsernameFromToken(token);
if (userId == null || username == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token解析失败\"}");
return false;
}
// 5. 将用户信息存入ThreadLocal
UserContext.setUserId(userId);
UserContext.setUsername(username);
log.debug("用户认证成功userId: {}, username: {}", userId, username);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清除ThreadLocal防止内存泄漏
UserContext.clear();
}
}

View File

@@ -0,0 +1,50 @@
package com.bicloud.Interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.LocalDateTime;
/*
拦截器,请求打印请求状态
*/
@Component
public class RequestLogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLogInterceptor.class);
private static final String START_TIME = "REQ_START_TIME";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
request.setAttribute(START_TIME, System.currentTimeMillis());
log.info("[REQ] time={}, method={}, uri={}, ip={}",
LocalDateTime.now(),
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
Long start = (Long) request.getAttribute(START_TIME);
long cost = (start == null) ? -1 : (System.currentTimeMillis() - start);
if (ex == null) {
log.info("[RES] status={}, costMs={}, uri={}",
response.getStatus(), cost, request.getRequestURI());
} else {
log.warn("[RES] status={}, costMs={}, uri={}, ex={}",
response.getStatus(), cost, request.getRequestURI(), ex.toString());
}
}
}

View File

@@ -1,46 +0,0 @@
package com.bicloud.common.context;
/**
* 用户上下文 - 使用ThreadLocal存储当前请求的用户信息
*/
public class UserContext {
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
private static final ThreadLocal<String> USERNAME = new ThreadLocal<>();
/**
* 设置用户ID
*/
public static void setUserId(Long userId) {
USER_ID.set(userId);
}
/**
* 获取用户ID
*/
public static Long getUserId() {
return USER_ID.get();
}
/**
* 设置用户名
*/
public static void setUsername(String username) {
USERNAME.set(username);
}
/**
* 获取用户名
*/
public static String getUsername() {
return USERNAME.get();
}
/**
* 清除用户信息
*/
public static void clear() {
USER_ID.remove();
USERNAME.remove();
}
}

View File

@@ -1,16 +0,0 @@
package com.bicloud.common.enums;
/**
* 登录类型枚举
*/
public enum LoginType {
/**
* 密码登录
*/
PASSWORD,
/**
* 短信验证码登录
*/
SMS_CODE
}

View File

@@ -1,15 +0,0 @@
package com.bicloud.common.exception;
/**
* 业务异常类
*/
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,121 +0,0 @@
package com.bicloud.common.utils;
import com.bicloud.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* JWT工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtils {
private final JwtProperties jwtProperties;
private SecretKey secretKey;
@PostConstruct
public void init() {
// 初始化密钥
this.secretKey = Keys.hmacShaKeyFor(
jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
);
}
/**
* 生成Token
*/
public String generateToken(Long userId, String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtProperties.getTokenExpiration() * 1000);
return Jwts.builder()
.subject(String.valueOf(userId))
.claim("username", username)
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
/**
* 解析Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
} catch (Exception e) {
log.error("解析Token失败: {}", e.getMessage());
return null;
}
}
/**
* 从Token中获取用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return null;
}
return Long.parseLong(claims.getSubject());
}
/**
* 从Token中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return null;
}
return claims.get("username", String.class);
}
/**
* 验证Token是否有效
*/
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
if (claims == null) {
return false;
}
// 检查是否过期
Date expiration = claims.getExpiration();
return expiration.after(new Date());
} catch (Exception e) {
log.error("验证Token失败: {}", e.getMessage());
return false;
}
}
/**
* 从请求头中提取Token
*/
public String extractToken(HttpServletRequest request) {
String token = request.getHeader(jwtProperties.getTokenHeader());
if (StringUtils.hasText(token)) {
return token;
}
return null;
}
}

View File

@@ -1,60 +0,0 @@
package com.bicloud.common.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 密码加密工具类
*/
public class PasswordUtils {
private static final SecureRandom RANDOM = new SecureRandom();
/**
* 生成随机盐值
*/
public static String generateSalt() {
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
return Base64.getEncoder().encodeToString(salt);
}
/**
* 使用MD5加盐加密密码
* @param password 原始密码
* @param salt 盐值
* @return 加密后的密码
*/
public static String encryptPassword(String password, String salt) {
try {
// 将密码和盐值拼接
String combined = password + salt;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(combined.getBytes(StandardCharsets.UTF_8));
// 转换为16进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("密码加密失败", e);
}
}
/**
* 验证密码是否正确
* @param inputPassword 输入的密码
* @param salt 盐值
* @param encryptedPassword 加密后的密码
* @return 是否匹配
*/
public static boolean verifyPassword(String inputPassword, String salt, String encryptedPassword) {
String encrypted = encryptPassword(inputPassword, salt);
return encrypted.equals(encryptedPassword);
}
}

View File

@@ -26,8 +26,4 @@ public class RedisConstants {
public static final String FEED_KEY = "feed:";
public static final String SHOP_GEO_KEY = "shop:geo:";
public static final String USER_SIGN_KEY = "sign:";
// JWT相关常量
public static final String JWT_BLACKLIST_KEY = "jwt:blacklist:";
public static final Long JWT_BLACKLIST_TTL = 7L; // 7天与RefreshToken过期时间一致
}

View File

@@ -29,24 +29,6 @@ public class RegexUtils {
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
/**
* 是否是有效手机格式
* @param phone 要校验的手机号
* @return true:有效false无效
*/
public static boolean isPhoneValid(String phone) {
return !isPhoneInvalid(phone);
}
/**
* 是否是有效邮箱格式
* @param email 要校验的邮箱
* @return true:有效false无效
*/
public static boolean isEmailValid(String email) {
return !isEmailInvalid(email);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {

View File

@@ -1,29 +0,0 @@
package com.bicloud.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* JWT配置属性类
*/
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
/**
* JWT密钥
*/
private String secret;
/**
* Token过期时间
*/
private Long tokenExpiration;
/**
* Token请求头名称
*/
private String tokenHeader;
}

View File

@@ -1,8 +1,6 @@
package com.bicloud.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;

View File

@@ -1,6 +1,6 @@
package com.bicloud.config;
import com.bicloud.Interceptor.JwtAuthInterceptor;
import com.bicloud.Interceptor.RequestLogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -8,38 +8,15 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final JwtAuthInterceptor jwtAuthInterceptor;
private final RequestLogInterceptor requestLogInterceptor;
public WebMvcConfig(JwtAuthInterceptor jwtAuthInterceptor) {
this.jwtAuthInterceptor = jwtAuthInterceptor;
public WebMvcConfig(RequestLogInterceptor requestLogInterceptor) {
this.requestLogInterceptor = requestLogInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// JWT认证拦截器
registry.addInterceptor(jwtAuthInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
// Swagger/Knife4j 文档路径
"/doc.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/webjars/**",
"/favicon.ico",
// 静态资源路径放行
"/img/**",
"/video/**",
// 用户相关放行路径
"/user/code",
"/user/register",
"/user/login",
"/course/cats",
"/course/page",
"/document/page",
"/document/types"
);
registry.addInterceptor(requestLogInterceptor)
.addPathPatterns("/**");
}
}

View File

@@ -2,17 +2,11 @@ package com.bicloud.controller;
import com.bicloud.common.result.Result;
import com.bicloud.common.utils.JwtUtils;
import com.bicloud.pojo.dto.LoginDto;
import com.bicloud.pojo.dto.RegisterDto;
import com.bicloud.pojo.vo.LoginVo;
import com.bicloud.pojo.vo.UserInfoVo;
import com.bicloud.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -26,56 +20,14 @@ public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
/**
* 发送手机验证码
*/
@PostMapping("/code")
@PostMapping("code")
@Operation(summary = "发送验证码")
public Result sendCode(@RequestParam("mobile") String mobile, HttpSession session, HttpServletRequest request) {
public Result sendCode(@RequestParam("phone") String phone, HttpSession session, HttpServletRequest request) {
// 发送短信验证码并保存验证码
return userService.sendCode(mobile, session, request);
}
/**
* 用户注册
*/
@PostMapping("/register")
@Operation(summary = "用户注册")
public Result register(@Valid @RequestBody RegisterDto registerDto) {
return userService.register(registerDto);
}
/**
* 用户登录
*/
@PostMapping("/login")
@Operation(summary = "用户登录")
public Result<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
return userService.login(loginDto);
}
/**
* 用户登出
*/
@PostMapping("/logout")
@Operation(summary = "用户登出")
public Result logout(HttpServletRequest request) {
String token = jwtUtils.extractToken(request);
userService.logout(token);
return Result.success("登出成功!");
}
/**
* 获取当前用户信息
*/
@GetMapping("/info")
@Operation(summary = "获取当前用户信息")
public Result<UserInfoVo> getUserInfo() {
UserInfoVo userInfo = userService.getCurrentUserInfo();
return Result.success(userInfo);
return userService.sendCode(phone, session, request);
}
}

View File

@@ -1,38 +0,0 @@
package com.bicloud.mapper;
import com.bicloud.pojo.entity.User;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper {
/**
* 根据手机号查询用户
*/
User selectByMobile(@Param("mobile") String mobile);
/**
* 根据邮箱查询用户
*/
User selectByEmail(@Param("email") String email);
/**
* 根据用户名查询用户
*/
User selectByUsername(@Param("username") String username);
/**
* 插入用户
*/
int insert(User user);
/**
* 根据ID查询用户
*/
User selectById(@Param("id") Long id);
}

View File

@@ -1,29 +0,0 @@
package com.bicloud.pojo.dto;
import com.bicloud.common.enums.LoginType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 登录请求DTO
*/
@Data
@Schema(description = "登录请求")
public class LoginDto {
@Schema(description = "登录标识(手机号/邮箱/用户名)", example = "13800138000")
@NotBlank(message = "登录标识不能为空")
private String identifier;
@Schema(description = "密码(密码登录时必填)", example = "Password123")
private String password;
@Schema(description = "验证码(验证码登录时必填)", example = "123123")
private String code;
@Schema(description = "登录类型PASSWORD-密码登录SMS_CODE-短信验证码登录", example = "PASSWORD")
@NotNull(message = "登录类型不能为空")
private LoginType loginType;
}

View File

@@ -1,55 +0,0 @@
package com.bicloud.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
@Schema(description = "用户注册请求DTO")
public class RegisterDto {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "张三")
private String username;
@NotBlank(message = "所属单位不能为空")
@Schema(description = "所属单位", example = "某某大学")
private String company;
@NotBlank(message = "职位不能为空")
@Schema(description = "职位", example = "研究员")
private String job;
@NotBlank(message = "地址不能为空")
@Schema(description = "所在地址", example = "北京市海淀区")
private String address;
@Schema(description = "感兴趣标签", example = "[\"生物信息学\", \"基因组学\"]")
private java.util.List<String> interestTags;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", example = "13800138000")
private String mobile;
@NotBlank(message = "验证码不能为空")
@Schema(description = "验证码", example = "123123")
private String vcode;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Schema(description = "邮箱地址", example = "example@example.com")
private String email;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^[a-zA-Z\\d]{8,20}$",
message = "密码只能由字母和数字组成长度8-20位")
@Schema(description = "密码", example = "Password123")
private String pwd;
@NotBlank(message = "确认密码不能为空")
@Schema(description = "确认密码", example = "Password123")
private String cpwd;
}

View File

@@ -1,20 +0,0 @@
package com.bicloud.pojo.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String username;
private String company;
private String job;
private String address;
private String introduce;
private String mobile;
private String email;
private String pwd;
private String salt;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -1,30 +0,0 @@
package com.bicloud.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 登录响应VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "登录响应")
public class LoginVo {
@Schema(description = "访问令牌")
private String token;
@Schema(description = "令牌过期时间(秒)", example = "86400")
private Long expiresIn;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String username;
}

View File

@@ -1,35 +0,0 @@
package com.bicloud.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 用户信息VO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户信息")
public class UserInfoVo {
@Schema(description = "用户ID")
private Long id;
@Schema(description = "用户名")
private String username;
@Schema(description = "手机号")
private String phone;
@Schema(description = "邮箱")
private String email;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -1,30 +1,9 @@
package com.bicloud.service;
import com.bicloud.common.result.Result;
import com.bicloud.pojo.dto.LoginDto;
import com.bicloud.pojo.dto.RegisterDto;
import com.bicloud.pojo.vo.LoginVo;
import com.bicloud.pojo.vo.UserInfoVo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
public interface UserService {
Result sendCode(String mobile, HttpSession session, HttpServletRequest request);
Result register(RegisterDto registerDto);
/**
* 用户登录
*/
Result<LoginVo> login(LoginDto loginDto);
/**
* 用户登出
*/
void logout(String token);
/**
* 获取当前用户信息
*/
UserInfoVo getCurrentUserInfo();
Result sendCode(String phone, HttpSession session, HttpServletRequest request);
}

View File

@@ -1,19 +1,7 @@
package com.bicloud.service.impl;
import com.bicloud.common.context.UserContext;
import com.bicloud.common.enums.LoginType;
import com.bicloud.common.exception.BusinessException;
import com.bicloud.common.result.Result;
import com.bicloud.common.utils.JwtUtils;
import com.bicloud.common.utils.PasswordUtils;
import com.bicloud.common.utils.RegexUtils;
import com.bicloud.config.JwtProperties;
import com.bicloud.mapper.UserMapper;
import com.bicloud.pojo.dto.LoginDto;
import com.bicloud.pojo.dto.RegisterDto;
import com.bicloud.pojo.entity.User;
import com.bicloud.pojo.vo.LoginVo;
import com.bicloud.pojo.vo.UserInfoVo;
import com.bicloud.service.UserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
@@ -21,8 +9,6 @@ import jakarta.servlet.http.HttpSession;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static com.bicloud.common.utils.RedisConstants.*;
@@ -34,19 +20,10 @@ public class UserServiceImpl implements UserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private JwtUtils jwtUtils;
@Resource
private JwtProperties jwtProperties;
@Override
public Result sendCode(String mobile, HttpSession session, HttpServletRequest request) {
public Result sendCode(String phone, HttpSession session, HttpServletRequest request) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(mobile)) {
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.error("手机号格式错误!");
}
@@ -55,35 +32,35 @@ public class UserServiceImpl implements UserService {
String clientIpAddr = request.getRemoteAddr();
// 4.如果存在锁,直接返回失败
String phoneGetCodeLock = stringRedisTemplate.opsForValue().get(GET_CODE_LOCK + mobile);
String phoneGetCodeLock = stringRedisTemplate.opsForValue().get(GET_CODE_LOCK + phone);
if (phoneGetCodeLock != null) {
return Result.error("获取验证码过快,请稍后重试!");
}
// 5.检查两个黑名单中的次数
String blacklistPhoneCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_PHONE + mobile);
String blacklistPhoneCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_PHONE + phone);
int getCodePhoneCount = (blacklistPhoneCount != null) ? Integer.parseInt(blacklistPhoneCount) : 0;
if (getCodePhoneCount >= 400) {
return Result.error("获取验证码次数过多,您的手机号已被限制!");
}
// String blacklistIpAddrCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr);
// int getCodeIpAddrCount = (blacklistIpAddrCount != null) ? Integer.parseInt(blacklistIpAddrCount) : 0;
// if (getCodeIpAddrCount >= 300) {
// return Result.error("获取验证码次数过多您的IP已被限制");
// }
String blacklistIpAddrCount = stringRedisTemplate.opsForValue().get(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr);
int getCodeIpAddrCount = (blacklistIpAddrCount != null) ? Integer.parseInt(blacklistIpAddrCount) : 0;
if (getCodeIpAddrCount >= 300) {
return Result.error("获取验证码次数过多您的IP已被限制");
}
// 6.更新锁和黑名单
stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_PHONE + mobile, String.valueOf(getCodePhoneCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
// stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr, String.valueOf(getCodeIpAddrCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
stringRedisTemplate.opsForValue().set(GET_CODE_LOCK + mobile, "1", GET_CODE_LOCK_TTL, TimeUnit.MINUTES);
stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_PHONE + phone, String.valueOf(getCodePhoneCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
stringRedisTemplate.opsForValue().set(GET_CODE_BLACKLIST_IP_ADDR + clientIpAddr, String.valueOf(getCodeIpAddrCount + 1), REFRESH_BLACKLIST_TTL, TimeUnit.HOURS);
stringRedisTemplate.opsForValue().set(GET_CODE_LOCK + phone, "1", GET_CODE_LOCK_TTL, TimeUnit.MINUTES);
// 7.校验通过,生成验证码
// String code = RandomUtil.randomNumbers(6);
String code = "123123"; //为了方便测试,改成固定值
// 8.保存验证码到 redis并上锁
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + mobile, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 9.发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
@@ -91,243 +68,5 @@ public class UserServiceImpl implements UserService {
return Result.success("发送短信验证码成功,验证码:"+code);
}
@Override
public Result register(RegisterDto registerDto) {
// 1. 校验两次密码是否一致
if (!registerDto.getPwd().equals(registerDto.getCpwd())) {
return Result.error("两次密码输入不一致!");
}
// 2. 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + registerDto.getMobile());
if (cacheCode == null) {
return Result.error("验证码已过期,请重新获取!");
}
if (!cacheCode.equals(registerDto.getVcode())) {
return Result.error("验证码错误!");
}
// 3. 检查手机号是否已注册
User existingUserByPhone = userMapper.selectByMobile(registerDto.getMobile());
if (existingUserByPhone != null) {
return Result.error("该手机号已被注册!");
}
// 4.检查邮箱是否已注册
User existingUserByEmail = userMapper.selectByEmail(registerDto.getEmail());
if (existingUserByEmail != null) {
return Result.error("该邮箱已被注册!");
}
// 检查用户名是否已注册
User existingUserByUsername = userMapper.selectByUsername(registerDto.getUsername());
if (existingUserByUsername != null) {
return Result.error("该用户名已被注册!");
}
// 5. 生成盐值
String salt = PasswordUtils.generateSalt();
// 6. 加密密码
String encryptedPassword = PasswordUtils.encryptPassword(registerDto.getPwd(), salt);
// 7. 将感兴趣标签数组转换为字符串
String introduce = (registerDto.getInterestTags() != null && !registerDto.getInterestTags().isEmpty())
? String.join(",", registerDto.getInterestTags())
: null;
// 8. 创建用户对象
User user = new User();
user.setUsername(registerDto.getUsername());
user.setCompany(registerDto.getCompany());
user.setJob(registerDto.getJob());
user.setAddress(registerDto.getAddress());
user.setIntroduce(introduce);
user.setMobile(registerDto.getMobile());
user.setEmail(registerDto.getEmail());
user.setPwd(encryptedPassword);
user.setSalt(salt);
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 9. 保存用户到数据库
int result = userMapper.insert(user);
if (result <= 0) {
return Result.error("注册失败,请稍后重试!");
}
// 10. 删除Redis中的验证码
stringRedisTemplate.delete(LOGIN_CODE_KEY + registerDto.getMobile());
log.info("用户注册成功,手机号:{}", registerDto.getMobile());
return Result.success("注册成功!");
}
@Override
public Result<LoginVo> login(LoginDto loginDto) {
String identifier = loginDto.getIdentifier();
LoginType loginType = loginDto.getLoginType();
// 根据登录类型执行不同的登录逻辑
Result<User> userResult = null;
if (loginType == LoginType.PASSWORD) {
// 密码登录
userResult = loginByPassword(loginDto);
} else if (loginType == LoginType.SMS_CODE) {
// 验证码登录
userResult = loginBySmsCode(loginDto);
} else {
return Result.error("不支持的登录类型!");
}
// 检查登录结果code != 200 表示失败)
if (userResult.getCode() != 200) {
return Result.error(userResult.getMsg());
}
User user = userResult.getData();
// 生成Token
String token = jwtUtils.generateToken(user.getId(), user.getUsername());
// 构建返回结果
LoginVo loginVo = LoginVo.builder()
.token(token)
.expiresIn(jwtProperties.getTokenExpiration())
.userId(user.getId())
.username(user.getUsername())
.build();
return Result.success(loginVo);
}
/**
* 密码登录
*/
private Result<User> loginByPassword(LoginDto loginDto) {
String identifier = loginDto.getIdentifier();
String password = loginDto.getPassword();
// 1. 校验密码不能为空
if (!StringUtils.hasText(password)) {
return Result.error("密码不能为空!");
}
// 2. 根据登录标识类型查询用户
User user = null;
if (RegexUtils.isPhoneValid(identifier)) {
// 手机号登录
user = userMapper.selectByMobile(identifier);
} else if (RegexUtils.isEmailValid(identifier)) {
// 邮箱登录
user = userMapper.selectByEmail(identifier);
} else {
// 用户名登录
user = userMapper.selectByUsername(identifier);
}
// 3. 验证用户是否存在
if (user == null) {
return Result.error("用户不存在!");
}
// 4. 验证密码
if (!PasswordUtils.verifyPassword(password, user.getSalt(), user.getPwd())) {
return Result.error("密码错误!");
}
return Result.success(user);
}
/**
* 短信验证码登录
*/
private Result<User> loginBySmsCode(LoginDto loginDto) {
String identifier = loginDto.getIdentifier();
String code = loginDto.getCode();
// 1. 校验验证码不能为空
if (!StringUtils.hasText(code)) {
return Result.error("验证码不能为空!");
}
// 2. 验证码登录只支持手机号
if (!RegexUtils.isPhoneValid(identifier)) {
return Result.error("验证码登录仅支持手机号!");
}
// 3. 从Redis获取验证码
String cacheCode = null;
try {
cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + identifier);
} catch (Exception e) {
log.error("从Redis获取验证码失败手机号{},错误信息:{}", identifier, e.getMessage());
return Result.error("验证码获取失败,请稍后重试!");
}
// 4. 验证码不存在或已过期
if (cacheCode == null) {
return Result.error("验证码已过期,请重新获取!");
}
// 5. 验证验证码是否正确
if (!cacheCode.equals(code)) {
return Result.error("验证码错误!");
}
// 6. 根据手机号查询用户
User user = null;
try {
user = userMapper.selectByMobile(identifier);
} catch (Exception e) {
log.error("查询用户失败,手机号:{},错误信息:{}", identifier, e.getMessage());
return Result.error("查询用户失败,请稍后重试!");
}
if (user == null) {
return Result.error("该手机号未注册!");
}
// 7. 验证码验证成功后删除Redis中的验证码防止重复使用
try {
stringRedisTemplate.delete(LOGIN_CODE_KEY + identifier);
} catch (Exception e) {
log.error("删除Redis验证码失败手机号{},错误信息:{}", identifier, e.getMessage());
// 删除失败不影响登录,只记录日志
}
return Result.success(user);
}
@Override
public void logout(String token) {
// 简化版不使用黑名单由前端删除token即可
log.info("用户登出成功");
}
@Override
public UserInfoVo getCurrentUserInfo() {
// 1. 从上下文获取用户ID
Long userId = UserContext.getUserId();
if (userId == null) {
throw new BusinessException("未登录!");
}
// 2. 查询用户信息
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在!");
}
// 3. 构建返回结果
return UserInfoVo.builder()
.id(user.getId())
.username(user.getUsername())
.phone(user.getMobile())
.email(user.getEmail())
.createTime(user.getCreateTime())
.build();
}
}

View File

@@ -52,8 +52,3 @@ knife4j:
logging:
level:
com.genepioneer.mapper: debug
jwt:
secret: bicloud-jwt-secret-key-2024-spring-boot-application-secure-token-generation
token-expiration: 86400
token-header: Authorization

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bicloud.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.bicloud.pojo.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="company" property="company"/>
<result column="job" property="job"/>
<result column="address" property="address"/>
<result column="introduce" property="introduce"/>
<result column="mobile" property="mobile"/>
<result column="email" property="email"/>
<result column="pwd" property="pwd"/>
<result column="salt" property="salt"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="selectByMobile" resultMap="BaseResultMap">
SELECT * FROM users WHERE mobile = #{mobile}
</select>
<select id="selectByEmail" resultMap="BaseResultMap">
SELECT * FROM users WHERE email = #{email}
</select>
<select id="selectByUsername" resultMap="BaseResultMap">
SELECT * FROM users WHERE username = #{username}
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.bicloud.pojo.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, company, job, address, introduce, mobile, email, pwd, salt, addtime, updtime)
VALUES (#{username}, #{company}, #{job}, #{address}, #{introduce}, #{mobile}, #{email}, #{pwd}, #{salt}, #{createTime}, #{updateTime})
</insert>
</mapper>

View File

@@ -1,18 +0,0 @@
-- 用户表
CREATE TABLE IF NOT EXISTS `users` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`company` VARCHAR(100) NOT NULL COMMENT '所属单位',
`job` VARCHAR(50) NOT NULL COMMENT '职位',
`address` VARCHAR(200) NOT NULL COMMENT '所在地址',
`introduce` VARCHAR(500) DEFAULT NULL COMMENT '感兴趣标签',
`phone` VARCHAR(11) NOT NULL COMMENT '手机号',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱地址',
`pwd` VARCHAR(64) NOT NULL COMMENT '密码(加密后)',
`salt` VARCHAR(32) NOT NULL COMMENT '盐值',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`) COMMENT '手机号唯一索引',
UNIQUE KEY `uk_email` (`email`) COMMENT '邮箱唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';