vue2 新增行为验证码
| @@ -57,6 +57,7 @@ | ||||
|         <commons-net.version>3.8.0</commons-net.version> | ||||
|         <jsch.version>0.1.55</jsch.version> | ||||
|         <tika-core.version>2.4.1</tika-core.version> | ||||
|         <aj-captcha.version>1.3.0</aj-captcha.version> | ||||
|         <!-- 三方云服务相关 --> | ||||
|         <minio.version>8.2.2</minio.version> | ||||
|         <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> | ||||
| @@ -129,6 +130,11 @@ | ||||
|                 <artifactId>yudao-spring-boot-starter-biz-error-code</artifactId> | ||||
|                 <version>${revision}</version> | ||||
|             </dependency> | ||||
|             <dependency> | ||||
|                 <groupId>cn.iocoder.boot</groupId> | ||||
|                 <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|                 <version>${revision}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- Spring 核心 --> | ||||
|             <dependency> | ||||
| @@ -451,6 +457,12 @@ | ||||
|                 <version>${tika-core.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
|                 <groupId>com.anji-plus</groupId> | ||||
|                 <artifactId>spring-boot-starter-captcha</artifactId> | ||||
|                 <version>${aj-captcha.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
|                 <groupId>org.apache.velocity</groupId> | ||||
|                 <artifactId>velocity-engine-core</artifactId> | ||||
|   | ||||
| @@ -39,6 +39,7 @@ | ||||
|         <module>yudao-spring-boot-starter-biz-error-code</module> | ||||
|  | ||||
|         <module>yudao-spring-boot-starter-flowable</module> | ||||
|         <module>yudao-spring-boot-starter-captcha</module> | ||||
|     </modules> | ||||
|  | ||||
|     <artifactId>yudao-framework</artifactId> | ||||
|   | ||||
							
								
								
									
										44
									
								
								yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
|          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
|     <parent> | ||||
|         <groupId>cn.iocoder.boot</groupId> | ||||
|         <artifactId>yudao-framework</artifactId> | ||||
|         <version>${revision}</version> | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|     <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|     <packaging>jar</packaging> | ||||
|  | ||||
|     <name>${project.artifactId}</name> | ||||
|     <description> | ||||
|         验证码 | ||||
|     </description> | ||||
|  | ||||
|     <dependencies> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|             <artifactId>yudao-common</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- Spring 核心 --> | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.boot</groupId> | ||||
|             <artifactId>spring-boot-starter</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|             <artifactId>yudao-spring-boot-starter-redis</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 验证码相关 --> | ||||
|         <dependency> | ||||
|             <groupId>com.anji-plus</groupId> | ||||
|             <artifactId>spring-boot-starter-captcha</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|     </dependencies> | ||||
|  | ||||
| </project> | ||||
| @@ -0,0 +1,45 @@ | ||||
| package cn.iocoder.yudao.captcha.core.service; | ||||
|  | ||||
| import com.anji.captcha.service.CaptchaCacheService; | ||||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| @Service | ||||
| public class CaptchaServiceImpl implements CaptchaCacheService { | ||||
|  | ||||
|     @Override | ||||
|     public String type() { | ||||
|         return "redis"; | ||||
|     } | ||||
|  | ||||
|     @Resource | ||||
|     private StringRedisTemplate stringRedisTemplate; | ||||
|  | ||||
|     @Override | ||||
|     public void set(String key, String value, long expiresInSeconds) { | ||||
|         stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean exists(String key) { | ||||
|         return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void delete(String key) { | ||||
|         stringRedisTemplate.delete(key); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String get(String key) { | ||||
|         return stringRedisTemplate.opsForValue().get(key); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Long increment(String key, long val) { | ||||
|         return stringRedisTemplate.opsForValue().increment(key,val); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1 @@ | ||||
| cn.iocoder.yudao.captcha.core.service.CaptchaServiceImpl | ||||
| After Width: | Height: | Size: 67 KiB | 
| After Width: | Height: | Size: 97 KiB | 
| After Width: | Height: | Size: 94 KiB | 
| After Width: | Height: | Size: 97 KiB | 
| After Width: | Height: | Size: 89 KiB | 
| After Width: | Height: | Size: 83 KiB | 
| After Width: | Height: | Size: 85 KiB | 
| After Width: | Height: | Size: 104 KiB | 
| After Width: | Height: | Size: 106 KiB | 
| After Width: | Height: | Size: 6.9 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.2 KiB | 
| After Width: | Height: | Size: 6.2 KiB | 
| After Width: | Height: | Size: 6.2 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 6.1 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 3.5 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 35 KiB | 
| After Width: | Height: | Size: 51 KiB | 
| After Width: | Height: | Size: 43 KiB | 
| After Width: | Height: | Size: 40 KiB | 
| After Width: | Height: | Size: 51 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 29 KiB | 
| After Width: | Height: | Size: 58 KiB | 
| After Width: | Height: | Size: 37 KiB | 
| After Width: | Height: | Size: 39 KiB | 
| @@ -81,7 +81,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|  | ||||
|     /** | ||||
|      * 配置 URL 的安全配置 | ||||
|      * | ||||
|      * <p> | ||||
|      * anyRequest          |   匹配所有请求路径 | ||||
|      * access              |   SpringEl表达式结果为true时可以访问 | ||||
|      * anonymous           |   匿名可以访问 | ||||
| @@ -109,8 +109,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|                 .headers().frameOptions().disable().and() | ||||
|                 // 一堆自定义的 Spring Security 处理器 | ||||
|                 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) | ||||
|                     .accessDeniedHandler(accessDeniedHandler); | ||||
|                 // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 | ||||
|                 .accessDeniedHandler(accessDeniedHandler); | ||||
|         // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 | ||||
|  | ||||
|         // 获得 @PermitAll 带来的 URL 列表,免登录 | ||||
|         Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations(); | ||||
| @@ -118,23 +118,25 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|         httpSecurity | ||||
|                 // ①:全局共享规则 | ||||
|                 .authorizeRequests() | ||||
|                     // 1.1 静态资源,可匿名访问 | ||||
|                     .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | ||||
|                     // 1.2 设置 @PermitAll 无需认证 | ||||
|                     .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() | ||||
|                     // 1.3 基于 yudao.security.permit-all-urls 无需认证 | ||||
|                     .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() | ||||
|                     // 1.4 设置 App API 无需认证 | ||||
|                     .antMatchers(buildAppApi("/**")).permitAll() | ||||
|                 // 1.1 静态资源,可匿名访问 | ||||
|                 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | ||||
|                 // 1.2 设置 @PermitAll 无需认证 | ||||
|                 .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() | ||||
|                 // 1.3 基于 yudao.security.permit-all-urls 无需认证 | ||||
|                 .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() | ||||
|                 // 1.4 设置 App API 无需认证 | ||||
|                 .antMatchers(buildAppApi("/**")).permitAll() | ||||
|                 // 1.5 验证码captcha 允许匿名访问 | ||||
|                 .antMatchers("/captcha/get", "/captcha/check").permitAll() | ||||
|                 // ②:每个项目的自定义规则 | ||||
|                 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 | ||||
|                         authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) | ||||
|                 // ③:兜底规则,必须认证 | ||||
|                 .authorizeRequests() | ||||
|                     .anyRequest().authenticated() | ||||
|                 .anyRequest().authenticated() | ||||
|         ; | ||||
|  | ||||
|         // 添加 Token Filter | ||||
|   | ||||
| @@ -72,6 +72,11 @@ | ||||
|             <artifactId>yudao-spring-boot-starter-redis</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|             <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- Job 定时任务相关 --> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|   | ||||
| @@ -55,7 +55,6 @@ public class AuthController { | ||||
|     private PermissionService permissionService; | ||||
|     @Resource | ||||
|     private SocialUserService socialUserService; | ||||
|  | ||||
|     @Resource | ||||
|     private SecurityProperties securityProperties; | ||||
|  | ||||
|   | ||||
| @@ -33,16 +33,6 @@ public class AuthLoginReqVO { | ||||
|     @Length(min = 4, max = 16, message = "密码长度为 4-16 位") | ||||
|     private String password; | ||||
|  | ||||
|     // ========== 图片验证码相关 ========== | ||||
|  | ||||
|     @ApiModelProperty(value = "验证码", required = true, example = "1024", notes = "验证码开启时,需要传递") | ||||
|     @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) | ||||
|     private String code; | ||||
|  | ||||
|     @ApiModelProperty(value = "验证码的唯一标识", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62", notes = "验证码开启时,需要传递") | ||||
|     @NotEmpty(message = "唯一标识不能为空", groups = CodeEnableGroup.class) | ||||
|     private String uuid; | ||||
|  | ||||
|     // ========== 绑定社交登录时,需要传递如下参数 ========== | ||||
|  | ||||
|     @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值") | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| ### 请求 /captcha/get-image 接口 => 成功 | ||||
| GET {{baseUrl}}/system/captcha/get-image | ||||
| tenant-id: {{adminTenentId}} | ||||
| @@ -1,32 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.controller.admin.common; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import io.swagger.annotations.Api; | ||||
| import io.swagger.annotations.ApiOperation; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import javax.annotation.security.PermitAll; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||||
|  | ||||
| @Api(tags = "管理后台 - 验证码") | ||||
| @RestController | ||||
| @RequestMapping("/system/captcha") | ||||
| public class CaptchaController { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaService captchaService; | ||||
|  | ||||
|     @GetMapping("/get-image") | ||||
|     @PermitAll | ||||
|     @ApiOperation("生成图片验证码") | ||||
|     public CommonResult<CaptchaImageRespVO> getCaptchaImage() { | ||||
|         return success(captchaService.getCaptchaImage()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.controller.admin.common.vo; | ||||
|  | ||||
| import io.swagger.annotations.ApiModel; | ||||
| import io.swagger.annotations.ApiModelProperty; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| @ApiModel("管理后台 - 验证码图片 Response VO") | ||||
| @Data | ||||
| @Builder | ||||
| @NoArgsConstructor | ||||
| @AllArgsConstructor | ||||
| public class CaptchaImageRespVO { | ||||
|  | ||||
|     @ApiModelProperty(value = "是否开启", required = true, example = "true", notes = "如果为 false,则关闭验证码功能") | ||||
|     private Boolean enable; | ||||
|  | ||||
|     @ApiModelProperty(value = "uuid", example = "1b3b7d00-83a8-4638-9e37-d67011855968", | ||||
|             notes = "enable = true 时,非空!通过该 uuid 作为该验证码的标识") | ||||
|     private String uuid; | ||||
|  | ||||
|     @ApiModelProperty(value = "图片", notes = "enable = true 时,非空!验证码的图片内容,使用 Base64 编码") | ||||
|     private String img; | ||||
|  | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.convert.common; | ||||
|  | ||||
| import cn.hutool.captcha.AbstractCaptcha; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import org.mapstruct.Mapper; | ||||
| import org.mapstruct.factory.Mappers; | ||||
|  | ||||
| @Mapper | ||||
| public interface CaptchaConvert { | ||||
|  | ||||
|     CaptchaConvert INSTANCE = Mappers.getMapper(CaptchaConvert.class); | ||||
|  | ||||
|     default CaptchaImageRespVO convert(String uuid, AbstractCaptcha captcha) { | ||||
|         return CaptchaImageRespVO.builder().uuid(uuid).img(captcha.getImageBase64()).build(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||||
| import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||||
| import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; | ||||
| import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; | ||||
| import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; | ||||
| import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; | ||||
| @@ -17,13 +16,11 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; | ||||
| import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import cn.iocoder.yudao.module.system.service.logger.LoginLogService; | ||||
| import cn.iocoder.yudao.module.system.service.member.MemberService; | ||||
| import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; | ||||
| import cn.iocoder.yudao.module.system.service.social.SocialUserService; | ||||
| import cn.iocoder.yudao.module.system.service.user.AdminUserService; | ||||
| import com.google.common.annotations.VisibleForTesting; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| @@ -47,8 +44,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     @Resource | ||||
|     private AdminUserService userService; | ||||
|     @Resource | ||||
|     private CaptchaService captchaService; | ||||
|     @Resource | ||||
|     private LoginLogService loginLogService; | ||||
|     @Resource | ||||
|     private OAuth2TokenService oauth2TokenService; | ||||
| @@ -86,9 +81,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|  | ||||
|     @Override | ||||
|     public AuthLoginRespVO login(AuthLoginReqVO reqVO) { | ||||
|         // 判断验证码是否正确 | ||||
|         verifyCaptcha(reqVO); | ||||
|  | ||||
|         // 使用账号密码,进行登录 | ||||
|         AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); | ||||
|  | ||||
| @@ -97,7 +89,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|             socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), | ||||
|                     reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); | ||||
|         } | ||||
|  | ||||
|         // 创建 Token 令牌,记录登录日志 | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); | ||||
|     } | ||||
| @@ -127,32 +118,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); | ||||
|     } | ||||
|  | ||||
|     @VisibleForTesting | ||||
|     void verifyCaptcha(AuthLoginReqVO reqVO) { | ||||
|         // 如果验证码关闭,则不进行校验 | ||||
|         if (!captchaService.isCaptchaEnable()) { | ||||
|             return; | ||||
|         } | ||||
|         // 校验验证码 | ||||
|         ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); | ||||
|         // 验证码不存在 | ||||
|         final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | ||||
|         String code = captchaService.getCaptchaCode(reqVO.getUuid()); | ||||
|         if (code == null) { | ||||
|             // 创建登录失败日志(验证码不存在) | ||||
|             createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND); | ||||
|             throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
|         } | ||||
|         // 验证码不正确 | ||||
|         if (!code.equals(reqVO.getCode())) { | ||||
|             // 创建登录失败日志(验证码不正确) | ||||
|             createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); | ||||
|             throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
|         } | ||||
|         // 正确,所以要删除下验证码 | ||||
|         captchaService.deleteCaptchaCode(reqVO.getUuid()); | ||||
|     } | ||||
|  | ||||
|     private void createLoginLog(Long userId, String username, | ||||
|                                 LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { | ||||
|         // 插入登录日志 | ||||
|   | ||||
| @@ -1,39 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
|  | ||||
| /** | ||||
|  * 验证码 Service 接口 | ||||
|  */ | ||||
| public interface CaptchaService { | ||||
|  | ||||
|     /** | ||||
|      * 获得验证码图片 | ||||
|      * | ||||
|      * @return 验证码图片 | ||||
|      */ | ||||
|     CaptchaImageRespVO getCaptchaImage(); | ||||
|  | ||||
|     /** | ||||
|      * 是否开启图片验证码 | ||||
|      * | ||||
|      * @return 是否 | ||||
|      */ | ||||
|     Boolean isCaptchaEnable(); | ||||
|  | ||||
|     /** | ||||
|      * 获得 uuid 对应的验证码 | ||||
|      * | ||||
|      * @param uuid 验证码编号 | ||||
|      * @return 验证码 | ||||
|      */ | ||||
|     String getCaptchaCode(String uuid); | ||||
|  | ||||
|     /** | ||||
|      * 删除 uuid 对应的验证码 | ||||
|      * | ||||
|      * @param uuid 验证码编号 | ||||
|      */ | ||||
|     void deleteCaptchaCode(String uuid); | ||||
|  | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.hutool.captcha.CaptchaUtil; | ||||
| import cn.hutool.captcha.CircleCaptcha; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.iocoder.yudao.module.system.convert.common.CaptchaConvert; | ||||
| import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| /** | ||||
|  * 验证码 Service 实现类 | ||||
|  */ | ||||
| @Service | ||||
| public class CaptchaServiceImpl implements CaptchaService { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaProperties captchaProperties; | ||||
|  | ||||
|     /** | ||||
|      * 验证码是否开关 | ||||
|      * | ||||
|      * 虽然 {@link CaptchaProperties#getEnable()} 有该属性,但是 Apollo 在 Spring Boot 下无法刷新 @ConfigurationProperties 注解, | ||||
|      * 所以暂时只能这么处理~ | ||||
|      */ | ||||
|     @Value("${yudao.captcha.enable}") | ||||
|     private Boolean enable; | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaRedisDAO captchaRedisDAO; | ||||
|  | ||||
|     @Override | ||||
|     public CaptchaImageRespVO getCaptchaImage() { | ||||
|         if (!Boolean.TRUE.equals(enable)) { | ||||
|             return CaptchaImageRespVO.builder().enable(enable).build(); | ||||
|         } | ||||
|         // 生成验证码 | ||||
|         CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(captchaProperties.getWidth(), captchaProperties.getHeight()); | ||||
|         // 缓存到 Redis 中 | ||||
|         String uuid = IdUtil.fastSimpleUUID(); | ||||
|         captchaRedisDAO.set(uuid, captcha.getCode(), captchaProperties.getTimeout()); | ||||
|         // 返回 | ||||
|         return CaptchaConvert.INSTANCE.convert(uuid, captcha).setEnable(enable); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Boolean isCaptchaEnable() { | ||||
|         return enable; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getCaptchaCode(String uuid) { | ||||
|         return captchaRedisDAO.get(uuid); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteCaptchaCode(String uuid) { | ||||
|         captchaRedisDAO.delete(uuid); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -11,13 +11,12 @@ import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; | ||||
| import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import cn.iocoder.yudao.module.system.service.logger.LoginLogService; | ||||
| import cn.iocoder.yudao.module.system.service.member.MemberService; | ||||
| import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; | ||||
| import cn.iocoder.yudao.module.system.service.social.SocialUserService; | ||||
| import cn.iocoder.yudao.module.system.service.user.AdminUserService; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import com.anji.captcha.service.CaptchaService; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.boot.test.mock.mockito.MockBean; | ||||
| import org.springframework.context.annotation.Import; | ||||
| @@ -57,11 +56,6 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | ||||
|     @MockBean | ||||
|     private Validator validator; | ||||
|  | ||||
|     @BeforeEach | ||||
|     public void setUp() { | ||||
|         when(captchaService.isCaptchaEnable()).thenReturn(true); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testAuthenticate_success() { | ||||
|         // 准备参数 | ||||
| @@ -138,82 +132,82 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_success() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| //    @Test | ||||
| //    public void testCaptcha_success() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // mock 验证码正确 | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| // | ||||
| //        // 调用 | ||||
| //        authService.verifyCaptcha(reqVO); | ||||
| //        // 断言 | ||||
| //        verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); | ||||
| //    } | ||||
| // | ||||
| //    @Test | ||||
| //    public void testCaptcha_notFound() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService, times(1)).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|         // mock 验证码正确 | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| //    @Test | ||||
| //    public void testCaptcha_codeError() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // mock 验证码不正确 | ||||
| //        String code = randomString(); | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|         // 调用 | ||||
|         authService.verifyCaptcha(reqVO); | ||||
|         // 断言 | ||||
|         verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_notFound() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService, times(1)).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_codeError() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
|  | ||||
|         // mock 验证码不正确 | ||||
|         String code = randomString(); | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testLogin_success() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> | ||||
|                 o.setUsername("test_username").setPassword("test_password")); | ||||
|  | ||||
|         // mock 验证码正确 | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
|         // mock user 数据 | ||||
|         AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") | ||||
|                 .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); | ||||
|         when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); | ||||
|         // mock password 匹配 | ||||
|         when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); | ||||
|         // mock 缓存登录用户到 Redis | ||||
|         OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) | ||||
|                 .setUserType(UserTypeEnum.ADMIN.getValue())); | ||||
|         when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) | ||||
|                 .thenReturn(accessTokenDO); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         AuthLoginRespVO loginRespVO = authService.login(reqVO); | ||||
|         assertPojoEquals(accessTokenDO, loginRespVO); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) | ||||
|                     && o.getUserId().equals(user.getId())) | ||||
|         ); | ||||
|     } | ||||
| //    @Test | ||||
| //    public void testLogin_success() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> | ||||
| //                o.setUsername("test_username").setPassword("test_password")); | ||||
| // | ||||
| //        // mock 验证码正确 | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| //        // mock user 数据 | ||||
| //        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") | ||||
| //                .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); | ||||
| //        when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); | ||||
| //        // mock password 匹配 | ||||
| //        when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); | ||||
| //        // mock 缓存登录用户到 Redis | ||||
| //        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) | ||||
| //                .setUserType(UserTypeEnum.ADMIN.getValue())); | ||||
| //        when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) | ||||
| //                .thenReturn(accessTokenDO); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        AuthLoginRespVO loginRespVO = authService.login(reqVO); | ||||
| //        assertPojoEquals(accessTokenDO, loginRespVO); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) | ||||
| //                    && o.getUserId().equals(user.getId())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|     @Test | ||||
|     public void testLogout_success() { | ||||
|   | ||||
| @@ -1,65 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; | ||||
| import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; | ||||
| import cn.iocoder.yudao.framework.test.core.ut.BaseRedisUnitTest; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.context.annotation.Import; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| @Import({CaptchaServiceImpl.class, CaptchaProperties.class, CaptchaRedisDAO.class}) | ||||
| public class CaptchaServiceTest extends BaseRedisUnitTest { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaServiceImpl captchaService; | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaRedisDAO captchaRedisDAO; | ||||
|     @Resource | ||||
|     private CaptchaProperties captchaProperties; | ||||
|  | ||||
|     @Test | ||||
|     public void testGetCaptchaImage() { | ||||
|         // 调用 | ||||
|         CaptchaImageRespVO respVO = captchaService.getCaptchaImage(); | ||||
|         // 断言 | ||||
|         assertNotNull(respVO.getUuid()); | ||||
|         assertNotNull(respVO.getImg()); | ||||
|         String captchaCode = captchaRedisDAO.get(respVO.getUuid()); | ||||
|         assertNotNull(captchaCode); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testGetCaptchaCode() { | ||||
|         // 准备参数 | ||||
|         String uuid = randomString(); | ||||
|         String code = randomString(); | ||||
|         // mock 数据 | ||||
|         captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); | ||||
|  | ||||
|         // 调用 | ||||
|         String resultCode = captchaService.getCaptchaCode(uuid); | ||||
|         // 断言 | ||||
|         assertEquals(code, resultCode); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDeleteCaptchaCode() { | ||||
|         // 准备参数 | ||||
|         String uuid = randomString(); | ||||
|         String code = randomString(); | ||||
|         // mock 数据 | ||||
|         captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); | ||||
|  | ||||
|         // 调用 | ||||
|         captchaService.deleteCaptchaCode(uuid); | ||||
|         // 断言 | ||||
|         assertNull(captchaRedisDAO.get(uuid)); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -57,6 +57,43 @@ mybatis-plus: | ||||
|       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) | ||||
|   type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject | ||||
|  | ||||
| --- #################### 验证码相关配置 #################### | ||||
|  | ||||
| aj: | ||||
|   captcha: | ||||
|     # 滑动验证,底图路径,不配置将使用默认图片 | ||||
|     # 支持全路径 | ||||
|     # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/jigsaw | ||||
|     jigsaw: classpath:images/jigsaw | ||||
|     #滑动验证,底图路径,不配置将使用默认图片 | ||||
|     ##支持全路径 | ||||
|     # 支持项目路径,以classpath:开头,取resource目录下路径,例:classpath:images/pic-click | ||||
|     pic-click: classpath:images/pic-click | ||||
|     # 缓存local/redis... | ||||
|     cache-type: redis | ||||
|     # local缓存的阈值,达到这个值,清除缓存 | ||||
|     cache-number: 1000 | ||||
|     # local定时清除过期缓存(单位秒),设置为0代表不执行 | ||||
|     timing-clear: 180 | ||||
|     # 验证码类型default两种都实例化。 | ||||
|     type: default | ||||
|     # 右下角水印文字(我的水印)https://tool.chinaz.com/tools/unicode.aspx 中文转Unicode | ||||
|     water-mark: 芋道源码 | ||||
|     # 滑动干扰项(0/1/2) | ||||
|     interference-options: 2 | ||||
|     # 接口请求次数一分钟限制是否开启 true|false | ||||
|     req-frequency-limit-enable: false | ||||
|     # 验证失败5次,get接口锁定 | ||||
|     req-get-lock-limit: 5 | ||||
|     # 验证失败后,锁定时间间隔,s | ||||
|     req-get-lock-seconds: 360 | ||||
|     # get接口一分钟内请求数限制 | ||||
|     req-get-minute-limit: 30 | ||||
|     # check接口一分钟内请求数限制 | ||||
|     req-check-minute-limit: 60 | ||||
|     # verify接口一分钟内请求数限制 | ||||
|     req-verify-minute-limit: 60 | ||||
|  | ||||
| --- #################### 芋道相关配置 #################### | ||||
|  | ||||
| yudao: | ||||
| @@ -92,7 +129,8 @@ yudao: | ||||
|     enable: true | ||||
|     ignore-urls: | ||||
|       - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 | ||||
|       - /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关 | ||||
|       - /captcha/get # 获取图片验证码,和租户无关 | ||||
|       - /captcha/check # 校验图片验证码,和租户无关 | ||||
|       - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 | ||||
|       - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 | ||||
|       - /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号 | ||||
|   | ||||
| @@ -20,31 +20,37 @@ const request = (option: AxiosConfig) => { | ||||
|  | ||||
| async function getFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   const res = await request({ method: 'GET', ...option }) | ||||
|   console.info(res) | ||||
|   return res.data | ||||
| } | ||||
|  | ||||
| async function postFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   const res = await request({ method: 'POST', ...option }) | ||||
|   console.info(res) | ||||
|   return res.data | ||||
| } | ||||
|  | ||||
| async function deleteFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   const res = await request({ method: 'DELETE', ...option }) | ||||
|   console.info(res) | ||||
|   return res.data | ||||
| } | ||||
|  | ||||
| async function putFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   const res = await request({ method: 'PUT', ...option }) | ||||
|   console.info(res) | ||||
|   return res.data | ||||
| } | ||||
| async function downloadFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   const res = await request({ method: 'GET', responseType: 'blob', ...option }) | ||||
|   console.info(res) | ||||
|   return res as unknown as Promise<T> | ||||
| } | ||||
|  | ||||
| async function uploadFn<T = any>(option: AxiosConfig): Promise<T> { | ||||
|   option.headersType = 'multipart/form-data' | ||||
|   const res = await request({ method: 'PUT', ...option }) | ||||
|   console.info(res) | ||||
|   return res as unknown as Promise<T> | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -51,6 +51,7 @@ | ||||
|     "highlight.js": "9.18.5", | ||||
|     "js-beautify": "1.13.0", | ||||
|     "jsencrypt": "3.0.0-rc.1", | ||||
|     "crypto-js": "^4.0.0", | ||||
|     "nprogress": "0.2.0", | ||||
|     "quill": "1.3.7", | ||||
|     "screenfull": "5.0.2", | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| import request from '@/utils/request' | ||||
| import {getRefreshToken} from "@/utils/auth"; | ||||
| import service from "@/utils/request"; | ||||
| import { getRefreshToken } from '@/utils/auth' | ||||
| import service from '@/utils/request' | ||||
|  | ||||
| // 登录方法 | ||||
| export function login(username, password, code, uuid, | ||||
|                       socialType, socialCode, socialState) { | ||||
| export function login(username, password, socialType, socialCode, socialState) { | ||||
|   const data = { | ||||
|     username, | ||||
|     password, | ||||
|     code, | ||||
|     uuid, | ||||
|     // 社交相关 | ||||
|     socialType, socialCode, socialState | ||||
|     socialType, | ||||
|     socialCode, | ||||
|     socialState | ||||
|   } | ||||
|   return request({ | ||||
|     url: '/system/auth/login', | ||||
| @@ -36,15 +35,6 @@ export function logout() { | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 获取验证码 | ||||
| export function getCodeImg() { | ||||
|   return request({ | ||||
|     url: '/system/captcha/get-image', | ||||
|     method: 'get', | ||||
|     timeout: 20000 | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 社交授权的跳转 | ||||
| export function socialAuthRedirect(type, redirectUri) { | ||||
|   return request({ | ||||
| @@ -108,20 +98,20 @@ export function getAuthorize(clientId) { | ||||
| } | ||||
|  | ||||
| export function authorize(responseType, clientId, redirectUri, state, | ||||
|                           autoApprove, checkedScopes, uncheckedScopes) { | ||||
|   autoApprove, checkedScopes, uncheckedScopes) { | ||||
|   // 构建 scopes | ||||
|   const scopes = {}; | ||||
|   const scopes = {} | ||||
|   for (const scope of checkedScopes) { | ||||
|     scopes[scope] = true; | ||||
|     scopes[scope] = true | ||||
|   } | ||||
|   for (const scope of uncheckedScopes) { | ||||
|     scopes[scope] = false; | ||||
|     scopes[scope] = false | ||||
|   } | ||||
|   // 发起请求 | ||||
|   return service({ | ||||
|     url: '/system/oauth2/authorize', | ||||
|     headers:{ | ||||
|       'Content-type': 'application/x-www-form-urlencoded', | ||||
|     headers: { | ||||
|       'Content-type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     params: { | ||||
|       response_type: responseType, | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								yudao-ui-admin/src/assets/images/default.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										464
									
								
								yudao-ui-admin/src/components/Verifition/Verify.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										266
									
								
								yudao-ui-admin/src/components/Verifition/Verify/VerifyPoints.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,266 @@ | ||||
| <template> | ||||
|   <div style="position: relative" > | ||||
|     <div class="verify-img-out"> | ||||
|       <div | ||||
|         class="verify-img-panel" | ||||
|         :style="{'width': setSize.imgWidth, | ||||
|                  'height': setSize.imgHeight, | ||||
|                  'background-size' : setSize.imgWidth + ' '+ setSize.imgHeight, | ||||
|                  'margin-bottom': vSpace + 'px'}" | ||||
|       > | ||||
|         <div v-show="showRefresh" class="verify-refresh" style="z-index:3" @click="refresh"> | ||||
|           <i class="iconfont icon-refresh" /> | ||||
|         </div> | ||||
|         <img | ||||
|           ref="canvas" | ||||
|           :src="pointBackImgBase?('data:image/png;base64,'+pointBackImgBase):defaultImg" | ||||
|           alt="" | ||||
|           style="width:100%;height:100%;display:block" | ||||
|           @click="bindingClick?canvasClick($event):undefined" | ||||
|         > | ||||
|  | ||||
|         <div | ||||
|           v-for="(tempPoint, index) in tempPoints" | ||||
|           :key="index" | ||||
|           class="point-area" | ||||
|           :style="{ | ||||
|             'background-color':'#1abd6c', | ||||
|             color:'#fff', | ||||
|             'z-index':9999, | ||||
|             width:'20px', | ||||
|             height:'20px', | ||||
|             'text-align':'center', | ||||
|             'line-height':'20px', | ||||
|             'border-radius': '50%', | ||||
|             position:'absolute', | ||||
|             top:parseInt(tempPoint.y-10) + 'px', | ||||
|             left:parseInt(tempPoint.x-10) + 'px' | ||||
|           }" | ||||
|         > | ||||
|           {{ index + 1 }} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <!-- 'height': this.barSize.height, --> | ||||
|     <div | ||||
|       class="verify-bar-area" | ||||
|       :style="{'width': setSize.imgWidth, | ||||
|                'color': this.barAreaColor, | ||||
|                'border-color': this.barAreaBorderColor, | ||||
|                'line-height':this.barSize.height}" | ||||
|     > | ||||
|       <span class="verify-msg">{{ text }}</span> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script type="text/babel"> | ||||
| /** | ||||
|  * VerifyPoints | ||||
|  * @description 点选 | ||||
|  * */ | ||||
| import { resetSize, _code_chars, _code_color1, _code_color2 } from './../utils/util' | ||||
| import { aesEncrypt } from '@/utils/ase' | ||||
| import { reqGet, reqCheck } from './../api/index' | ||||
|  | ||||
| export default { | ||||
|   name: 'VerifyPoints', | ||||
|   props: { | ||||
|     // 弹出式pop,固定fixed | ||||
|     mode: { | ||||
|       type: String, | ||||
|       default: 'fixed' | ||||
|     }, | ||||
|     captchaType: { | ||||
|       type: String, | ||||
|     }, | ||||
|     // 间隔 | ||||
|     vSpace: { | ||||
|       type: Number, | ||||
|       default: 5 | ||||
|     }, | ||||
|     imgSize: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return { | ||||
|           width: '310px', | ||||
|           height: '155px' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     barSize: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return { | ||||
|           width: '310px', | ||||
|           height: '40px' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     defaultImg: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       secretKey: '', // 后端返回的ase加密秘钥 | ||||
|       checkNum: 3, // 默认需要点击的字数 | ||||
|       fontPos: [], // 选中的坐标信息 | ||||
|       checkPosArr: [], // 用户点击的坐标 | ||||
|       num: 1, // 点击的记数 | ||||
|       pointBackImgBase: '', // 后端获取到的背景图片 | ||||
|       poinTextList: [], // 后端返回的点击字体顺序 | ||||
|       backToken: '', // 后端返回的token值 | ||||
|       setSize: { | ||||
|         imgHeight: 0, | ||||
|         imgWidth: 0, | ||||
|         barHeight: 0, | ||||
|         barWidth: 0 | ||||
|       }, | ||||
|       tempPoints: [], | ||||
|       text: '', | ||||
|       barAreaColor: undefined, | ||||
|       barAreaBorderColor: undefined, | ||||
|       showRefresh: true, | ||||
|       bindingClick: true | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     resetSize() { | ||||
|       return resetSize | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     // type变化则全面刷新 | ||||
|     type: { | ||||
|       immediate: true, | ||||
|       handler() { | ||||
|         this.init() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     // 禁止拖拽 | ||||
|     this.$el.onselectstart = function() { | ||||
|       return false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     init() { | ||||
|       // 加载页面 | ||||
|       this.fontPos.splice(0, this.fontPos.length) | ||||
|       this.checkPosArr.splice(0, this.checkPosArr.length) | ||||
|       this.num = 1 | ||||
|       this.getPictrue() | ||||
|       this.$nextTick(() => { | ||||
|         this.setSize = this.resetSize(this)	// 重新设置宽度高度 | ||||
|         this.$parent.$emit('ready', this) | ||||
|       }) | ||||
|     }, | ||||
|     canvasClick(e) { | ||||
|       this.checkPosArr.push(this.getMousePos(this.$refs.canvas, e)) | ||||
|       if (this.num === this.checkNum) { | ||||
|         this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e)) | ||||
|         // 按比例转换坐标值 | ||||
|         this.checkPosArr = this.pointTransfrom(this.checkPosArr, this.setSize) | ||||
|         // 等创建坐标执行完 | ||||
|         setTimeout(() => { | ||||
|           // var flag = this.comparePos(this.fontPos, this.checkPosArr); | ||||
|           // 发送后端请求 | ||||
|           var captchaVerification = this.secretKey ? aesEncrypt(this.backToken + '---' + JSON.stringify(this.checkPosArr), this.secretKey) : this.backToken + '---' + JSON.stringify(this.checkPosArr) | ||||
|           const data = { | ||||
|             captchaType: this.captchaType, | ||||
|             'pointJson': this.secretKey ? aesEncrypt(JSON.stringify(this.checkPosArr), this.secretKey) : JSON.stringify(this.checkPosArr), | ||||
|             'token': this.backToken | ||||
|           } | ||||
|           reqCheck(data).then(res => { | ||||
|             if (res.repCode === '0000') { | ||||
|               this.barAreaColor = '#4cae4c' | ||||
|               this.barAreaBorderColor = '#5cb85c' | ||||
|               this.text = '验证成功' | ||||
|               this.bindingClick = false | ||||
|               if (this.mode === 'pop') { | ||||
|                 setTimeout(() => { | ||||
|                   this.$parent.clickShow = false | ||||
|                   this.refresh() | ||||
|                 }, 1500) | ||||
|               } | ||||
|               this.$parent.$emit('success', { captchaVerification }) | ||||
|             } else { | ||||
|               this.$parent.$emit('error', this) | ||||
|               this.barAreaColor = '#d9534f' | ||||
|               this.barAreaBorderColor = '#d9534f' | ||||
|               this.text = '验证失败' | ||||
|               setTimeout(() => { | ||||
|                 this.refresh() | ||||
|               }, 700) | ||||
|             } | ||||
|           }) | ||||
|         }, 400) | ||||
|       } | ||||
|       if (this.num < this.checkNum) { | ||||
|         this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e)) | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     // 获取坐标 | ||||
|     getMousePos: function(obj, e) { | ||||
|       var x = e.offsetX | ||||
|       var y = e.offsetY | ||||
|       return { x, y } | ||||
|     }, | ||||
|     // 创建坐标点 | ||||
|     createPoint: function(pos) { | ||||
|       this.tempPoints.push(Object.assign({}, pos)) | ||||
|       return ++this.num | ||||
|     }, | ||||
|     refresh: function() { | ||||
|       this.tempPoints.splice(0, this.tempPoints.length) | ||||
|       this.barAreaColor = '#000' | ||||
|       this.barAreaBorderColor = '#ddd' | ||||
|       this.bindingClick = true | ||||
|       this.fontPos.splice(0, this.fontPos.length) | ||||
|       this.checkPosArr.splice(0, this.checkPosArr.length) | ||||
|       this.num = 1 | ||||
|       this.getPictrue() | ||||
|       this.text = '验证失败' | ||||
|       this.showRefresh = true | ||||
|     }, | ||||
|  | ||||
|     // 请求背景图片和验证图片 | ||||
|     getPictrue() { | ||||
|       const data = { | ||||
|         captchaType: this.captchaType, | ||||
|         clientUid: localStorage.getItem('point'), | ||||
|         ts: Date.now(), // 现在的时间戳 | ||||
|       } | ||||
|       reqGet(data).then(res => { | ||||
|         if (res.repCode === '0000') { | ||||
|           this.pointBackImgBase = res.repData.originalImageBase64 | ||||
|           this.backToken = res.repData.token | ||||
|           this.secretKey = res.repData.secretKey | ||||
|           this.poinTextList = res.repData.wordList | ||||
|           this.text = '请依次点击【' + this.poinTextList.join(',') + '】' | ||||
|         } else { | ||||
|           this.text = res.repMsg | ||||
|         } | ||||
|  | ||||
|         // 判断接口请求次数是否失效 | ||||
|         if (res.repCode === '6201') { | ||||
|           this.pointBackImgBase = null | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     // 坐标转换函数 | ||||
|     pointTransfrom(pointArr, imgSize) { | ||||
|       var newPointArr = pointArr.map(p => { | ||||
|         const x = Math.round(310 * p.x / parseInt(imgSize.imgWidth)) | ||||
|         const y = Math.round(155 * p.y / parseInt(imgSize.imgHeight)) | ||||
|         return { x, y } | ||||
|       }) | ||||
|       // console.log(newPointArr,"newPointArr"); | ||||
|       return newPointArr | ||||
|     } | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										377
									
								
								yudao-ui-admin/src/components/Verifition/Verify/VerifySlide.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,377 @@ | ||||
| <template> | ||||
|   <div style="position: relative;"> | ||||
|     <div | ||||
|       v-if="type === '2'" | ||||
|       class="verify-img-out" | ||||
|       :style="{height: (parseInt(setSize.imgHeight) + vSpace) + 'px'}" | ||||
|     > | ||||
|       <div | ||||
|         class="verify-img-panel" | ||||
|         :style="{width: setSize.imgWidth, | ||||
|                  height: setSize.imgHeight,}" | ||||
|       > | ||||
|         <img :src="backImgBase?('data:image/png;base64,'+backImgBase):defaultImg" alt="" style="width:100%;height:100%;display:block"> | ||||
|         <div v-show="showRefresh" class="verify-refresh" @click="refresh"><i class="iconfont icon-refresh" /> | ||||
|         </div> | ||||
|         <transition name="tips"> | ||||
|           <span v-if="tipWords" class="verify-tips" :class="passFlag ?'suc-bg':'err-bg'">{{ tipWords }}</span> | ||||
|         </transition> | ||||
|       </div> | ||||
|     </div> | ||||
|     <!-- 公共部分 --> | ||||
|     <div | ||||
|       class="verify-bar-area" | ||||
|       :style="{width: setSize.imgWidth, | ||||
|                height: barSize.height, | ||||
|                'line-height':barSize.height}" | ||||
|     > | ||||
|       <span class="verify-msg" v-text="text" /> | ||||
|       <div | ||||
|         class="verify-left-bar" | ||||
|         :style="{width: (leftBarWidth!==undefined)?leftBarWidth: barSize.height, height: barSize.height, 'border-color': leftBarBorderColor, transaction: transitionWidth}" | ||||
|       > | ||||
|         <span class="verify-msg" v-text="finishText" /> | ||||
|         <div | ||||
|           class="verify-move-block" | ||||
|           :style="{width: barSize.height, height: barSize.height, 'background-color': moveBlockBackgroundColor, left: moveBlockLeft, transition: transitionLeft}" | ||||
|           @touchstart="start" | ||||
|           @mousedown="start" | ||||
|         > | ||||
|           <i | ||||
|             :class="['verify-icon iconfont', iconClass]" | ||||
|             :style="{color: iconColor}" | ||||
|           /> | ||||
|           <div | ||||
|             v-if="type === '2'" | ||||
|             class="verify-sub-block" | ||||
|             :style="{'width':Math.floor(parseInt(setSize.imgWidth)*47/310)+ 'px', | ||||
|                      'height': setSize.imgHeight, | ||||
|                      'top':'-' + (parseInt(setSize.imgHeight) + vSpace) + 'px', | ||||
|                      'background-size': setSize.imgWidth + ' ' + setSize.imgHeight, | ||||
|             }" | ||||
|           > | ||||
|             <img :src="'data:image/png;base64,'+blockBackImgBase" alt="" style="width:100%;height:100%;display:block"> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| <script type="text/babel"> | ||||
| /** | ||||
|  * VerifySlide | ||||
|  * @description 滑块 | ||||
|  * */ | ||||
| import { aesEncrypt } from '@/utils/ase' | ||||
| import { resetSize } from './../utils/util' | ||||
| import { reqGet, reqCheck } from './../api/index' | ||||
|  | ||||
| //  "captchaType":"blockPuzzle", | ||||
| export default { | ||||
|   name: 'VerifySlide', | ||||
|   props: { | ||||
|     captchaType: { | ||||
|       type: String, | ||||
|     }, | ||||
|     type: { | ||||
|       type: String, | ||||
|       default: '1' | ||||
|     }, | ||||
|     // 弹出式pop,固定fixed | ||||
|     mode: { | ||||
|       type: String, | ||||
|       default: 'fixed' | ||||
|     }, | ||||
|     vSpace: { | ||||
|       type: Number, | ||||
|       default: 5 | ||||
|     }, | ||||
|     explain: { | ||||
|       type: String, | ||||
|       default: '向右滑动完成验证' | ||||
|     }, | ||||
|     imgSize: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return { | ||||
|           width: '310px', | ||||
|           height: '155px' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     blockSize: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return { | ||||
|           width: '50px', | ||||
|           height: '50px' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     barSize: { | ||||
|       type: Object, | ||||
|       default() { | ||||
|         return { | ||||
|           width: '310px', | ||||
|           height: '40px' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     defaultImg: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       secretKey: '', // 后端返回的加密秘钥 字段 | ||||
|       passFlag: '', // 是否通过的标识 | ||||
|       backImgBase: '', // 验证码背景图片 | ||||
|       blockBackImgBase: '', // 验证滑块的背景图片 | ||||
|       backToken: '', // 后端返回的唯一token值 | ||||
|       startMoveTime: '', // 移动开始的时间 | ||||
|       endMovetime: '', // 移动结束的时间 | ||||
|       tipsBackColor: '', // 提示词的背景颜色 | ||||
|       tipWords: '', | ||||
|       text: '', | ||||
|       finishText: '', | ||||
|       setSize: { | ||||
|         imgHeight: 0, | ||||
|         imgWidth: 0, | ||||
|         barHeight: 0, | ||||
|         barWidth: 0 | ||||
|       }, | ||||
|       top: 0, | ||||
|       left: 0, | ||||
|       moveBlockLeft: undefined, | ||||
|       leftBarWidth: undefined, | ||||
|       // 移动中样式 | ||||
|       moveBlockBackgroundColor: undefined, | ||||
|       leftBarBorderColor: '#ddd', | ||||
|       iconColor: undefined, | ||||
|       iconClass: 'icon-right', | ||||
|       status: false, // 鼠标状态 | ||||
|       isEnd: false,		// 是够验证完成 | ||||
|       showRefresh: true, | ||||
|       transitionLeft: '', | ||||
|       transitionWidth: '' | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     barArea() { | ||||
|       return this.$el.querySelector('.verify-bar-area') | ||||
|     }, | ||||
|     resetSize() { | ||||
|       return resetSize | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     // type变化则全面刷新 | ||||
|     type: { | ||||
|       immediate: true, | ||||
|       handler() { | ||||
|         this.init() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     // 禁止拖拽 | ||||
|     this.$el.onselectstart = function() { | ||||
|       return false | ||||
|     } | ||||
|     console.log(this.defaultImg) | ||||
|   }, | ||||
|   methods: { | ||||
|     init() { | ||||
|       this.text = this.explain | ||||
|       this.getPictrue() | ||||
|       this.$nextTick(() => { | ||||
|         const setSize = this.resetSize(this)	// 重新设置宽度高度 | ||||
|         for (const key in setSize) { | ||||
|           this.$set(this.setSize, key, setSize[key]) | ||||
|         } | ||||
|         this.$parent.$emit('ready', this) | ||||
|       }) | ||||
|  | ||||
|       var _this = this | ||||
|  | ||||
|       window.removeEventListener('touchmove', function(e) { | ||||
|         _this.move(e) | ||||
|       }) | ||||
|       window.removeEventListener('mousemove', function(e) { | ||||
|         _this.move(e) | ||||
|       }) | ||||
|  | ||||
|       // 鼠标松开 | ||||
|       window.removeEventListener('touchend', function() { | ||||
|         _this.end() | ||||
|       }) | ||||
|       window.removeEventListener('mouseup', function() { | ||||
|         _this.end() | ||||
|       }) | ||||
|  | ||||
|       window.addEventListener('touchmove', function(e) { | ||||
|         _this.move(e) | ||||
|       }) | ||||
|       window.addEventListener('mousemove', function(e) { | ||||
|         _this.move(e) | ||||
|       }) | ||||
|  | ||||
|       // 鼠标松开 | ||||
|       window.addEventListener('touchend', function() { | ||||
|         _this.end() | ||||
|       }) | ||||
|       window.addEventListener('mouseup', function() { | ||||
|         _this.end() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     // 鼠标按下 | ||||
|     start: function(e) { | ||||
|       e = e || window.event | ||||
|       if (!e.touches) { // 兼容PC端 | ||||
|         var x = e.clientX | ||||
|       } else { // 兼容移动端 | ||||
|         var x = e.touches[0].pageX | ||||
|       } | ||||
|       this.startLeft = Math.floor(x - this.barArea.getBoundingClientRect().left) | ||||
|       this.startMoveTime = +new Date() // 开始滑动的时间 | ||||
|       if (this.isEnd === false) { | ||||
|         this.text = '' | ||||
|         this.moveBlockBackgroundColor = '#337ab7' | ||||
|         this.leftBarBorderColor = '#337AB7' | ||||
|         this.iconColor = '#fff' | ||||
|         e.stopPropagation() | ||||
|         this.status = true | ||||
|       } | ||||
|     }, | ||||
|     // 鼠标移动 | ||||
|     move: function(e) { | ||||
|       e = e || window.event | ||||
|       if (this.status && this.isEnd === false) { | ||||
|         if (!e.touches) { // 兼容PC端 | ||||
|           var x = e.clientX | ||||
|         } else { // 兼容移动端 | ||||
|           var x = e.touches[0].pageX | ||||
|         } | ||||
|         var bar_area_left = this.barArea.getBoundingClientRect().left | ||||
|         var move_block_left = x - bar_area_left // 小方块相对于父元素的left值 | ||||
|         if (move_block_left >= this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2) { | ||||
|           move_block_left = this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2 | ||||
|         } | ||||
|         if (move_block_left <= 0) { | ||||
|           move_block_left = parseInt(parseInt(this.blockSize.width) / 2) | ||||
|         } | ||||
|         // 拖动后小方块的left值 | ||||
|         this.moveBlockLeft = (move_block_left - this.startLeft) + 'px' | ||||
|         this.leftBarWidth = (move_block_left - this.startLeft) + 'px' | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     // 鼠标松开 | ||||
|     end: function() { | ||||
|       this.endMovetime = +new Date() | ||||
|       var _this = this | ||||
|       // 判断是否重合 | ||||
|       if (this.status && this.isEnd === false) { | ||||
|         var moveLeftDistance = parseInt((this.moveBlockLeft || '').replace('px', '')) | ||||
|         moveLeftDistance = moveLeftDistance * 310 / parseInt(this.setSize.imgWidth) | ||||
|         const data = { | ||||
|           captchaType: this.captchaType, | ||||
|           'pointJson': this.secretKey ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey) : JSON.stringify({ x: moveLeftDistance, y: 5.0 }), | ||||
|           'token': this.backToken | ||||
|         } | ||||
|         reqCheck(data).then(res => { | ||||
|           if (res.repCode === '0000') { | ||||
|             this.moveBlockBackgroundColor = '#5cb85c' | ||||
|             this.leftBarBorderColor = '#5cb85c' | ||||
|             this.iconColor = '#fff' | ||||
|             this.iconClass = 'icon-check' | ||||
|             this.showRefresh = false | ||||
|             this.isEnd = true | ||||
|             if (this.mode === 'pop') { | ||||
|               setTimeout(() => { | ||||
|                 this.$parent.clickShow = false | ||||
|                 this.refresh() | ||||
|               }, 1500) | ||||
|             } | ||||
|             this.passFlag = true | ||||
|             this.tipWords = `${((this.endMovetime - this.startMoveTime) / 1000).toFixed(2)}s验证成功` | ||||
|             var captchaVerification = this.secretKey ? aesEncrypt(this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey) : this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }) | ||||
|             setTimeout(() => { | ||||
|               this.tipWords = '' | ||||
|               this.$parent.closeBox() | ||||
|               this.$parent.$emit('success', { captchaVerification }) | ||||
|             }, 1000) | ||||
|           } else { | ||||
|             this.moveBlockBackgroundColor = '#d9534f' | ||||
|             this.leftBarBorderColor = '#d9534f' | ||||
|             this.iconColor = '#fff' | ||||
|             this.iconClass = 'icon-close' | ||||
|             this.passFlag = false | ||||
|             setTimeout(function() { | ||||
|               _this.refresh() | ||||
|             }, 1000) | ||||
|             this.$parent.$emit('error', this) | ||||
|             this.tipWords = '验证失败' | ||||
|             setTimeout(() => { | ||||
|               this.tipWords = '' | ||||
|             }, 1000) | ||||
|           } | ||||
|         }) | ||||
|         this.status = false | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     refresh: function() { | ||||
|       this.showRefresh = true | ||||
|       this.finishText = '' | ||||
|  | ||||
|       this.transitionLeft = 'left .3s' | ||||
|       this.moveBlockLeft = 0 | ||||
|  | ||||
|       this.leftBarWidth = undefined | ||||
|       this.transitionWidth = 'width .3s' | ||||
|  | ||||
|       this.leftBarBorderColor = '#ddd' | ||||
|       this.moveBlockBackgroundColor = '#fff' | ||||
|       this.iconColor = '#000' | ||||
|       this.iconClass = 'icon-right' | ||||
|       this.isEnd = false | ||||
|  | ||||
|       this.getPictrue() | ||||
|       setTimeout(() => { | ||||
|         this.transitionWidth = '' | ||||
|         this.transitionLeft = '' | ||||
|         this.text = this.explain | ||||
|       }, 300) | ||||
|     }, | ||||
|  | ||||
|     // 请求背景图片和验证图片 | ||||
|     getPictrue() { | ||||
|       const data = { | ||||
|         captchaType: this.captchaType, | ||||
|         clientUid: localStorage.getItem('slider'), | ||||
|         ts: Date.now(), // 现在的时间戳 | ||||
|       } | ||||
|       reqGet(data).then(res => { | ||||
|         if (res.repCode === '0000') { | ||||
|           this.backImgBase = res.repData.originalImageBase64 | ||||
|           this.blockBackImgBase = res.repData.jigsawImageBase64 | ||||
|           this.backToken = res.repData.token | ||||
|           this.secretKey = res.repData.secretKey | ||||
|         } else { | ||||
|           this.tipWords = res.repMsg | ||||
|         } | ||||
|  | ||||
|         // 判断接口请求次数是否失效 | ||||
|         if (res.repCode === '6201') { | ||||
|           this.backImgBase = null | ||||
|           this.blockBackImgBase = null | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										25
									
								
								yudao-ui-admin/src/components/Verifition/api/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| /** | ||||
|  * 此处可直接引用自己项目封装好的 axios 配合后端联调 | ||||
|  */ | ||||
|  | ||||
| import request from './../utils/axios' // 组件内部封装的axios | ||||
| // import request from "@/api/axios.js"       //调用项目封装的axios | ||||
|  | ||||
| // 获取验证图片  以及token | ||||
| export function reqGet(data) { | ||||
|   return request({ | ||||
|     url: '/captcha/get', | ||||
|     method: 'post', | ||||
|     data | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 滑动或者点选验证 | ||||
| export function reqCheck(data) { | ||||
|   return request({ | ||||
|     url: '/captcha/check', | ||||
|     method: 'post', | ||||
|     data | ||||
|   }) | ||||
| } | ||||
|  | ||||
							
								
								
									
										30
									
								
								yudao-ui-admin/src/components/Verifition/utils/axios.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| import axios from 'axios' | ||||
|  | ||||
| axios.defaults.baseURL = process.env.VUE_APP_BASE_API | ||||
|  | ||||
| const service = axios.create({ | ||||
|   timeout: 40000, | ||||
|   headers: { | ||||
|     'X-Requested-With': 'XMLHttpRequest', | ||||
|     'Content-Type': 'application/json; charset=UTF-8' | ||||
|   }, | ||||
| }) | ||||
| service.interceptors.request.use( | ||||
|   config => { | ||||
|     return config | ||||
|   }, | ||||
|   error => { | ||||
|     Promise.reject(error) | ||||
|   } | ||||
| ) | ||||
|  | ||||
| // response interceptor | ||||
| service.interceptors.response.use( | ||||
|   response => { | ||||
|     const res = response.data | ||||
|     return res | ||||
|   }, | ||||
|   error => { | ||||
|   } | ||||
| ) | ||||
| export default service | ||||
							
								
								
									
										36
									
								
								yudao-ui-admin/src/components/Verifition/utils/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| export function resetSize(vm) { | ||||
|   let img_width, img_height, bar_width, bar_height	// 图片的宽度、高度,移动条的宽度、高度 | ||||
|  | ||||
|   let parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth | ||||
|   let parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight | ||||
|  | ||||
|   if (vm.imgSize.width.indexOf('%') !== -1) { | ||||
|     img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px' | ||||
|   } else { | ||||
|     img_width = this.imgSize.width | ||||
|   } | ||||
|  | ||||
|   if (vm.imgSize.height.indexOf('%') !== -1) { | ||||
|     img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px' | ||||
|   } else { | ||||
|     img_height = this.imgSize.height | ||||
|   } | ||||
|  | ||||
|   if (vm.barSize.width.indexOf('%') !== -1) { | ||||
|     bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px' | ||||
|   } else { | ||||
|     bar_width = this.barSize.width | ||||
|   } | ||||
|  | ||||
|   if (vm.barSize.height.indexOf('%') !== -1) { | ||||
|     bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px' | ||||
|   } else { | ||||
|     bar_height = this.barSize.height | ||||
|   } | ||||
|  | ||||
|   return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height } | ||||
| } | ||||
|  | ||||
| export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] | ||||
| export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'] | ||||
| export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC'] | ||||
| @@ -36,14 +36,11 @@ const user = { | ||||
|     Login({ commit }, userInfo) { | ||||
|       const username = userInfo.username.trim() | ||||
|       const password = userInfo.password | ||||
|       const code = userInfo.code | ||||
|       const uuid = userInfo.uuid | ||||
|       const socialCode = userInfo.socialCode | ||||
|       const socialState = userInfo.socialState | ||||
|       const socialType = userInfo.socialType | ||||
|       return new Promise((resolve, reject) => { | ||||
|         login(username, password, code, uuid, | ||||
|           socialType, socialCode, socialState).then(res => { | ||||
|         login(username, password, socialType, socialCode, socialState).then(res => { | ||||
|           res = res.data; | ||||
|           // 设置 token | ||||
|           setToken(res) | ||||
|   | ||||
							
								
								
									
										21
									
								
								yudao-ui-admin/src/utils/ase.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| import CryptoJS from 'crypto-js' | ||||
| /** | ||||
|  * @word 要加密的内容 | ||||
|  * @keyWord String  服务器随机返回的关键字 | ||||
|  */ | ||||
| export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { | ||||
|   const key = CryptoJS.enc.Utf8.parse(keyWord) | ||||
|   const secs = CryptoJS.enc.Utf8.parse(word) | ||||
|   const encrypted = CryptoJS.AES.encrypt(secs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) | ||||
|   return encrypted.toString() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @word 要解密的内容 | ||||
|  * @keyWord String  服务器随机返回的关键字 | ||||
|  */ | ||||
| export function aesDecrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') { | ||||
|   const key = CryptoJS.enc.Utf8.parse(keyWord) | ||||
|   const decrypt = CryptoJS.AES.decrypt(word, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) | ||||
|   return CryptoJS.enc.Utf8.stringify(decrypt).toString() | ||||
| } | ||||
| @@ -36,19 +36,10 @@ | ||||
|                 </el-form-item> | ||||
|                 <el-form-item prop="password"> | ||||
|                   <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" | ||||
|                             @keyup.enter.native="handleLogin"> | ||||
|                             @keyup.enter.native="getCode"> | ||||
|                     <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/> | ||||
|                   </el-input> | ||||
|                 </el-form-item> | ||||
|                 <el-form-item prop="code" v-if="captchaEnable"> | ||||
|                   <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" | ||||
|                             @keyup.enter.native="handleLogin"> | ||||
|                     <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon"/> | ||||
|                   </el-input> | ||||
|                   <div class="login-code"> | ||||
|                     <img :src="codeUrl" @click="getCode" class="login-code-img"/> | ||||
|                   </div> | ||||
|                 </el-form-item> | ||||
|                 <el-checkbox v-model="loginForm.rememberMe" style="margin:0 0 25px 0;">记住密码</el-checkbox> | ||||
|               </div> | ||||
|  | ||||
| @@ -76,7 +67,7 @@ | ||||
|               <!-- 下方的登录按钮 --> | ||||
|               <el-form-item style="width:100%;"> | ||||
|                 <el-button :loading="loading" size="medium" type="primary" style="width:100%;" | ||||
|                     @click.native.prevent="handleLogin"> | ||||
|                     @click.native.prevent="getCode"> | ||||
|                   <span v-if="!loading">登 录</span> | ||||
|                   <span v-else>登 录 中...</span> | ||||
|                 </el-button> | ||||
| @@ -96,6 +87,12 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <Verify | ||||
|       ref="verify" | ||||
|       :captcha-type="'blockPuzzle'" | ||||
|       :img-size="{width:'400px',height:'200px'}" | ||||
|       @success="handleLogin" | ||||
|     /> | ||||
|     <!-- footer --> | ||||
|     <div class="footer"> | ||||
|       Copyright © 2020-2022 iocoder.cn All Rights Reserved. | ||||
| @@ -104,7 +101,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import {getCodeImg, sendSmsCode, socialAuthRedirect} from "@/api/login"; | ||||
| import {sendSmsCode, socialAuthRedirect} from "@/api/login"; | ||||
| import {getTenantIdByName} from "@/api/system/tenant"; | ||||
| import {SystemUserSocialTypeEnum} from "@/utils/constants"; | ||||
| import {getTenantEnable} from "@/utils/ruoyi"; | ||||
| @@ -118,8 +115,13 @@ import { | ||||
|   setUsername | ||||
| } from "@/utils/auth"; | ||||
|  | ||||
| import Verify from '@/components/Verifition/Verify'; | ||||
|  | ||||
| export default { | ||||
|   name: "Login", | ||||
|   components: { | ||||
|     Verify | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       codeUrl: "", | ||||
| @@ -133,8 +135,6 @@ export default { | ||||
|         mobile: "", | ||||
|         mobileCode: "", | ||||
|         rememberMe: false, | ||||
|         code: "", | ||||
|         uuid: "", | ||||
|         tenantName: "芋道源码", | ||||
|       }, | ||||
|       scene: 21, | ||||
| @@ -146,7 +146,6 @@ export default { | ||||
|         password: [ | ||||
|           {required: true, trigger: "blur", message: "密码不能为空"} | ||||
|         ], | ||||
|         code: [{required: true, trigger: "change", message: "验证码不能为空"}], | ||||
|         mobile: [ | ||||
|           {required: true, trigger: "blur", message: "手机号不能为空"}, | ||||
|           { | ||||
| @@ -185,20 +184,11 @@ export default { | ||||
|       SysUserSocialTypeEnum: SystemUserSocialTypeEnum, | ||||
|     }; | ||||
|   }, | ||||
|   // watch: { | ||||
|   //   $route: { | ||||
|   //     handler: function(route) { | ||||
|   //       this.redirect = route.query && route.query.redirect; | ||||
|   //     }, | ||||
|   //     immediate: true | ||||
|   //   } | ||||
|   // }, | ||||
|   created() { | ||||
|     // 租户开关 | ||||
|     this.tenantEnable = getTenantEnable(); | ||||
|     // 重定向地址 | ||||
|     this.redirect = this.$route.query.redirect; | ||||
|     this.getCode(); | ||||
|     this.getCookie(); | ||||
|   }, | ||||
|   methods: { | ||||
| @@ -207,15 +197,8 @@ export default { | ||||
|       if (!this.captchaEnable) { | ||||
|         return; | ||||
|       } | ||||
|       // 请求远程,获得验证码 | ||||
|       getCodeImg().then(res => { | ||||
|         res = res.data; | ||||
|         this.captchaEnable = res.enable; | ||||
|         if (this.captchaEnable) { | ||||
|           this.codeUrl = "data:image/gif;base64," + res.img; | ||||
|           this.loginForm.uuid = res.uuid; | ||||
|         } | ||||
|       }); | ||||
|       // 弹出验证码 | ||||
|       this.$refs.verify.show() | ||||
|     }, | ||||
|     getCookie() { | ||||
|       const username = getUsername(); | ||||
| @@ -253,7 +236,6 @@ export default { | ||||
|             }); | ||||
|           }).catch(() => { | ||||
|             this.loading = false; | ||||
|             this.getCode(); | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|   | ||||
| @@ -10,7 +10,7 @@ const CompressionPlugin = require('compression-webpack-plugin') | ||||
|  | ||||
| const name = process.env.VUE_APP_TITLE || '芋道管理系统' // 网页标题 | ||||
|  | ||||
| const port = process.env.port || process.env.npm_config_port || 80 // 端口 | ||||
| const port = process.env.port || process.env.npm_config_port || 8081 // 端口 | ||||
|  | ||||
| // vue.config.js 配置说明 | ||||
| //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions | ||||
|   | ||||
| @@ -3197,6 +3197,11 @@ crypto-browserify@^3.11.0: | ||||
|     randombytes "^2.0.0" | ||||
|     randomfill "^1.0.3" | ||||
|  | ||||
| crypto-js@^4.0.0: | ||||
|   version "4.1.1" | ||||
|   resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" | ||||
|   integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== | ||||
|  | ||||
| css-color-names@0.0.4, css-color-names@^0.0.4: | ||||
|   version "0.0.4" | ||||
|   resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz" | ||||
| @@ -5773,11 +5778,6 @@ js-beautify@1.13.0: | ||||
|     mkdirp "^1.0.4" | ||||
|     nopt "^5.0.0" | ||||
|  | ||||
| js-cookie@3.0.1: | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.1.tgz" | ||||
|   integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== | ||||
|  | ||||
| js-message@1.0.7: | ||||
|   version "1.0.7" | ||||
|   resolved "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz" | ||||
|   | ||||
 xingyu
					xingyu