Commit fb1e7076 authored by ma's avatar ma

新增找回密码功能,以及一些密码限制和频率限制

parent 8c7af349
...@@ -2,8 +2,6 @@ package iot.sixiang.license.controller; ...@@ -2,8 +2,6 @@ package iot.sixiang.license.controller;
import com.acc.secret.util.RSAUtil; import com.acc.secret.util.RSAUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import iot.sixiang.license.consts.ResultCode; import iot.sixiang.license.consts.ResultCode;
import iot.sixiang.license.entity.User; import iot.sixiang.license.entity.User;
...@@ -15,24 +13,22 @@ import iot.sixiang.license.log.MyLog; ...@@ -15,24 +13,22 @@ import iot.sixiang.license.log.MyLog;
import iot.sixiang.license.mapper.UserMapper; import iot.sixiang.license.mapper.UserMapper;
import iot.sixiang.license.model.BaseResult; import iot.sixiang.license.model.BaseResult;
import iot.sixiang.license.model.ResResult; import iot.sixiang.license.model.ResResult;
import iot.sixiang.license.model.dto.CheckCodeDto;
import iot.sixiang.license.model.vo.LoginReqVo; import iot.sixiang.license.model.vo.LoginReqVo;
import iot.sixiang.license.model.vo.LoginVo; import iot.sixiang.license.model.vo.LoginVo;
import iot.sixiang.license.model.vo.UserResetPwdVo;
import iot.sixiang.license.service.UserService;
import iot.sixiang.license.util.CommonUtil; import iot.sixiang.license.util.CommonUtil;
import iot.sixiang.license.util.EmailUtils; import iot.sixiang.license.util.EmailUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.DigestUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore; import springfox.documentation.annotations.ApiIgnore;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/** /**
* 登录Controller * 登录Controller
...@@ -47,13 +43,24 @@ public class LoginController { ...@@ -47,13 +43,24 @@ public class LoginController {
EmailUtils emailUtils; EmailUtils emailUtils;
@Resource @Resource
UserMapper userMapper; UserMapper userMapper;
@Resource
UserService userService;
@Value("${spring.mail.to}") @Value("${spring.mail.to}")
private String account; private String account;
@Value("${rsa.private_key}") @Value("${rsa.private_key}")
private String PRIVATE_KRY; private String PRIVATE_KRY;
@Value("${other.error_count.forget_pwd}")
private Integer forgetPwdMaxErrCount;
@Value("${other.error_count.check_code}")
private Integer checkCodeMaxErrCount;
@Value("${other.code_exp_time}")
private Integer codeExpTimeStr;
private static final String USER_NAME = "root"; private static final String USER_NAME = "root";
private static final String OPERATION_CHECK = "check";
private static final String OPERATION_RESET = "reset";
/** /**
* 模拟用户登录 * 模拟用户登录
*/ */
...@@ -69,7 +76,7 @@ public class LoginController { ...@@ -69,7 +76,7 @@ public class LoginController {
} }
User user = userMapper.getUserByUserName(USER_NAME); User user = userMapper.getUserByUserName(USER_NAME);
String name = USER_NAME; String name = USER_NAME;
String pwd = user.getPassword(); String pwd = user.getPassword();
LoginUser dbUser = new LoginUser(String.valueOf(user.getUserId()), user.getUserName(), user.getPassword()); LoginUser dbUser = new LoginUser(String.valueOf(user.getUserId()), user.getUserName(), user.getPassword());
if (name.equals(userName) && RSAUtil.getDecryptString(password, PRIVATE_KRY).equals(pwd)) { if (name.equals(userName) && RSAUtil.getDecryptString(password, PRIVATE_KRY).equals(pwd)) {
// 登录错误次数 // 登录错误次数
...@@ -81,7 +88,7 @@ public class LoginController { ...@@ -81,7 +88,7 @@ public class LoginController {
} else { } else {
Date curCodeDate = new Date(); Date curCodeDate = new Date();
if (code.equals(UserUtils.getEmailCode(account)) && curCodeDate.before(UserUtils.getEmailCodeExpTime(account))) { if (code.equals(UserUtils.getEmailCode(account)) && curCodeDate.before(UserUtils.getEmailCodeExpTime(account))) {
//if (code.equals("123456")) { //if (code.equals("123456")) {
String token = JwtUtil.createToken(dbUser); String token = JwtUtil.createToken(dbUser);
LoginVo loginVo = new LoginVo(); LoginVo loginVo = new LoginVo();
loginVo.setAuthorization(token); loginVo.setAuthorization(token);
...@@ -154,11 +161,128 @@ public class LoginController { ...@@ -154,11 +161,128 @@ public class LoginController {
return BaseResult.failed().setMsgValue("验证码还在有效期内"); return BaseResult.failed().setMsgValue("验证码还在有效期内");
} }
String code = CommonUtil.getValidateCode(); String code = CommonUtil.getValidateCode();
String content = "感谢您使用实名制服务器" + "\n" + "此次登录验证码为:" + code + "(有效期分钟)。验证码提供给他人可能导致账号被盗,请勿转发或泄露。" + "\n" + "--------------------------------------------------------------" + "此邮件由系统自动发送,请勿回复此邮件" + "--------------------------------------------------------------"; String content = "感谢您使用实名制服务器" + "\n" + "此次登录验证码为:" + code + "(有效期" + codeExpTimeStr + "分钟)。验证码提供给他人可能导致账号被盗,请勿转发或泄露。" + "\n" + "--------------------------------------------------------------" + "此邮件由系统自动发送,请勿回复此邮件" + "--------------------------------------------------------------";
emailUtils.sendSimpleMail(account, "感谢您使用实名制服务器", content); emailUtils.sendSimpleMail(account, "感谢您使用实名制服务器", content);
UserUtils.setEmailCode(account, code); UserUtils.setEmailCode(account, code);
Date codeExpTime = new Date(System.currentTimeMillis() + 3 * 60 * 1000); Date codeExpTime = new Date(System.currentTimeMillis() + codeExpTimeStr * 60 * 1000);
UserUtils.setEmailCodeExpTime(account, codeExpTime); UserUtils.setEmailCodeExpTime(account, codeExpTime);
return BaseResult.success(); return BaseResult.success();
} }
@ApiOperation(value = "发送修改密码验证码", notes = "发送修改密码验证码到邮箱")
@GetMapping("send_code/change_pwd")
public BaseResult sendChangePwdCode() {
Date emailCodeExpTime = UserUtils.getEmailCodeExpTime(account + OPERATION_CHECK);
if (emailCodeExpTime != null && emailCodeExpTime.after(new Date())) {
return BaseResult.failed().setMsgValue("验证码还在有效期内");
}
String code = CommonUtil.getValidateCode();
String content = "感谢您使用实名制服务器" + "\n" + "此次修改密码的验证码为:" + code + "(有效期" + codeExpTimeStr + "分钟)。验证码提供给他人可能导致账号被盗,请勿转发或泄露。" + "\n" + "--------------------------------------------------------------" + "此邮件由系统自动发送,请勿回复此邮件" + "--------------------------------------------------------------";
emailUtils.sendSimpleMail(account, "感谢您使用实名制服务器", content);
UserUtils.setEmailCode(account + OPERATION_CHECK, code);
Date codeExpTime = new Date(System.currentTimeMillis() + codeExpTimeStr * 60 * 1000);
UserUtils.setEmailCodeExpTime(account + OPERATION_CHECK, codeExpTime);
return BaseResult.success();
}
@ApiOperation(value = "校验验证码", notes = "校验验证码")
@PostMapping("check_code")
public BaseResult checkCode(@RequestBody CheckCodeDto checkCodeDto) {
String code = checkCodeDto.getCode();
String emailCode = UserUtils.getEmailCode(account + OPERATION_CHECK);
String codeFreezeTimeStr = UserUtils.getCodeFreezeTimeMap(account + OPERATION_CHECK);
// codeFreezeTimeStr不为空且冻结时间是今天直接报错,不是今天的话清空数据
if (!StringUtils.isEmpty(codeFreezeTimeStr)) {
if (codeFreezeTimeStr.equals(CommonUtil.getCurDateStr())) {
return BaseResult.failed().setMsgValue("今日校验次数已达" + checkCodeMaxErrCount + "次,请明日再试");
} else {
UserUtils.removeCodeErrCntMap(account + OPERATION_CHECK);
UserUtils.removeCodeFreezeTimeMap(account + OPERATION_CHECK);
}
}
Date emailCodeExpTime = UserUtils.getEmailCodeExpTime(account + OPERATION_CHECK);
if (StringUtils.isEmpty(code)) {
return BaseResult.failed().setMsgValue("验证码不能为空");
} else {
Integer codeErrCnt = UserUtils.getCodeErrCntMap(account + OPERATION_CHECK);
if (codeErrCnt == null) {
codeErrCnt = 0;
}
if (emailCodeExpTime == null || emailCodeExpTime.before(new Date())) {
if (codeErrCnt < checkCodeMaxErrCount - 1) {
UserUtils.setCodeErrCntMap(account + OPERATION_CHECK, codeErrCnt + 1);
} else {
UserUtils.setCodeFreezeTimeMap(account + OPERATION_CHECK, CommonUtil.getCurDateStr());
}
return BaseResult.failed().setMsgValue("验证码已过期,请重发");
}
if (!code.equals(emailCode)) {
if (codeErrCnt < checkCodeMaxErrCount - 1) {
UserUtils.setCodeErrCntMap(account + OPERATION_CHECK, codeErrCnt + 1);
} else {
UserUtils.setCodeFreezeTimeMap(account + OPERATION_CHECK, CommonUtil.getCurDateStr());
}
return BaseResult.failed().setMsgValue("验证码不正确,请重试");
} else {
return BaseResult.success();
}
}
}
@ApiOperation(value = "重置密码", notes = "重置密码功能")
@PostMapping("reset_pwd")
@MyLog(title = "重置密码", businessType = BusinessType.UPDATE)
public BaseResult resetPwd(@RequestBody UserResetPwdVo userResetPwdVo) {
String errCntTimeMap = UserUtils.getErrCntTimeMap(account + OPERATION_RESET);
if (!StringUtils.isEmpty(errCntTimeMap)) {
if (errCntTimeMap.equals(CommonUtil.getCurDateStr())) {
return BaseResult.failed().setMsgValue("今日尝试重置密码次数已达" + forgetPwdMaxErrCount + "次,请明日再试");
} else {
UserUtils.removeErrCntTimeMap(account + OPERATION_RESET);
UserUtils.removeErrCnt(account + OPERATION_RESET);
}
}
String newPassWord = userResetPwdVo.getPassword();
newPassWord = RSAUtil.getDecryptString(newPassWord, PRIVATE_KRY);
if (StringUtils.isEmpty(newPassWord)) {
return BaseResult.failed().setMsgValue("密码不能为空");
}
Integer errCnt = UserUtils.getErrCnt(account + OPERATION_RESET);
if (errCnt == null) {
errCnt = 0;
}
User user = userService.getUserByName(USER_NAME);
if (newPassWord.length() < 8) {
computeResetPwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码不得小于8位");
}
if (CommonUtil.verifyPasswordContainAccount(newPassWord, user.getUserName())) {
computeResetPwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码中不得包含用户名的完整字符串、大小写变位或形似变换的字符串");
}
if (CommonUtil.isKeyBoardContinuousChar(newPassWord)) {
computeResetPwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码不得包含键盘连续字符4个及以上");
}
if (!CommonUtil.checkPassword(newPassWord)) {
computeResetPwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("至少由大写字母、小写字母、数字与特殊符号等4类中3类混合");
}
user.setPassword(newPassWord);
boolean b = userService.updateUser(user);
if (b) {
return BaseResult.success().setMsgValue("密码修改成功");
} else {
return BaseResult.failed().setMsgValue("密码修改失败");
}
}
private void computeResetPwdErrCnt(int errCnt) {
if (errCnt < forgetPwdMaxErrCount - 1) {
UserUtils.setErrCnt(account + OPERATION_RESET, errCnt + 1);
} else {
UserUtils.setErrCntTimeMap(account + OPERATION_RESET, CommonUtil.getCurDateStr());
}
}
} }
...@@ -48,6 +48,13 @@ public class UserController { ...@@ -48,6 +48,13 @@ public class UserController {
private UserService userService; private UserService userService;
@Value("${rsa.private_key}") @Value("${rsa.private_key}")
private String PRIVATE_KRY; private String PRIVATE_KRY;
@Value("${other.error_count.change_pwd}")
private Integer changePwdMaxErrCount;
@Value("${spring.mail.to}")
private String account;
private static final String OPERATION_CHANGE = "change";
@InitBinder @InitBinder
public void initBinder(WebDataBinder binder) { public void initBinder(WebDataBinder binder) {
...@@ -127,6 +134,15 @@ public class UserController { ...@@ -127,6 +134,15 @@ public class UserController {
@PostMapping("update_pwd") @PostMapping("update_pwd")
@MyLog(title = "修改密码", businessType = BusinessType.UPDATE) @MyLog(title = "修改密码", businessType = BusinessType.UPDATE)
public BaseResult updatePwd(@RequestBody UserUpdatePwdVo userUpdatePwdVo) { public BaseResult updatePwd(@RequestBody UserUpdatePwdVo userUpdatePwdVo) {
String errCntTimeMap = UserUtils.getErrCntTimeMap(account + OPERATION_CHANGE);
if (!StringUtils.isEmpty(errCntTimeMap)) {
if (errCntTimeMap.equals(CommonUtil.getCurDateStr())) {
return BaseResult.failed().setMsgValue("今日尝试修改密码次数已达" + changePwdMaxErrCount + "次,请明日再试");
} else {
UserUtils.removeErrCntTimeMap(account + OPERATION_CHANGE);
UserUtils.removeErrCnt(account + OPERATION_CHANGE);
}
}
String oldPassWord = userUpdatePwdVo.getOldPassWord(); String oldPassWord = userUpdatePwdVo.getOldPassWord();
String newPassWord = userUpdatePwdVo.getNewPassWord(); String newPassWord = userUpdatePwdVo.getNewPassWord();
String userId = UserUtils.getLoginUserId(); String userId = UserUtils.getLoginUserId();
...@@ -138,7 +154,27 @@ public class UserController { ...@@ -138,7 +154,27 @@ public class UserController {
} }
oldPassWord = RSAUtil.getDecryptString(oldPassWord, PRIVATE_KRY); oldPassWord = RSAUtil.getDecryptString(oldPassWord, PRIVATE_KRY);
newPassWord = RSAUtil.getDecryptString(newPassWord, PRIVATE_KRY); newPassWord = RSAUtil.getDecryptString(newPassWord, PRIVATE_KRY);
Integer errCnt = UserUtils.getErrCnt(account + OPERATION_CHANGE);
if (errCnt == null) {
errCnt = 0;
}
if (oldPassWord.equals(user.getPassword())) { if (oldPassWord.equals(user.getPassword())) {
if (newPassWord.length() < 8) {
computeChangePwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码不得小于8位");
}
if (CommonUtil.verifyPasswordContainAccount(newPassWord, user.getUserName())) {
computeChangePwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码中不得包含用户名的完整字符串、大小写变位或形似变换的字符串");
}
if (CommonUtil.isKeyBoardContinuousChar(newPassWord)) {
computeChangePwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("密码不得包含键盘连续字符4个及以上");
}
if (!CommonUtil.checkPassword(newPassWord)) {
computeChangePwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("至少由大写字母、小写字母、数字与特殊符号等4类中3类混合");
}
user.setPassword(newPassWord); user.setPassword(newPassWord);
boolean b = userService.updateUser(user); boolean b = userService.updateUser(user);
if (b) { if (b) {
...@@ -147,10 +183,19 @@ public class UserController { ...@@ -147,10 +183,19 @@ public class UserController {
return BaseResult.failed().setMsgValue("密码修改失败"); return BaseResult.failed().setMsgValue("密码修改失败");
} }
} else { } else {
computeChangePwdErrCnt(errCnt);
return BaseResult.failed().setMsgValue("原密码出错"); return BaseResult.failed().setMsgValue("原密码出错");
} }
} }
private void computeChangePwdErrCnt(int errCnt) {
if (errCnt < changePwdMaxErrCount - 1) {
UserUtils.setErrCnt(account + OPERATION_CHANGE, errCnt + 1);
} else {
UserUtils.setErrCntTimeMap(account + OPERATION_CHANGE, CommonUtil.getCurDateStr());
}
}
/** /**
* 分页查询所有的user * 分页查询所有的user
...@@ -184,7 +229,7 @@ public class UserController { ...@@ -184,7 +229,7 @@ public class UserController {
List<UserVo> result = records.getResult(); List<UserVo> result = records.getResult();
String str = "uBtWZTiPMYkQLsp7rNly3RUIXKGqFbjnSg56H8ve49AC0mfO"; String str = "uBtWZTiPMYkQLsp7rNly3RUIXKGqFbjnSg56H8ve49AC0mfO";
for (UserVo u : result) { for (UserVo u : result) {
u.setPassword(DigestUtils.md5DigestAsHex((str+u.getPassword()).getBytes())); u.setPassword(DigestUtils.md5DigestAsHex((str + u.getPassword()).getBytes()));
} }
return new PageResult(200, "查找成功", pageNo, pages, total, result); return new PageResult(200, "查找成功", pageNo, pages, total, result);
} }
......
...@@ -25,6 +25,8 @@ public class JwtFilter implements Filter { ...@@ -25,6 +25,8 @@ public class JwtFilter implements Filter {
private static final String url4 = "/v2/api-docs"; private static final String url4 = "/v2/api-docs";
private static final String url7 = "/swagger-resources"; private static final String url7 = "/swagger-resources";
private static final String url8 = "/webjars/"; private static final String url8 = "/webjars/";
private static final String url9 = "/check_code";
private static final String url10 = "/reset_pwd";
@Override @Override
public void init(FilterConfig filterConfig) { public void init(FilterConfig filterConfig) {
...@@ -49,7 +51,7 @@ public class JwtFilter implements Filter { ...@@ -49,7 +51,7 @@ public class JwtFilter implements Filter {
boolean check = true; boolean check = true;
String uri = request.getRequestURI(); String uri = request.getRequestURI();
if (uri.contains(url1) || uri.contains(url2) || uri.contains(url3) || uri.contains(url4) || uri.contains(url7) || uri.contains(url8)) { if (uri.contains(url1) || uri.contains(url2) || uri.contains(url3) || uri.contains(url4) || uri.contains(url7) || uri.contains(url8) || uri.contains(url9) || uri.contains(url10)) {
if (uri.contains(url1)) { if (uri.contains(url1)) {
uri = XssUtil.checkXSS(uri); uri = XssUtil.checkXSS(uri);
UserUtils.setUri(uri); UserUtils.setUri(uri);
......
...@@ -13,9 +13,13 @@ public abstract class UserUtils { ...@@ -13,9 +13,13 @@ public abstract class UserUtils {
static Map<String, String> tokenMap = new HashMap<>(); static Map<String, String> tokenMap = new HashMap<>();
static Map<String, Date> tokenExpTimeMap = new HashMap<>(); static Map<String, Date> tokenExpTimeMap = new HashMap<>();
static Map<String, Integer> errCntMap = new HashMap<>(); static Map<String, Integer> errCntMap = new HashMap<>();
static Map<String, String> errCntTimeMap = new HashMap<>();
static Map<String, String> emailCodeMap = new HashMap<>(); static Map<String, String> emailCodeMap = new HashMap<>();
static Map<String, Date> emailCodeExpTimeMap = new HashMap<>(); static Map<String, Date> emailCodeExpTimeMap = new HashMap<>();
static Map<String, Date> countFreezeDateMap = new HashMap<>(); static Map<String, Date> countFreezeDateMap = new HashMap<>();
static Map<String, Integer> codeErrCntMap = new HashMap<>();
static Map<String, String> codeFreezeTimeMap = new HashMap<>();
/** /**
* 线程变量,存放user实体类信息,即使是静态的也与其他线程也是隔离的 * 线程变量,存放user实体类信息,即使是静态的也与其他线程也是隔离的
*/ */
...@@ -136,8 +140,43 @@ public abstract class UserUtils { ...@@ -136,8 +140,43 @@ public abstract class UserUtils {
return emailCodeExpTimeMap.get(email); return emailCodeExpTimeMap.get(email);
} }
public static void removeEmailCodeExpTime(String email) { public static void removeEmailCodeExpTime(String email) {
emailCodeExpTimeMap.remove(email); emailCodeExpTimeMap.remove(email);
} }
public static void setCodeErrCntMap(String account, int count) {
codeErrCntMap.put(account, count);
}
public static Integer getCodeErrCntMap(String account) {
return codeErrCntMap.get(account);
}
public static void removeCodeErrCntMap(String account) {
codeErrCntMap.remove(account);
}
public static void setCodeFreezeTimeMap(String account, String timeStr) {
codeFreezeTimeMap.put(account, timeStr);
}
public static String getCodeFreezeTimeMap(String account) {
return codeFreezeTimeMap.get(account);
}
public static void removeCodeFreezeTimeMap(String account) {
codeFreezeTimeMap.remove(account);
}
public static void setErrCntTimeMap(String account, String timeStr) {
errCntTimeMap.put(account, timeStr);
}
public static String getErrCntTimeMap(String account) {
return errCntTimeMap.get(account);
}
public static void removeErrCntTimeMap(String account) {
errCntTimeMap.remove(account);
}
} }
package iot.sixiang.license.model.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* Created by m33
* Date 2022/9/23 17:46
* Description
*/
@Data
public class CheckCodeDto {
@ApiModelProperty("验证码")
private String code;
}
package iot.sixiang.license.model.vo;
import lombok.Data;
/**
* Created by m33
* Date 2022/9/23 18:32
* Description
*/
@Data
public class UserResetPwdVo {
private String password;
}
...@@ -6,13 +6,13 @@ import iot.sixiang.license.model.vo.UserVo; ...@@ -6,13 +6,13 @@ import iot.sixiang.license.model.vo.UserVo;
/** /**
* <p> * <p>
* 服务类 * 服务类
* </p> * </p>
* *
* @author m33 * @author m33
* @since 2022-06-06 * @since 2022-06-06
*/ */
public interface UserService{ public interface UserService {
boolean deleteUser(int userIdVo); boolean deleteUser(int userIdVo);
...@@ -23,4 +23,6 @@ public interface UserService{ ...@@ -23,4 +23,6 @@ public interface UserService{
PageInfoModel<UserVo> getUserList(int pageNo, int pageSize, String userName, String company); PageInfoModel<UserVo> getUserList(int pageNo, int pageSize, String userName, String company);
User getUserById(int userId); User getUserById(int userId);
User getUserByName(String root);
} }
package iot.sixiang.license.service.impl; package iot.sixiang.license.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import iot.sixiang.license.consts.ResultCode; import iot.sixiang.license.consts.ResultCode;
import iot.sixiang.license.entity.User; import iot.sixiang.license.entity.User;
...@@ -21,7 +22,7 @@ import java.util.stream.Collectors; ...@@ -21,7 +22,7 @@ import java.util.stream.Collectors;
/** /**
* <p> * <p>
* 服务实现类 * 服务实现类
* </p> * </p>
* *
* @author m33 * @author m33
...@@ -34,11 +35,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us ...@@ -34,11 +35,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
UserMapper userMapper; UserMapper userMapper;
@Override @Override
public PageInfoModel<UserVo> getUserList(int pageNo, int pageSize,String userName, String company) { public PageInfoModel<UserVo> getUserList(int pageNo, int pageSize, String userName, String company) {
if(pageNo == 0 || pageSize == 0) { if (pageNo == 0 || pageSize == 0) {
throw new IotLicenseException(ResultCode.VALIDATE_FAILED.getCode(),ResultCode.VALIDATE_FAILED.getMsg()); throw new IotLicenseException(ResultCode.VALIDATE_FAILED.getCode(), ResultCode.VALIDATE_FAILED.getMsg());
} }
List<UserVo> records = userMapper.getUserList(userName,company); List<UserVo> records = userMapper.getUserList(userName, company);
records = records.stream().sorted(Comparator.comparing(UserVo::getCreateTime, Comparator.reverseOrder())).collect(Collectors.toList()); records = records.stream().sorted(Comparator.comparing(UserVo::getCreateTime, Comparator.reverseOrder())).collect(Collectors.toList());
List<UserVo> result = new ArrayList<>(); List<UserVo> result = new ArrayList<>();
int begin = (pageNo - 1) * pageSize; int begin = (pageNo - 1) * pageSize;
...@@ -56,10 +57,17 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us ...@@ -56,10 +57,17 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return userMapper.getUserById(userId); return userMapper.getUserById(userId);
} }
@Override
public User getUserByName(String root) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, root).last("limit 1");
return userMapper.selectOne(wrapper);
}
@Override @Override
public boolean deleteUser(int userId) { public boolean deleteUser(int userId) {
if(userId == 0) { if (userId == 0) {
throw new IotLicenseException(ResultCode.VALIDATE_FAILED.getCode(),ResultCode.VALIDATE_FAILED.getMsg()); throw new IotLicenseException(ResultCode.VALIDATE_FAILED.getCode(), ResultCode.VALIDATE_FAILED.getMsg());
} }
return userMapper.deleteUser(userId); return userMapper.deleteUser(userId);
} }
...@@ -73,7 +81,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us ...@@ -73,7 +81,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
if (res != null) { if (res != null) {
throw new IotLicenseException(403, "用户名已存在"); throw new IotLicenseException(403, "用户名已存在");
} }
return userMapper.addUser(userName,company,password); return userMapper.addUser(userName, company, password);
} }
@Override @Override
......
...@@ -4,6 +4,7 @@ import iot.sixiang.license.consts.Consts; ...@@ -4,6 +4,7 @@ import iot.sixiang.license.consts.Consts;
import iot.sixiang.license.model.ResResult; import iot.sixiang.license.model.ResResult;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.util.TextUtils;
import org.springframework.boot.system.ApplicationHome; import org.springframework.boot.system.ApplicationHome;
import java.io.File; import java.io.File;
...@@ -14,149 +15,289 @@ import java.text.SimpleDateFormat; ...@@ -14,149 +15,289 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j @Slf4j
public class CommonUtil { public class CommonUtil {
/** /**
* 获取随机验证码 * 验证密码-是否包含用户名字符(密码应与用户名无相关性,密码中不得包含用户名的完整字符串、大小写变位或形似变换的字符串)
*/ */
public static String getValidateCode() { public static boolean verifyPasswordContainAccount(String password, String account) {
SecureRandom random = new SecureRandom(); boolean isContain = false;
try { if (!TextUtils.isEmpty(password) && !TextUtils.isEmpty(account)) {
random = SecureRandom.getInstance("SHA1PRNG"); password = password.toLowerCase();
} catch (NoSuchAlgorithmException e) { account = account.toLowerCase();
log.error(e.getMessage()); if (password.contains(account)) {
} return true;
StringBuilder validateCode = new StringBuilder(); }
for (int i = 0; i < 6; i++) { String[] likes = {"a", "l", "o"};
validateCode.append(random.nextInt(10)); String[] likeSign = {"@", "!", "0"};
} String originalAccount = account + "";
return validateCode.toString(); for (int i = 0; i < likes.length; i++) {
} String tempAccount = originalAccount.replace(likes[i], likeSign[i]);
account = account.replace(likes[i], likeSign[i]);
if (password.contains(tempAccount) || password.contains(account)) {
/** return true;
* 随机生成指定长度的字符串 }
* }
* @param length
* @return }
*/ return isContain;
public static String genRandomNum(int length) { }
int maxNum = 36;
int i = -1; /**
int count = 0; * 键盘连续字符统计4个
char[] str = { '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', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; * @param str
StringBuilder pwd = new StringBuilder(""); * @return
SecureRandom secureRandom = null; */
try { public static boolean isKeyBoardContinuousChar(String str) {
secureRandom = SecureRandom.getInstance("SHA1PRNG"); boolean result = false;
} catch (NoSuchAlgorithmException e) { char[][] c1 = {
log.error("随机生成字符串失败"); {'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+'},
} {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '{', '}', '|'},
while (count < length) { {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ':', '"'},
if (secureRandom != null) { {'z', 'x', 'c', 'v', 'b', 'n', 'm', '<', '>', '?'}
i = Math.abs(secureRandom.nextInt(maxNum)); };
} char[][] c2 = {
if (i >= 0 && i < str.length) { {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='},
pwd.append(str[i]); {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '{', '}', '\\'},
count++; {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\''},
} {'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'}
} };
return pwd.toString(); for (char[][] c : new char[][][]{c1, c2}) {
} //横向
for (char[] chars : c) {
for (int j = 0; j < chars.length - 3; j++) {
//创建连续字符
public static String getSystemTime() { StringBuffer sb = new StringBuffer();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式 for (int k = j; k < j + 4; k++) {
String time = df.format(new Date()); sb.append(chars[k]);
return time; }
} String keyStr = sb.toString();
if (str.contains(keyStr)) {
return true;
public static boolean regularMessage(String message) { }
String regex = "^[0-9a-zA-Z_]{1,}$"; }
return message.matches(regex); }
}
//纵向
public static String toUpperCaseByEnglish(String message) { for (int i = 0; i < c[3].length; i++) {
//创建连续字符--每列只有4个
return message.toUpperCase(Locale.ENGLISH); StringBuffer sb = new StringBuffer();
} for (int j = 0; j < 4; j++) {
sb.append(c[j][i]);
public static String bytesToStr(byte[] bytes) { }
String str = null; String keyStr = sb.toString();
if (bytes == null) { if (str.contains(keyStr)) {
return str; return true;
} else { }
try { }
str = new String(bytes, 0, bytes.length, "utf-8");
} catch (UnsupportedEncodingException e) { }
log.error("数组转换成字符串异常,{}", e.getMessage()); return result;
} }
return str;
} public static void main(String[] args) {
} boolean b = checkPassword("1234qwe123");
System.out.println(b);
public static String getServerParentDirectory() { }
return new File(new ApplicationHome(Consts.class).getSource().getParentFile().getPath()).getParent()+ File.separator + "lib";
} /**
* 密码验证
/** * (至少由8位及以上大写字母、小写字母、数字与特殊符号等4类中3类混合)
* 名字脱敏 *
* 规则,张三丰,脱敏为:张*丰 * @param password
* * @return
* @param name */
* @return public static boolean checkPassword(String password) {
*/ boolean flag = false;
public static String nameDesensitization(String name) { try {
// 已经脱敏了直接返回 int c = 0;
if (name == null || name.contains("*")) { if (find("[a-z]+", password)) {
return name; c++;
} }
if (name == null || name.isEmpty()) { if (find("[A-Z]+", password)) {
return ""; c++;
} }
String myName = null; if (find("[0-9]+", password)) {
char[] chars = name.toCharArray(); c++;
if (chars.length == 1) { }
myName = name; if (find("\\W+|_", password) && !find("\\s+", password)) {//特殊符号
} c++;
if (chars.length == 2) { }
myName = StringUtils.overlay(name, "*", 1, 2); if (c >= 3) {
} flag = true;
if (chars.length > 2) { }
int n = chars.length - 2; } catch (Exception e) {
StringBuilder s = new StringBuilder(); flag = false;
for (int i = 0; i < n; i++) { }
s.append("*");
} return flag;
myName = StringUtils.overlay(name, String.valueOf(s), 1, chars.length - 1); }
}
return myName; //通用匹配
} public static boolean find(String regexStr, String input) {
boolean flag;
//身份证前三后四脱敏 try {
public static String idCardEncrypt(String idcard) { Pattern regex = Pattern.compile(regexStr);
if (idcard == null || idcard.length() == 0 || idcard.contains("*")) return idcard; Matcher matcher = regex.matcher(input);
if (StringUtils.isEmpty(idcard) || (idcard.length() < 8)) { // 部分进行匹配
return idcard; flag = matcher.find();
} } catch (Exception e) {
String res = StringUtils.overlay(idcard, "**************", 0, 14); flag = false;
return res; }
} return flag;
}
// 用于测试存储型xss /**
public static Object reverseData(Object obj, Class clazz) { * 获取随机验证码
HashMap<String, Object> resMap = new HashMap<>(); */
resMap.put("data", obj); public static String getValidateCode() {
if (!PubUtils.isNull()) { SecureRandom random = new SecureRandom();
return ResResult.success().goRecord(resMap); try {
} else { random = SecureRandom.getInstance("SHA1PRNG");
return null; } catch (NoSuchAlgorithmException e) {
} log.error(e.getMessage());
}
} StringBuilder validateCode = new StringBuilder();
for (int i = 0; i < 6; i++) {
validateCode.append(random.nextInt(10));
}
return validateCode.toString();
}
/**
* 随机生成指定长度的字符串
*
* @param length
* @return
*/
public static String genRandomNum(int length) {
int maxNum = 36;
int i = -1;
int count = 0;
char[] str = {'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', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
StringBuilder pwd = new StringBuilder("");
SecureRandom secureRandom = null;
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
log.error("随机生成字符串失败");
}
while (count < length) {
if (secureRandom != null) {
i = Math.abs(secureRandom.nextInt(maxNum));
}
if (i >= 0 && i < str.length) {
pwd.append(str[i]);
count++;
}
}
return pwd.toString();
}
public static String getSystemTime() {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 设置日期格式
String time = df.format(new Date());
return time;
}
public static boolean regularMessage(String message) {
String regex = "^[0-9a-zA-Z_]{1,}$";
return message.matches(regex);
}
public static String toUpperCaseByEnglish(String message) {
return message.toUpperCase(Locale.ENGLISH);
}
public static String bytesToStr(byte[] bytes) {
String str = null;
if (bytes == null) {
return str;
} else {
try {
str = new String(bytes, 0, bytes.length, "utf-8");
} catch (UnsupportedEncodingException e) {
log.error("数组转换成字符串异常,{}", e.getMessage());
}
return str;
}
}
public static String getServerParentDirectory() {
return new File(new ApplicationHome(Consts.class).getSource().getParentFile().getPath()).getParent() + File.separator + "lib";
}
/**
* 名字脱敏
* 规则,张三丰,脱敏为:张*丰
*
* @param name
* @return
*/
public static String nameDesensitization(String name) {
// 已经脱敏了直接返回
if (name == null || name.contains("*")) {
return name;
}
if (name == null || name.isEmpty()) {
return "";
}
String myName = null;
char[] chars = name.toCharArray();
if (chars.length == 1) {
myName = name;
}
if (chars.length == 2) {
myName = StringUtils.overlay(name, "*", 1, 2);
}
if (chars.length > 2) {
int n = chars.length - 2;
StringBuilder s = new StringBuilder();
for (int i = 0; i < n; i++) {
s.append("*");
}
myName = StringUtils.overlay(name, String.valueOf(s), 1, chars.length - 1);
}
return myName;
}
//身份证前三后四脱敏
public static String idCardEncrypt(String idcard) {
if (idcard == null || idcard.length() == 0 || idcard.contains("*")) return idcard;
if (StringUtils.isEmpty(idcard) || (idcard.length() < 8)) {
return idcard;
}
String res = StringUtils.overlay(idcard, "**************", 0, 14);
return res;
}
public static String getCurDateStr() {
Date date = new Date();
String pattern = "yyyy-MM-dd";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
return simpleDateFormat.format(date);
}
// 用于测试存储型xss
public static Object reverseData(Object obj, Class clazz) {
HashMap<String, Object> resMap = new HashMap<>();
resMap.put("data", obj);
if (!PubUtils.isNull()) {
return ResResult.success().goRecord(resMap);
} else {
return null;
}
}
} }
...@@ -32,12 +32,18 @@ server: ...@@ -32,12 +32,18 @@ server:
cros: cros:
# 需要设置访问白名单 # 需要设置访问白名单
cros_allowed_origins: http://192.168.1.88:8080, http://localhost:8868, http://localhost:8080 cros_allowed_origins: http://192.168.1.88:8080, http://192.168.1.88:8081, http://localhost:8868, http://localhost:8080, http://192.168.1.54:8080
cros_allowed_method: GET,POST cros_allowed_method: GET,POST
other: other:
md5: md5:
salt: PI7dBYlEfeP8IZ6vogqFL1U5pVnyCuNAGja3lsREx4M9r0SX salt: PI7dBYlEfeP8IZ6vogqFL1U5pVnyCuNAGja3lsREx4M9r0SX
error_count:
change_pwd: 5 # 修改密码的最大错误次数
forget_pwd: 5 # 忘记密码的最大错误次数
check_code: 5 # 校验验证码的最大错误次数
code_exp_time: 3 # 验证码失效时间,单位:分钟
rsa: rsa:
public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA18W2H1hO98dUWf1PNKNWTWmxCyvvy0NOR7iSvp76J0LdzyMJxs8WHVAmRfSGOb9SvpDZhBVx11bhTBqkl1qMzJWzn+F2ZtTCH2nXZcJHwSfLuGqin5FRBYW1WrFkqwg+R80aOuRSrbo0k1bZg3JPkkCxISHieEZPjSV5a4r7+Xopj0a9Dnh3rh4nDmH2p/wvotkx1oMKdhFglYcAITlk9ucEUf+CDuSdTAFFeKg9+fPqwKqWZRJZPQXqV3pGZ1/JS7gPnBFGZojW44eJufkBeiW3pbBvm/cKOkTnb8o4oltYUJsirYSQCCG+sDtxUAuGxuDCv8p+r8dWE1z5+xKclQIDAQAB public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA18W2H1hO98dUWf1PNKNWTWmxCyvvy0NOR7iSvp76J0LdzyMJxs8WHVAmRfSGOb9SvpDZhBVx11bhTBqkl1qMzJWzn+F2ZtTCH2nXZcJHwSfLuGqin5FRBYW1WrFkqwg+R80aOuRSrbo0k1bZg3JPkkCxISHieEZPjSV5a4r7+Xopj0a9Dnh3rh4nDmH2p/wvotkx1oMKdhFglYcAITlk9ucEUf+CDuSdTAFFeKg9+fPqwKqWZRJZPQXqV3pGZ1/JS7gPnBFGZojW44eJufkBeiW3pbBvm/cKOkTnb8o4oltYUJsirYSQCCG+sDtxUAuGxuDCv8p+r8dWE1z5+xKclQIDAQAB
private_key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXxbYfWE73x1RZ/U80o1ZNabELK+/LQ05HuJK+nvonQt3PIwnGzxYdUCZF9IY5v1K+kNmEFXHXVuFMGqSXWozMlbOf4XZm1MIfaddlwkfBJ8u4aqKfkVEFhbVasWSrCD5HzRo65FKtujSTVtmDck+SQLEhIeJ4Rk+NJXlrivv5eimPRr0OeHeuHicOYfan/C+i2THWgwp2EWCVhwAhOWT25wRR/4IO5J1MAUV4qD358+rAqpZlElk9BepXekZnX8lLuA+cEUZmiNbjh4m5+QF6JbelsG+b9wo6ROdvyjiiW1hQmyKthJAIIb6wO3FQC4bG4MK/yn6vx1YTXPn7EpyVAgMBAAECggEAR7YQ+kfqLtVThnj2mwLyCtZmndTjZEWhPZrtQmcpsmS5vT7i3+0xZ1qc7cD/3y9j+6u+bvSFmlDondd4/kh85P2X7joLlM9/GNufV9WC7YIhZdAi7i9ooxI2HMc6MtGRiWF0J0B87folwRYrQlF6epv/goh1cQ3FIJ7kxMYzSk0gF0JcmOZn3KH8tMt3t/GK+uVUIycuEQQKsaTTq45nIM7oqhlAwD/M+IO2pGFkLXJ23FzFACI10qBdpn+xL2xFGRO6EE8EAeDslT4OvvN3/vtnSnRNn8CJqfoEG40XO0xrZzH1noI35iPWX1WrN7qFLAhl2oLhu1ZSIA1tz0+I/QKBgQD3X/islXfVmV2XeuHvaf4qcMAdrgLQwtmLlHhFxfFURPh6au8uatDgUA8HWcRACmhtTruytlFRGKKFwZxuQE/LOZh67Uts3GTs5eHN8xvZTL+en+n7B/cCRYrrg3+yAM/k0eezIlk65iW700o+icEHxkwTXhhVBmIROBzpXsVCwwKBgQDfS6kVhgZxLMQePbXUQ1NBr2KbfhXLzceINhsoyWLa1rIk8+9HLSxw8q0zma12Jqkd22OAgkbeLDy+niYPi3pUrWAm8O59Ot1aaOarxMTvEv7+eS+urKId57sli/hQTWsS+xghA4+VfW2+EY++pYyZIp+j+1/Q2ciXWVJy4iv9xwKBgFzW8+kxn2vWxz1WrPzBdtZOwothB0V6G1M7QXhONag+ylKHV4TAKexFn55Onky6mz6K0f7cVeBtsnEonKD0Gf5Xe1aHQEt225ndHMXCe60uFKxfr9y6vIVpvB1vmLkhfOSPsrmUJpDoVzkKr06RPJTCY0LRiag/YQa9XHxpSPcpAoGAKbsiJnudyJjtLhmqWbkbXjNA4n515FjY6YPzH3RDnVJyiKVuGoc+vv0bkYEvAd3HzWSq++FdDTiHQbictFsEyb59McnlSFIv/C2Orptfkq6iKTzMxIBO6/fa6fF2vss5L5rtr33S38VJNTRjAOY/mH74BtV72rRY4LA40G+keRECgYEAiGg0DYxcSGf2bEP1WESYPTTdgS5ke1niIwZ00SgtkIjPSVgTCmf4Tciys6lGSe/Oqnvk24VR6pz07wzcbH92AURVaeqiEvVuVonzA6Yl0jxeOIM31S1BSBJRT8kDijuvwoJu2tPoZG0306KF9L8pyy1Z6cTTmIfGR0NpZCHWPSg= private_key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDXxbYfWE73x1RZ/U80o1ZNabELK+/LQ05HuJK+nvonQt3PIwnGzxYdUCZF9IY5v1K+kNmEFXHXVuFMGqSXWozMlbOf4XZm1MIfaddlwkfBJ8u4aqKfkVEFhbVasWSrCD5HzRo65FKtujSTVtmDck+SQLEhIeJ4Rk+NJXlrivv5eimPRr0OeHeuHicOYfan/C+i2THWgwp2EWCVhwAhOWT25wRR/4IO5J1MAUV4qD358+rAqpZlElk9BepXekZnX8lLuA+cEUZmiNbjh4m5+QF6JbelsG+b9wo6ROdvyjiiW1hQmyKthJAIIb6wO3FQC4bG4MK/yn6vx1YTXPn7EpyVAgMBAAECggEAR7YQ+kfqLtVThnj2mwLyCtZmndTjZEWhPZrtQmcpsmS5vT7i3+0xZ1qc7cD/3y9j+6u+bvSFmlDondd4/kh85P2X7joLlM9/GNufV9WC7YIhZdAi7i9ooxI2HMc6MtGRiWF0J0B87folwRYrQlF6epv/goh1cQ3FIJ7kxMYzSk0gF0JcmOZn3KH8tMt3t/GK+uVUIycuEQQKsaTTq45nIM7oqhlAwD/M+IO2pGFkLXJ23FzFACI10qBdpn+xL2xFGRO6EE8EAeDslT4OvvN3/vtnSnRNn8CJqfoEG40XO0xrZzH1noI35iPWX1WrN7qFLAhl2oLhu1ZSIA1tz0+I/QKBgQD3X/islXfVmV2XeuHvaf4qcMAdrgLQwtmLlHhFxfFURPh6au8uatDgUA8HWcRACmhtTruytlFRGKKFwZxuQE/LOZh67Uts3GTs5eHN8xvZTL+en+n7B/cCRYrrg3+yAM/k0eezIlk65iW700o+icEHxkwTXhhVBmIROBzpXsVCwwKBgQDfS6kVhgZxLMQePbXUQ1NBr2KbfhXLzceINhsoyWLa1rIk8+9HLSxw8q0zma12Jqkd22OAgkbeLDy+niYPi3pUrWAm8O59Ot1aaOarxMTvEv7+eS+urKId57sli/hQTWsS+xghA4+VfW2+EY++pYyZIp+j+1/Q2ciXWVJy4iv9xwKBgFzW8+kxn2vWxz1WrPzBdtZOwothB0V6G1M7QXhONag+ylKHV4TAKexFn55Onky6mz6K0f7cVeBtsnEonKD0Gf5Xe1aHQEt225ndHMXCe60uFKxfr9y6vIVpvB1vmLkhfOSPsrmUJpDoVzkKr06RPJTCY0LRiag/YQa9XHxpSPcpAoGAKbsiJnudyJjtLhmqWbkbXjNA4n515FjY6YPzH3RDnVJyiKVuGoc+vv0bkYEvAd3HzWSq++FdDTiHQbictFsEyb59McnlSFIv/C2Orptfkq6iKTzMxIBO6/fa6fF2vss5L5rtr33S38VJNTRjAOY/mH74BtV72rRY4LA40G+keRECgYEAiGg0DYxcSGf2bEP1WESYPTTdgS5ke1niIwZ00SgtkIjPSVgTCmf4Tciys6lGSe/Oqnvk24VR6pz07wzcbH92AURVaeqiEvVuVonzA6Yl0jxeOIM31S1BSBJRT8kDijuvwoJu2tPoZG0306KF9L8pyy1Z6cTTmIfGR0NpZCHWPSg=
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment