Pre Merge pull request !62 from keppel/master

This commit is contained in:
keppel 2021-06-30 02:46:57 +00:00 committed by Gitee
commit 0c44dcc675
62 changed files with 1388 additions and 463 deletions

View File

@ -32,6 +32,7 @@
<redisson.version>3.15.2</redisson.version> <redisson.version>3.15.2</redisson.version>
<lock4j.version>2.2.1</lock4j.version> <lock4j.version>2.2.1</lock4j.version>
<datasource.version>3.4.0</datasource.version> <datasource.version>3.4.0</datasource.version>
<captcha.version>1.2.8</captcha.version>
</properties> </properties>
<!-- 依赖声明 --> <!-- 依赖声明 -->
@ -181,6 +182,13 @@
<version>${ruoyi-vue-plus.version}</version> <version>${ruoyi-vue-plus.version}</version>
</dependency> </dependency>
<!-- 滑动验证码 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
<version>${captcha.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -54,6 +54,12 @@
<artifactId>ruoyi-demo</artifactId> <artifactId>ruoyi-demo</artifactId>
</dependency> </dependency>
<!-- 滑动验证码 -->
<dependency>
<groupId>com.anji-plus</groupId>
<artifactId>spring-boot-starter-captcha</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,121 +0,0 @@
package com.ruoyi.web.controller.common;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.ShearCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.framework.captcha.UnsignedMathGenerator;
import com.ruoyi.framework.config.properties.CaptchaProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 验证码操作处理
*
* @author Lion Li
*/
@RestController
public class CaptchaController {
// 圆圈干扰验证码
@Resource(name = "CircleCaptcha")
private CircleCaptcha circleCaptcha;
// 线段干扰的验证码
@Resource(name = "LineCaptcha")
private LineCaptcha lineCaptcha;
// 扭曲干扰验证码
@Resource(name = "ShearCaptcha")
private ShearCaptcha shearCaptcha;
@Autowired
private RedisCache redisCache;
@Autowired
private CaptchaProperties captchaProperties;
/**
* 生成验证码
*/
@GetMapping("/captchaImage")
public AjaxResult getCode() {
Map<String, Object> ajax = new HashMap<>();
Boolean enabled = captchaProperties.getEnabled();
ajax.put("enabled", enabled);
if (!enabled) {
return AjaxResult.success(ajax);
}
// 保存验证码信息
String uuid = IdUtil.simpleUUID();
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String code = null;
// 生成验证码
CodeGenerator codeGenerator;
AbstractCaptcha captcha;
switch (captchaProperties.getType()) {
case "math":
codeGenerator = new UnsignedMathGenerator(captchaProperties.getNumberLength());
break;
case "char":
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
break;
default:
throw new IllegalArgumentException("验证码类型异常");
}
switch (captchaProperties.getCategory()) {
case "line":
captcha = lineCaptcha;
break;
case "circle":
captcha = circleCaptcha;
break;
case "shear":
captcha = shearCaptcha;
break;
default:
throw new IllegalArgumentException("验证码类别异常");
}
captcha.setGenerator(codeGenerator);
captcha.createCode();
if ("math".equals(captchaProperties.getType())) {
code = getCodeResult(captcha.getCode());
} else if ("char".equals(captchaProperties.getType())) {
code = captcha.getCode();
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
ajax.put("uuid", uuid);
ajax.put("img", captcha.getImageBase64());
return AjaxResult.success(ajax);
}
private String getCodeResult(String capStr) {
int numberLength = captchaProperties.getNumberLength();
int a = Convert.toInt(StrUtil.sub(capStr, 0, numberLength).trim());
char operator = capStr.charAt(numberLength);
int b = Convert.toInt(StrUtil.sub(capStr, numberLength + 1, numberLength + 1 + numberLength).trim());
switch (operator) {
case '*':
return a * b + "";
case '+':
return a + b + "";
case '-':
return a - b + "";
default:
return "";
}
}
}

View File

@ -1,11 +1,15 @@
package com.ruoyi.web.controller.system; package com.ruoyi.web.controller.system;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysMenu; import com.ruoyi.common.core.domain.entity.SysMenu;
import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginBody; import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.BaseException;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.framework.web.service.SysLoginService; import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService; import com.ruoyi.framework.web.service.SysPermissionService;
@ -17,10 +21,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* 登录验证 * 登录验证
@ -28,70 +29,74 @@ import java.util.Set;
* @author ruoyi * @author ruoyi
*/ */
@RestController @RestController
public class SysLoginController public class SysLoginController {
{ @Autowired
@Autowired private SysLoginService loginService;
private SysLoginService loginService;
@Autowired @Autowired
private ISysMenuService menuService; private ISysMenuService menuService;
@Autowired @Autowired
private SysPermissionService permissionService; private SysPermissionService permissionService;
@Autowired @Autowired
private TokenService tokenService; private TokenService tokenService;
/** @Autowired
* 登录方法 private CaptchaService captchaService;
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
Map<String,Object> ajax = new HashMap<>();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return AjaxResult.success(ajax);
}
/** /**
* 获取用户信息 * 登录方法
* *
* @return 用户信息 * @param loginBody 登录信息
*/ * @return 结果
@GetMapping("getInfo") */
public AjaxResult getInfo() @PostMapping("/login")
{ public AjaxResult login(@RequestBody LoginBody loginBody) {
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); CaptchaVO vo = new CaptchaVO();
SysUser user = loginUser.getUser(); vo.setCaptchaVerification(loginBody.getCode());
// 角色集合 ResponseModel verification = captchaService.verification(vo);
Set<String> roles = permissionService.getRolePermission(user); if (!Objects.equals("0000", verification.getRepCode())) {
// 权限集合 throw new BaseException("验证码校验失败!");
Set<String> permissions = permissionService.getMenuPermission(user); }
Map<String,Object> ajax = new HashMap<>(); // 生成令牌
ajax.put("user", user); String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
ajax.put("roles", roles); Map<String, String> result = new HashMap<>();
ajax.put("permissions", permissions); result.put(Constants.TOKEN, token);
return AjaxResult.success(ajax); return AjaxResult.success(result);
} }
/** /**
* 获取路由信息 * 获取用户信息
* *
* @return 路由信息 * @return 用户信息
*/ */
@GetMapping("getRouters") @GetMapping("getInfo")
public AjaxResult getRouters() public AjaxResult getInfo() {
{ LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); SysUser user = loginUser.getUser();
// 用户信息 // 角色集合
SysUser user = loginUser.getUser(); Set<String> roles = permissionService.getRolePermission(user);
List<SysMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId()); // 权限集合
return AjaxResult.success(menuService.buildMenus(menus)); Set<String> permissions = permissionService.getMenuPermission(user);
} Map<String, Object> ajax = new HashMap<>();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return AjaxResult.success(ajax);
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters() {
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
// 用户信息
SysUser user = loginUser.getUser();
List<SysMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId());
return AjaxResult.success(menuService.buildMenus(menus));
}
} }

View File

@ -13,17 +13,26 @@ ruoyi:
# 获取ip地址开关 # 获取ip地址开关
addressEnabled: true addressEnabled: true
captcha: aj:
# 验证码开关 captcha:
enabled: true jigsaw: classpath:images/jigsaw
# 验证码类型 math 数组计算 char 字符验证 pic-click: classpath:images/pic-click
type: math cache-type: local
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 type: default
category: circle water-mark: 美甲啦
# 数字验证码位数 slip-offset: 5
numberLength: 1 aes-status: true
# 字符验证码长度 interference-options: 2
charLength: 4 history-data-clear-enable: true
req-frequency-limit-enable: true
req-get-lock-limit: 5
req-get-lock-seconds: 360
# get接口一分钟内请求数限制
req-get-minute-limit: 30
# check接口一分钟内请求数限制
req-check-minute-limit: 60
# verify接口一分钟内请求数限制
req-verify-minute-limit: 60
# 开发环境配置 # 开发环境配置
server: server:

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,85 +0,0 @@
package com.ruoyi.framework.captcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.math.Calculator;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
/**
* 无符号计算生成器
*
* @author Lion Li
*/
public class UnsignedMathGenerator implements CodeGenerator {
private static final long serialVersionUID = -5514819971774091076L;
private static final String operators = "+-*";
/**
* 参与计算数字最大长度
*/
private final int numberLength;
/**
* 构造
*/
public UnsignedMathGenerator() {
this(2);
}
/**
* 构造
*
* @param numberLength 参与计算最大数字位数
*/
public UnsignedMathGenerator(int numberLength) {
this.numberLength = numberLength;
}
@Override
public String generate() {
final int limit = getLimit();
int min = RandomUtil.randomInt(limit);
int max = RandomUtil.randomInt(min, limit);
String number1 = Integer.toString(max);
String number2 = Integer.toString(min);
number1 = StrUtil.padAfter(number1, this.numberLength, CharUtil.SPACE);
number2 = StrUtil.padAfter(number2, this.numberLength, CharUtil.SPACE);
return number1 + RandomUtil.randomChar(operators) + number2 + '=';
}
@Override
public boolean verify(String code, String userInputCode) {
int result;
try {
result = Integer.parseInt(userInputCode);
} catch (NumberFormatException e) {
// 用户输入非数字
return false;
}
final int calculateResult = (int) Calculator.conversion(code);
return result == calculateResult;
}
/**
* 获取验证码长度
*
* @return 验证码长度
*/
public int getLength() {
return this.numberLength * 2 + 2;
}
/**
* 根据长度获取参与计算数字最大值
*
* @return 最大值
*/
private int getLimit() {
return Integer.parseInt("1" + StrUtil.repeat('0', this.numberLength));
}
}

View File

@ -1,55 +0,0 @@
package com.ruoyi.framework.config;
import java.awt.*;
import cn.hutool.captcha.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author Lion Li
*/
@Configuration
public class CaptchaConfig {
private final int width = 160;
private final int height = 60;
private final Color background = Color.PINK;
private final Font font = new Font("Arial", Font.BOLD, 48);
/**
* 圆圈干扰验证码
*/
@Bean(name = "CircleCaptcha")
public CircleCaptcha getCircleCaptcha() {
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(width, height);
captcha.setBackground(background);
captcha.setFont(font);
return captcha;
}
/**
* 线段干扰的验证码
*/
@Bean(name = "LineCaptcha")
public LineCaptcha getLineCaptcha() {
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(width, height);
captcha.setBackground(background);
captcha.setFont(font);
return captcha;
}
/**
* 扭曲干扰验证码
*/
@Bean(name = "ShearCaptcha")
public ShearCaptcha getShearCaptcha() {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(width, height);
captcha.setBackground(background);
captcha.setFont(font);
return captcha;
}
}

View File

@ -101,7 +101,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
// 过滤请求 // 过滤请求
.authorizeRequests() .authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问 // 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous() .antMatchers("/login", "/captcha/get", "/captcha/check").anonymous()
.antMatchers( .antMatchers(
HttpMethod.GET, HttpMethod.GET,
"/*.html", "/*.html",

View File

@ -1,41 +0,0 @@
package com.ruoyi.framework.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码 配置属性
*
* @author Lion Li
*/
@Data
@Component
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
/**
* 验证码开关
*/
private Boolean enabled;
/**
* 验证码类型
*/
private String type;
/**
* 验证码类别
*/
private String category;
/**
* 数字验证码位数
*/
private Integer numberLength;
/**
* 字符验证码长度
*/
private Integer charLength;
}

View File

@ -5,13 +5,10 @@ import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.CustomException; import com.ruoyi.common.exception.CustomException;
import com.ruoyi.common.exception.user.CaptchaException;
import com.ruoyi.common.exception.user.CaptchaExpireException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException; import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.framework.config.properties.CaptchaProperties;
import com.ruoyi.system.service.ISysUserService; import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
@ -29,87 +26,63 @@ import javax.servlet.http.HttpServletRequest;
* @author ruoyi * @author ruoyi
*/ */
@Component @Component
public class SysLoginService public class SysLoginService {
{ @Autowired
@Autowired private TokenService tokenService;
private TokenService tokenService;
@Resource @Resource
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Autowired @Autowired
private CaptchaProperties captchaProperties; private RedisCache redisCache;
/* @Autowired
private CaptchaProperties captchaProperties;*/
@Autowired @Autowired
private ISysUserService userService; private ISysUserService userService;
@Autowired @Autowired
private AsyncService asyncService; private AsyncService asyncService;
/** /**
* 登录验证 * 登录验证
* *
* @param username 用户名 * @param username 用户名
* @param password 密码 * @param password 密码
* @param code 验证码 * @return 结果
* @param uuid 唯一标识 */
* @return 结果 public String login(String username, String password) {
*/
public String login(String username, String password, String code, String uuid)
{
HttpServletRequest request = ServletUtils.getRequest(); HttpServletRequest request = ServletUtils.getRequest();
if(captchaProperties.getEnabled()) { // 用户验证
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; Authentication authentication;
String captcha = redisCache.getCacheObject(verifyKey); try {
redisCache.deleteObject(verifyKey); // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
if (captcha == null) { authentication = authenticationManager
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"), request); .authenticate(new UsernamePasswordAuthenticationToken(username, password));
throw new CaptchaExpireException(); } catch (Exception e) {
} if (e instanceof BadCredentialsException) {
if (!code.equalsIgnoreCase(captcha)) { asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"), request);
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"), request); throw new UserPasswordNotMatchException();
throw new CaptchaException(); } else {
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage(), request);
throw new CustomException(e.getMessage());
} }
} }
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"), request);
throw new UserPasswordNotMatchException();
}
else
{
asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage(), request);
throw new CustomException(e.getMessage());
}
}
asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"), request); asyncService.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"), request);
LoginUser loginUser = (LoginUser) authentication.getPrincipal(); LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUser()); recordLoginInfo(loginUser.getUser());
// 生成token // 生成token
return tokenService.createToken(loginUser); return tokenService.createToken(loginUser);
} }
/** /**
* 记录登录信息 * 记录登录信息
*/ */
public void recordLoginInfo(SysUser user) public void recordLoginInfo(SysUser user) {
{ user.setLoginIp(ServletUtils.getClientIP());
user.setLoginIp(ServletUtils.getClientIP()); user.setLoginDate(DateUtils.getNowDate());
user.setLoginDate(DateUtils.getNowDate());
user.setUpdateBy(user.getUserName()); user.setUpdateBy(user.getUserName());
userService.updateUserProfile(user); userService.updateUserProfile(user);
} }
} }

View File

@ -40,6 +40,7 @@
"axios": "0.21.0", "axios": "0.21.0",
"clipboard": "2.0.6", "clipboard": "2.0.6",
"core-js": "3.8.1", "core-js": "3.8.1",
"crypto-js": "^4.0.0",
"echarts": "4.9.0", "echarts": "4.9.0",
"element-ui": "2.15.2", "element-ui": "2.15.2",
"file-saver": "2.0.4", "file-saver": "2.0.4",
@ -50,6 +51,7 @@
"jsencrypt": "3.0.0-rc.1", "jsencrypt": "3.0.0-rc.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "1.3.7", "quill": "1.3.7",
"quill-image-super-solution-module": "^2.0.0",
"screenfull": "5.0.2", "screenfull": "5.0.2",
"sortablejs": "1.10.2", "sortablejs": "1.10.2",
"vue": "2.6.12", "vue": "2.6.12",

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,256 @@
<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 class="verify-refresh" style="z-index:3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<img :src="pointBackImgBase?('data:image/png;base64,'+pointBackImgBase):defaultImg"
ref="canvas"
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: {
//popfixed
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
}
},
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)
let 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(){
let 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=>{
let x = Math.round(310 * p.x/parseInt(imgSize.imgWidth))
let y =Math.round(155 * p.y/parseInt(imgSize.imgHeight))
return {x,y}
})
// console.log(newPointArr,"newPointArr");
return newPointArr
}
},
watch: {
// type
type: {
immediate: true,
handler() {
this.init()
}
}
},
mounted() {
//
this.$el.onselectstart = function () {
return false
}
},
}
</script>

View File

@ -0,0 +1,359 @@
<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 class="verify-refresh" @click="refresh" v-show="showRefresh"><i class="iconfont icon-refresh"></i>
</div>
<transition name="tips">
<span class="verify-tips" v-if="tipWords" :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"></span>
<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"></span>
<div class="verify-move-block"
@touchstart="start"
@mousedown="start"
:style="{width: barSize.height, height: barSize.height, 'background-color': moveBlockBackgroundColor, left: moveBlockLeft, transition: transitionLeft}">
<i :class="['verify-icon iconfont', iconClass]"
:style="{color: iconColor}"></i>
<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'
},
//popfixed
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
}
},
methods: {
init() {
this.text = this.explain
this.getPictrue();
this.$nextTick(() => {
let setSize = this.resetSize(this) //
for (let 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)
let 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(){
let 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
}
})
},
},
watch: {
// type
type: {
immediate: true,
handler() {
this.init()
}
}
},
mounted() {
//
this.$el.onselectstart = function () {
return false
}
},
}
</script>

View File

@ -0,0 +1,27 @@
/**
* 此处可直接引用自己项目封装好的 axios 配合后端联调
*/
import request from "./../utils/axios" //组件内部封装的axios
// import request from "@/api/axios.js" //调用项目封装的axios
//获取验证图片 以及token
export function reqGet(data) {
return request({
url: '/dev-api/captcha/get',
method: 'post',
data
})
}
//滑动或者点选验证
export function reqCheck(data) {
return request({
url: '/dev-api/captcha/check',
method: 'post',
data
})
}

View File

@ -0,0 +1,11 @@
import CryptoJS from 'crypto-js'
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
export function aesEncrypt(word,keyWord="XwKsGlMcdPMEhR1B"){
var key = CryptoJS.enc.Utf8.parse(keyWord);
var srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
return encrypted.toString();
}

View File

@ -0,0 +1,30 @@
import axios from 'axios';
axios.defaults.baseURL = process.env.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

View File

@ -0,0 +1,36 @@
export function resetSize(vm) {
var img_width, img_height, bar_width, bar_height; //图片的宽度、高度,移动条的宽度、高度
var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
var 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']

View File

@ -28,19 +28,26 @@
> >
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input> </el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item prop="code">
<Verify
@success="verifySuccess"
:mode="'pop'"
:captchaType="'blockPuzzle'"
:imgSize="{ width: '330px', height: '155px' }"
ref="verify"
/>
</el-form-item>
<el-form-item style="width:100%;"> <el-form-item style="width:100%;">
<el-button <el-button
:loading="loading" :loading="loading"
size="medium" size="medium"
type="primary" type="primary"
style="width:100%;" style="width:100%;"
@click.native.prevent="handleLogin" @click.native.prevent="useVerify"
> >
<!-- @click.native.prevent="handleLogin"-->
<span v-if="!loading"> </span> <span v-if="!loading"> </span>
<span v-else> 中...</span> <span v-else> 中...</span>
</el-button> </el-button>
@ -54,12 +61,15 @@
</template> </template>
<script> <script>
import { getCodeImg } from "@/api/login"; import Verify from "@/components/Verifition/Verify";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { encrypt, decrypt } from '@/utils/jsencrypt' import { encrypt, decrypt } from '@/utils/jsencrypt'
export default { export default {
name: "Login", name: "Login",
components: {
Verify
},
data() { data() {
return { return {
codeUrl: "", codeUrl: "",
@ -78,7 +88,6 @@ export default {
password: [ password: [
{ required: true, trigger: "blur", message: "密码不能为空" } { required: true, trigger: "blur", message: "密码不能为空" }
], ],
code: [{ required: true, trigger: "change", message: "验证码不能为空" }]
}, },
loading: false, loading: false,
redirect: undefined, redirect: undefined,
@ -94,19 +103,24 @@ export default {
} }
}, },
created() { created() {
this.getCode();
this.getCookie(); this.getCookie();
}, },
methods: { methods: {
getCode() { verifySuccess(params){
getCodeImg().then(res => { // params , 便
this.captchaEnabled = res.data.enabled; this.loginForm.code = params.captchaVerification;
if(res.data.enabled){
this.codeUrl = "data:image/gif;base64," + res.data.img; this.handleLogin();
this.loginForm.uuid = res.data.uuid; },
useVerify() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.$refs.verify.show();
} }
}); });
}, },
getCookie() { getCookie() {
const username = Cookies.get("username"); const username = Cookies.get("username");
const password = Cookies.get("password"); const password = Cookies.get("password");
@ -134,7 +148,6 @@ export default {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{}); this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => { }).catch(() => {
this.loading = false; this.loading = false;
this.getCode();
}); });
} }
}); });