vue2 新增行为验证码
@ -57,6 +57,7 @@
|
|||||||
<commons-net.version>3.8.0</commons-net.version>
|
<commons-net.version>3.8.0</commons-net.version>
|
||||||
<jsch.version>0.1.55</jsch.version>
|
<jsch.version>0.1.55</jsch.version>
|
||||||
<tika-core.version>2.4.1</tika-core.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>
|
<minio.version>8.2.2</minio.version>
|
||||||
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.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>
|
<artifactId>yudao-spring-boot-starter-biz-error-code</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring 核心 -->
|
<!-- Spring 核心 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -451,6 +457,12 @@
|
|||||||
<version>${tika-core.version}</version>
|
<version>${tika-core.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.anji-plus</groupId>
|
||||||
|
<artifactId>spring-boot-starter-captcha</artifactId>
|
||||||
|
<version>${aj-captcha.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.velocity</groupId>
|
<groupId>org.apache.velocity</groupId>
|
||||||
<artifactId>velocity-engine-core</artifactId>
|
<artifactId>velocity-engine-core</artifactId>
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
<module>yudao-spring-boot-starter-biz-error-code</module>
|
<module>yudao-spring-boot-starter-biz-error-code</module>
|
||||||
|
|
||||||
<module>yudao-spring-boot-starter-flowable</module>
|
<module>yudao-spring-boot-starter-flowable</module>
|
||||||
|
<module>yudao-spring-boot-starter-captcha</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<artifactId>yudao-framework</artifactId>
|
<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 的安全配置
|
* 配置 URL 的安全配置
|
||||||
*
|
* <p>
|
||||||
* anyRequest | 匹配所有请求路径
|
* anyRequest | 匹配所有请求路径
|
||||||
* access | SpringEl表达式结果为true时可以访问
|
* access | SpringEl表达式结果为true时可以访问
|
||||||
* anonymous | 匿名可以访问
|
* anonymous | 匿名可以访问
|
||||||
@ -109,8 +109,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
|||||||
.headers().frameOptions().disable().and()
|
.headers().frameOptions().disable().and()
|
||||||
// 一堆自定义的 Spring Security 处理器
|
// 一堆自定义的 Spring Security 处理器
|
||||||
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
|
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
|
||||||
.accessDeniedHandler(accessDeniedHandler);
|
.accessDeniedHandler(accessDeniedHandler);
|
||||||
// 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
|
// 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
|
||||||
|
|
||||||
// 获得 @PermitAll 带来的 URL 列表,免登录
|
// 获得 @PermitAll 带来的 URL 列表,免登录
|
||||||
Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
|
Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
|
||||||
@ -118,23 +118,25 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
|
|||||||
httpSecurity
|
httpSecurity
|
||||||
// ①:全局共享规则
|
// ①:全局共享规则
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
// 1.1 静态资源,可匿名访问
|
// 1.1 静态资源,可匿名访问
|
||||||
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
|
||||||
// 1.2 设置 @PermitAll 无需认证
|
// 1.2 设置 @PermitAll 无需认证
|
||||||
.antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).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.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
|
||||||
.antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).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()
|
.antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
|
||||||
// 1.3 基于 yudao.security.permit-all-urls 无需认证
|
// 1.3 基于 yudao.security.permit-all-urls 无需认证
|
||||||
.antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
|
.antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
|
||||||
// 1.4 设置 App API 无需认证
|
// 1.4 设置 App API 无需认证
|
||||||
.antMatchers(buildAppApi("/**")).permitAll()
|
.antMatchers(buildAppApi("/**")).permitAll()
|
||||||
|
// 1.5 验证码captcha 允许匿名访问
|
||||||
|
.antMatchers("/captcha/get", "/captcha/check").permitAll()
|
||||||
// ②:每个项目的自定义规则
|
// ②:每个项目的自定义规则
|
||||||
.and().authorizeRequests(registry -> // 下面,循环设置自定义规则
|
.and().authorizeRequests(registry -> // 下面,循环设置自定义规则
|
||||||
authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
|
authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
|
||||||
// ③:兜底规则,必须认证
|
// ③:兜底规则,必须认证
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
;
|
;
|
||||||
|
|
||||||
// 添加 Token Filter
|
// 添加 Token Filter
|
||||||
|
@ -72,6 +72,11 @@
|
|||||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-captcha</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Job 定时任务相关 -->
|
<!-- Job 定时任务相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
@ -55,7 +55,6 @@ public class AuthController {
|
|||||||
private PermissionService permissionService;
|
private PermissionService permissionService;
|
||||||
@Resource
|
@Resource
|
||||||
private SocialUserService socialUserService;
|
private SocialUserService socialUserService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SecurityProperties securityProperties;
|
private SecurityProperties securityProperties;
|
||||||
|
|
||||||
|
@ -33,16 +33,6 @@ public class AuthLoginReqVO {
|
|||||||
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
|
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
|
||||||
private String password;
|
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 枚举值")
|
@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.enums.UserTypeEnum;
|
||||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
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.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.logger.dto.LoginLogCreateReqDTO;
|
||||||
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
|
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
|
||||||
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
|
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.logger.LoginResultEnum;
|
||||||
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
|
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
|
||||||
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
|
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.logger.LoginLogService;
|
||||||
import cn.iocoder.yudao.module.system.service.member.MemberService;
|
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.oauth2.OAuth2TokenService;
|
||||||
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
||||||
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
|
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -47,8 +44,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
|||||||
@Resource
|
@Resource
|
||||||
private AdminUserService userService;
|
private AdminUserService userService;
|
||||||
@Resource
|
@Resource
|
||||||
private CaptchaService captchaService;
|
|
||||||
@Resource
|
|
||||||
private LoginLogService loginLogService;
|
private LoginLogService loginLogService;
|
||||||
@Resource
|
@Resource
|
||||||
private OAuth2TokenService oauth2TokenService;
|
private OAuth2TokenService oauth2TokenService;
|
||||||
@ -86,9 +81,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
|
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
|
||||||
// 判断验证码是否正确
|
|
||||||
verifyCaptcha(reqVO);
|
|
||||||
|
|
||||||
// 使用账号密码,进行登录
|
// 使用账号密码,进行登录
|
||||||
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
|
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
|
||||||
|
|
||||||
@ -97,7 +89,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
|
|||||||
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
|
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
|
||||||
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
|
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 Token 令牌,记录登录日志
|
// 创建 Token 令牌,记录登录日志
|
||||||
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
|
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);
|
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,
|
private void createLoginLog(Long userId, String username,
|
||||||
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
|
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.dal.dataobject.user.AdminUserDO;
|
||||||
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
|
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.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.logger.LoginLogService;
|
||||||
import cn.iocoder.yudao.module.system.service.member.MemberService;
|
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.oauth2.OAuth2TokenService;
|
||||||
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
||||||
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
|
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.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
@ -57,11 +56,6 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
|
|||||||
@MockBean
|
@MockBean
|
||||||
private Validator validator;
|
private Validator validator;
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
when(captchaService.isCaptchaEnable()).thenReturn(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAuthenticate_success() {
|
public void testAuthenticate_success() {
|
||||||
// 准备参数
|
// 准备参数
|
||||||
@ -138,82 +132,82 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
public void testCaptcha_success() {
|
// public void testCaptcha_success() {
|
||||||
// 准备参数
|
// // 准备参数
|
||||||
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
|
// 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 验证码正确
|
// @Test
|
||||||
when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
|
// 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
|
||||||
authService.verifyCaptcha(reqVO);
|
// public void testLogin_success() {
|
||||||
// 断言
|
// // 准备参数
|
||||||
verify(captchaService).deleteCaptchaCode(reqVO.getUuid());
|
// AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o ->
|
||||||
}
|
// o.setUsername("test_username").setPassword("test_password"));
|
||||||
|
//
|
||||||
@Test
|
// // mock 验证码正确
|
||||||
public void testCaptcha_notFound() {
|
// when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
|
||||||
// 准备参数
|
// // mock user 数据
|
||||||
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
|
// 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);
|
||||||
assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
|
// // mock password 匹配
|
||||||
// 校验调用参数
|
// when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true);
|
||||||
verify(loginLogService, times(1)).createLoginLog(
|
// // mock 缓存登录用户到 Redis
|
||||||
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
|
// OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L)
|
||||||
&& o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult()))
|
// .setUserType(UserTypeEnum.ADMIN.getValue()));
|
||||||
);
|
// when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull()))
|
||||||
}
|
// .thenReturn(accessTokenDO);
|
||||||
|
//
|
||||||
@Test
|
// // 调用, 并断言异常
|
||||||
public void testCaptcha_codeError() {
|
// AuthLoginRespVO loginRespVO = authService.login(reqVO);
|
||||||
// 准备参数
|
// assertPojoEquals(accessTokenDO, loginRespVO);
|
||||||
AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
|
// // 校验调用参数
|
||||||
|
// verify(loginLogService).createLoginLog(
|
||||||
// mock 验证码不正确
|
// argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
|
||||||
String code = randomString();
|
// && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())
|
||||||
when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
|
// && o.getUserId().equals(user.getId()))
|
||||||
|
// );
|
||||||
// 调用, 并断言异常
|
// }
|
||||||
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
|
@Test
|
||||||
public void testLogout_success() {
|
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)
|
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||||
type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
|
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:
|
yudao:
|
||||||
@ -92,7 +129,8 @@ yudao:
|
|||||||
enable: true
|
enable: true
|
||||||
ignore-urls:
|
ignore-urls:
|
||||||
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
|
- /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/infra/file/*/get/** # 获取图片,和租户无关
|
||||||
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
|
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
|
||||||
- /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号
|
- /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号
|
||||||
|
@ -20,31 +20,37 @@ const request = (option: AxiosConfig) => {
|
|||||||
|
|
||||||
async function getFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function getFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
const res = await request({ method: 'GET', ...option })
|
const res = await request({ method: 'GET', ...option })
|
||||||
|
console.info(res)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function postFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
const res = await request({ method: 'POST', ...option })
|
const res = await request({ method: 'POST', ...option })
|
||||||
|
console.info(res)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function deleteFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
const res = await request({ method: 'DELETE', ...option })
|
const res = await request({ method: 'DELETE', ...option })
|
||||||
|
console.info(res)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async function putFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function putFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
const res = await request({ method: 'PUT', ...option })
|
const res = await request({ method: 'PUT', ...option })
|
||||||
|
console.info(res)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
async function downloadFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function downloadFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
const res = await request({ method: 'GET', responseType: 'blob', ...option })
|
const res = await request({ method: 'GET', responseType: 'blob', ...option })
|
||||||
|
console.info(res)
|
||||||
return res as unknown as Promise<T>
|
return res as unknown as Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFn<T = any>(option: AxiosConfig): Promise<T> {
|
async function uploadFn<T = any>(option: AxiosConfig): Promise<T> {
|
||||||
option.headersType = 'multipart/form-data'
|
option.headersType = 'multipart/form-data'
|
||||||
const res = await request({ method: 'PUT', ...option })
|
const res = await request({ method: 'PUT', ...option })
|
||||||
|
console.info(res)
|
||||||
return res as unknown as Promise<T>
|
return res as unknown as Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"highlight.js": "9.18.5",
|
"highlight.js": "9.18.5",
|
||||||
"js-beautify": "1.13.0",
|
"js-beautify": "1.13.0",
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
"jsencrypt": "3.0.0-rc.1",
|
||||||
|
"crypto-js": "^4.0.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"quill": "1.3.7",
|
"quill": "1.3.7",
|
||||||
"screenfull": "5.0.2",
|
"screenfull": "5.0.2",
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import {getRefreshToken} from "@/utils/auth";
|
import { getRefreshToken } from '@/utils/auth'
|
||||||
import service from "@/utils/request";
|
import service from '@/utils/request'
|
||||||
|
|
||||||
// 登录方法
|
// 登录方法
|
||||||
export function login(username, password, code, uuid,
|
export function login(username, password, socialType, socialCode, socialState) {
|
||||||
socialType, socialCode, socialState) {
|
|
||||||
const data = {
|
const data = {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
code,
|
|
||||||
uuid,
|
|
||||||
// 社交相关
|
// 社交相关
|
||||||
socialType, socialCode, socialState
|
socialType,
|
||||||
|
socialCode,
|
||||||
|
socialState
|
||||||
}
|
}
|
||||||
return request({
|
return request({
|
||||||
url: '/system/auth/login',
|
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) {
|
export function socialAuthRedirect(type, redirectUri) {
|
||||||
return request({
|
return request({
|
||||||
@ -108,20 +98,20 @@ export function getAuthorize(clientId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function authorize(responseType, clientId, redirectUri, state,
|
export function authorize(responseType, clientId, redirectUri, state,
|
||||||
autoApprove, checkedScopes, uncheckedScopes) {
|
autoApprove, checkedScopes, uncheckedScopes) {
|
||||||
// 构建 scopes
|
// 构建 scopes
|
||||||
const scopes = {};
|
const scopes = {}
|
||||||
for (const scope of checkedScopes) {
|
for (const scope of checkedScopes) {
|
||||||
scopes[scope] = true;
|
scopes[scope] = true
|
||||||
}
|
}
|
||||||
for (const scope of uncheckedScopes) {
|
for (const scope of uncheckedScopes) {
|
||||||
scopes[scope] = false;
|
scopes[scope] = false
|
||||||
}
|
}
|
||||||
// 发起请求
|
// 发起请求
|
||||||
return service({
|
return service({
|
||||||
url: '/system/oauth2/authorize',
|
url: '/system/oauth2/authorize',
|
||||||
headers:{
|
headers: {
|
||||||
'Content-type': 'application/x-www-form-urlencoded',
|
'Content-type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
response_type: responseType,
|
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) {
|
Login({ commit }, userInfo) {
|
||||||
const username = userInfo.username.trim()
|
const username = userInfo.username.trim()
|
||||||
const password = userInfo.password
|
const password = userInfo.password
|
||||||
const code = userInfo.code
|
|
||||||
const uuid = userInfo.uuid
|
|
||||||
const socialCode = userInfo.socialCode
|
const socialCode = userInfo.socialCode
|
||||||
const socialState = userInfo.socialState
|
const socialState = userInfo.socialState
|
||||||
const socialType = userInfo.socialType
|
const socialType = userInfo.socialType
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
login(username, password, code, uuid,
|
login(username, password, socialType, socialCode, socialState).then(res => {
|
||||||
socialType, socialCode, socialState).then(res => {
|
|
||||||
res = res.data;
|
res = res.data;
|
||||||
// 设置 token
|
// 设置 token
|
||||||
setToken(res)
|
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>
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
|
<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"/>
|
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon"/>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</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>
|
<el-checkbox v-model="loginForm.rememberMe" style="margin:0 0 25px 0;">记住密码</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -76,7 +67,7 @@
|
|||||||
<!-- 下方的登录按钮 -->
|
<!-- 下方的登录按钮 -->
|
||||||
<el-form-item style="width:100%;">
|
<el-form-item style="width:100%;">
|
||||||
<el-button :loading="loading" size="medium" type="primary" 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-if="!loading">登 录</span>
|
||||||
<span v-else>登 录 中...</span>
|
<span v-else>登 录 中...</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -96,6 +87,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Verify
|
||||||
|
ref="verify"
|
||||||
|
:captcha-type="'blockPuzzle'"
|
||||||
|
:img-size="{width:'400px',height:'200px'}"
|
||||||
|
@success="handleLogin"
|
||||||
|
/>
|
||||||
<!-- footer -->
|
<!-- footer -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Copyright © 2020-2022 iocoder.cn All Rights Reserved.
|
Copyright © 2020-2022 iocoder.cn All Rights Reserved.
|
||||||
@ -104,7 +101,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getCodeImg, sendSmsCode, socialAuthRedirect} from "@/api/login";
|
import {sendSmsCode, socialAuthRedirect} from "@/api/login";
|
||||||
import {getTenantIdByName} from "@/api/system/tenant";
|
import {getTenantIdByName} from "@/api/system/tenant";
|
||||||
import {SystemUserSocialTypeEnum} from "@/utils/constants";
|
import {SystemUserSocialTypeEnum} from "@/utils/constants";
|
||||||
import {getTenantEnable} from "@/utils/ruoyi";
|
import {getTenantEnable} from "@/utils/ruoyi";
|
||||||
@ -118,8 +115,13 @@ import {
|
|||||||
setUsername
|
setUsername
|
||||||
} from "@/utils/auth";
|
} from "@/utils/auth";
|
||||||
|
|
||||||
|
import Verify from '@/components/Verifition/Verify';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Login",
|
name: "Login",
|
||||||
|
components: {
|
||||||
|
Verify
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
codeUrl: "",
|
codeUrl: "",
|
||||||
@ -133,8 +135,6 @@ export default {
|
|||||||
mobile: "",
|
mobile: "",
|
||||||
mobileCode: "",
|
mobileCode: "",
|
||||||
rememberMe: false,
|
rememberMe: false,
|
||||||
code: "",
|
|
||||||
uuid: "",
|
|
||||||
tenantName: "芋道源码",
|
tenantName: "芋道源码",
|
||||||
},
|
},
|
||||||
scene: 21,
|
scene: 21,
|
||||||
@ -146,7 +146,6 @@ export default {
|
|||||||
password: [
|
password: [
|
||||||
{required: true, trigger: "blur", message: "密码不能为空"}
|
{required: true, trigger: "blur", message: "密码不能为空"}
|
||||||
],
|
],
|
||||||
code: [{required: true, trigger: "change", message: "验证码不能为空"}],
|
|
||||||
mobile: [
|
mobile: [
|
||||||
{required: true, trigger: "blur", message: "手机号不能为空"},
|
{required: true, trigger: "blur", message: "手机号不能为空"},
|
||||||
{
|
{
|
||||||
@ -185,20 +184,11 @@ export default {
|
|||||||
SysUserSocialTypeEnum: SystemUserSocialTypeEnum,
|
SysUserSocialTypeEnum: SystemUserSocialTypeEnum,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// watch: {
|
|
||||||
// $route: {
|
|
||||||
// handler: function(route) {
|
|
||||||
// this.redirect = route.query && route.query.redirect;
|
|
||||||
// },
|
|
||||||
// immediate: true
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
created() {
|
created() {
|
||||||
// 租户开关
|
// 租户开关
|
||||||
this.tenantEnable = getTenantEnable();
|
this.tenantEnable = getTenantEnable();
|
||||||
// 重定向地址
|
// 重定向地址
|
||||||
this.redirect = this.$route.query.redirect;
|
this.redirect = this.$route.query.redirect;
|
||||||
this.getCode();
|
|
||||||
this.getCookie();
|
this.getCookie();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -207,15 +197,8 @@ export default {
|
|||||||
if (!this.captchaEnable) {
|
if (!this.captchaEnable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 请求远程,获得验证码
|
// 弹出验证码
|
||||||
getCodeImg().then(res => {
|
this.$refs.verify.show()
|
||||||
res = res.data;
|
|
||||||
this.captchaEnable = res.enable;
|
|
||||||
if (this.captchaEnable) {
|
|
||||||
this.codeUrl = "data:image/gif;base64," + res.img;
|
|
||||||
this.loginForm.uuid = res.uuid;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
getCookie() {
|
getCookie() {
|
||||||
const username = getUsername();
|
const username = getUsername();
|
||||||
@ -253,7 +236,6 @@ export default {
|
|||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.getCode();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
|
|||||||
|
|
||||||
const name = process.env.VUE_APP_TITLE || '芋道管理系统' // 网页标题
|
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 配置说明
|
||||||
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
|
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
|
||||||
|
@ -3197,6 +3197,11 @@ crypto-browserify@^3.11.0:
|
|||||||
randombytes "^2.0.0"
|
randombytes "^2.0.0"
|
||||||
randomfill "^1.0.3"
|
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:
|
css-color-names@0.0.4, css-color-names@^0.0.4:
|
||||||
version "0.0.4"
|
version "0.0.4"
|
||||||
resolved "https://registry.npmmirror.com/css-color-names/-/css-color-names-0.0.4.tgz"
|
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"
|
mkdirp "^1.0.4"
|
||||||
nopt "^5.0.0"
|
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:
|
js-message@1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz"
|
resolved "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz"
|
||||||
|