diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java new file mode 100644 index 000000000..0e0add244 --- /dev/null +++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/util/PasswordWeakCheckUtil.java @@ -0,0 +1,57 @@ +package com.bsd.pjcloud.common.core.util; + +import cn.hutool.core.util.StrUtil; + +/** + * 弱口令校验:禁止密码与手机号/用户名相同等低强度规则。 + */ +public final class PasswordWeakCheckUtil { + + private PasswordWeakCheckUtil() { + } + + /** + * @param rawPassword 明文密码 + * @param phone 手机号(可空) + * @param username 用户名(可空) + * @return 不通过时的中文原因;通过返回 null + */ + public static String validate(String rawPassword, String phone, String username) { + if (StrUtil.isBlank(rawPassword)) { + return "密码不能为空"; + } + String pwd = StrUtil.trim(rawPassword); + String phoneTrim = StrUtil.trim(phone); + String userTrim = StrUtil.trim(username); + if (StrUtil.isNotBlank(phoneTrim) && pwd.equals(phoneTrim)) { + return "密码不能与手机号相同"; + } + if (StrUtil.isNotBlank(userTrim) && pwd.equalsIgnoreCase(userTrim)) { + return "密码不能与用户名相同"; + } + if (pwd.length() < 8) { + return "密码长度不能少于8位"; + } + boolean hasLetter = false; + boolean hasDigit = false; + for (int i = 0; i < pwd.length(); i++) { + char c = pwd.charAt(i); + if (Character.isLetter(c)) { + hasLetter = true; + } else if (Character.isDigit(c)) { + hasDigit = true; + } + } + if (!hasLetter || !hasDigit) { + return "密码必须同时包含字母和数字"; + } + return null; + } + + public static void assertStrong(String rawPassword, String phone, String username) { + String err = validate(rawPassword, phone, username); + if (err != null) { + throw new IllegalArgumentException(err); + } + } +} diff --git a/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties b/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties index 0699ff526..8e19e265d 100644 --- a/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties +++ b/pjcloud-common/pjcloud-common-core/src/main/resources/i18n/messages_zh_CN.properties @@ -1,6 +1,6 @@ sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25 sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25 -sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7 +sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u89C1\u7CFB\u7EDF\u53C2\u6570USER_DEFAULT_PASSWORD sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728 sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728 sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A diff --git a/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java index 564abb52f..8c4d7aca5 100644 --- a/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java +++ b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/resolver/SqlFilterArgumentResolver.java @@ -35,76 +35,107 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** * @author pjcloud * @date 2019-06-24 *

- * 解决Mybatis Plus Order By SQL注入问题 + * 解决Mybatis Plus Order By SQL注入问题:ascs/descs 仅允许简单列名白名单,禁止表达式/函数注入。 */ @Slf4j public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver { + /** + * 排序字段白名单:仅允许标识符(可选表别名.列名),禁止空格、括号、逗号、注释等。 + * 例:create_time、user_id、u.createTime + */ + private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile( + "^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$"); - /** - * 判断Controller是否包含page 参数 - * - * @param parameter 参数 - * @return 是否过滤 - */ - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(Page.class); - } + /** 单次请求最多允许的排序列数量,防止恶意堆叠。 */ + private static final int MAX_ORDER_COLUMNS = 5; - /** - * @param parameter 入参集合 - * @param mavContainer model 和 view - * @param webRequest web相关 - * @param binderFactory 入参解析 - * @return 检查后新的page对象 - *

- * page 只支持查询 GET .如需解析POST获取请求报文体处理 - */ - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + /** + * 判断Controller是否包含page 参数 + * + * @param parameter 参数 + * @return 是否过滤 + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(Page.class); + } - HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + /** + * @param parameter 入参集合 + * @param mavContainer model 和 view + * @param webRequest web相关 + * @param binderFactory 入参解析 + * @return 检查后新的page对象 + *

+ * page 只支持查询 GET .如需解析POST获取请求报文体处理 + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { - String ascs = request.getParameter("ascs"); - String descs = request.getParameter("descs"); + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String current = request.getParameter("current"); - String size = request.getParameter("size"); + String ascs = request.getParameter("ascs"); + String descs = request.getParameter("descs"); - Page page = new Page(); - if (StrUtil.isNotBlank(current)) { - // 如果current page 小于零 视为不合法数据 - if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) { - throw new CheckedException("current page error"); - } - page.setCurrent(Long.parseLong(current)); - } + String current = request.getParameter("current"); + String size = request.getParameter("size"); - if (StrUtil.isNotBlank(size)) { - page.setSize(Long.parseLong(size)); - } + Page page = new Page(); + if (StrUtil.isNotBlank(current)) { + // 如果current page 小于零 视为不合法数据 + if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) { + throw new CheckedException("current page error"); + } + page.setCurrent(Long.parseLong(current)); + } - List orderItemList = new ArrayList<>(); - Optional.ofNullable(ascs) - .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA)) - .filter(asc -> !SqlInjectionUtils.check(asc)) - .map(OrderItem::asc) - .collect(Collectors.toList()))); - Optional.ofNullable(descs) - .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA)) - .filter(desc -> !SqlInjectionUtils.check(desc)) - .map(OrderItem::desc) - .collect(Collectors.toList()))); - page.addOrder(orderItemList); + if (StrUtil.isNotBlank(size)) { + page.setSize(Long.parseLong(size)); + } + + List orderItemList = new ArrayList<>(); + Optional.ofNullable(ascs) + .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA)) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .map(col -> toSafeOrderColumn(col, true)) + .collect(Collectors.toList()))); + Optional.ofNullable(descs) + .ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA)) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .map(col -> toSafeOrderColumn(col, false)) + .collect(Collectors.toList()))); + if (orderItemList.size() > MAX_ORDER_COLUMNS) { + throw new CheckedException("排序字段数量超限"); + } + page.addOrder(orderItemList); + + return page; + } + + /** + * 将前端排序字段转为安全的 OrderItem:必须通过标识符白名单,且通过 SqlInjectionUtils 黑名单兜底。 + */ + private static OrderItem toSafeOrderColumn(String column, boolean asc) { + if (!SAFE_ORDER_COLUMN.matcher(column).matches()) { + log.warn("拒绝非法排序字段: {}", column); + throw new CheckedException("排序字段不合法"); + } + if (SqlInjectionUtils.check(column)) { + log.warn("排序字段命中注入检测: {}", column); + throw new CheckedException("排序字段不合法"); + } + return asc ? OrderItem.asc(column) : OrderItem.desc(column); + } - return page; - } } diff --git a/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java b/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java index 7e5036f6d..5856038be 100644 --- a/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java +++ b/pjcloud-common/pjcloud-common-sentinel/src/main/java/com/bsd/pjcloud/common/sentinel/handle/GlobalBizExceptionHandler.java @@ -18,8 +18,10 @@ package com.bsd.pjcloud.common.sentinel.handle; import com.alibaba.csp.sentinel.Tracer; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.bsd.pjcloud.common.core.exception.CheckedException; +import com.bsd.pjcloud.common.core.exception.ServiceException; import com.bsd.pjcloud.common.core.util.R; +import com.fasterxml.jackson.databind.ObjectMapper; import feign.FeignException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -40,111 +42,145 @@ import java.util.List; @RestControllerAdvice public class GlobalBizExceptionHandler { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); - /** - * 全局异常. - * - * @param e the e - * @return R - */ - @ExceptionHandler(Exception.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public R handleGlobalException(Exception e) { + /** + * 业务可控异常:允许把明确的业务文案返回前端。 + */ + @ExceptionHandler({CheckedException.class, ServiceException.class, IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleBizException(RuntimeException e) { + log.warn("业务异常 ex={}", e.getMessage()); + return R.failed(e.getMessage()); + } - log.error("全局异常信息 ex={}", e.getMessage(), e); + /** + * SQLException:禁止把完整 SQL / 表结构回显给前端。 + */ + @ExceptionHandler({SQLException.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleSQLException(SQLException exception) { + log.error("数据库调用异常 ex={}", exception.getMessage(), exception); + return R.failed("数据库调用异常,请联系管理员处理"); + } - // 业务异常交由 sentinel 记录 - Tracer.trace(e); - return R.failed(e.getLocalizedMessage()); - } + /** + * 全局异常:默认不回显底层异常原文(MyBatis 常把 SQL、mapper 路径塞进 message)。 + * + * @param e the e + * @return R + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleGlobalException(Exception e) { + log.error("全局异常信息 ex={}", e.getMessage(), e); + Tracer.trace(e); + if (isDataAccessRelated(e)) { + return R.failed("数据库调用异常,请联系管理员处理"); + } + return R.failed("系统繁忙,请稍后重试"); + } - /** - * SQLException Exception - * - * @param exception 数据库调用异常 - * @return R - */ - @ExceptionHandler({SQLException.class}) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public R handleSQLException(SQLException exception) { - log.error("数据库调用异常 ex={}", exception.getMessage(), exception); - return R.failed("数据库调用异常,请联系管理员处理"); - } + /** + * 识别 Spring DAO / MyBatis / JDBC 相关异常(本模块不直接依赖 mybatis,用类名与特征串判断)。 + */ + private static boolean isDataAccessRelated(Throwable e) { + Throwable t = e; + while (t != null) { + if (t instanceof SQLException) { + return true; + } + String name = t.getClass().getName(); + if (name.startsWith("org.springframework.dao.") + || name.startsWith("org.springframework.jdbc.") + || name.contains("MyBatis") + || name.contains("PersistenceException") + || name.contains("BadSqlGrammar")) { + return true; + } + String msg = t.getMessage(); + if (msg != null && (msg.contains("### Error") || msg.contains("### SQL") + || msg.contains("SQL:") || msg.contains("Cause: java.sql") + || msg.contains(" bad SQL grammar"))) { + return true; + } + t = t.getCause(); + } + return false; + } - @ExceptionHandler(FeignException.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public R handleGlobalException(FeignException e) { - log.error("全局异常信息 ex={}", e.getMessage(), e); + @ExceptionHandler(FeignException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleGlobalException(FeignException e) { + log.error("全局异常信息 ex={}", e.getMessage(), e); - // 业务异常交由 sentinel 记录 - Tracer.trace(e); + // 业务异常交由 sentinel 记录 + Tracer.trace(e); - if (e.responseBody().isPresent()) { - try { - return objectMapper.readValue(e.responseBody().get().array(), R.class); - } catch (Exception parseEx) { - // 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端 - log.warn("Feign 错误响应非 JSON,status={}", e.status()); - return R.failed("下游服务异常,请稍后重试"); - } - } + if (e.responseBody().isPresent()) { + try { + return objectMapper.readValue(e.responseBody().get().array(), R.class); + } catch (Exception parseEx) { + // 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端 + log.warn("Feign 错误响应非 JSON,status={}", e.status()); + return R.failed("下游服务异常,请稍后重试"); + } + } - return R.failed("服务调用失败,请稍后重试"); - } + return R.failed("服务调用失败,请稍后重试"); + } - /** - * AccessDeniedException - * - * @param e the e - * @return R - */ - @ExceptionHandler(AccessDeniedException.class) - @ResponseStatus(HttpStatus.FORBIDDEN) - public R handleAccessDeniedException(AccessDeniedException e) { - log.error("拒绝授权异常信息 ex={}", e.getMessage()); - return R.failed("权限不足,不允许访问"); - } + /** + * AccessDeniedException + * + * @param e the e + * @return R + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public R handleAccessDeniedException(AccessDeniedException e) { + log.error("拒绝授权异常信息 ex={}", e.getMessage()); + return R.failed("权限不足,不允许访问"); + } - /** - * validation Exception - * - * @param exception - * @return R - */ - @ExceptionHandler({BindException.class}) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public R handleBodyValidException(BindException exception) { - List fieldErrors = exception.getBindingResult().getFieldErrors(); - // 插入log 的逻辑 - return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage())); - } + /** + * validation Exception + * + * @param exception + * @return R + */ + @ExceptionHandler({BindException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleBodyValidException(BindException exception) { + List fieldErrors = exception.getBindingResult().getFieldErrors(); + return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage())); + } - /** - * 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点 - * - * @return R - */ - @DeleteMapping("/error") - @ResponseStatus(HttpStatus.NOT_FOUND) - public R noHandlerFoundException() { - return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase()); - } + /** + * 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点 + * + * @return R + */ + @DeleteMapping("/error") + @ResponseStatus(HttpStatus.NOT_FOUND) + public R noHandlerFoundException() { + return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase()); + } - /** - * 保持和低版本请求路径不存在的行为一致 - *

- * [Spring Boot - * 3.2.0] 404 Not Found behavior #38733 - * - * @param exception - * @return R - */ - @ExceptionHandler({NoResourceFoundException.class}) - @ResponseStatus(HttpStatus.NOT_FOUND) - public R noResourceFoundException(NoResourceFoundException exception) { - log.debug("请求路径 404 {}", exception.getMessage()); - return R.failed(exception.getMessage()); - } + /** + * 保持和低版本请求路径不存在的行为一致 + *

+ * [Spring Boot + * 3.2.0] 404 Not Found behavior #38733 + * + * @param exception + * @return R + */ + @ExceptionHandler({NoResourceFoundException.class}) + @ResponseStatus(HttpStatus.NOT_FOUND) + public R noResourceFoundException(NoResourceFoundException exception) { + log.debug("请求路径 404 {}", exception.getMessage()); + return R.failed(exception.getMessage()); + } } diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java new file mode 100644 index 000000000..eddde9612 --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyZzdPhoneBindController.java @@ -0,0 +1,129 @@ +package com.bsd.pjcloud.thirdparty.controller; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.log.annotation.SysLog; +import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity; +import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdPhoneBindService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.http.HttpHeaders; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 浙政钉短信核验手机号绑定:后台查询/改号(绑错时可修正) + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/zzdPhoneBind") +@Tag(description = "zzdPhoneBind", name = "浙政钉手机号绑定管理") +@SecurityRequirement(name = HttpHeaders.AUTHORIZATION) +public class ThirdPartyZzdPhoneBindController { + + private final ThirdPartyZzdPhoneBindService thirdPartyZzdPhoneBindService; + + @Operation(summary = "分页查询") + @GetMapping("/page") + @PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_view')") + public R page(@ParameterObject Page page, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "verifiedPhone", required = false) String verifiedPhone, + @RequestParam(value = "employeeCode", required = false) String employeeCode) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(ThirdPartyZzdPhoneBindEntity::getDelFlag, "0"); + wrapper.like(StringUtils.isNotBlank(verifiedPhone), ThirdPartyZzdPhoneBindEntity::getVerifiedPhone, verifiedPhone); + wrapper.like(StringUtils.isNotBlank(employeeCode), ThirdPartyZzdPhoneBindEntity::getEmployeeCode, employeeCode); + // 名称在 zzd_profile_json(nickName/username)里,模糊匹配 JSON 文本 + wrapper.like(StringUtils.isNotBlank(name), ThirdPartyZzdPhoneBindEntity::getZzdProfileJson, name); + wrapper.orderByDesc(ThirdPartyZzdPhoneBindEntity::getUpdateTime) + .orderByDesc(ThirdPartyZzdPhoneBindEntity::getCreateTime); + Page result = thirdPartyZzdPhoneBindService.page(page, wrapper); + List> records = result.getRecords().stream().map(this::toRow).collect(Collectors.toList()); + Page> voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(records); + return R.ok(voPage); + } + + @Operation(summary = "详情") + @GetMapping("/{id}") + @PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_view')") + public R getById(@PathVariable("id") Long id) { + ThirdPartyZzdPhoneBindEntity e = thirdPartyZzdPhoneBindService.getById(id); + if (e == null || "1".equals(e.getDelFlag())) { + return R.failed("记录不存在"); + } + return R.ok(toRow(e)); + } + + @Operation(summary = "修改核验手机号") + @SysLog("修改浙政钉绑定手机号") + @PutMapping("/phone") + @PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_edit')") + public R updatePhone(@RequestBody UpdatePhoneBody body) { + try { + thirdPartyZzdPhoneBindService.updateVerifiedPhone(body.getId(), body.getVerifiedPhone()); + return R.ok(Boolean.TRUE, "手机号已更新"); + } + catch (IllegalArgumentException ex) { + return R.failed(ex.getMessage()); + } + } + + @Data + public static class UpdatePhoneBody { + private Long id; + private String verifiedPhone; + } + + private Map toRow(ThirdPartyZzdPhoneBindEntity e) { + Map m = new HashMap<>(12); + m.put("id", e.getId()); + m.put("employeeCode", e.getEmployeeCode()); + m.put("openId", e.getOpenId()); + m.put("verifiedPhone", e.getVerifiedPhone()); + m.put("zzdProfileJson", e.getZzdProfileJson()); + m.put("createTime", e.getCreateTime()); + m.put("updateTime", e.getUpdateTime()); + m.put("createBy", e.getCreateBy()); + m.put("updateBy", e.getUpdateBy()); + String displayName = parseDisplayName(e.getZzdProfileJson()); + m.put("displayName", displayName); + return m; + } + + private static String parseDisplayName(String json) { + if (StringUtils.isBlank(json)) { + return ""; + } + try { + JSONObject o = JSON.parseObject(json); + if (o == null) { + return ""; + } + String nick = o.getString("nickName"); + if (StringUtils.isNotBlank(nick)) { + return nick; + } + String user = o.getString("username"); + return StringUtils.defaultString(user); + } + catch (Exception ignored) { + return ""; + } + } +} diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyZzdPhoneBindService.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyZzdPhoneBindService.java new file mode 100644 index 000000000..7a173ce1f --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdPartyZzdPhoneBindService.java @@ -0,0 +1,12 @@ +package com.bsd.pjcloud.thirdparty.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity; + +public interface ThirdPartyZzdPhoneBindService extends IService { + + /** + * 修正短信核验手机号(绑错号时后台改) + */ + boolean updateVerifiedPhone(Long id, String verifiedPhone); +} diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyZzdPhoneBindServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyZzdPhoneBindServiceImpl.java new file mode 100644 index 000000000..c530e6c7f --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/ThirdPartyZzdPhoneBindServiceImpl.java @@ -0,0 +1,35 @@ +package com.bsd.pjcloud.thirdparty.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity; +import com.bsd.pjcloud.thirdparty.mapper.ThirdPartyZzdPhoneBindMapper; +import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdPhoneBindService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ThirdPartyZzdPhoneBindServiceImpl + extends ServiceImpl + implements ThirdPartyZzdPhoneBindService { + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateVerifiedPhone(Long id, String verifiedPhone) { + if (id == null || StringUtils.isBlank(verifiedPhone)) { + throw new IllegalArgumentException("id与手机号不能为空"); + } + String phone = verifiedPhone.trim(); + if (!phone.matches("^1\\d{10}$")) { + throw new IllegalArgumentException("手机号格式不正确,需为11位大陆手机号"); + } + ThirdPartyZzdPhoneBindEntity exist = getById(id); + if (exist == null || "1".equals(exist.getDelFlag())) { + throw new IllegalArgumentException("绑定记录不存在"); + } + ThirdPartyZzdPhoneBindEntity upd = new ThirdPartyZzdPhoneBindEntity(); + upd.setId(id); + upd.setVerifiedPhone(phone); + return updateById(upd); + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysClientController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysClientController.java index 2d3d61119..4636a1817 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysClientController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysClientController.java @@ -140,7 +140,10 @@ public class SysClientController { return R.ok(clientDetailsService.updateClientById(clientDetailsDTO)); } - @Inner(false) + /** + * 仅服务间调用(需 FROM 头),禁止匿名公网读取客户端密钥。 + */ + @Inner @GetMapping("/getClientDetailsById/{clientId}") public R getClientDetailsById(@PathVariable String clientId) { return R.ok(clientDetailsService.getOne( @@ -148,10 +151,10 @@ public class SysClientController { } /** - * 查询全部客户端 + * 查询全部客户端(仅服务间调用,需 FROM 头) * @return */ - @Inner(false) + @Inner @GetMapping("/list") public R listClients() { return R.ok(clientDetailsService.list()); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysI18nController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysI18nController.java index bebd8d832..25a4423f5 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysI18nController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysI18nController.java @@ -146,7 +146,7 @@ public class SysI18nController { } /** - * 获取系统I18N配置 + * 获取系统I18N配置(登录页初始化需要匿名可读;仅文案映射,无密钥/账号数据) * @return I18N 配置 */ @Operation(summary = "获取系统配置-国际化", description = "获取系统配置-国际化") diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysTenantController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysTenantController.java index 62f9df95f..2ab7113d9 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysTenantController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysTenantController.java @@ -137,7 +137,7 @@ public class SysTenantController { } /** - * 查询全部有效的租户 + * 查询全部有效的租户(登录页匿名可用,仅返回展示字段,不含菜单等内部配置) * @return */ @Inner(value = false) @@ -147,10 +147,27 @@ public class SysTenantController { .stream() .filter(tenant -> tenant.getStartTime().isBefore(LocalDateTime.now())) .filter(tenant -> tenant.getEndTime().isAfter(LocalDateTime.now())) + .map(this::toPublicTenant) .collect(Collectors.toList()); return R.ok(tenants); } + /** 登录页/对外列表脱敏:只保留选租户与品牌展示所需字段。 */ + private SysTenant toPublicTenant(SysTenant src) { + SysTenant t = new SysTenant(); + t.setId(src.getId()); + t.setName(src.getName()); + t.setCode(src.getCode()); + t.setTenantDomain(src.getTenantDomain()); + t.setWebsiteName(src.getWebsiteName()); + t.setLogo(src.getLogo()); + t.setFooter(src.getFooter()); + t.setMiniQr(src.getMiniQr()); + t.setBackground(src.getBackground()); + t.setStatus(src.getStatus()); + return t; + } + /** * 导出excel 表格 * @return diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java index b5b8d5683..ca6ca7b87 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java @@ -96,6 +96,9 @@ public class SysUserController { /** 复用原 UserExcelVO 的分批导出。 */ private final com.bsd.pjcloud.admin.service.impl.SysUserBatchExportService batchExportService; + /** 异步修复「密码=手机号」弱口令。 */ + private final com.bsd.pjcloud.admin.service.impl.SysUserFixPhonePasswordService fixPhonePasswordService; + private final SysUserService userService; private final RemoterVisitorService remoterVisitorService; @@ -437,6 +440,21 @@ public class SysUserController { return userService.checkPassword(SecurityUtils.getUser().getUsername(), password); } + /** + * 异步修复「密码=手机号」弱口令:立即返回任务进度,前端轮询;完成后可下载名单 txt。 + * body: { "password": "PjHq@2024Fix!" } + */ + @SysLog("修复密码等于手机号的弱口令") + @PostMapping("/fix-phone-as-password") + @HasPermission("sys_user_edit") + public R fixPhoneAsPassword(@RequestBody UserDTO body) { + String newPassword = body != null ? body.getPassword() : null; + if (newPassword == null && body != null) { + newPassword = body.getNewpassword1(); + } + return R.ok(fixPhonePasswordService.submit(newPassword), "弱口令修复任务已提交"); + } + /** * 根据角色ID列表获取用户ID列表接口 * diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFixPhonePasswordService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFixPhonePasswordService.java new file mode 100644 index 000000000..6d7ab7978 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserFixPhonePasswordService.java @@ -0,0 +1,234 @@ +package com.bsd.pjcloud.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.mapper.SysUserMapper; +import com.bsd.pjcloud.admin.service.SysFileService; +import com.bsd.pjcloud.common.asynctask.config.AsyncTaskAutoConfiguration; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskException; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressService; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskResult; +import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO; +import com.bsd.pjcloud.common.core.constant.CacheConstants; +import com.bsd.pjcloud.common.core.util.PasswordWeakCheckUtil; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.data.tenant.TenantContextHolder; +import com.bsd.pjcloud.common.security.util.NonWebTokenContextHolder; +import com.bsd.pjcloud.common.security.util.SecurityUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.core.task.TaskExecutor; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 异步扫描并修复「密码=手机号」弱口令:分页处理,避免 HTTP 超时;逐条提交,避免超长事务。 + */ +@Slf4j +@Service +public class SysUserFixPhonePasswordService { + + public static final String REDIS_KEY = "admin:fix:phone-password"; + + private static final String TASK_NAME = "修复弱口令"; + + private static final int PAGE_SIZE = 200; + + private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder(); + + private final SysUserMapper sysUserMapper; + + private final SysFileService sysFileService; + + private final CacheManager cacheManager; + + private final AsyncTaskProgressService asyncTaskProgressService; + + private final TaskExecutor asyncTaskWorkerExecutor; + + public SysUserFixPhonePasswordService(SysUserMapper sysUserMapper, SysFileService sysFileService, + CacheManager cacheManager, AsyncTaskProgressService asyncTaskProgressService, + @Qualifier(AsyncTaskAutoConfiguration.ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME) TaskExecutor asyncTaskWorkerExecutor) { + this.sysUserMapper = sysUserMapper; + this.sysFileService = sysFileService; + this.cacheManager = cacheManager; + this.asyncTaskProgressService = asyncTaskProgressService; + this.asyncTaskWorkerExecutor = asyncTaskWorkerExecutor; + } + + /** + * 提交异步修复任务,立即返回进度对象供前端轮询。 + */ + public AsyncTaskProgressVO submit(String newPassword) { + if (StrUtil.isBlank(newPassword)) { + throw new AsyncTaskException("新密码不能为空"); + } + String pwd = newPassword.trim(); + String strength = PasswordWeakCheckUtil.validate(pwd, null, null); + if (strength != null) { + throw new AsyncTaskException(strength); + } + AsyncTaskProgressVO progress = asyncTaskProgressService.create(REDIS_KEY, TASK_NAME, 1); + Long tenantId = TenantContextHolder.getTenantId(); + Authentication authentication = SecurityUtils.getAuthentication(); + String token = SecurityUtils.getToken(); + SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy(); + SecurityContext securityContext = strategy.createEmptyContext(); + securityContext.setAuthentication(authentication); + try { + asyncTaskWorkerExecutor.execute(() -> run(progress, pwd, tenantId, token, strategy, securityContext)); + } + catch (RuntimeException ex) { + log.error("|修复弱口令|任务提交失败,taskId:{}", progress.getTaskId(), ex); + asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), "任务提交失败,请稍后重试"); + throw new AsyncTaskException("任务提交失败,请稍后重试"); + } + return progress; + } + + private void run(AsyncTaskProgressVO progress, String pwd, Long tenantId, String token, + SecurityContextHolderStrategy strategy, SecurityContext securityContext) { + try { + TenantContextHolder.setTenantId(tenantId); + strategy.setContext(securityContext); + NonWebTokenContextHolder.setToken(token); + asyncTaskProgressService.start(progress.getRedisKey(), progress.getTaskId()); + + long totalLong = sysUserMapper.selectCount(Wrappers.lambdaQuery() + .isNotNull(SysUser::getPhone) + .ne(SysUser::getPhone, "")); + if (totalLong <= 0) { + asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(), + "没有带手机号的用户需要扫描", AsyncTaskResult.empty()); + return; + } + int total = (int) Math.min(totalLong, Integer.MAX_VALUE); + asyncTaskProgressService.updateTotal(progress.getRedisKey(), progress.getTaskId(), total, + "共 " + total + " 个有手机号账号,开始扫描"); + + String encoded = ENCODER.encode(pwd); + List lines = new ArrayList<>(); + int scanned = 0; + int fixed = 0; + long current = 1; + while (true) { + Page page = sysUserMapper.selectPage(new Page<>(current, PAGE_SIZE, false), + Wrappers.lambdaQuery() + .select(SysUser::getUserId, SysUser::getUsername, SysUser::getName, + SysUser::getPhone, SysUser::getPassword) + .isNotNull(SysUser::getPhone) + .ne(SysUser::getPhone, "") + .orderByAsc(SysUser::getUserId)); + List records = page.getRecords(); + if (records == null || records.isEmpty()) { + break; + } + for (SysUser u : records) { + scanned++; + if (u == null || StrUtil.isBlank(u.getPassword()) || StrUtil.isBlank(u.getPhone())) { + continue; + } + String phone = StrUtil.trim(u.getPhone()); + if (!ENCODER.matches(phone, u.getPassword())) { + continue; + } + String weak = PasswordWeakCheckUtil.validate(pwd, phone, u.getUsername()); + if (weak != null) { + throw new AsyncTaskException("新密码不满足强度要求(用户 " + u.getUsername() + "):" + weak + + ";请换一个不与任何用户手机号/用户名相同的强密码"); + } + sysUserMapper.update(null, Wrappers.lambdaUpdate() + .set(SysUser::getPassword, encoded) + .set(SysUser::getPasswordModifyTime, LocalDateTime.now()) + .eq(SysUser::getUserId, u.getUserId())); + Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); + if (cache != null && StrUtil.isNotBlank(u.getUsername())) { + cache.evict(u.getUsername()); + } + fixed++; + lines.add(fixed + ". " + StrUtil.blankToDefault(u.getName(), "-") + + " / " + StrUtil.blankToDefault(u.getUsername(), "-") + + " / " + phone); + } + asyncTaskProgressService.update(progress.getRedisKey(), progress.getTaskId(), + Math.min(scanned, total), + "已扫描 " + scanned + "/" + total + ",已重置 " + fixed + " 人"); + if (records.size() < PAGE_SIZE) { + break; + } + current++; + } + + String message = "已将 " + fixed + " 个「密码=手机号」账号重置为统一新密码"; + if (fixed == 0) { + asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(), + "扫描完成,未发现「密码=手机号」账号", AsyncTaskResult.empty()); + return; + } + AsyncTaskResult result = uploadResultFile(fixed, lines); + asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(), message, result); + } + catch (AsyncTaskException ex) { + log.warn("|修复弱口令|业务失败,taskId:{},msg:{}", progress.getTaskId(), ex.getMessage()); + asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), ex.getMessage()); + } + catch (Exception ex) { + log.error("|修复弱口令|执行失败,taskId:{}", progress.getTaskId(), ex); + asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), "修复弱口令任务执行失败,请重试"); + } + finally { + NonWebTokenContextHolder.clear(); + strategy.clearContext(); + TenantContextHolder.clear(); + } + } + + @SuppressWarnings("unchecked") + private AsyncTaskResult uploadResultFile(int fixed, List lines) { + StringBuilder sb = new StringBuilder(); + sb.append("弱口令(密码=手机号)账号已重置,共 ").append(fixed).append(" 人。\n"); + sb.append("新密码请按单位通知另行获取,原「手机号当密码」将无法登录。\n\n"); + sb.append("涉及人员名单(姓名 / 用户名 / 手机号):\n"); + for (String line : lines) { + sb.append(line).append('\n'); + } + byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8); + String fileName = "弱口令修复名单.txt"; + try { + MockMultipartFile multipartFile = new MockMultipartFile("file", fileName, "text/plain", bytes); + R upload = sysFileService.uploadFile(multipartFile, "export", null, "txt"); + Object data = upload != null ? upload.getData() : null; + if (!(data instanceof Map)) { + throw new AsyncTaskException("名单文件保存失败,请重试"); + } + Object url = ((Map) data).get("url"); + if (url == null || StrUtil.isBlank(String.valueOf(url))) { + throw new AsyncTaskException("名单文件保存失败,请重试"); + } + return AsyncTaskResult.file(String.valueOf(url), fileName); + } + catch (AsyncTaskException ex) { + throw ex; + } + catch (Exception ex) { + log.error("|修复弱口令|上传名单失败", ex); + throw new AsyncTaskException("名单文件保存失败,请重试"); + } + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java index db2d39a3d..277a3d91b 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java @@ -59,6 +59,7 @@ import com.bsd.pjcloud.common.core.constant.CommonRoleConstant; import com.bsd.pjcloud.common.core.exception.ErrorCodes; import com.bsd.pjcloud.common.core.util.CollectionUtils; import com.bsd.pjcloud.common.core.util.MsgUtils; +import com.bsd.pjcloud.common.core.util.PasswordWeakCheckUtil; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.data.resolver.ParamResolver; @@ -178,6 +179,7 @@ public class SysUserServiceImpl extends ServiceImpl impl sysUser.setLockFlag(StrUtil.isBlank(userDto.getLockFlag()) ? UserStateEnum.NORMAL.getCode() : userDto.getLockFlag()); sysUser.setCreateBy(userDto.getUsername()); sysUser.setUpdateBy(userDto.getUsername()); + PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername()); sysUser.setPassword(ENCODER.encode(userDto.getPassword())); sysUser.setPasswordModifyTime(LocalDateTime.now()); baseMapper.insert(sysUser); @@ -392,6 +394,7 @@ public class SysUserServiceImpl extends ServiceImpl impl sysUser.setUpdateTime(LocalDateTime.now()); if (StrUtil.isNotBlank(userDto.getPassword())) { + PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername()); sysUser.setPassword(ENCODER.encode(userDto.getPassword())); } @@ -688,9 +691,9 @@ public class SysUserServiceImpl extends ServiceImpl impl userDTO.setEmail(excel.getEmail()); userDTO.setLockFlag(excel.getLockFlag()); userDTO.setStaffingType(excel.getStaffingTypeParse()); - // 批量导入初始密码为手机号 + // 批量导入:默认密码取系统参数 USER_DEFAULT_PASSWORD(禁止再用手机号当密码) userDTO.setPasswordModifyTime(LocalDateTime.now()); - userDTO.setPassword(userDTO.getPhone()); + userDTO.setPassword(resolveImportDefaultPassword()); // 根据部门名称查询部门ID userDTO.setDeptId(deptOptional.get().getDeptId()); // 插入岗位名称 @@ -712,9 +715,9 @@ public class SysUserServiceImpl extends ServiceImpl impl userDTO.setPhone(excel.getMobile().replaceAll("\\s+", "")); userDTO.setDeptId(deptId); userDTO.setStaffingType(excel.getStaffingType()); - // 批量导入初始密码为手机号 + // 批量导入:默认密码取系统参数 USER_DEFAULT_PASSWORD(禁止再用手机号当密码) userDTO.setPasswordModifyTime(LocalDateTime.now()); - userDTO.setPassword(userDTO.getPhone().replaceAll("\\s+", "")); + userDTO.setPassword(resolveImportDefaultPassword()); // 根据角色名称查询角色ID userDTO.setRole(roleList); // 插入用户 @@ -794,6 +797,7 @@ public class SysUserServiceImpl extends ServiceImpl impl sysUser.setLockFlag(UserStateEnum.NORMAL.getCode()); sysUser.setCreateBy(userDto.getUsername()); sysUser.setUpdateBy(userDto.getUsername()); + PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername()); sysUser.setPassword(ENCODER.encode(userDto.getPassword())); sysUser.setPasswordModifyTime(LocalDateTime.now()); sysUser.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode()); @@ -868,6 +872,10 @@ public class SysUserServiceImpl extends ServiceImpl impl if (StrUtil.isEmpty(userDto.getNewpassword1())) { return R.failed("新密码不能为空"); } + String weak = PasswordWeakCheckUtil.validate(userDto.getNewpassword1(), user.getPhone(), user.getUsername()); + if (weak != null) { + return R.failed(weak); + } String password = ENCODER.encode(userDto.getNewpassword1()); this.update(Wrappers.lambdaUpdate().set(SysUser::getPassword, password).set(SysUser::getPasswordModifyTime, LocalDateTime.now()).eq(SysUser::getUserId, user.getUserId())); @@ -931,6 +939,12 @@ public class SysUserServiceImpl extends ServiceImpl impl if (StrUtil.equals(userDto.getPassword(), userDto.getNewpassword1())) { return R.failed("新旧密码不能相同"); } + SysUser sysUser = this.getOne(Wrappers.lambdaQuery().eq(SysUser::getUsername, userDto.getUsername())); + String weak = PasswordWeakCheckUtil.validate(userDto.getNewpassword1(), + sysUser != null ? sysUser.getPhone() : null, userDto.getUsername()); + if (weak != null) { + return R.failed(weak); + } // 重置密码 String password = ENCODER.encode(userDto.getNewpassword1()); @@ -1086,6 +1100,19 @@ public class SysUserServiceImpl extends ServiceImpl impl return count > 0; } + /** + * Excel 导入默认密码:系统参数 USER_DEFAULT_PASSWORD,未配置则用内置强口令。 + */ + private String resolveImportDefaultPassword() { + String def = ParamResolver.getStr("USER_DEFAULT_PASSWORD"); + if (StrUtil.isBlank(def)) { + def = "PjHq@2024Init!"; + } + // 兜底:导入默认密码也走同一套强度规则(字母+数字、大于8位) + PasswordWeakCheckUtil.assertStrong(def.trim(), null, null); + return def.trim(); + } + @Override public void deleteDiscountsCacheByUserId(Long userId) { if (ObjectUtil.isEmpty(userId)) return; diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/utils/page/PaginationOrderSqlExtractor.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/utils/page/PaginationOrderSqlExtractor.java index f6feee7f6..96f441ba0 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/utils/page/PaginationOrderSqlExtractor.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/utils/page/PaginationOrderSqlExtractor.java @@ -1,15 +1,21 @@ package com.bsd.pjcloud.wallet.utils.page; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class PaginationOrderSqlExtractor { + /** 与 SqlFilterArgumentResolver 一致:仅允许简单标识符,防止 ORDER BY 拼接注入。 */ + private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile( + "^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$"); + public static String getOrderSql(Page page) { return getOrderSql(page, null); } @@ -18,14 +24,25 @@ public class PaginationOrderSqlExtractor { List orderItems = page.orders(); if (ObjectUtil.isEmpty(orderItems)) return null; if (ObjectUtil.isEmpty(alias)) alias = ""; - if (alias.length() > 0) alias += "."; + if (alias.length() > 0) { + if (!SAFE_ORDER_COLUMN.matcher(alias).matches()) { + throw new IllegalArgumentException("排序表别名不合法"); + } + alias += "."; + } String finalAlias = alias; return orderItems.stream() .map(item -> { + String column = item.getColumn(); + if (StrUtil.isBlank(column) || !SAFE_ORDER_COLUMN.matcher(column).matches()) { + throw new IllegalArgumentException("排序字段不合法"); + } + // 若列名已带别名(a.col),不再重复拼 alias + String prefix = column.contains(".") ? "" : finalAlias; if (item.isAsc()) { - return finalAlias + item.getColumn() + " asc"; + return prefix + column + " asc"; } else { - return finalAlias + item.getColumn() + " desc"; + return prefix + column + " desc"; } }) .collect(Collectors.joining(", "));