From 036c4b47befc1259f23a7b3ab6ab99d78cf94dbe Mon Sep 17 00:00:00 2001 From: lensferno Date: Tue, 6 Dec 2022 23:17:45 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A0=94=E7=A9=B6=E7=94=9F=E7=99=BB=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E9=AA=8C=E8=AF=81=E7=A0=81=E8=A7=A3=E5=86=B3=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mywust/core/exception/ApiException.java | 12 ++- .../graduate/GraduateRequestFactory.java | 2 +- .../core/service/auth/GraduateLogin.java | 95 +++++++++++++++++++ .../captcha/solver/CaptchaSolver.java | 5 +- .../LinghangOcrServiceCaptchaSolver.java | 77 +++++++++++++++ .../solver/LocalPaddleOcrCaptchaSolver.java | 2 +- .../mywust/captcha/SolvedImageCaptcha.java | 22 ++++- .../mywust/captcha/UnsolvedImageCaptcha.java | 2 + .../LinghangOcrServiceCaptchaSolver.java | 11 --- .../cn/linghang/mywust/util/StringUtil.java | 20 ++++ 10 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 mywust-core/src/main/java/cn/linghang/mywust/core/service/auth/GraduateLogin.java rename {mywust-model/src/main/java/cn/linghang/mywust => mywust-core/src/main/java/cn/linghang/mywust/core/service}/captcha/solver/CaptchaSolver.java (60%) create mode 100644 mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LinghangOcrServiceCaptchaSolver.java rename {mywust-model/src/main/java/cn/linghang/mywust => mywust-core/src/main/java/cn/linghang/mywust/core/service}/captcha/solver/LocalPaddleOcrCaptchaSolver.java (84%) delete mode 100644 mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LinghangOcrServiceCaptchaSolver.java diff --git a/mywust-core/src/main/java/cn/linghang/mywust/core/exception/ApiException.java b/mywust-core/src/main/java/cn/linghang/mywust/core/exception/ApiException.java index d09aab3..ffd197c 100644 --- a/mywust-core/src/main/java/cn/linghang/mywust/core/exception/ApiException.java +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/exception/ApiException.java @@ -35,6 +35,11 @@ public class ApiException extends BasicException { */ UNKNOWN_EXCEPTION(-1, "未知错误(开发又有活干啦)"), + /** + * 网络异常 + */ + NETWORK_EXCEPTION(-2, "网络错误..."), + // -------------------------------- // 统一认证的异常(本科生、图书馆) @@ -84,11 +89,16 @@ public class ApiException extends BasicException { * 研究生密码错误 */ GRADUATE_PASSWORD_WRONG(140100, "研究生登录: 密码错误"), - ; + // -------------------------------- // 图书馆API异常代码 + // -------------------------------- + // 内部API异常代码 + INTERNAL_EXCEPTION(160100, "内部服务出错...后端要背锅"), + ; + private final int value; private final String describe; diff --git a/mywust-core/src/main/java/cn/linghang/mywust/core/request/graduate/GraduateRequestFactory.java b/mywust-core/src/main/java/cn/linghang/mywust/core/request/graduate/GraduateRequestFactory.java index 00f9b00..6101b75 100644 --- a/mywust-core/src/main/java/cn/linghang/mywust/core/request/graduate/GraduateRequestFactory.java +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/request/graduate/GraduateRequestFactory.java @@ -33,7 +33,7 @@ public class GraduateRequestFactory extends RequestFactory { byte[] requestData = StringUtil.generateQueryString(params).getBytes(StandardCharsets.UTF_8); - return makeHttpRequest(Graduate.GRADUATE_LOGIN_API, requestData); + return makeHttpRequest(Graduate.GRADUATE_LOGIN_API, requestData, captcha.getBindInfo()); } public static HttpRequest studentInfoRequest(String cookie) { diff --git a/mywust-core/src/main/java/cn/linghang/mywust/core/service/auth/GraduateLogin.java b/mywust-core/src/main/java/cn/linghang/mywust/core/service/auth/GraduateLogin.java new file mode 100644 index 0000000..724b24d --- /dev/null +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/service/auth/GraduateLogin.java @@ -0,0 +1,95 @@ +package cn.linghang.mywust.core.service.auth; + +import cn.linghang.mywust.captcha.SolvedImageCaptcha; +import cn.linghang.mywust.captcha.UnsolvedImageCaptcha; +import cn.linghang.mywust.core.exception.ApiException; +import cn.linghang.mywust.core.exception.BasicException; +import cn.linghang.mywust.core.request.graduate.GraduateRequestFactory; +import cn.linghang.mywust.core.service.captcha.solver.CaptchaSolver; +import cn.linghang.mywust.network.RequestClientOption; +import cn.linghang.mywust.network.Requester; +import cn.linghang.mywust.network.entitys.HttpRequest; +import cn.linghang.mywust.network.entitys.HttpResponse; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class GraduateLogin { + private final Requester requester; + + private final CaptchaSolver captchaSolver; + + public GraduateLogin(Requester requester, CaptchaSolver captchaSolver) { + this.requester = requester; + this.captchaSolver = captchaSolver; + } + + public String getLoginCookie(String username, String password, RequestClientOption option) throws IOException, BasicException { + // 请求获取验证码 + HttpRequest captchaImageRequest = GraduateRequestFactory.captchaRequest(); + HttpResponse captchaImageResponse = requester.get(captchaImageRequest, option); + + UnsolvedImageCaptcha unsolvedImageCaptcha = new UnsolvedImageCaptcha(); + unsolvedImageCaptcha.setBindInfo(captchaImageResponse.getCookies()); + + byte[] processedImage = ImageUtil.process(captchaImageResponse.getBody()); + unsolvedImageCaptcha.setImage(processedImage); + + // 通过传入的captchaSolver来处理验证码 + SolvedImageCaptcha solvedImageCaptcha = captchaSolver.solve(unsolvedImageCaptcha); + + // 进行登录 + HttpRequest loginRequest = GraduateRequestFactory.loginRequest(username, password, solvedImageCaptcha); + HttpResponse loginResponse = requester.post(loginRequest, option); + + // 登陆成功,应该会是302跳转,不是的话多半是认证错误 + if (loginResponse.getStatusCode() != HttpResponse.HTTP_REDIRECT_302) { + throw new ApiException(ApiException.Code.GRADUATE_PASSWORD_WRONG); + } + + // 使用当初通过验证码得到的cookie来作为登录cookie,至于是否真正可行待验证 + return captchaImageResponse.getCookies(); + } +} + +class ImageUtil { + // 验证码图片的长宽 + private static final int IMAGE_WIDTH = 75; + private static final int IMAGE_HEIGHT = 35; + + // 二值化阈值,实测339效果最佳 + private static final int LIMIT = 339; + + private static final int BLACK = Color.BLACK.getRGB(); + private static final int WHITE = Color.WHITE.getRGB(); + + /** + * 初步处理验证码图片,对图片去色处理以更好地进行ocr处理 + */ + public static byte[] process(byte[] data) throws ApiException { + try { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); + BufferedImage bufferedImage = ImageIO.read(byteArrayInputStream); + + for (int i = 0; i < IMAGE_WIDTH; ++i) { + for (int j = 0; j < IMAGE_HEIGHT; ++j) { + int rgb = bufferedImage.getRGB(i, j); + int bright = ((rgb >> 16) & 0xff) + ((rgb >> 8) & 0xff) + (rgb & 0xff); + + bufferedImage.setRGB(i, j, bright <= LIMIT ? BLACK : WHITE); + } + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, "png", outputStream); + + return byteArrayInputStream.readAllBytes(); + } catch (Exception e) { + throw new ApiException(ApiException.Code.INTERNAL_EXCEPTION); + } + } +} \ No newline at end of file diff --git a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/CaptchaSolver.java b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/CaptchaSolver.java similarity index 60% rename from mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/CaptchaSolver.java rename to mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/CaptchaSolver.java index 8aec58d..735d58e 100644 --- a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/CaptchaSolver.java +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/CaptchaSolver.java @@ -1,8 +1,9 @@ -package cn.linghang.mywust.captcha.solver; +package cn.linghang.mywust.core.service.captcha.solver; import cn.linghang.mywust.captcha.SolvedImageCaptcha; import cn.linghang.mywust.captcha.UnsolvedImageCaptcha; +import cn.linghang.mywust.core.exception.ApiException; public interface CaptchaSolver { - SolvedImageCaptcha solve(UnsolvedImageCaptcha unsolvedImageCaptcha); + SolvedImageCaptcha solve(UnsolvedImageCaptcha unsolvedImageCaptcha) throws ApiException; } diff --git a/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LinghangOcrServiceCaptchaSolver.java b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LinghangOcrServiceCaptchaSolver.java new file mode 100644 index 0000000..0d0b99b --- /dev/null +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LinghangOcrServiceCaptchaSolver.java @@ -0,0 +1,77 @@ +package cn.linghang.mywust.core.service.captcha.solver; + +import cn.linghang.mywust.captcha.SolvedImageCaptcha; +import cn.linghang.mywust.captcha.UnsolvedImageCaptcha; +import cn.linghang.mywust.core.exception.ApiException; +import cn.linghang.mywust.core.request.RequestFactory; +import cn.linghang.mywust.network.RequestClientOption; +import cn.linghang.mywust.network.Requester; +import cn.linghang.mywust.network.entitys.HttpRequest; +import cn.linghang.mywust.network.entitys.HttpResponse; +import cn.linghang.mywust.util.StringUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; + +import java.io.IOException; +import java.util.Map; +import java.util.TreeMap; + +public class LinghangOcrServiceCaptchaSolver implements CaptchaSolver { + private final String baseUrl; + private final Requester requester; + + private final String appId; + private final String secretKey; + + private static final RequestClientOption requestOption = new RequestClientOption(); + + public LinghangOcrServiceCaptchaSolver(String baseUrl, Requester requester, + String appId, String secretKey) { + + this.baseUrl = baseUrl; + this.requester = requester; + + this.appId = appId; + this.secretKey = secretKey; + } + + @Override + public SolvedImageCaptcha solve(UnsolvedImageCaptcha unsolvedImageCaptcha) throws ApiException { + Map urlParams = new TreeMap<>(String::compareTo); + urlParams.put("id", appId); + urlParams.put("t", Long.toString(System.currentTimeMillis() / 1000)); + urlParams.put("type", "json"); + + String sign = StringUtil.generateSignText(appId, secretKey, urlParams); + urlParams.put("sign", sign); + + String url = baseUrl + StringUtil.generateQueryString(urlParams); + byte[] imageBase64Data = Base64.encodeBase64(unsolvedImageCaptcha.getImage()); + + HttpRequest request; + HttpResponse response; + OcrResult result; + try { + request = RequestFactory.makeHttpRequest(url, imageBase64Data); + response = requester.post(request, requestOption); + result = new ObjectMapper().readValue(response.getBody(), OcrResult.class); + } catch (IOException e) { + throw new ApiException(ApiException.Code.NETWORK_EXCEPTION); + } + + if (result.code != OcrResult.CODE_SUCCESS) { + throw new ApiException(ApiException.Code.INTERNAL_EXCEPTION); + } + + return new SolvedImageCaptcha(unsolvedImageCaptcha, result.result); + } + + private static class OcrResult { + private int code; + private String result; + private String message; + + public static final int CODE_SUCCESS = 0; + } + +} diff --git a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LocalPaddleOcrCaptchaSolver.java b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LocalPaddleOcrCaptchaSolver.java similarity index 84% rename from mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LocalPaddleOcrCaptchaSolver.java rename to mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LocalPaddleOcrCaptchaSolver.java index d02c511..30ffe14 100644 --- a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LocalPaddleOcrCaptchaSolver.java +++ b/mywust-core/src/main/java/cn/linghang/mywust/core/service/captcha/solver/LocalPaddleOcrCaptchaSolver.java @@ -1,4 +1,4 @@ -package cn.linghang.mywust.captcha.solver; +package cn.linghang.mywust.core.service.captcha.solver; import cn.linghang.mywust.captcha.SolvedImageCaptcha; import cn.linghang.mywust.captcha.UnsolvedImageCaptcha; diff --git a/mywust-model/src/main/java/cn/linghang/mywust/captcha/SolvedImageCaptcha.java b/mywust-model/src/main/java/cn/linghang/mywust/captcha/SolvedImageCaptcha.java index 05c22d0..fe2e815 100644 --- a/mywust-model/src/main/java/cn/linghang/mywust/captcha/SolvedImageCaptcha.java +++ b/mywust-model/src/main/java/cn/linghang/mywust/captcha/SolvedImageCaptcha.java @@ -1,25 +1,37 @@ package cn.linghang.mywust.captcha; import lombok.Data; +import lombok.Getter; /** - * 已处理的图片验证码 + * 已处理的图片验证码,必须要和特定的未处理验证码绑定 */ -@Data +@Getter public class SolvedImageCaptcha { /** * 和验证码绑定的信息,如cookie,某种id等 */ - private String bindInfo; + private final String bindInfo; /** * 验证码验证数据 */ private String result; + public SolvedImageCaptcha(String bindInfo) { + this.bindInfo = bindInfo; + } + public SolvedImageCaptcha(UnsolvedImageCaptcha unsolvedImageCaptcha) { - this.bindInfo = unsolvedImageCaptcha.getBindInfo(); + this(unsolvedImageCaptcha.getBindInfo()); } - private void setBindInfo(String bindInfo) {} + public SolvedImageCaptcha(UnsolvedImageCaptcha unsolvedImageCaptcha, String result) { + this(unsolvedImageCaptcha); + this.result = result; + } + + public void setResult(String result) { + this.result = result; + } } diff --git a/mywust-model/src/main/java/cn/linghang/mywust/captcha/UnsolvedImageCaptcha.java b/mywust-model/src/main/java/cn/linghang/mywust/captcha/UnsolvedImageCaptcha.java index 4123729..6cd220b 100644 --- a/mywust-model/src/main/java/cn/linghang/mywust/captcha/UnsolvedImageCaptcha.java +++ b/mywust-model/src/main/java/cn/linghang/mywust/captcha/UnsolvedImageCaptcha.java @@ -1,6 +1,8 @@ package cn.linghang.mywust.captcha; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; /** * 待处理的图片验证码 diff --git a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LinghangOcrServiceCaptchaSolver.java b/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LinghangOcrServiceCaptchaSolver.java deleted file mode 100644 index c3df6c7..0000000 --- a/mywust-model/src/main/java/cn/linghang/mywust/captcha/solver/LinghangOcrServiceCaptchaSolver.java +++ /dev/null @@ -1,11 +0,0 @@ -package cn.linghang.mywust.captcha.solver; - -import cn.linghang.mywust.captcha.SolvedImageCaptcha; -import cn.linghang.mywust.captcha.UnsolvedImageCaptcha; - -public class LinghangOcrServiceCaptchaSolver implements CaptchaSolver { - @Override - public SolvedImageCaptcha solve(UnsolvedImageCaptcha unsolvedImageCaptcha) { - return null; - } -} diff --git a/mywust-util/src/main/java/cn/linghang/mywust/util/StringUtil.java b/mywust-util/src/main/java/cn/linghang/mywust/util/StringUtil.java index 4d4d57d..c6997d4 100644 --- a/mywust-util/src/main/java/cn/linghang/mywust/util/StringUtil.java +++ b/mywust-util/src/main/java/cn/linghang/mywust/util/StringUtil.java @@ -1,6 +1,7 @@ package cn.linghang.mywust.util; import com.google.common.base.Joiner; +import org.apache.commons.codec.binary.Base64; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -44,4 +45,23 @@ public class StringUtil { .skipNulls() .join(allCookies); } + + /** + * 生成参数签名 + * @param appId appId + * @param secretKey secretKey + * @return 生成得到的签名sign字段 + */ + public static String generateSignText(String appId, String secretKey, Map params) { + StringBuilder rawText = new StringBuilder(appId + secretKey); + Set keys = params.keySet(); + for (String key : keys) { + rawText + .append(key) + .append('=') + .append(params.get(key)); + } + + return Base64.encodeBase64String(rawText.toString().getBytes(StandardCharsets.UTF_8)); + } }