一为什么会有人刷接口?
我们知道,总有些人吃饱了没事干就喜欢到处搞破坏,就比如之前我得redis服务器没有设置密码就被某些孤儿攻击力了,因此现在接口安全一直是一个热门话题,比如有些黄牛党在12306网上抢票进行倒卖,还有些企业之间进行竞争去恶意攻击对方服务器,举个例子,比如某个短信接口被请求一次,会触发几分钱的运营商费用,可想而知,当某些懂点技术的狗写点脚本去疯狂冲击这个接口,那你的短信扣费就非常客观了。。。。
还有一些人去疯狂请求你的服务器,导致服务器不断生成JessionId等从而导致服务器内存溢出,因此宕机,所以就需要对一些接口做防止某一时间段内大量请求的操作,这个就是所谓的接口防盗刷
二。接口防盗刷思路
限制同一个ip的用户在限定的时间内,只能访问固定的次数
实现思路:使用redis在缓存中搞一个计数器,将该用户的ip+其它拼接组成redis的key,同一个用户访问的次数为Value,第一次将这个计数器置1后存入缓存,并给其设定有效期。每次点击后,取出这个值,计数器加一,如果超过限定次数,就抛出业务异常。
首先展示一下项目的代码结构,可自行根据该结构构建代码
三。代码实现
1.首先自己定义一个注解类,该注解作用在接口上,代表访问这个接口会有次数访问限制
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD}) // 标注该注解在方法上面有效
@Retention(RetentionPolicy.RUNTIME) // 在运行时有效
public @interface AccessLimit {
/**
* 意思就是在规定seconds时间内同一个ip不能超过最大访问次数,否则抛出业务异常
* @return
*/
int maxCount();// 最大访问次数
int seconds();// 固定时间, 单位: s
}
2.ResponseCode枚举类,用于返回接口的一些提示信息
public enum ResponseCode {
// 系统模块
SUCCESS(0, "操作成功"),
ERROR(1, "操作失败"),
SERVER_ERROR(500, "服务器异常"),
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),
REPETITIVE_OPERATION(10001, "请勿重复操作");
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
ServerResponse类,同样用于返回一些接口调用返回提示信息
public class ServerResponse implements Serializable {
private static final long serialVersionUID = 7498483649536881777L;
private Integer status;
private String msg;
private Object data;
public ServerResponse() {
}
public ServerResponse(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
@JsonIgnore
public boolean isSuccess() {
return this.status == ResponseCode.SUCCESS.getCode();
}
public static ServerResponse success() {
return new ServerResponse(ResponseCode.SUCCESS.getCode(), null, null);
}
public static ServerResponse success(String msg) {
return new ServerResponse(ResponseCode.SUCCESS.getCode(), msg, null);
}
public static ServerResponse success(Object data) {
return new ServerResponse(ResponseCode.SUCCESS.getCode(), null, data);
}
public static ServerResponse success(String msg, Object data) {
return new ServerResponse(ResponseCode.SUCCESS.getCode(), msg, data);
}
public static ServerResponse error(String msg) {
return new ServerResponse(ResponseCode.ERROR.getCode(), msg, null);
}
public static ServerResponse error(Object data) {
return new ServerResponse(ResponseCode.ERROR.getCode(), null, data);
}
public static ServerResponse error(String msg, Object data) {
return new ServerResponse(ResponseCode.ERROR.getCode(), msg, data);
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
3.Jedis连接池配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class JedisConfig {
@Bean(name = "jedisPoolConfig")
public JedisPoolConfig jedisPoolConfig(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(500);
jedisPoolConfig.setMaxIdle(200);
jedisPoolConfig.setNumTestsPerEvictionRun(1024);
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
jedisPoolConfig.setMinEvictableIdleTimeMillis(-1);
jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(10000);
jedisPoolConfig.setMaxWaitMillis(1500);
jedisPoolConfig.setTestOnBorrow(true);
jedisPoolConfig.setTestWhileIdle(true);
jedisPoolConfig.setTestOnReturn(false);
jedisPoolConfig.setJmxEnabled(true);
jedisPoolConfig.setBlockWhenExhausted(false);
return jedisPoolConfig;
}
@Bean
public JedisPool redisPool() {
String host = "127.0.0.1";
int port = 6379;
return new JedisPool(jedisPoolConfig(), host, port);
}
}
4.接口防刷拦截器配置
/**
* 接口防刷限流拦截器
*/
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
private static final String ACCESS_LIMIT_PREFIX = "accessLimit:";
@Autowired
private JedisUtil jedisUtil;
// 在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 判断拦截的是否是方法类型
if (!(handler instanceof HandlerMethod)) {//如果是HandlerMethod 类,强转,拿到注解
return true;
}
// 将handler转换为 HandlerMethod 类型 ,为方便后续操作
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取到拦截的方法的method对象
Method method = handlerMethod.getMethod();
// 获取到方法上的注解
AccessLimit annotation = method.getAnnotation(AccessLimit.class);
if (annotation != null) {
check(annotation, request);
}
return true;
}
private void check(AccessLimit annotation, HttpServletRequest request) {
获取方法上注解的参数
int maxCount = annotation.maxCount();
int seconds = annotation.seconds();
StringBuilder sb = new StringBuilder();
sb.append(ACCESS_LIMIT_PREFIX).append(IpUtil.getIpAddress(request)).append(request.getRequestURI());
String key = sb.toString();
Boolean exists = jedisUtil.exists(key);
if (!exists) {//如果没有,说明没访问过,置1
jedisUtil.set(key, String.valueOf(1), seconds);
} else {
// 获取到redis中对应key的值(说白了这里的value的值就是同一个第三方客户端访问了几次这个接口)
int count = Integer.parseInt(jedisUtil.get(key));
if (count < maxCount) {//设置 如果小于我们的防刷次数
Long ttl = jedisUtil.ttl(key);
if (ttl <= 0) {
// 说明key已经过期了
jedisUtil.set(key, String.valueOf(1), seconds);
} else {//小于5 就+1
jedisUtil.set(key, String.valueOf(++count), ttl.intValue());
}
} else {//说明大于最大次数
throw new ServiceException(ResponseCode.ACCESS_LIMIT.getMsg());
}
}
}
// 在controller之后执行
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
// 在模板引擎之后执行
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
将该拦截器进行注册
import com.cd.interceptor.AccessLimitInterceptor;
import com.cd.interceptor.ApiIdempotentInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web配置文件
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 跨域
* @return
*/
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
//关键,将拦截器作为bean写入配置中
@Bean
public AccessLimitInterceptor accessLimitInterceptor() {
return new AccessLimitInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 接口防刷限流拦截器
registry.addInterceptor(accessLimitInterceptor()).addPathPatterns("/**");
}
}
5.编写自定义异常类,用于处理业务异常
/**
* 业务逻辑异常
*/
public class ServiceException extends RuntimeException{
private String code;
private String msg;
public ServiceException() {
}
public ServiceException(String msg) {
this.msg = msg;
}
public ServiceException(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
6.编写service层业务代码
import com.cd.common.ServerResponse;
import javax.servlet.http.HttpServletRequest;
public interface TokenService {
// 接口盗刷测试
ServerResponse accessLimit();
}
对应实现类
@Override
@Service
public class TokenServiceImpl implements TokenService {
// 接口盗刷实现
public ServerResponse accessLimit() {
return ServerResponse.success("accessLimit: success");
}
}
7.工具类
ip工具类
import javax.servlet.http.HttpServletRequest;
public class IpUtil {
/**
* 获取客户端真实ip地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
Jedis工具类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Component
@Slf4j
public class JedisUtil {
@Autowired
private JedisPool jedisPool;
// 获取到操作redis的客户端对象
private Jedis getJedis() {
return jedisPool.getResource();
}
/**
* 往redis中设置值,不带过期时间
* @param key
* @param value
* @return
*/
public String set(String key, String value) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.set(key, value);
} catch (Exception e) {
log.error("set key:{} value:{} error", key, value, e);
return null;
} finally {
close(jedis);
}
}
/**
* 设值,并附带key的过期时间
*
* @param key
* @param value
* @param expireTime 过期时间, 单位: s
* @return
*/
public String set(String key, String value, int expireTime) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.setex(key, expireTime, value);
} catch (Exception e) {
log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);
return null;
} finally {
close(jedis);
}
}
/**
* 取值
*
* @param key
* @return
*/
public String get(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.get(key);
} catch (Exception e) {
log.error("get key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 删除key
*
* @param key
* @return
*/
public Long del(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.del(key.getBytes());
} catch (Exception e) {
log.error("del key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 判断key是否存在
*
* @param key
* @return
*/
public Boolean exists(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.exists(key.getBytes());
} catch (Exception e) {
log.error("exists key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 设值key过期时间
*
* @param key
* @param expireTime 过期时间, 单位: s
* @return
*/
public Long expire(String key, int expireTime) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.expire(key.getBytes(), expireTime);
} catch (Exception e) {
log.error("expire key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
/**
* 获取剩余时间
*
* @param key
* @return
*/
public Long ttl(String key) {
Jedis jedis = null;
try {
jedis = getJedis();
return jedis.ttl(key);
} catch (Exception e) {
log.error("ttl key:{} error", key, e);
return null;
} finally {
close(jedis);
}
}
private void close(Jedis jedis) {
if (null != jedis) {
jedis.close();
}
}
}
时间工具类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Date;
@Slf4j
public class JodaTimeUtil {
private static final String STANDARD_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* date类型 -> string类型
*
* @param date
* @return
*/
public static String dateToStr(Date date) {
return dateToStr(date, STANDARD_FORMAT);
}
/**
* date类型 -> string类型
*
* @param date
* @param format 自定义日期格式
* @return
*/
public static String dateToStr(Date date, String format) {
if (date == null) {
return null;
}
format = StringUtils.isBlank(format) ? STANDARD_FORMAT : format;
DateTime dateTime = new DateTime(date);
return dateTime.toString(format);
}
/**
* string类型 -> date类型
*
* @param timeStr
* @return
*/
public static Date strToDate(String timeStr) {
return strToDate(timeStr, STANDARD_FORMAT);
}
/**
* string类型 -> date类型
*
* @param timeStr
* @param format 自定义日期格式
* @return
*/
public static Date strToDate(String timeStr, String format) {
if (StringUtils.isBlank(timeStr)) {
return null;
}
format = StringUtils.isBlank(format) ? STANDARD_FORMAT : format;
org.joda.time.format.DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(format);
DateTime dateTime;
try {
dateTime = dateTimeFormatter.parseDateTime(timeStr);
} catch (Exception e) {
log.error("strToDate error: timeStr: {}", timeStr, e);
return null;
}
return dateTime.toDate();
}
/**
* 判断date日期是否过期(与当前时刻比较)
*
* @param date
* @return
*/
public static Boolean isTimeExpired(Date date) {
String timeStr = dateToStr(date);
return isBeforeNow(timeStr);
}
/**
* 判断date日期是否过期(与当前时刻比较)
*
* @param timeStr
* @return
*/
public static Boolean isTimeExpired(String timeStr) {
if (StringUtils.isBlank(timeStr)) {
return true;
}
return isBeforeNow(timeStr);
}
/**
* 判断timeStr是否在当前时刻之前
*
* @param timeStr
* @return
*/
private static Boolean isBeforeNow(String timeStr) {
DateTimeFormatter format = DateTimeFormat.forPattern(STANDARD_FORMAT);
DateTime dateTime;
try {
dateTime = DateTime.parse(timeStr, format);
} catch (Exception e) {
log.error("isBeforeNow error: timeStr: {}", timeStr, e);
return null;
}
return dateTime.isBeforeNow();
}
/**
* 日期加天数
*
* @param date
* @param days
* @return
*/
public static Date plusDays(Date date, int days) {
return plusOrMinusDays(date, days, 0);
}
/**
* 日期减天数
*
* @param date
* @param days
* @return
*/
public static Date minusDays(Date date, int days) {
return plusOrMinusDays(date, days, 1);
}
/**
* 加减天数
*
* @param date
* @param days
* @param type 0:加天数 1:减天数
* @return
*/
private static Date plusOrMinusDays(Date date, int days, Integer type) {
if (null == date) {
return null;
}
DateTime dateTime = new DateTime(date);
if (type == 0) {
dateTime = dateTime.plusDays(days);
} else {
dateTime = dateTime.minusDays(days);
}
return dateTime.toDate();
}
/**
* 日期加分钟
*
* @param date
* @param minutes
* @return
*/
public static Date plusMinutes(Date date, int minutes) {
return plusOrMinusMinutes(date, minutes, 0);
}
/**
* 日期减分钟
*
* @param date
* @param minutes
* @return
*/
public static Date minusMinutes(Date date, int minutes) {
return plusOrMinusMinutes(date, minutes, 1);
}
/**
* 加减分钟
*
* @param date
* @param minutes
* @param type 0:加分钟 1:减分钟
* @return
*/
private static Date plusOrMinusMinutes(Date date, int minutes, Integer type) {
if (null == date) {
return null;
}
DateTime dateTime = new DateTime(date);
if (type == 0) {
dateTime = dateTime.plusMinutes(minutes);
} else {
dateTime = dateTime.minusMinutes(minutes);
}
return dateTime.toDate();
}
/**
* 日期加月份
*
* @param date
* @param months
* @return
*/
public static Date plusMonths(Date date, int months) {
return plusOrMinusMonths(date, months, 0);
}
/**
* 日期减月份
*
* @param date
* @param months
* @return
*/
public static Date minusMonths(Date date, int months) {
return plusOrMinusMonths(date, months, 1);
}
/**
* 加减月份
*
* @param date
* @param months
* @param type 0:加月份 1:减月份
* @return
*/
private static Date plusOrMinusMonths(Date date, int months, Integer type) {
if (null == date) {
return null;
}
DateTime dateTime = new DateTime(date);
if (type == 0) {
dateTime = dateTime.plusMonths(months);
} else {
dateTime = dateTime.minusMonths(months);
}
return dateTime.toDate();
}
/**
* 判断target是否在开始和结束时间之间
*
* @param target
* @param startTime
* @param endTime
* @return
*/
public static Boolean isBetweenStartAndEndTime(Date target, Date startTime, Date endTime) {
if (null == target || null == startTime || null == endTime) {
return false;
}
DateTime dateTime = new DateTime(target);
return dateTime.isAfter(startTime.getTime()) && dateTime.isBefore(endTime.getTime());
}
}
随机数工具类
import java.util.Random;
import java.util.UUID;
public class RandomUtil {
public static final String allChar = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String letterChar = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String numberChar = "0123456789";
public static String UUID32() {
String str = UUID.randomUUID().toString();
return str.replaceAll("-", "");
}
public static String UUID36() {
return UUID.randomUUID().toString();
}
/**
* 生成包含大、小写字母、数字的字符串
*
* @param length
* @return 如: zsK8rCCi
*/
public static String generateStr(int length) {
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(allChar.charAt(random.nextInt(allChar.length())));
}
return sb.toString();
}
/**
* 生成纯数字字符串
*
* @param length
* @return 如: 77914
*/
public static String generateDigitalStr(int length) {
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(numberChar.charAt(random.nextInt(numberChar.length())));
}
return sb.toString();
}
/**
* 生成只包含大小写字母的字符串
*
* @param length
* @return 如: XetrWaYc
*/
public static String generateLetterStr(int length) {
StringBuffer sb = new StringBuffer();
Random random = new Random();
for (int i = 0; i < length; i++) {
sb.append(letterChar.charAt(random.nextInt(letterChar.length())));
}
return sb.toString();
}
/**
* 生成只包含小写字母的字符串
*
* @param length
* @return 如: nzcaunmk
*/
public static String generateLowerStr(int length) {
return generateLetterStr(length).toLowerCase();
}
/**
* 生成只包含大写字母的字符串
*
* @param length
* @return 如: KZMQXSXW
*/
public static String generateUpperStr(int length) {
return generateLetterStr(length).toUpperCase();
}
/**
* 生成纯0字符串
*
* @param length
* @return 如: 00000000
*/
public static String generateZeroStr(int length) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
sb.append('0');
}
return sb.toString();
}
/**
* 根据数字生成字符串,长度不够前面补0
*
* @param num 数字
* @param strLength 字符串长度
* @return 如: 00000099
*/
public static String generateStrWithZero(int num, int strLength) {
StringBuffer sb = new StringBuffer();
String strNum = String.valueOf(num);
if (strLength - strNum.length() >= 0) {
sb.append(generateZeroStr(strLength - strNum.length()));
} else {
throw new RuntimeException("将数字" + num + "转化为长度为" + strLength + "的字符串异常!");
}
sb.append(strNum);
return sb.toString();
}
}
7.最后controller层加上注解,代表访问这个接口对于同一个ip在规定时间内有访问次数限制
import com.cd.annotation.AccessLimit;
import com.cd.annotation.ApiIdempotent;
import com.cd.common.ServerResponse;
import com.cd.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
/**
* 代表在5秒时间内,同一个ip的最大访问次数为5次,超过就会业务异常
* @return
*/
@AccessLimit(maxCount = 5, seconds = 5)
@PostMapping("accessLimit")
public ServerResponse accessLimit() {
return tokenService.accessLimit();
}
最后附上该项目pom文件依赖如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cd</groupId>
<artifactId>springboot-daoshuan</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-daoshuan</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>mysql</groupId>-->
<!-- <artifactId>mysql-connector-java</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--字符串工具类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- Redis-Jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
至此,一个防盗刷功能就完成了,当然有些公司可能会用一些更加复杂的方案来实现,我这里可能算是比较简单的一种实现了