项目基于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
*/
@Bean
public 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
*/
@Bean
public 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")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (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 {
@Autowired
private Producer captchaProducer;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private 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 {
@Autowired
private MailService mailService;
@Autowired
private 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
*/
@Override
public 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)){
//TODO
long 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_32131231
String 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文档:
?
?
?
|