diff --git a/server/like-admin/pom.xml b/server/like-admin/pom.xml index f6a85e82..3555915a 100644 --- a/server/like-admin/pom.xml +++ b/server/like-admin/pom.xml @@ -30,6 +30,12 @@ 1.0.0 + + + com.github.penggle + kaptcha + + cn.dev33 diff --git a/server/like-admin/src/main/java/com/mdd/admin/config/AdminConfig.java b/server/like-admin/src/main/java/com/mdd/admin/config/AdminConfig.java index 49941c75..4411b016 100644 --- a/server/like-admin/src/main/java/com/mdd/admin/config/AdminConfig.java +++ b/server/like-admin/src/main/java/com/mdd/admin/config/AdminConfig.java @@ -13,8 +13,9 @@ public class AdminConfig { // 免登录验证 public static String[] notLoginUri = new String[]{ - "system:login", // 登录接口 - "index:config" // 配置接口 + "system:captcha", // 验证码 + "system:login", // 登录接口 + "index:config" // 配置接口 }; // 免权限验证 diff --git a/server/like-admin/src/main/java/com/mdd/admin/config/KaptChaConfig.java b/server/like-admin/src/main/java/com/mdd/admin/config/KaptChaConfig.java new file mode 100644 index 00000000..b2541b2e --- /dev/null +++ b/server/like-admin/src/main/java/com/mdd/admin/config/KaptChaConfig.java @@ -0,0 +1,45 @@ +package com.mdd.admin.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +import static com.google.code.kaptcha.Constants.*; + +/** + * 验证码配置 + */ +@Configuration +public class KaptChaConfig { + + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否边框 + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 字符颜色 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 图片宽度 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); + // 图片高度 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); + // 字符大小 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // 验证键码 + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); + // 字符长度 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 字体样式 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } +} + diff --git a/server/like-admin/src/main/java/com/mdd/admin/controller/system/SystemLoginController.java b/server/like-admin/src/main/java/com/mdd/admin/controller/system/SystemLoginController.java index 4401d038..b0896d22 100644 --- a/server/like-admin/src/main/java/com/mdd/admin/controller/system/SystemLoginController.java +++ b/server/like-admin/src/main/java/com/mdd/admin/controller/system/SystemLoginController.java @@ -2,17 +2,14 @@ package com.mdd.admin.controller.system; import com.mdd.admin.service.ISystemLoginService; import com.mdd.admin.validate.system.SystemAdminLoginsValidate; +import com.mdd.admin.vo.system.SystemCaptchaVo; import com.mdd.admin.vo.system.SystemLoginVo; import com.mdd.common.core.AjaxResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; -import java.util.Map; /** * 系统登录管理 @@ -24,6 +21,18 @@ public class SystemLoginController { @Resource ISystemLoginService iSystemLoginService; + /** + * 验证码 + * + * @author fzr + * @return AjaxResult + */ + @GetMapping("/captcha") + public AjaxResult captcha() { + SystemCaptchaVo vo = iSystemLoginService.captcha(); + return AjaxResult.success(vo); + } + /** * 登录系统 * diff --git a/server/like-admin/src/main/java/com/mdd/admin/service/ISystemLoginService.java b/server/like-admin/src/main/java/com/mdd/admin/service/ISystemLoginService.java index 7e4be333..b9113754 100644 --- a/server/like-admin/src/main/java/com/mdd/admin/service/ISystemLoginService.java +++ b/server/like-admin/src/main/java/com/mdd/admin/service/ISystemLoginService.java @@ -1,6 +1,7 @@ package com.mdd.admin.service; import com.mdd.admin.validate.system.SystemAdminLoginsValidate; +import com.mdd.admin.vo.system.SystemCaptchaVo; import com.mdd.admin.vo.system.SystemLoginVo; import java.util.Map; @@ -10,6 +11,14 @@ import java.util.Map; */ public interface ISystemLoginService { + /** + * 验证码 + * + * @author fzr + * @return SystemCaptchaVo + */ + SystemCaptchaVo captcha(); + /** * 登录 * diff --git a/server/like-admin/src/main/java/com/mdd/admin/service/impl/SystemLoginServiceImpl.java b/server/like-admin/src/main/java/com/mdd/admin/service/impl/SystemLoginServiceImpl.java index 777ab434..18fcba72 100644 --- a/server/like-admin/src/main/java/com/mdd/admin/service/impl/SystemLoginServiceImpl.java +++ b/server/like-admin/src/main/java/com/mdd/admin/service/impl/SystemLoginServiceImpl.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.mdd.admin.service.ISystemAuthAdminService; import com.mdd.admin.service.ISystemLoginService; import com.mdd.admin.validate.system.SystemAdminLoginsValidate; +import com.mdd.admin.vo.system.SystemCaptchaVo; import com.mdd.admin.vo.system.SystemLoginVo; import com.mdd.common.entity.system.SystemAuthAdmin; import com.mdd.common.entity.system.SystemLogLogin; @@ -14,13 +15,19 @@ import com.mdd.common.exception.OperateException; import com.mdd.common.mapper.system.SystemAuthAdminMapper; import com.mdd.common.mapper.system.SystemLogLoginMapper; import com.mdd.common.util.*; +import com.google.code.kaptcha.Producer; import nl.bitwalker.useragentutils.UserAgent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.FastByteArrayOutputStream; import javax.annotation.Resource; +import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; +import java.awt.image.BufferedImage; +import java.io.IOException; import java.util.*; /** @@ -29,6 +36,9 @@ import java.util.*; @Service public class SystemLoginServiceImpl implements ISystemLoginService { + @Resource + Producer captchaProducer; + @Resource SystemLogLoginMapper systemLogLoginMapper; @@ -41,6 +51,42 @@ public class SystemLoginServiceImpl implements ISystemLoginService { private static final Logger log = LoggerFactory.getLogger(SystemLoginServiceImpl.class); + /** + * 验证码 + * + * @author fzr + * @return SystemCaptchaVo + */ + @Override + public SystemCaptchaVo captcha() { + // 验证码信息 + String capStr, code; + BufferedImage image; + String uuid = ToolsUtils.makeUUID(); + String ip = IpUtils.getIpAddress().replaceAll("\\.", ""); + String verifyKey = YmlUtils.get("like.captcha.token") + ip + ":" + uuid; + long expireTime = Long.parseLong(YmlUtils.get("like.captcha.expire")); + + // 生成验证码 + capStr = code = captchaProducer.createText(); + image = captchaProducer.createImage(capStr); + RedisUtils.set(verifyKey, code.toLowerCase(), expireTime); + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + try { + ImageIO.write(image, "jpg", os); + } catch (IOException e) { + log.error("verifyCode Error:" + e.getMessage()); + throw new OperateException(e.getMessage()); + } + + // 返回验证码 + String base64 = "data:image/jpeg;base64,"+ Base64Util.encode(os.toByteArray()); + SystemCaptchaVo vo = new SystemCaptchaVo(); + vo.setUuid(uuid); + vo.setImg(base64); + return vo; + } + /** * 登录 * @@ -53,6 +99,19 @@ public class SystemLoginServiceImpl implements ISystemLoginService { String username = loginsValidate.getUsername(); String password = loginsValidate.getPassword(); + String captchaStatus = YmlUtils.get("like.captcha.status"); + if (StringUtils.isNotNull(captchaStatus) && captchaStatus.equals("true")) { + Assert.notNull(loginsValidate.getCode(), "code参数缺失"); + Assert.notNull(loginsValidate.getUuid(), "uuid参数缺失"); + String ip = IpUtils.getIpAddress().replaceAll("\\.", ""); + String captchaKey = YmlUtils.get("like.captcha.token") + ip + ":" + loginsValidate.getUuid(); + Object code = RedisUtils.get(captchaKey); + RedisUtils.del(captchaKey); + if (StringUtils.isNull(code) || StringUtils.isEmpty(code.toString()) || !loginsValidate.getCode().equals(code.toString())) { + throw new LoginException(HttpEnum.CAPTCHA_ERROR.getCode(), HttpEnum.CAPTCHA_ERROR.getMsg()); + } + } + SystemAuthAdmin sysAdmin = systemAuthAdminMapper.selectOne(new QueryWrapper() .eq("username", username) .last("limit 1")); diff --git a/server/like-admin/src/main/java/com/mdd/admin/validate/system/SystemAdminLoginsValidate.java b/server/like-admin/src/main/java/com/mdd/admin/validate/system/SystemAdminLoginsValidate.java index bbacd8e1..1f501028 100644 --- a/server/like-admin/src/main/java/com/mdd/admin/validate/system/SystemAdminLoginsValidate.java +++ b/server/like-admin/src/main/java/com/mdd/admin/validate/system/SystemAdminLoginsValidate.java @@ -22,4 +22,8 @@ public class SystemAdminLoginsValidate implements Serializable { @Length(min = 6, max = 18, message = "账号或密码错误") private String password; + private String code; + + private String uuid; + } diff --git a/server/like-admin/src/main/java/com/mdd/admin/vo/system/SystemCaptchaVo.java b/server/like-admin/src/main/java/com/mdd/admin/vo/system/SystemCaptchaVo.java new file mode 100644 index 00000000..17793f6e --- /dev/null +++ b/server/like-admin/src/main/java/com/mdd/admin/vo/system/SystemCaptchaVo.java @@ -0,0 +1,14 @@ +package com.mdd.admin.vo.system; + +import lombok.Data; + +/** + * 验证码 + */ +@Data +public class SystemCaptchaVo { + + private String uuid; + private String img; + +} diff --git a/server/like-admin/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/server/like-admin/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 379bbd72..dde58ec5 100644 --- a/server/like-admin/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/server/like-admin/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -9,6 +9,21 @@ "name": "like.production", "type": "java.lang.String", "description": "Description for like.production." + }, + { + "name": "like.captcha.status", + "type": "java.lang.String", + "description": "Description for like.captcha.status." + }, + { + "name": "like.captcha.expire", + "type": "java.lang.String", + "description": "Description for like.captcha.expire." + }, + { + "name": "like.captcha.token", + "type": "java.lang.String", + "description": "Description for like.captcha.token." } ] } \ No newline at end of file diff --git a/server/like-admin/src/main/resources/application.yml b/server/like-admin/src/main/resources/application.yml index d079e679..f65f928d 100644 --- a/server/like-admin/src/main/resources/application.yml +++ b/server/like-admin/src/main/resources/application.yml @@ -1,6 +1,14 @@ # 项目配置 like: upload-directory: /www/uploads/likeadmin-java/ # 上传目录 + # 验证码配置 + captcha: + # 是否开启验证码 + status: true + # 验证码有效时长 + expire: 120 + # 验证码缓存键名 + token: "captcha:key:" # 服务配置 server: diff --git a/server/like-common/src/main/java/com/mdd/common/enums/HttpEnum.java b/server/like-common/src/main/java/com/mdd/common/enums/HttpEnum.java index 79c61a6d..893d6c7d 100644 --- a/server/like-common/src/main/java/com/mdd/common/enums/HttpEnum.java +++ b/server/like-common/src/main/java/com/mdd/common/enums/HttpEnum.java @@ -14,6 +14,7 @@ public enum HttpEnum { LOGIN_DISABLE_ERROR(331, "登录账号已被禁用了"), TOKEN_EMPTY(332, "token参数为空"), TOKEN_INVALID(333, "token参数无效"), + CAPTCHA_ERROR(334, "验证码错误"), NO_PERMISSION(403, "无相关权限"), REQUEST_404_ERROR(404, "请求接口不存在"), diff --git a/server/like-common/src/main/java/com/mdd/common/util/Base64Util.java b/server/like-common/src/main/java/com/mdd/common/util/Base64Util.java new file mode 100644 index 00000000..30d8ada1 --- /dev/null +++ b/server/like-common/src/main/java/com/mdd/common/util/Base64Util.java @@ -0,0 +1,266 @@ +package com.mdd.common.util; + +/** + * Base64工具类 + */ +public final class Base64Util +{ + static private final int BASE_LENGTH = 128; + static private final int LOOK_UP_LENGTH = 64; + static private final int TWENTY_FOUR_BIT_GROUP = 24; + static private final int EIGHT_BIT = 8; + static private final int SIXTEEN_BIT = 16; + static private final int FOUR_BYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASE_LENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOK_UP_LENGTH]; + + static { + for (int i = 0; i < BASE_LENGTH; ++i) { + base64Alphabet[i] = -1; + } + + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + + lookUpBase64Alphabet[62] = '+'; + lookUpBase64Alphabet[63] = '/'; + } + + /** + * 转码 + * + * @param binaryData 未转码数据 + * @return 转码后数据串 + */ + public static String encode(byte[] binaryData) { + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHT_BIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTY_FOUR_BIT_GROUP; + int numberTriplets = lengthDataBits / TWENTY_FOUR_BIT_GROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char[] encodedData; + + encodedData = new char[numberQuartet * 4]; + + byte k, l, b1, b2, b3; + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + k = (byte) (b1 & 0x03); + l = (byte) (b2 & 0x0f); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + if (fewerThan24bits == EIGHT_BIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEEN_BIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * 解码 + * + * @param encoded 已转码数据 + * @return 解码后数据 + */ + public static byte[] decode(String encoded) { + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + int len = removeWhiteSpace(base64Data); + + if (len % FOUR_BYTE != 0) { + return null; + } + + int numberQuadruple = (len / FOUR_BYTE); + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte[] decodedData; + byte b1, b2, b3, b4; + char d1, d2, d3, d4; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) + || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) + { + return null; + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null; + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) { + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b4 | b3 << 6); + + } + return decodedData; + } + + /** + * 是否空白 + * + * @param octEct 位 + * @return boolean + */ + private static boolean isWhiteSpace(char octEct) { + return (octEct == 0x20 || octEct == 0xd || octEct == 0xa || octEct == 0x9); + } + + /** + * 是否填充 + * + * @param octEct 位 + * @return boolean + */ + private static boolean isPad(char octEct) { + return (octEct == PAD); + } + + /** + * 是否数据 + * + * @param octEct 位 + * @return Boolean + */ + private static Boolean isData(char octEct) { + return (octEct < BASE_LENGTH && base64Alphabet[octEct] != -1); + } + + /** + * 移除空白 + * + * @param data 数据 + * @return int + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } + +} diff --git a/server/pom.xml b/server/pom.xml index 3f6b82bf..f153e314 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -39,6 +39,7 @@ 2.11.1 2.9.0 + 2.3.2 1.2.4 6.1.2 1.32.0 @@ -116,6 +117,13 @@ ${fastJson2.version} + + + com.github.penggle + kaptcha + ${kaptcha.version} + + cn.dev33