Commit 3abb82e1 authored by 万成波's avatar 万成波

移动端身份认证

parent eaa38867
...@@ -98,3 +98,11 @@ spring: ...@@ -98,3 +98,11 @@ spring:
jms: jms:
listener: listener:
max-concurrency: 3 max-concurrency: 3
# 企业微信配置
wx:
cp:
corp-id: ww63ca87d5f8647514
app-config:
agent-id: 1000044
secret: GqApjJ2aDuntiU5iQ9yqx8JKQwYDMrg1tTHdeeF0BWA
...@@ -119,3 +119,14 @@ xss: ...@@ -119,3 +119,14 @@ xss:
excludes: /system/notice excludes: /system/notice
# 匹配链接 # 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/* urlPatterns: /system/*,/monitor/*,/tool/*
mobile:
auth:
res-token-name: token
req-token-name: Authorization
issuer: Mobile-Auth
algorithm-id: HS512
sign-key: SignKey2025@.
effective-time: 7d
path-patterns: /bbs/mobile/**
package com.tangguo.common.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
/**
* 实体类参数验证器操作类
*
* @author TanXiao
*
* @createTime 2022-07-28 14:51:58 星期四
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidateOperations {
/**
* 校验器
*/
private static Validator validator;
/**
* 非法参数消息提示类型枚举
*/
public enum PromptEnum {
FIRST, ALL
}
/**
* 初始化参数校验器
*
* @return 默认校验器
*/
private static Validator getDefaultValidator() {
if (Objects.isNull(validator)) {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
return validator;
}
/**
* 非法参数提示信息
*
* @param illegalParams 错误参数
* @param prompt 提示类型
*/
private static void promptMessage(Set<ConstraintViolation<Object>> illegalParams, PromptEnum prompt) {
if (illegalParams.isEmpty()) {
return;
}
if (prompt == PromptEnum.ALL) {
// 提示所有参数校验结果
StringBuilder message = new StringBuilder();
illegalParams.forEach(illegalParam -> message.append(illegalParam.getMessage()).append(";"));
throw new RuntimeException(message.toString());
} else {
// 提示首个参数校验结果
ConstraintViolation<Object> illegalParam = illegalParams.stream().findFirst().orElse(null);
throw new RuntimeException(illegalParam.getMessage());
}
}
/**
* 常规验证 (常规验证和分组验证选择校验)
*
* @param entity 实体类
* @param prompt 提示类型
* @param groups 校验分组
*/
public static void generalValidate(Object entity, PromptEnum prompt, Class<?> ...groups) {
Set<ConstraintViolation<Object>> illegalParams = new LinkedHashSet<>(1);
if (Objects.isNull(groups)) {
// 普通校验
illegalParams.addAll(getDefaultValidator().validate(entity));
} else {
// 分组校验
illegalParams.addAll(getDefaultValidator().validate(entity, groups));
}
promptMessage(illegalParams, prompt);
}
/**
* 常规验证 (常规验证和分组验证选择校验)
*
* @param entity 实体类
* @param groups 校验分组
*/
public static void generalValidate(Object entity, Class<?> ...groups) {
generalValidate(entity, PromptEnum.FIRST, groups);
}
/**
* 常规验证 (常规验证和分组验证选择校验)
*
* @param entity 实体类
*/
public static void generalValidate(Object entity) {
generalValidate(entity, PromptEnum.FIRST);
}
/**
* 增强验证 (常规验证加分组验证一起校验)
*
* @param entity 实体类
* @param prompt 提示类型
* @param groups 校验分组
*/
public static void enhancedValidate(Object entity, PromptEnum prompt, Class<?> ...groups) {
Set<ConstraintViolation<Object>> illegalParams = new LinkedHashSet<>(1);
illegalParams.addAll(getDefaultValidator().validate(entity));
if (Objects.nonNull(groups)) {
illegalParams.addAll(getDefaultValidator().validate(entity, groups));
}
promptMessage(illegalParams, prompt);
}
/**
* 增强验证 (常规验证加分组验证一起校验)
*
* @param entity 实体类
* @param groups 校验分组
*/
public static void enhancedValidate(Object entity, Class<?> ...groups) {
enhancedValidate(entity, PromptEnum.FIRST, groups);
}
/**
* 增强验证 (常规验证加分组验证一起校验)
*
* @param entity 实体类
*/
public static void enhancedValidate(Object entity) {
enhancedValidate(entity, PromptEnum.FIRST);
}
}
...@@ -3,6 +3,7 @@ package com.tangguo.framework.config; ...@@ -3,6 +3,7 @@ package com.tangguo.framework.config;
import com.tangguo.common.config.RuoYiConfig; import com.tangguo.common.config.RuoYiConfig;
import com.tangguo.common.constant.Constants; import com.tangguo.common.constant.Constants;
import com.tangguo.framework.interceptor.RepeatSubmitInterceptor; import com.tangguo.framework.interceptor.RepeatSubmitInterceptor;
import com.tangguo.framework.mauth.MobileProperties;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -14,6 +15,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; ...@@ -14,6 +15,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
...@@ -26,6 +28,10 @@ public class ResourcesConfig implements WebMvcConfigurer { ...@@ -26,6 +28,10 @@ public class ResourcesConfig implements WebMvcConfigurer {
@Autowired @Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor; private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Resource
private MobileProperties mobileProperties;
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** 本地文件上传路径 */ /** 本地文件上传路径 */
...@@ -44,7 +50,8 @@ public class ResourcesConfig implements WebMvcConfigurer { ...@@ -44,7 +50,8 @@ public class ResourcesConfig implements WebMvcConfigurer {
*/ */
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**")
.excludePathPatterns(this.mobileProperties.getPathPatterns());
} }
/** /**
......
package com.tangguo.framework.mauth;
import lombok.Getter;
/**
* 移动端用户身份认证失败异常类
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Getter
public class Mobile401Exception extends RuntimeException {
private static final long serialVersionUID = -4189845692674690824L;
/**
* 状态码
*/
private final int code = 401;
/**
* 异常消息
*/
private final String message;
public Mobile401Exception(String message) {
this.message = message;
}
public Mobile401Exception(String message, Throwable cause) {
super(message, cause);
this.message = message;
}
}
package com.tangguo.framework.mauth;
import lombok.Getter;
/**
* 移动端用户鉴权失败异常类
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Getter
public class Mobile403Exception extends RuntimeException {
private static final long serialVersionUID = 4027402152375922779L;
/**
* 状态码
*/
private final int code = 403;
/**
* 异常消息
*/
private final String message;
public Mobile403Exception(String message) {
this.message = message;
}
public Mobile403Exception(String message, Throwable cause) {
super(message, cause);
this.message = message;
}
}
package com.tangguo.framework.mauth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 移动端用户身份认证注解
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MobileAuth {
}
package com.tangguo.framework.mauth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* 移动端用户认证拦截器
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Slf4j
@Component
public class MobileAuthInterceptor implements HandlerInterceptor {
/**
* 请求被处理之前调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 获取处理器方法上的标识注解
Method method = ((HandlerMethod) handler).getMethod();
MobileAuth mobileAuth = method.getAnnotation(MobileAuth.class);
if (Objects.isNull(mobileAuth)) {
return true;
}
return true;
}
}
package com.tangguo.framework.mauth;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* 移动端用户Token配置参数
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "mobile.auth")
public class MobileProperties {
/**
* 生成时Token参数名
*/
private String resTokenName;
/**
* 请求时Token参数名
*/
private String reqTokenName;
/**
* Token签发人
*/
private String issuer = "MobileAuth";
/**
* Token签名算法
*/
private String algorithmId = "HS512";
/**
* Token签名密钥
*/
private String signKey = "SignKey2025@.";
/**
* Token有效时间
*/
private Duration effectiveTime = Duration.ofDays(7);
/**
* 拦截器拦截路径
*/
private String[] pathPatterns = {"/mobile/**"};
}
package com.tangguo.framework.mauth;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.Instant;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* 移动端用户Token处理
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Component
public class MobileTokenHelper {
private static MobileProperties properties;
@Resource
public void setProperties(MobileProperties properties) {
MobileTokenHelper.properties = properties;
}
/**
* 计算Token过期时间
*
* @return Token过期时间
*/
private static Date getTokenExpiredTime() {
return Date.from(Instant.now().plus(properties.getEffectiveTime()));
}
/**
* 获取Token签名器
*
* @return Token签名器
*/
private static JWTSigner getTokenSigner() {
return JWTSignerUtil.createSigner(
properties.getAlgorithmId(), properties.getSignKey().getBytes()
);
}
/**
* 构建JWT实体 (只填充了关键参数,并未签名。)
*
* @return JWT实体
*/
private static JWT buildJWT() {
JWT jwtToken = JWT.create();
jwtToken.setIssuer(properties.getIssuer());
jwtToken.setIssuedAt(new Date());
jwtToken.setExpiresAt(getTokenExpiredTime());
jwtToken.setSigner(getTokenSigner());
return jwtToken;
}
/**
* 创建Token
*
* @param payloads 荷载参数
* @return Token字符串
*/
public static String createToken(Map<String, Object> payloads) {
JWT jwtToken = buildJWT();
if (MapUtil.isNotEmpty(payloads)) {
payloads.forEach(jwtToken::setPayload);
}
return jwtToken.sign();
}
/**
* 创建Token,并返回Token明细信息。
*
* @param payloads 荷载参数
* @return Token字符串
*/
public static Map<String, Object> createDetailToken(Map<String, Object> payloads) {
String token = createToken(payloads);
Map<String, Object> detailTokenMap = new LinkedHashMap<>(2);
detailTokenMap.put(properties.getResTokenName(), token);
detailTokenMap.put("expiredTime", DateUtil.formatDateTime(getTokenExpiredTime()));
return detailTokenMap;
}
/**
* 获取当前用户Token
*
* @return 用户Token
*/
public static String getCurrentToken() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) attributes;
if (Objects.isNull(requestAttributes)) {
return null;
}
HttpServletRequest request = requestAttributes.getRequest();
String tokenValue = request.getHeader(properties.getReqTokenName());
if (tokenValue.startsWith("Bearer")) {
tokenValue = tokenValue.replace("Bearer ", "");
}
return tokenValue;
} catch (Exception e) {
return null;
}
}
/**
* 验证Token
*
* @param token Token字符串
* @return 验证结果
*/
public static boolean verifyToken(String token) {
if (StrUtil.isBlank(token)) {
return false;
}
try {
// 验证Token的签名和时效
return JWT.of(token).setSigner(getTokenSigner()).validate(0L);
} catch (Exception e) {
return false;
}
}
/**
* 校验Token
*
* @param token Token字符串
*/
public static JWT verifyParseToken(String token) {
if (StrUtil.isBlank(token)) {
throw new Mobile401Exception("身份认证失败,未获取到用户认证信息。");
}
try {
// 验证Token的签名和时效
JWTValidator.of(token).validateAlgorithm(getTokenSigner()).validateDate();
return JWT.of(token);
} catch (Exception e) {
throw new Mobile401Exception("身份认证失败,当前用户认证信息已失效或已过期。");
}
}
/**
* 获取当前用户Token荷载信息
*
* @return Token荷载信息
*/
public static JSONObject getTokenPayloads() {
return verifyParseToken(getCurrentToken()).getPayloads();
}
/**
* 获取当前用户名
*
* @return 用户名
*/
public static String getUsername() {
try {
String username = getTokenPayloads().getStr("userName");
if (StrUtil.isNotBlank(username)) {
return username;
} else {
throw new Mobile401Exception("身份认证失败,未获取到当前用户身份信息。");
}
} catch (Mobile401Exception e) {
throw e;
} catch (Exception e) {
throw new Mobile401Exception("获取当前用户身份信息获取失败。");
}
}
}
package com.tangguo.framework.mauth;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 移动端Web配置
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@RequiredArgsConstructor
@Configuration
public class MobileWebMvcConfig implements WebMvcConfigurer {
private final MobileProperties mobileProperties;
private final MobileAuthInterceptor mobileAuthInterceptor;
/**
* 自定义拦截规则
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.mobileAuthInterceptor).addPathPatterns(this.mobileProperties.getPathPatterns());
}
}
package com.tangguo.framework.wxcp;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Objects;
/**
* 企业微信配置类
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@RequiredArgsConstructor
@Configuration
@EnableConfigurationProperties(WxCpProperties.class)
public class WxCpConfiguration {
private final WxCpProperties properties;
/**
* 配置 WxCpService 实例
*/
@Bean
public WxCpService wxCpService() {
WxCpProperties.AppConfig appConfig = this.properties.getAppConfig();
WxCpDefaultConfigImpl configStorage = new WxCpDefaultConfigImpl();
configStorage.setCorpId(this.properties.getCorpId());
configStorage.setAgentId(appConfig.getAgentId());
configStorage.setCorpSecret(appConfig.getSecret());
configStorage.setToken(appConfig.getToken());
configStorage.setAesKey(appConfig.getAesKey());
WxCpService service = new WxCpServiceImpl();
service.setWxCpConfigStorage(configStorage);
return service;
}
}
package com.tangguo.framework.wxcp;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 企业微信配置类
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@Data
@ConfigurationProperties(prefix = "wx.cp")
public class WxCpProperties {
/**
* 设置企业微信的corpId
*/
private String corpId;
/**
* 多应用配置
*/
private AppConfig appConfig;
@Data
public static class AppConfig {
/**
* 设置企业微信应用的AgentId
*/
private Integer agentId;
/**
* 设置企业微信应用的Secret
*/
private String secret;
/**
* 设置企业微信应用的token
*/
private String token;
/**
* 设置企业微信应用的EncodingAESKey
*/
private String aesKey;
}
}
package com.tangguo.controller.mobile;
import com.tangguo.common.core.domain.AjaxResult;
import com.tangguo.domain.bo.CodeLoginBO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 动态用户管理控制器
*
* @author 谈笑
* @createTime 2025-09-02 14:42:17 星期二
*/
@RestController
@RequestMapping("/bbs/mobile/user")
public class BbsUserController {
/**
* 企微用户Code登录
*
* @param bo 登录参数
* @return 登录结果
*/
@PostMapping("/code/login")
public AjaxResult codeLogin(@RequestBody CodeLoginBO bo) {
return AjaxResult.success();
}
/**
* 查询用户信息
*
* @return 用户信息
*/
@PostMapping("/profile")
public AjaxResult profile() {
return AjaxResult.success();
}
}
package com.tangguo.controller; package com.tangguo.controller.pc;
import com.tangguo.common.annotation.Log; import com.tangguo.common.annotation.Log;
import com.tangguo.common.core.controller.BaseController; import com.tangguo.common.core.controller.BaseController;
......
package com.tangguo.controller; package com.tangguo.controller.pc;
import com.tangguo.common.annotation.Log; import com.tangguo.common.annotation.Log;
import com.tangguo.common.core.controller.BaseController; import com.tangguo.common.core.controller.BaseController;
......
package com.tangguo.controller; package com.tangguo.controller.pc;
import com.tangguo.common.annotation.Log; import com.tangguo.common.annotation.Log;
import com.tangguo.common.core.controller.BaseController; import com.tangguo.common.core.controller.BaseController;
......
package com.tangguo.controller; package com.tangguo.controller.pc;
import com.tangguo.common.annotation.Log; import com.tangguo.common.annotation.Log;
import com.tangguo.common.core.controller.BaseController; import com.tangguo.common.core.controller.BaseController;
......
package com.tangguo.domain.bo;
import lombok.Data;
/**
*
*
* @author 谈笑
* @createTime 2025-09-02 14:53:19 星期二
*/
@Data
public class CodeLoginBO {
/**
* 企微Code
*/
private String code;
}
package com.tangguo.service.impl;
import org.springframework.stereotype.Service;
/**
* 动态用户业务类
*
* @author 谈笑
* @createTime 2025-09-02 14:55:04 星期二
*/
@Service
public class BbsUserService {
}
package com.tangguo.event; package com.tangguo.listener;
import com.tangguo.common.constant.ActiveMQConstant; import com.tangguo.common.constant.ActiveMQConstant;
import com.tangguo.domain.dto.PointsDetail; import com.tangguo.domain.dto.PointsDetail;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment