项目基于springcloudalibaba,模块功能大致概括就是登录页面的时候先完成图形验证码的校验,输入的数字和字母与图片上的相对应之后,会向对应的邮箱或手机号发送邮箱/短信验证码二次验证。这里展示的是邮箱验证码。
用到的技术点有:基于SpringCloudAlibaba框架+redis缓存+swagger开发文档
首先要在common项目中封装一些通用模块的工具类与枚举类(用于生成随机数与验证码)以及swaggerconfig直接代码展示:
SwaggerConfiguration:用于自动生成接口文档后面会展示
package net.xdclass.config;import lombok.Data;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import springfox.documentation.builders.*;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.schema.ScalarType;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;import java.util.ArrayList;
import java.util.List;@Component
@Data
@EnableOpenApi
public class SwaggerConfiguration {/*** 对c端用户的接口文档* @return*/@Beanpublic Docket webApiDoc() {return new Docket(DocumentationType.OAS_30).groupName("用户端接口文档").pathMapping("/")// 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭.enable(true)//配置api文档元信息.apiInfo(apiInfo())// 选择哪些接口作为swagger的doc发布.select()//扫描对应的所有包.apis(RequestHandlerSelectors.basePackage("net.xdclass"))//正则匹配请求路径,并分配至当前分组.paths(PathSelectors.ant("/api/**")).build().globalRequestParameters(getGlobalRequestParameters()).globalResponses(HttpMethod.GET,getGlobalResponseMessage()).globalResponses(HttpMethod.POST,getGlobalResponseMessage());}/*** 对管理端用户的接口文档* @return*/@Beanpublic Docket adminApiDoc() {return new Docket(DocumentationType.OAS_30).groupName("管理端接口文档").pathMapping("/")// 定义是否开启swagger,false为关闭,可以通过变量控制,线上关闭.enable(true)//配置api文档元信息.apiInfo(apiInfo())// 选择哪些接口作为swagger的doc发布.select()//扫描对应的所有包.apis(RequestHandlerSelectors.basePackage("net.xdclass"))//正则匹配请求路径,并分配至当前分组.paths(PathSelectors.ant("/admin/**")).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("1024电商平台").description("微服务接口文档").contact(new Contact("孙翊轩", "https://xdclass.net", "2026913461@qq.com")).version("v1.0").build();}/*** 配置全局通用参数, 支持配置多个响应参数* @return*/private List<RequestParameter> getGlobalRequestParameters() {List<RequestParameter> parameters = new ArrayList<>();parameters.add(new RequestParameterBuilder().name("token").description("登录令牌").in(ParameterType.HEADER).query(q -> q.model(m -> m.scalarModel(ScalarType.STRING))).required(false).build());// parameters.add(new RequestParameterBuilder()
// .name("version")
// .description("版本号")
// .required(true)
// .in(ParameterType.HEADER)
// .query(q -> q.model(m -> m.scalarModel(ScalarType.STRING)))
// .required(false)
// .build());return parameters;}/*** 生成通用响应信息* @return*/private List<Response> getGlobalResponseMessage() {List<Response> responseList = new ArrayList<>();responseList.add(new ResponseBuilder().code("4xx").description("请求错误,根据code和msg检查").build());return responseList;}
}
CheckUtils正则工具类:用于邮箱或手机号的正则
package net.xdclass.util;import java.util.regex.Matcher;
import java.util.regex.Pattern;public class CheckUtil {/*** 邮箱正则*/private static final Pattern MAIL_PATTERN = Pattern.compile("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");/*** 手机号正则,暂时未用*/private static final Pattern PHONE_PATTERN = Pattern.compile("^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$");/*** @param email* @return*/public static boolean isEmail(String email) {if (null == email || "".equals(email)) {return false;}Matcher m = MAIL_PATTERN.matcher(email);return m.matches();}/*** 暂时未用* @param phone* @return*/public static boolean isPhone(String phone) {if (null == phone || "".equals(phone)) {return false;}Matcher m = PHONE_PATTERN.matcher(phone);return m.matches();}
}
CommonUtils:1.获取ip 后面将ip做成redis的key保证不重复
2.Md5加密
3.获取验证码的随机数
4.获取当前时间戳 后面会在md5加密的验证码后面拼接时间戳
package net.xdclass.util;import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.Random;public class CommonUtil {/*** 获取ip* @param request* @return*/public static String getIpAddr(HttpServletRequest request) {String ipAddress = null;try {ipAddress = request.getHeader("x-forwarded-for");if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("WL-Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {ipAddress = request.getRemoteAddr();if (ipAddress.equals("127.0.0.1")) {// 根据网卡取本机配置的IPInetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割if (ipAddress != null && ipAddress.length() > 15) {// "***.***.***.***".length()// = 15if (ipAddress.indexOf(",") > 0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));}}} catch (Exception e) {ipAddress="";}return ipAddress;}/*** MD5加密* @param data* @return*/public static String MD5(String data) {try {java.security.MessageDigest md = MessageDigest.getInstance("MD5");byte[] array = md.digest(data.getBytes("UTF-8"));StringBuilder sb = new StringBuilder();for (byte item : array) {sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));}return sb.toString().toUpperCase();} catch (Exception exception) {}return null;}/*** 获取验证码随机数* @return*/public static String getRandom(int length){String sources="0123456789";Random random =new Random();StringBuilder sb=new StringBuilder();for (int i = 0; i < length; i++) {sb.append(sources.charAt(random.nextInt(9)));}return sb.toString();}/*** 获取当前时间戳* @return*/public static long getCurrentTimestamp(){return System.currentTimeMillis();}
}
JsonData状态码:
package net.xdclass.util;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.xdclass.enums.BizCodeEnum;@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonData {/*** 状态码 0 表示成功,1表示处理中,-1表示失败*/private Integer code;/*** 数据*/private Object data;/*** 描述*/private String msg;/*** 成功,传入数据* @return*/public static JsonData buildSuccess() {return new JsonData(0, null, null);}/*** 成功,传入数据* @param data* @return*/public static JsonData buildSuccess(Object data) {return new JsonData(0, data, null);}/*** 失败,传入描述信息* @param msg* @return*/public static JsonData buildError(String msg) {return new JsonData(-1, null, msg);}/*** 自定义状态码和错误信息* @param code* @param msg* @return*/public static JsonData buildCodeAndMsg(int code, String msg) {return new JsonData(code, null, msg);}/*** 传入枚举,返回信息* @param codeEnum* @return*/public static JsonData buildResult(BizCodeEnum codeEnum){return JsonData.buildCodeAndMsg(codeEnum.getCode(),codeEnum.getMessage());}
}
其次,我们要在user-service服务中创建此服务需要的配置(验证码的样式:干扰线,字体间隔,文本来源,图片样式):
package net.xdclass.config;import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Properties;@Configuration
public class CaptchaConfig {/*** 验证码配置* Kaptcha配置类名** @return*/@Bean@Qualifier("captchaProducer")public DefaultKaptcha kaptcha() {DefaultKaptcha kaptcha = new DefaultKaptcha();Properties properties = new Properties();
// properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
// //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
// properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "147");
// properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "34");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
// //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");//验证码个数properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");//字体间隔properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");//干扰线颜色
// properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");//干扰实现类properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");//图片样式properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");//文字来源properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789qwertyuiopasdfghjklzxcvbnm");Config config = new Config(properties);kaptcha.setConfig(config);return kaptcha;}
}
然后就可以开始正式的编写了,首先从controller层开始需要做两个接口:1.获取图形验证码 2:发送邮箱验证码 3.获取缓存的key
1.获取图形验证码需要存储在redis中,这里提前在linux服务器上安装并配置了redis和docker
2.发送邮箱验证码需要两个步骤:1.匹配图形验证码是否正确 2.发送验证码
3.将redis key-value的key设置成ip以用来防止重复
package net.xdclass.controller;import com.google.code.kaptcha.Producer;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.enums.SendCodeEnum;
import net.xdclass.service.NotifyService;
import net.xdclass.util.CommonUtil;
import net.xdclass.util.JsonData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Protocol;import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;@Api(tags = "通知模块")
@RestController
@RequestMapping("/api/user/v1")
@Slf4j
public class NotifyController {@Autowiredprivate Producer captchaProducer;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate NotifyService notifyService;private static final long CAPTCHA_CODE_EXPIRED=60*1000*10;/*** 获取图形验证码* @param request* @param response*/@ApiOperation("获取图形验证码")@GetMapping ("captcha")public void getCaptcha(HttpServletRequest request, HttpServletResponse response){String captchaTest = captchaProducer.createText();log.info("图形验证码:{}",captchaTest);//存储redisTemplate.opsForValue().set(getCaptchaKey(request),captchaTest,CAPTCHA_CODE_EXPIRED,TimeUnit.MILLISECONDS);BufferedImage bufferedImage =captchaProducer.createImage(captchaTest);ServletOutputStream outputStream=null;try {outputStream= response.getOutputStream();ImageIO.write(bufferedImage,"jpg",outputStream);outputStream.flush();outputStream.close();}catch (IOException e){log.error("获取图形验证码异常:{}",e);}}/*** 发送验证码* 1.匹配图形验证码是否正常* 2.发送验证码** @param to* @param captcha* @param request* @return*/@ApiOperation("发送邮箱注册验证码")@GetMapping("send_code")public JsonData sendRegisterCode(@ApiParam("收信人") @RequestParam(value = "to", required = true)String to,@ApiParam("图形验证码") @RequestParam(value = "captcha", required = true)String captcha,HttpServletRequest request){String key = getCaptchaKey(request);String cacheCaptcha = redisTemplate.opsForValue().get(key);//匹配验证码是否一样if(captcha!=null && cacheCaptcha!=null && cacheCaptcha.equalsIgnoreCase(captcha)) {//成功redisTemplate.delete(key);JsonData jsonData = notifyService.sendCode(SendCodeEnum.USER_REGISTER,to);return jsonData;}else {return JsonData.buildResult(BizCodeEnum.CODE_CAPTCHA_ERROR);}}/*** 获取缓存key* @param request* @return*/private String getCaptchaKey(HttpServletRequest request){String ip= CommonUtil.getIpAddr(request);String userAgent=request.getHeader("User-Agent");String key ="user-service:captcha:"+CommonUtil.MD5(ip+userAgent);log.info("ip={}",ip);log.info("userAgent={}",userAgent);log.info("key={}",key);return key;}}
Service:
package net.xdclass.service;import net.xdclass.enums.SendCodeEnum;
import net.xdclass.util.JsonData;public interface NotifyService {JsonData sendCode(SendCodeEnum sendCodeEnum,String to);
}
ServiceImpl:
步骤在注释里:前置判断是否发送--发送验证码--存储到缓存里--后置存储发送记录
package net.xdclass.service.impl;import lombok.extern.slf4j.Slf4j;
import net.xdclass.constant.CacheKey;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.enums.SendCodeEnum;
import net.xdclass.service.MailService;
import net.xdclass.service.NotifyService;
import net.xdclass.util.CheckUtil;
import net.xdclass.util.CommonUtil;
import net.xdclass.util.JsonData;
import org.apache.commons.lang3.StringUtils;
import org.mockito.internal.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {@Autowiredprivate MailService mailService;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 验证码标题*/private static final String SUBJECT="项目验证码";/*** 验证码内容*/private static final String CONTENT="您的验证码是%s,有效时间是60s,打死也不告诉任何人";/*** 10分钟有效*/private static final int CODE_EXPPIRED=60*1000*10;/*** 前置:判断是否重复发送** 1.发送验证码** 2.存储到缓存** 后置:存储发送记录* @param sendCodeEnum* @param to* @return*/@Overridepublic JsonData sendCode(SendCodeEnum sendCodeEnum, String to) {String cachekey =String.format(CacheKey.CHECK_CODE_KEY,sendCodeEnum.name(),to);String cacheValue= redisTemplate.opsForValue().get(cachekey);//如果不为空,则判断是否60秒内重复发送if (StringUtils.isNotBlank(cacheValue)){//TODOlong ttl =Long.parseLong(cacheValue.split("_")[1]);//小于60秒,则不给重复发送if(CommonUtil.getCurrentTimestamp() - ttl<1000*60){log.info("重复发送验证码,时间间隔:{}",(CommonUtil.getCurrentTimestamp()-ttl)/1000);return JsonData.buildResult(BizCodeEnum.CODE_LIMITED);}}//拼接验证码 2233_32131231String code=CommonUtil.getRandom(6);String value=code+"_"+CommonUtil.getCurrentTimestamp();redisTemplate.opsForValue().set(cachekey,value,CODE_EXPPIRED, TimeUnit.MILLISECONDS);if(CheckUtil.isEmail(to)){//设置验证码的位数mailService.sendMail(to,SUBJECT,String.format(CONTENT,code));return JsonData.buildSuccess();}else if (CheckUtil.isPhone(to)){}return JsonData.buildResult(BizCodeEnum.CODE_TO_ERROR);}
}
效果展示:
登录到1156571678@qq.com查看
展示swagger文档: