Commit 2f655a816beba1d5dd02d6c49e992f667b0392ad

Authored by YunaiV
1 parent 19829c6c

fix: 修复钱包充值退款权限与 IoT 固件上传校验

- 钱包充值退款复用用户余额修改权限,阻止无权限用户发起退款(#1328)
- 允许 .bin 固件上传、预签名及直传登记,保留未知后缀和内容类型校验
- 补充方法权限拦截与固件上传回归用例

星球反馈:https://t.zsxq.com/bAppp
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
@@ -22,9 +22,11 @@ import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; @@ -22,9 +22,11 @@ import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
22 import com.google.common.annotations.VisibleForTesting; 22 import com.google.common.annotations.VisibleForTesting;
23 import jakarta.annotation.Resource; 23 import jakarta.annotation.Resource;
24 import lombok.SneakyThrows; 24 import lombok.SneakyThrows;
  25 +import org.apache.tika.mime.MimeTypes;
25 import org.springframework.stereotype.Service; 26 import org.springframework.stereotype.Service;
26 27
27 import java.util.List; 28 import java.util.List;
  29 +import java.util.Locale;
28 import java.util.Set; 30 import java.util.Set;
29 31
30 import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; 32 import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
@@ -63,6 +65,14 @@ public class FileServiceImpl implements FileService { @@ -63,6 +65,14 @@ public class FileServiceImpl implements FileService {
63 ); 65 );
64 66
65 /** 67 /**
  68 + * 允许上传的二进制文件后缀,例如说 IoT OTA 固件的 .bin
  69 + *
  70 + * 为什么不直接放行 application/octet-stream?它也是所有未知后缀、未知内容的识别结果,放行后等于不限制类型。
  71 + * 所以只放行这里的后缀,并且上传内容也必须识别为 application/octet-stream(不能是可执行文件、脚本、HTML 等)
  72 + */
  73 + private static final Set<String> ALLOWED_BINARY_EXTENSIONS = Set.of("bin");
  74 +
  75 + /**
66 * 上传文件的前缀,是否包含日期(yyyyMMdd) 76 * 上传文件的前缀,是否包含日期(yyyyMMdd)
67 * 77 *
68 * 目的:按照日期,进行分目录 78 * 目的:按照日期,进行分目录
@@ -106,8 +116,10 @@ public class FileServiceImpl implements FileService { @@ -106,8 +116,10 @@ public class FileServiceImpl implements FileService {
106 } 116 }
107 type = FileTypeUtils.getMineType(content, name); 117 type = FileTypeUtils.getMineType(content, name);
108 String nameType = StrUtil.isNotEmpty(FileUtil.extName(name)) ? validateFileType(name) : null; 118 String nameType = StrUtil.isNotEmpty(FileUtil.extName(name)) ? validateFileType(name) : null;
109 - if (!ALLOWED_FILE_TYPES.contains(type)  
110 - || (FileTypeUtils.isImage(nameType) && ObjectUtil.notEqual(type, nameType))) { 119 + if (!isAllowedFileType(type, name)
  120 + // 图片、二进制文件(例如说 .bin 固件):内容识别的类型,必须与后缀一致
  121 + || ((FileTypeUtils.isImage(nameType) || isAllowedBinaryFile(nameType, name))
  122 + && ObjectUtil.notEqual(type, nameType))) {
111 throw exception(FILE_TYPE_NOT_ALLOWED); 123 throw exception(FILE_TYPE_NOT_ALLOWED);
112 } 124 }
113 // 1.2.2 处理 name 为空的情况 125 // 1.2.2 处理 name 为空的情况
@@ -221,12 +233,24 @@ public class FileServiceImpl implements FileService { @@ -221,12 +233,24 @@ public class FileServiceImpl implements FileService {
221 233
222 private String validateFileType(String name) { 234 private String validateFileType(String name) {
223 String type = FileTypeUtils.getMineType(name); 235 String type = FileTypeUtils.getMineType(name);
224 - if (!ALLOWED_FILE_TYPES.contains(type)) { 236 + if (!isAllowedFileType(type, name)) {
225 throw exception(FILE_TYPE_NOT_ALLOWED); 237 throw exception(FILE_TYPE_NOT_ALLOWED);
226 } 238 }
227 return type; 239 return type;
228 } 240 }
229 241
  242 + private static boolean isAllowedFileType(String type, String name) {
  243 + return ALLOWED_FILE_TYPES.contains(type) || isAllowedBinaryFile(type, name);
  244 + }
  245 +
  246 + private static boolean isAllowedBinaryFile(String type, String name) {
  247 + if (ObjectUtil.notEqual(type, MimeTypes.OCTET_STREAM)) {
  248 + return false;
  249 + }
  250 + String extension = FileUtil.extName(name);
  251 + return StrUtil.isNotEmpty(extension) && ALLOWED_BINARY_EXTENSIONS.contains(extension.toLowerCase(Locale.ROOT));
  252 + }
  253 +
230 @Override 254 @Override
231 public FileDO getFile(Long id) { 255 public FileDO getFile(Long id) {
232 return validateFileExists(id); 256 return validateFileExists(id);
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
@@ -322,6 +322,79 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -322,6 +322,79 @@ public class FileServiceImplTest extends BaseDbUnitTest {
322 } 322 }
323 323
324 @Test 324 @Test
  325 + public void testCreateFile_success_bin() throws Exception {
  326 + for (String name : new String[]{"固件.bin", "FIRMWARE.BIN", "firmware.Bin"}) {
  327 + // 准备参数:IoT OTA 固件,Tika 无法识别的二进制内容
  328 + byte[] content = buildFirmwareContent();
  329 + // mock 方法
  330 + FileClient client = mock(FileClient.class);
  331 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  332 + String url = randomString();
  333 + when(client.upload(same(content), anyString(), eq("application/octet-stream"))).thenReturn(url);
  334 +
  335 + // 调用:不信任客户端传入的 type
  336 + String result = fileService.createFile(content, name, "iot/ota", "image/png");
  337 + // 断言
  338 + assertEquals(url, result);
  339 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
  340 + assertEquals(name, file.getName());
  341 + assertEquals("application/octet-stream", file.getType());
  342 + assertEquals((long) content.length, file.getSize());
  343 + }
  344 + }
  345 +
  346 + @Test
  347 + public void testCreateFile_typeNotAllowed_binContent() {
  348 + byte[] elf = buildFirmwareContent();
  349 + System.arraycopy(new byte[]{0x7F, 'E', 'L', 'F', 2, 1, 1}, 0, elf, 0, 7);
  350 + elf[16] = 2; // e_type = ET_EXEC
  351 + byte[] exe = buildFirmwareContent();
  352 + exe[0] = 'M';
  353 + exe[1] = 'Z';
  354 + for (byte[] content : new byte[][]{elf, exe,
  355 + "#!/bin/sh\nrm -rf /tmp/test\n".getBytes(StandardCharsets.UTF_8),
  356 + "<html><script>alert(1)</script></html>".getBytes(StandardCharsets.UTF_8),
  357 + "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>".getBytes(StandardCharsets.UTF_8),
  358 + ResourceUtil.readBytes("file/erweima.jpg")}) {
  359 + // 调用,并断言异常:.bin 的内容,必须是无法识别为其他类型的二进制
  360 + assertServiceException(() -> fileService.createFile(content, "固件.bin", null, "application/octet-stream"),
  361 + FILE_TYPE_NOT_ALLOWED);
  362 + // 断言未上传文件、未保存记录
  363 + verifyNoInteractions(fileConfigService);
  364 + assertEquals(0L, fileMapper.selectCount());
  365 + }
  366 + }
  367 +
  368 + @Test
  369 + public void testCreateFile_typeNotAllowed_binaryOtherExtension() {
  370 + for (String name : new String[]{"固件.so", "固件.dump", "固件.unknown", "固件"}) {
  371 + // 准备参数:同样识别为 application/octet-stream 的其它后缀,不放行
  372 + byte[] content = buildFirmwareContent();
  373 +
  374 + // 调用,并断言异常
  375 + assertServiceException(() -> fileService.createFile(content, name, null, "application/octet-stream"),
  376 + FILE_TYPE_NOT_ALLOWED);
  377 + // 断言未上传文件、未保存记录
  378 + verifyNoInteractions(fileConfigService);
  379 + assertEquals(0L, fileMapper.selectCount());
  380 + }
  381 + }
  382 +
  383 + /**
  384 + * 构造固件内容:类似 ARM Cortex-M 的向量表开头,包含 0x00 等不可打印字节,Tika 无法识别
  385 + */
  386 + private static byte[] buildFirmwareContent() {
  387 + byte[] content = new byte[256];
  388 + byte[] header = new byte[]{0x00, 0x50, 0x00, 0x20, (byte) 0xC1, 0x01, 0x00, 0x08,
  389 + (byte) 0xC9, 0x01, 0x00, 0x08, (byte) 0xCB, 0x01, 0x00, 0x08};
  390 + System.arraycopy(header, 0, content, 0, header.length);
  391 + for (int i = header.length; i < content.length; i++) {
  392 + content[i] = (byte) (i * 7);
  393 + }
  394 + return content;
  395 + }
  396 +
  397 + @Test
325 public void testCreateFile_fileIsEmpty() { 398 public void testCreateFile_fileIsEmpty() {
326 // 准备参数 399 // 准备参数
327 byte[] content = new byte[0]; 400 byte[] content = new byte[0];
@@ -337,7 +410,7 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -337,7 +410,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
337 for (String extension : new String[]{"jpg", "JPEG", "jfif", "png", "gif", "webp", "bmp", "ico", "tiff", 410 for (String extension : new String[]{"jpg", "JPEG", "jfif", "png", "gif", "webp", "bmp", "ico", "tiff",
338 "avif", "heic", "heif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "md", 411 "avif", "heic", "heif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "md",
339 "zip", "rar", "mp3", "mp4", "svg", "html", "htm", "xml", "markdown", "eml", "msg", "epub", 412 "zip", "rar", "mp3", "mp4", "svg", "html", "htm", "xml", "markdown", "eml", "msg", "epub",
340 - "json", "opus", "ogg", "m4a", "webm", "amr", "wma", "wmv", "flv", "mkv"}) { 413 + "json", "opus", "ogg", "m4a", "webm", "amr", "wma", "wmv", "flv", "mkv", "bin", "BIN"}) {
341 // 准备参数 414 // 准备参数
342 String name = "测试文件." + extension; 415 String name = "测试文件." + extension;
343 // mock 方法 416 // mock 方法
@@ -360,7 +433,8 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -360,7 +433,8 @@ public class FileServiceImplTest extends BaseDbUnitTest {
360 @Test 433 @Test
361 public void testPresignPutUrl_typeNotAllowed() { 434 public void testPresignPutUrl_typeNotAllowed() {
362 // 准备参数 435 // 准备参数
363 - for (String name : new String[]{"test.php", "test.jsp", "test.js", "test.exe", "test.unknown", "test"}) { 436 + for (String name : new String[]{"test.php", "test.jsp", "test.js", "test.exe", "test.unknown", "test",
  437 + "test.so", "test.dump", "test.sh", "test.bin.exe"}) {
364 // 调用,并断言异常 438 // 调用,并断言异常
365 assertServiceException(() -> fileService.presignPutUrl(name, null), FILE_TYPE_NOT_ALLOWED); 439 assertServiceException(() -> fileService.presignPutUrl(name, null), FILE_TYPE_NOT_ALLOWED);
366 // 校验调用 440 // 校验调用
@@ -491,6 +565,47 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -491,6 +565,47 @@ public class FileServiceImplTest extends BaseDbUnitTest {
491 } 565 }
492 566
493 @Test 567 @Test
  568 + public void testCreateFileByPresignedPath_success_bin() {
  569 + // 准备参数:IoT OTA 固件,后缀大小写不敏感
  570 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  571 + o.setPath("iot/ota/firmware.bin");
  572 + o.setName("FIRMWARE.BIN");
  573 + o.setUrl("https://www.iocoder.cn/iot/ota/firmware.bin");
  574 + o.setType("text/html"); // 不信任客户端传入的 type
  575 + });
  576 +
  577 + // 调用
  578 + Long fileId = fileService.createFile(reqVO);
  579 +
  580 + // 断言
  581 + FileDO file = fileMapper.selectById(fileId);
  582 + assertEquals("iot/ota/firmware.bin", file.getPath());
  583 + assertEquals("FIRMWARE.BIN", file.getName());
  584 + assertEquals("application/octet-stream", file.getType());
  585 + }
  586 +
  587 + @Test
  588 + public void testCreateFileByPresignedPath_typeNotAllowed_bin() {
  589 + for (String[] file : new String[][]{{"firmware.bin", "iot/firmware.exe"}, {"firmware.bin", "iot/firmware.so"},
  590 + {"firmware.so", "iot/firmware.so"}, {"firmware.exe", "iot/firmware.bin"},
  591 + {"firmware.bin", "iot/firmware.html"}, {"firmware", "iot/firmware.bin"}}) {
  592 + // 准备参数
  593 + String name = file[0];
  594 + String path = file[1];
  595 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  596 + o.setName(name);
  597 + o.setPath(path);
  598 + o.setType("application/octet-stream");
  599 + });
  600 +
  601 + // 调用,并断言异常
  602 + assertServiceException(() -> fileService.createFile(reqVO), FILE_TYPE_NOT_ALLOWED);
  603 + // 断言未保存记录
  604 + assertEquals(0L, fileMapper.selectCount());
  605 + }
  606 + }
  607 +
  608 + @Test
494 public void testCreateFileByPresignedPath_nameInvalid() { 609 public void testCreateFileByPresignedPath_nameInvalid() {
495 // 准备参数 610 // 准备参数
496 FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { 611 FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeController.java
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.Operation; @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.Operation;
8 import io.swagger.v3.oas.annotations.Parameter; 8 import io.swagger.v3.oas.annotations.Parameter;
9 import io.swagger.v3.oas.annotations.tags.Tag; 9 import io.swagger.v3.oas.annotations.tags.Tag;
10 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.security.access.prepost.PreAuthorize;
11 import org.springframework.validation.annotation.Validated; 12 import org.springframework.validation.annotation.Validated;
12 import org.springframework.web.bind.annotation.*; 13 import org.springframework.web.bind.annotation.*;
13 14
@@ -40,6 +41,7 @@ public class PayWalletRechargeController { @@ -40,6 +41,7 @@ public class PayWalletRechargeController {
40 @PostMapping("/refund") 41 @PostMapping("/refund")
41 @Operation(summary = "发起钱包充值退款") 42 @Operation(summary = "发起钱包充值退款")
42 @Parameter(name = "id", description = "编号", required = true, example = "1024") 43 @Parameter(name = "id", description = "编号", required = true, example = "1024")
  44 + @PreAuthorize("@ss.hasPermission('pay:wallet:update-balance')") // 退款会扣减用户钱包余额,复用「用户余额修改」权限
43 public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) { 45 public CommonResult<Boolean> refundWalletRecharge(@RequestParam("id") Long id) {
44 walletRechargeService.refundWalletRecharge(id, getClientIP()); 46 walletRechargeService.refundWalletRecharge(id, getClientIP());
45 return success(true); 47 return success(true);
yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/PayWalletRechargeControllerSecurityTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.controller.admin.wallet;
  2 +
  3 +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
  4 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  5 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  6 +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
  7 +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkServiceImpl;
  8 +import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
  9 +import cn.iocoder.yudao.module.pay.service.wallet.PayWalletRechargeService;
  10 +import jakarta.annotation.Resource;
  11 +import org.junit.jupiter.api.AfterEach;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.springframework.aop.support.AopUtils;
  15 +import org.springframework.context.annotation.Bean;
  16 +import org.springframework.context.annotation.Configuration;
  17 +import org.springframework.security.access.AccessDeniedException;
  18 +import org.springframework.security.authentication.AnonymousAuthenticationToken;
  19 +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
  20 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  21 +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
  22 +import org.springframework.security.core.authority.AuthorityUtils;
  23 +import org.springframework.security.core.context.SecurityContextHolder;
  24 +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
  25 +
  26 +import java.util.Collections;
  27 +
  28 +import static org.junit.jupiter.api.Assertions.*;
  29 +import static org.mockito.ArgumentMatchers.*;
  30 +import static org.mockito.Mockito.*;
  31 +
  32 +/**
  33 + * {@link PayWalletRechargeController} 的方法级权限测试
  34 + *
  35 + * 通过真实的 Spring Security 方法拦截({@link EnableMethodSecurity})+ 真实的 {@code @ss} 权限判断,
  36 + * 校验发起钱包充值退款需要 pay:wallet:update-balance 权限,拒绝时不会调用 Service
  37 + *
  38 + * @author 芋道源码
  39 + */
  40 +@SpringJUnitConfig(PayWalletRechargeControllerSecurityTest.Config.class)
  41 +public class PayWalletRechargeControllerSecurityTest {
  42 +
  43 + private static final String PERMISSION = "pay:wallet:update-balance";
  44 + private static final Long USER_ID = 1L;
  45 + private static final Long RECHARGE_ID = 1024L;
  46 +
  47 + @Resource
  48 + private PayWalletRechargeController controller;
  49 + @Resource
  50 + private PayWalletRechargeService walletRechargeService;
  51 + @Resource
  52 + private PermissionCommonApi permissionApi;
  53 +
  54 + @BeforeEach
  55 + public void setUp() {
  56 + reset(walletRechargeService, permissionApi);
  57 + SecurityContextHolder.clearContext();
  58 + }
  59 +
  60 + @AfterEach
  61 + public void tearDown() {
  62 + SecurityContextHolder.clearContext();
  63 + }
  64 +
  65 + @Test
  66 + public void testControllerIsProxied() {
  67 + // 确认测试走的是方法安全代理,而不是裸对象
  68 + assertTrue(AopUtils.isAopProxy(controller));
  69 + }
  70 +
  71 + @Test
  72 + public void testRefundWalletRecharge_hasPermission() {
  73 + // mock 方法
  74 + loginAdmin();
  75 + when(permissionApi.hasAnyPermissions(eq(USER_ID), eq(PERMISSION))).thenReturn(true);
  76 +
  77 + // 调用
  78 + assertTrue(controller.refundWalletRecharge(RECHARGE_ID).getData());
  79 + // 断言
  80 + verify(permissionApi).hasAnyPermissions(eq(USER_ID), eq(PERMISSION));
  81 + verify(walletRechargeService).refundWalletRecharge(eq(RECHARGE_ID), isNull());
  82 + }
  83 +
  84 + @Test
  85 + public void testRefundWalletRecharge_noPermission() {
  86 + // mock 方法
  87 + loginAdmin();
  88 + when(permissionApi.hasAnyPermissions(anyLong(), any(String[].class))).thenReturn(false);
  89 +
  90 + // 调用,并断言
  91 + assertThrows(AccessDeniedException.class, () -> controller.refundWalletRecharge(RECHARGE_ID));
  92 + verify(permissionApi).hasAnyPermissions(eq(USER_ID), eq(PERMISSION));
  93 + verifyNoInteractions(walletRechargeService);
  94 + }
  95 +
  96 + @Test
  97 + public void testRefundWalletRecharge_anonymous() {
  98 + // 未登录:Web 过滤链会放入匿名用户
  99 + SecurityContextHolder.getContext().setAuthentication(new AnonymousAuthenticationToken("key", "anonymousUser",
  100 + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
  101 +
  102 + // 调用,并断言
  103 + assertThrows(AccessDeniedException.class, () -> controller.refundWalletRecharge(RECHARGE_ID));
  104 + verifyNoInteractions(permissionApi, walletRechargeService);
  105 + }
  106 +
  107 + @Test
  108 + public void testRefundWalletRecharge_noAuthentication() {
  109 + // 没有任何认证信息,同样拒绝:取决于 Spring Security 是否读取认证信息,抛出拒绝访问或认证缺失
  110 + Exception ex = assertThrows(Exception.class, () -> controller.refundWalletRecharge(RECHARGE_ID));
  111 + assertTrue(ex instanceof AccessDeniedException || ex instanceof AuthenticationCredentialsNotFoundException,
  112 + "非预期的异常:" + ex);
  113 + verifyNoInteractions(permissionApi, walletRechargeService);
  114 + }
  115 +
  116 + @Test
  117 + public void testUpdateWalletRechargerPaid_callbackNotRestricted() {
  118 + // 支付回调:仍由 @PermitAll + 内部校验,不受后台权限约束
  119 + PayOrderNotifyReqDTO reqDTO = PayOrderNotifyReqDTO.builder().merchantOrderId("1").payOrderId(2L).build();
  120 +
  121 + // 调用
  122 + assertTrue(controller.updateWalletRechargerPaid(reqDTO).getData());
  123 + // 断言
  124 + verify(walletRechargeService).updateWalletRechargerPaid(eq(1L), eq(2L));
  125 + verifyNoInteractions(permissionApi);
  126 + }
  127 +
  128 + private static void loginAdmin() {
  129 + LoginUser loginUser = new LoginUser();
  130 + loginUser.setId(USER_ID);
  131 + loginUser.setUserType(UserTypeEnum.ADMIN.getValue());
  132 + loginUser.setTenantId(1L);
  133 + SecurityContextHolder.getContext().setAuthentication(
  134 + new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
  135 + }
  136 +
  137 + @Configuration
  138 + @EnableMethodSecurity(securedEnabled = true)
  139 + public static class Config {
  140 +
  141 + @Bean
  142 + public PayWalletRechargeController payWalletRechargeController() {
  143 + return new PayWalletRechargeController();
  144 + }
  145 +
  146 + @Bean
  147 + public PayWalletRechargeService payWalletRechargeService() {
  148 + return mock(PayWalletRechargeService.class);
  149 + }
  150 +
  151 + @Bean
  152 + public PermissionCommonApi permissionCommonApi() {
  153 + return mock(PermissionCommonApi.class);
  154 + }
  155 +
  156 + @Bean("ss")
  157 + public SecurityFrameworkService securityFrameworkService(PermissionCommonApi permissionApi) {
  158 + return new SecurityFrameworkServiceImpl(permissionApi);
  159 + }
  160 +
  161 + }
  162 +
  163 +}