首先是 JWT
java
package org.springblade.community.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.WeakKeyException;
import org.springblade.community.config.CommunityLoginConfig;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 社区端JWT工具类(适配 JJWT 0.13.0 版本)
*/
@Component
public class CommunityJwtUtil {
/**
* 生成token(0.13.0 版本正确写法)
*/
public String generateToken(Long patientId, String phone) {
try {
// 1. 生成合规的密钥(0.13.0 要求 HS256 密钥必须 ≥ 32 字节)
SecretKey secretKey = generateSecretKey();
// 2. 构建用户信息Claims
Map<String, Object> claims = new HashMap<>();
claims.put("patientId", patientId);
claims.put("phone", phone);
// 3. 生成Token(0.13.0 无需显式指定算法,密钥已包含算法信息)
return Jwts.builder()
.claims(claims) // 替代旧版 setClaims
.subject(phone)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + CommunityLoginConfig.JWT_EXPIRE))
.signWith(secretKey) // 仅需传入密钥,自动匹配算法
.compact();
} catch (WeakKeyException e) {
throw new RuntimeException("JWT密钥长度不足,HS256算法要求密钥至少32字节", e);
} catch (Exception e) {
throw new RuntimeException("生成Token失败", e);
}
}
/**
* 解析token获取Claims(0.13.0 版本正确写法)
*/
public Claims parseToken(String token) {
try {
SecretKey secretKey = generateSecretKey();
// 0.13.0 版本 parserBuilder() 用法正确,核心是密钥合规
return Jwts.parser() // 0.13.0 也可以用 parser(),和 parserBuilder() 等效
.verifyWith(secretKey) // 替代旧版 setSigningKey
.build()
.parseSignedClaims(token) // 替代旧版 parseClaimsJws
.getPayload(); // 替代旧版 getBody
} catch (WeakKeyException e) {
throw new RuntimeException("JWT密钥长度不足", e);
} catch (Exception e) {
throw new RuntimeException("解析Token失败:" + e.getMessage(), e);
}
}
/**
* 验证token是否过期
*/
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
/**
* 生成合规的JWT密钥(适配 0.13.0 版本)
*/
private SecretKey generateSecretKey() {
String secret = CommunityLoginConfig.JWT_SECRET;
// 1. 检查密钥长度,不足则补全(HS256 要求 ≥ 32 字节)
if (secret.length() < 32) {
// 补全到32字节(示例:不足部分用下划线填充,你也可以自定义规则)
secret = String.format("%-32s", secret).replace(' ', '_');
}
// 2. 生成密钥(0.13.0 推荐用 Keys.hmacShaKeyFor)
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 刷新Token(核心方法)
* @param oldToken 旧的Token(无需Bearer前缀)
* @return 新的Token
*/
public String refreshToken(String oldToken) {
try {
// 1. 解析旧Token(即使过期也先解析,后续单独判断过期时间)
Claims claims = parseTokenIgnoreExpiration(oldToken);
// 2. 获取旧Token中的用户信息
Long patientId = Long.valueOf(claims.get("patientId").toString());
String phone = claims.get("phone").toString();
// 3. 可选:校验旧Token过期时间(允许过期30分钟内刷新,避免无限刷新)
Date expiration = claims.getExpiration();
long expiredTime = System.currentTimeMillis() - expiration.getTime();
long maxRefreshExpiredTime = 30 * 60 * 1000; // 30分钟,可配置化
if (expiredTime > maxRefreshExpiredTime) {
throw new RuntimeException("Token已过期过久,无法刷新,请重新登录");
}
// 4. 生成新Token(复用原有生成逻辑)
return generateToken(patientId, phone);
} catch (Exception e) {
throw new RuntimeException("刷新Token失败:" + e.getMessage(), e);
}
}
/**
* 解析Token(忽略过期校验,用于刷新场景)
*/
private Claims parseTokenIgnoreExpiration(String token) {
SecretKey secretKey = generateSecretKey();
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
还有jwt配置:
java
package org.springblade.community.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 社区端登录相关配置
*/
@Configuration
public class CommunityLoginConfig {
/**
* 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// JWT配置常量(也可放到application.yml中)
public static final String JWT_SECRET = "community_login_2026@secret_key"; // 密钥,建议放到配置文件
public static final long JWT_EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 过期时间7天
public static final String JWT_HEADER = "Authorization"; // token请求头
}
这样就能实现账号的token生成和 解析了
然后是登录上下文工具:
java
package org.springblade.community.context;
import lombok.Data;
/**
* 社区登录用户上下文(ThreadLocal存储,保证请求线程隔离)
*/
public class CommunityUserContext {
// ThreadLocal存储当前登录用户信息
private static final ThreadLocal<CommunityLoginUser> USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 设置当前登录用户
*/
public static void setUser(CommunityLoginUser user) {
USER_THREAD_LOCAL.set(user);
}
/**
* 获取当前登录用户
*/
public static CommunityLoginUser getUser() {
return USER_THREAD_LOCAL.get();
}
/**
* 获取当前登录用户ID
*/
public static Long getCurrentPatientId() {
CommunityLoginUser user = getUser();
return user == null ? null : user.getPatientId();
}
/**
* 获取当前登录用户手机号
*/
public static String getCurrentPhone() {
CommunityLoginUser user = getUser();
return user == null ? null : user.getPhone();
}
/**
* 清除ThreadLocal(防止内存泄漏)
*/
public static void clear() {
USER_THREAD_LOCAL.remove();
}
/**
* 登录用户信息封装类
*/
@Data
public static class CommunityLoginUser {
/**
* 患者ID
*/
private Long patientId;
/**
* 手机号
*/
private String phone;
}
}
再然后就是接口了:
java
package org.springblade.community.controller;
import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.context.CommunityUserContext;
import org.springblade.community.pojo.dto.AdminResetPwdDTO;
import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;
import org.springblade.community.pojo.dto.RefreshTokenDTO;
import org.springblade.community.service.ICommunityLoginService;
import org.springblade.core.tool.api.R;
import org.springblade.patient.pojo.vo.PatientInfoVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 社区端登录接口
*/
@RestController
@RequestMapping("/community") // 社区端专属前缀
public class CommunityLoginController {
@Autowired
private ICommunityLoginService communityLoginService;
/**
* 社区患者登录接口
*/
@PostMapping("/login")
public R login(@Validated @RequestBody CommunityLoginDTO loginDTO) {
try {
PatientInfoVO loginVO = communityLoginService.login(loginDTO);
return R.data(loginVO);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
/**
* 社区患者Token刷新接口
*/
@PostMapping("/refresh-token")
public R refreshToken(@RequestBody RefreshTokenDTO refreshTokenDTO) {
try {
// 1. 提取旧Token(兼容前端传递的Bearer前缀)
String oldToken = refreshTokenDTO.getOldToken();
if (oldToken.startsWith("Bearer ")) {
oldToken = oldToken.substring(7).trim();
}
// 2. 调用Service刷新Token
String newToken = communityLoginService.refreshToken(oldToken);
// 3. 封装返回结果(包含新Token和过期时间)
Map<String, Object> result = new HashMap<>();
result.put("newToken", newToken);
result.put("expireTime", CommunityLoginConfig.JWT_EXPIRE);
return R.data(result);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
/**
* 重置密码接口(用户自主重置/管理员重置)
* 用户自主重置:需要验证旧密码,确保是本人操作;
* 管理员重置:无需验证旧密码,直接修改用户密码。
*/
@PostMapping("/reset-password")
public R resetPassword(@Validated @RequestBody CommunitySetPasswordDTO passwordDTO) {
try {
communityLoginService.setPassword(passwordDTO);
return R.success("密码修改成功");
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
// ========== 新增测试接口:获取当前登录用户信息 ==========
/**
* 测试接口:获取当前登录用户的详细信息(需携带Token)
*/
@GetMapping("/test/current-user")
public R getCurrentUserInfo() {
try {
// 1. 从ThreadLocal中获取当前登录用户上下文
CommunityUserContext.CommunityLoginUser loginUser = CommunityUserContext.getUser();
if (loginUser == null) {
return R.fail("未获取到用户信息,请先登录");
}
// 2. 封装用户信息返回(可按需扩展字段)
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("患者ID", loginUser.getPatientId());
userInfo.put("手机号", loginUser.getPhone());
// 可选:如果上下文存了完整PatientInfoEntity,可返回更多字段
// userInfo.put("完整用户信息", loginUser.getPatientInfo());
return R.data(userInfo);
} catch (Exception e) {
return R.fail("获取用户信息失败:" + e.getMessage());
}
}
}
java
package org.springblade.community.service;
import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;
import org.springblade.patient.pojo.vo.PatientInfoVO;
/**
* 社区端登录服务接口
*/
public interface ICommunityLoginService {
/**
* 社区患者登录
*/
PatientInfoVO login(CommunityLoginDTO loginDTO);
/**
* 设置/重置患者密码
* 用户自主重置:需要验证旧密码,确保是本人操作;
* 管理员重置:无需验证旧密码,直接修改用户密码。
*/
void setPassword(CommunitySetPasswordDTO passwordDTO);
/**
* 新增患者(注册)时初始化密码(供管理员/注册接口调用)
*/
String encryptPassword(String rawPassword);
String refreshToken(String oldToken);
}
java
package org.springblade.community.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.pojo.dto.CommunityLoginDTO;
import org.springblade.community.pojo.dto.CommunitySetPasswordDTO;
import org.springblade.community.service.ICommunityLoginService;
import org.springblade.community.util.CommunityJwtUtil;
import org.springblade.patient.pojo.entity.PatientInfoEntity;
import org.springblade.patient.pojo.vo.PatientInfoVO;
import org.springblade.patient.service.IPatientInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 社区端登录服务实现
*/
@Service
public class CommunityLoginServiceImpl implements ICommunityLoginService {
@Autowired
private IPatientInfoService patientInfoService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private CommunityJwtUtil jwtUtil;
@Override
public PatientInfoVO login(CommunityLoginDTO loginDTO) {
// 1. 根据手机号查询患者信息(排除已删除、状态异常的用户)
LambdaQueryWrapper<PatientInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(PatientInfoEntity::getPhone, loginDTO.getPhone())
.eq(PatientInfoEntity::getIsDeleted, 0) // 未删除
.isNotNull(PatientInfoEntity::getStatus) // 状态不为空
.eq(PatientInfoEntity::getStatus, 1); // 假设1为正常状态,根据你的业务调整
PatientInfoEntity patient = patientInfoService.getOne(queryWrapper);
if (patient == null) {
throw new RuntimeException("手机号不存在或账号已禁用");
}
// 2. 校验密码(数据库中密码是加密存储的,此处比对)
if (!passwordEncoder.matches(loginDTO.getPassword(), patient.getPassword())) {
throw new RuntimeException("密码错误");
}
// 3. 生成JWT token
String token = jwtUtil.generateToken(patient.getId(), patient.getPhone());
// 4. 封装返回结果
PatientInfoVO loginVO = new PatientInfoVO();
loginVO.setId(patient.getId());
loginVO.setName(patient.getName());
loginVO.setPhone(patient.getPhone());
loginVO.setToken(token);
loginVO.setExpireTime(CommunityLoginConfig.JWT_EXPIRE);
return loginVO;
}
/**
* 设置/重置密码核心逻辑
*/
@Override
public void setPassword(CommunitySetPasswordDTO passwordDTO) {
// 1. 基础参数校验
if (!passwordDTO.getNewPassword().equals(passwordDTO.getConfirmPassword())) {
throw new RuntimeException("新密码与确认密码不一致");
}
// 2. 查询用户是否存在
LambdaQueryWrapper<PatientInfoEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(PatientInfoEntity::getPhone, passwordDTO.getPhone())
.eq(PatientInfoEntity::getIsDeleted, 0);
PatientInfoEntity patient = patientInfoService.getOne(queryWrapper);
if (patient == null) {
throw new RuntimeException("用户不存在");
}
// 3. 非管理员重置时,验证旧密码
if (!passwordDTO.getIsAdminReset()) {
if (!StringUtils.hasText(passwordDTO.getOldPassword())) {
throw new RuntimeException("请输入旧密码");
}
if (!passwordEncoder.matches(passwordDTO.getOldPassword(), patient.getPassword())) {
throw new RuntimeException("旧密码错误");
}
}
// 4. 加密新密码并更新数据库
String encryptedNewPwd = encryptPassword(passwordDTO.getNewPassword());
LambdaUpdateWrapper<PatientInfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(PatientInfoEntity::getId, patient.getId())
.set(PatientInfoEntity::getPassword, encryptedNewPwd)
// 可选:更新修改时间
.set(PatientInfoEntity::getUpdateTime, new java.util.Date());
boolean updateSuccess = patientInfoService.update(updateWrapper);
if (!updateSuccess) {
throw new RuntimeException("密码修改失败");
}
}
/**
* 密码加密工具方法(供注册/重置密码调用)
*/
@Override
public String encryptPassword(String rawPassword) {
// BCrypt加密(不可逆,每次加密结果不同,但校验时可匹配)
return passwordEncoder.encode(rawPassword);
}
@Override
public String refreshToken(String oldToken) {
// 1. 校验旧Token非空
if (oldToken == null || oldToken.trim().isEmpty()) {
throw new RuntimeException("旧Token不能为空");
}
// 2. 调用JwtUtil刷新Token(内部已做合法性校验)
try {
return jwtUtil.refreshToken(oldToken);
} catch (Exception e) {
throw new RuntimeException("Token刷新失败:" + e.getMessage());
}
}
}
接下来是一些dto,vo 这里我就放一起了
java
package org.springblade.community.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 管理员重置患者密码请求参数
*/
@Data
public class AdminResetPwdDTO {
/**
* 患者ID(二选一:ID或手机号必填)
*/
private Long patientId;
/**
* 患者手机号(二选一:ID或手机号必填)
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 新密码(必填,强制复杂度)
*/
@NotBlank(message = "新密码不能为空")
@Pattern(regexp = "^[a-zA-Z0-9@#$%^&*]{6,20}$", message = "新密码需6-20位,包含字母/数字/特殊字符")
private String newPassword;
/**
* 操作管理员ID(必填,用于日志记录)
*/
@NotNull(message = "操作人ID不能为空")
private Long operatorId;
/**
* 操作备注(可选,如"用户忘记密码,人工重置")
*/
private String remark;
}
java
package org.springblade.community.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 社区端患者登录请求参数
*/
@Data
public class CommunityLoginDTO {
/**
* 手机号(必填,格式校验)
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 登录密码(必填)
*/
@NotBlank(message = "密码不能为空")
private String password;
}
java
package org.springblade.community.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 社区患者设置/重置密码请求参数
*/
@Data
public class CommunitySetPasswordDTO {
/**
* 手机号(必填,用于定位用户)
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 旧密码(用户自主重置时必填,管理员重置时可选)
*/
private String oldPassword;
/**
* 新密码(必填,建议增加复杂度校验)
*/
@NotBlank(message = "新密码不能为空")
@Pattern(regexp = "^[a-zA-Z0-9@#$%^&*]{6,20}$", message = "新密码需6-20位,包含字母/数字/特殊字符")
private String newPassword;
/**
* 确认新密码(必填,需和newPassword一致)
*/
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
/**
* 是否管理员重置(true=无需验证旧密码,false=需验证)
*/
private Boolean isAdminReset = false;
}
java
package org.springblade.community.pojo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class RefreshTokenDTO {
@NotBlank(message = "旧Token不能为空")
private String oldToken;
}
接口能跑了之后 ,得需要一个拦截器 拦住这个接口吧:
java
package org.springblade.community.interceptor;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springblade.community.config.CommunityLoginConfig;
import org.springblade.community.context.CommunityUserContext;
import org.springblade.community.util.CommunityJwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 社区端Token拦截器(验证登录状态)
*/
@Slf4j
@Component
public class CommunityTokenInterceptor implements HandlerInterceptor {
@Autowired
private CommunityJwtUtil jwtUtil;
/**
* 请求处理前验证Token
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头获取Token(格式:Bearer xxxxx)
String token = request.getHeader(CommunityLoginConfig.JWT_HEADER);
if (token == null || !token.startsWith("Bearer ")) {
// Token为空或格式错误,返回401未授权
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或Token格式错误\",\"data\":null}");
log.warn("社区端未登录或Token格式错误:{}, 请求路径:{}", token, request.getRequestURI());
return false;
}
// 2. 提取纯Token(去掉Bearer前缀)
String pureToken = token.substring(7);
try {
// 3. 解析Token
Claims claims = jwtUtil.parseToken(pureToken);
// 4. 验证Token是否过期
if (jwtUtil.isTokenExpired(claims)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"Token已过期\",\"data\":null}");
return false;
}
// 5. 封装用户信息并存入ThreadLocal
CommunityUserContext.CommunityLoginUser loginUser = new CommunityUserContext.CommunityLoginUser();
loginUser.setPatientId(Long.valueOf(claims.get("patientId").toString()));
loginUser.setPhone(claims.get("phone").toString());
// 扩展:可根据patientId查询完整用户信息存入(按需)
// loginUser.setPatientInfo(patientInfoService.getById(loginUser.getPatientId()));
CommunityUserContext.setUser(loginUser);
// 6. Token验证通过,放行请求
return true;
} catch (Exception e) {
// Token解析失败(伪造、篡改等)
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"Token无效:" + e.getMessage() + "\",\"data\":null}");
return false;
}
}
/**
* 请求处理完成后清除ThreadLocal(防止内存泄漏)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
CommunityUserContext.clear();
}
}
然后再写一个webConfig配置,拦住所有这个开头得接口,就留下登录接口就行:
java
package org.springblade.community.config;
import org.springblade.community.interceptor.CommunityTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 社区端拦截器配置
*/
@Configuration
public class CommunityWebConfig implements WebMvcConfigurer {
@Autowired
private CommunityTokenInterceptor communityTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册社区端Token拦截器
registry.addInterceptor(communityTokenInterceptor)
// 拦截所有社区端接口
.addPathPatterns("/community/**")
// 放行登录接口(无需Token即可访问)
.excludePathPatterns("/community/login");
// 可选:放行其他无需登录的接口(如验证码、注册等)
// .excludePathPatterns("/api/community/sendCode", "/api/community/register");
}
}