Java tutorial
/* * Copyright 2016 TomeOkin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package app.service; import app.config.LsPushProperties; import app.config.ResultCode; import app.data.crypt.Crypto; import app.data.local.UserRepository; import app.data.model.BaseResponse; import app.data.model.CaptchaRequest; import app.data.model.CryptoToken; import app.data.model.RegisterData; import app.data.model.internal.Captcha; import app.data.validator.UserInfoValidator; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.CharMatcher; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnel; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static app.config.ResultCode.*; /** * ??? */ @Service public class CaptchaService { private static final Logger logger = LoggerFactory.getLogger(CaptchaService.class); private final String serverName; private final String serverUrl; private final String serverEmail; private final UserInfoValidator mUserInfoValidator; private final JavaMailSender mMailSender; private final TemplateEngine mTemplateEngine; private final UserRepository mUserRepo; private final ObjectMapper mObjectMapper; private final Cache<CaptchaRequest, Captcha> mAuthCodeMap; private final Funnel<String> mStringFunnel; private BloomFilter<String> mAuthCodeFilter; @Autowired public CaptchaService(UserInfoValidator userInfoValidator, LsPushProperties lsPushProperties, JavaMailSender mailSender, TemplateEngine templateEngine, ObjectMapper objectMapper, UserRepository userRepo) { mUserInfoValidator = userInfoValidator; serverName = lsPushProperties.getServerName(); serverUrl = lsPushProperties.getServerUrl(); serverEmail = lsPushProperties.getServerEmail(); mMailSender = mailSender; mTemplateEngine = templateEngine; mObjectMapper = objectMapper; mUserRepo = userRepo; mAuthCodeMap = CacheBuilder.newBuilder().initialCapacity(100).maximumSize(500) .expireAfterWrite(30, TimeUnit.MINUTES).build(); mStringFunnel = (Funnel<String>) (from, into) -> into.putString(from, StandardCharsets.UTF_8); resetBloomFilter(); } public boolean isValidSendTarget(CaptchaRequest request, boolean checkUserExist) { String recipient = request != null ? request.getSendObject() : null; if (StringUtils.isEmpty(recipient)) { return false; } if (recipient.contains("@")) { // ? return mUserInfoValidator.isEmailValid(recipient) && (!checkUserExist || mUserRepo.findFirstByEmail(recipient) == null); } else { return mUserInfoValidator.isPhoneValid(recipient, request.getRegion()); } } public int sendAuthCode(final CaptchaRequest request) { // ? if (!isValidSendTarget(request, true)) { return INVALID_CAPTCHA; } final Captcha found = mAuthCodeMap.getIfPresent(request); if (found != null) { // ?? 1 ??? if (System.currentTimeMillis() - found.lastSentTime < 60_000) { return TIME_INTERVAL_TOO_CLOSE; } } try { Captcha captcha = new Captcha(); captcha.authCode = generateCaptcha(6); captcha.lastSentTime = System.currentTimeMillis(); mAuthCodeMap.put(request, captcha); if (request.getSendObject().contains("@")) { sendEmail(request.getSendObject(), captcha.authCode); } else { sendSMS(request.getSendObject(), request.getRegion(), captcha.authCode); } } catch (Exception e) { logger.error("send auth code failure", e); return ResultCode.SEND_CAPTCHA_FAILED; } return BaseResponse.COMMON_SUCCESS; } public int checkCaptcha(CryptoToken cryptToken) { RegisterData registerData; try { byte[] json = Crypto.decrypt(cryptToken); registerData = mObjectMapper.readValue(json, RegisterData.class); } catch (Exception e) { logger.warn("decrypt register-base check-captcha crypt-token failure", e); return INVALID_TOKEN; } CaptchaRequest captchaRequest = registerData.getCaptchaRequest(); return checkCaptcha(captchaRequest, registerData.getAuthCode(), false); } public int checkCaptcha(CaptchaRequest request, String authCode) { return checkCaptcha(request, authCode, true); } /** * @param successWithInvalidate provide it for pre-check captcha */ public int checkCaptcha(CaptchaRequest request, String authCode, boolean successWithInvalidate) { // phone captcha is not send by server, // when parse request success and satisfy follow conditions, assume it is correct if (!request.getSendObject().contains("@") && mUserInfoValidator.isPhoneValid(request.getSendObject(), request.getRegion()) && StringUtils.isNotEmpty(authCode) && authCode.length() >= 4 && CharMatcher.digit().negate().matchesNoneOf(authCode)) { return BaseResponse.COMMON_SUCCESS; } Captcha found = mAuthCodeMap.getIfPresent(request); if (found != null) { if (found.accessTimes < 3) { if (found.authCode.equals(authCode)) { if (successWithInvalidate) { // when auth success, it will discards the key mAuthCodeMap.invalidate(request); } return BaseResponse.COMMON_SUCCESS; } else { found.accessTimes++; return ResultCode.MATCHING_FAILED; } } // when auth failure beyond 3 times, you can only auth success until the key is invalidate. mAuthCodeMap.put(request, found); // refresh key alive return TRYING_TOO_MUCH; } // not found or try beyond 3 times return ResultCode.MATCHING_FAILED; } protected void sendEmail(String email, String authCode) throws MessagingException, UnsupportedEncodingException { MimeMessage mimeMessage = mMailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "UTF-8"); helper.setSubject(" LsPush "); helper.setFrom(serverEmail, serverName); helper.setTo(email); String authLink = String.format("%s/user/auth?auth_code=%s", serverUrl, authCode); final Context ctx = new Context(Locale.CHINA); ctx.setVariable("serverUrl", serverUrl); ctx.setVariable("serverName", serverName); ctx.setVariable("email", email); ctx.setVariable("authCode", authCode); ctx.setVariable("authLink", authLink); String html = mTemplateEngine.process("lspush_captcha_email", ctx); helper.setText(html, true); mMailSender.send(mimeMessage); } protected void sendSMS(String phone, String region, String authCode) { } private void resetBloomFilter() { // ??? 200 ? 1 ????????? mAuthCodeFilter = BloomFilter.create(mStringFunnel, 5000, 0.01); } protected String generateCaptcha(int length) throws ExecutionException { if (length <= 4) { length = 4; } String authCode; authCode = RandomStringUtils.random(length, false, true); // according to test, conflict is less than 45 per 10,000. if (mAuthCodeFilter.mightContain(authCode)) { if (mAuthCodeFilter.expectedFpp() >= 0.01f) { // has put too much (beyond 5000) into it resetBloomFilter(); } authCode = RandomStringUtils.random(length, false, true); } mAuthCodeFilter.put(authCode); // mask authCode return authCode; } }