一、安全优化
1.1 思路
- 秒杀接口地址隐藏
- 数学公式验证码
- 接口限流防刷
1.2 隐藏秒杀地址原因
秒杀未开始前,如果抓包先获取path,在将path拼到秒杀地址上,也可以秒杀到商品(ps:不加验证码的情况下)
二、接口地址隐藏
2.1 秒杀之前,先去接口请求获取秒杀地址
- 思路
将生成的path存到redis中
@RequestMapping(value = "/path",method = RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(Model model,MiaoshaUser miaoshaUser,@RequestParam("godsId")Long goodsId){
model.addAttribute("user",miaoshaUser);
String path = miaoshaService.createPath(miaoshaUser, goodsId);
return Result.success(path);
}
2.2 秒杀时,先验证path
秒杀收到请求,先验证PathVarible ,如果不一一致,则显示错误信息
String realpath = miaoshaService.getPath(miaoshaUser, goodsId);
if(!realpath.equals(path)){
return Result.error(CodeMsg.GETPASS_ERROR);
}
2.3 接口地址隐藏的具体步骤
- 验证码验证正确后,进入获取秒杀地址的阶段
- 随机生成一个字符串,返回给前端页面,并且将字符串作为value,userid+goodsid+“killpath”,作为key存到redis缓存中
- 用户根据key从redis中取到有关秒杀路径的字符串,与从前端取到的字秒杀路径的字符串进行比对,正确,则将字符串与秒杀路径组合,这样用户就得到真正的秒杀路径。
- 这样的作用是每个用户,每个商品,每次请求都对应一个真正的秒杀路径,防止其他用户窃取了当前用户的秒杀路径和通过抓包的方式,获取到真正的秒杀路径。
三、数学验证码
3.1 作用
- 防机器人
- 分散用户的请求
3.2 生成验证码接口
@RequestMapping("/verifyCode")
@ResponseBody
public Result<Long> getMiaoshaVerifyCode(Model model, MiaoshaUser miaoshaUser, @RequestParam("godsId")Long goodsId, HttpServletResponse response){
try {
if(miaoshaUser == null){
return Result.error(CodeMsg.SESSION_ERROR);
}
BufferedImage image = miaoshaService.createVerifyCode(miaoshaUser,goodsId);
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(image,"JPEG",outputStream);
outputStream.flush();
outputStream.close();
//数据已经通过ServletOutputStream返回,不需要再返回
return null;
} catch (IOException e) {
e.printStackTrace();
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
}
3.2.1 生成带数学公式的验证码,保存到redis中
返回BufferedImage 类型的验证码
public BufferedImage createVerifyCode(MiaoshaUser miaoshaUser, Long goodsId)
{
if(miaoshaUser==null||goodsId<=0) {
return null;
}
int width=80;
int height=30;
BufferedImage img=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics g=img.getGraphics();
g.setColor(new Color(0xDCDCDC));
g.fillRect(0, 0, width, height);
g.setColor(Color.BLACK);
g.drawRect(0, 0, width-1, height-1);
Random rdm=new Random();
for(int i=0;i<50;i++) {
int x=rdm.nextInt(width);
int y=rdm.nextInt(height);
g.drawOval(x, y, 0, 0);
}
//生成验证码
String vertifyCode=generateVertifyCode(rdm);
//颜色,字体
g.setColor(new Color(0,100,0));
g.setFont(new Font("Candara",Font.BOLD,24));
//将验证码写在图片上
g.drawString(vertifyCode, 8, 24);
g.dispose();
//计算存值
int rnd=calc(vertifyCode);
//将计算结果保存到redis上面去
redisService.set(MiaoshaUserKey.getMiaoshaVertifyCode, ""+miaoshaUser.getId()+"_"+goodsId, rnd);
return img;
}
3.2.2 生成验证码上的数学公式
只做加减乘,除法为0会出错
private char[] ops = new char[]{'+','-','*'};
private String generateVertifyCode(Random rdm) {
int num1 = rdm.nextInt(10);
int num2 = rdm.nextInt(10);
int num3 = rdm.nextInt(10);
char op1 = ops[rdm.nextInt(3)];
char op2 = ops[rdm.nextInt(3)];
String exp = "" + num1 + op1 + num2 + op2 + num3;
return exp;
}
3.2.3 返回验证码的结果,便于与输入的进行比较
作用可以解析验证码上的字符串,并进行计算,返回结果
private static int calc(String exp)
{
try {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine javaScript = scriptEngineManager.getEngineByName("JavaScript");
return (Integer)javaScript.eval(exp);
} catch (ScriptException e) {
e.printStackTrace();
return 0;
}
3.3 点击验证码,就能更换新的验证码
加一个onClick事件
3.4 验证验证码
- 在获得秒杀路径后,就可以进行验证验证码
- 验证成功后要删除验证的验证码
@RequestMapping(value = "/path",method = RequestMethod.GET)
@ResponseBody
public Result<String> getMiaoshaPath(Model model,MiaoshaUser miaoshaUser,@RequestParam("godsId")Long goodsId,@RequestParam("verifyCode")int verifyCode){
if(miaoshaUser == null || goodsId < 0){
return Result.error(CodeMsg.SESSION_ERROR);
}
//验证验证码
Boolean check = miaoshaService.checkVerifyCode(miaoshaUser,goodsId,verifyCode);
if(!check){
return Result.error(CodeMsg.REQUEST_ILLEAGAL);
}
model.addAttribute("user",miaoshaUser);
String path = miaoshaService.createPath(miaoshaUser, goodsId);
return Result.success(path);
}
验证验证码具体函数:
public Boolean checkVerifyCode(MiaoshaUser miaoshaUser, Long goodsId , int verifyCode) {
Integer integer = redisService.get(MiaoshaUserKey.getMiaoshaVertifyCode, "" + miaoshaUser.getId() + "_" + goodsId, Integer.class);
if (integer == null || integer - verifyCode != 0) {
return false;
} else {//验证完,要删除验证码
redisService.delete(MiaoshaUserKey.getMiaoshaVertifyCode, "" + miaoshaUser.getId() + "_" + goodsId);
return true;
}
}
3.5 验证码的验证流程
- 将生成的验证码的结果放在redis缓存中中,userid+goodsid作为key,值作为value存到redis中
- 用户在前端输入的验证码结果,与从redis中取出的结果进行比对,错误返回验证码输入错误,不用删除redis中的验证码
- 用户点击验证码,生成新的验证码,会把新的验证码的结果存到redis中,并且将原来的相同的key的值进行覆盖。
- 比对正确则删除当前redis缓存中的验证码的结果。
四、防刷限流
4. 1 作用
限制一个用户在一段时间内访问的次数
4.2 思路
功能在redis中实现,访问一次对应的值加1,超过访问次数,限制访问,过了一分钟,则重新设置有效期和访问次数。
4.3 简单的防刷限流
- 思路:用户的 ID和请求的路径作为key,首先从redis中根据该key去取得用户的访问次数,如果为null,则根据该key重新设置key,value,value的值是1,能取到key对应的value,则判断vlaue是否小于5,是则处理请求,否则返回error。
//2.查询访问的次数
String requestURI = request.getRequestURI();
String key = requestURI + miaoshaUser.getId() + "";
Integer count = redisService.get(AccessKey.accessKey, key, Integer.class);
if(count == null){
redisService.set(AccessKey.accessKey, key,1);
}else if(count < 5) {
redisService.incr(AccessKey.accessKey, key);
}else {
return Result.error(CodeMsg.ACCESS_LIMIT);
}
4.4 规范的防刷限流
4.4.1 自定义注解
@AccessLimit(seconds=5,maxCount=10,needLogin=true)
4.4.2 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
4.4.3 自定义ThreadLocal存放变量
public class UserContext {
//每个线程中有一个ThreadLocal
public static ThreadLocal<MiaoshaUser> user = new ThreadLocal<>();
public static void setUser(MiaoshaUser miaoshaUser){
user.set(miaoshaUser);
}
public static MiaoshaUser getUser(){
return (MiaoshaUser)user.get();
}
}
4.5 自定义拦截器斤进行防刷限流
public class AccessInteceptor extends HandlerInterceptorAdapter {
@Autowired
MiaoShaUserService miaoShaUserService;
@Autowired
RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
//渠取到用户
MiaoshaUser user = getUser(request, response);
HandlerMethod handler1 = (HandlerMethod) handler;
//拿到注解
AccessLimit accessLimit = handler1.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null){
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
String key = request.getRequestURI();
boolean needLogin = accessLimit.needLogin();
if(needLogin){
if(user == null){
render(response, CodeMsg.SESSION_ERROR);
return false;
}
key += "_" + user.getId();
}else {
}
Integer count = redisService.get(AccessKey.withExpireTime(seconds), key, Integer.class);
if(count == null){
redisService.set(AccessKey.withExpireTime(seconds), key,1);
}else if(count < maxCount) {
redisService.incr(AccessKey.withExpireTime(seconds), key);
}else {
render(response,CodeMsg.ACCESS_LIMIT);
return false;
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg msg) throws IOException {
OutputStream outputStream = response.getOutputStream();
String string = JSON.toJSONString(msg);
outputStream.write(string.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
public MiaoshaUser getUser(HttpServletRequest request, HttpServletResponse response){
String paramToken=request.getParameter(MiaoShaUserService.COOKIE_NAME);
System.out.println("@UserArgumentResolver-resolveArgument paramToken:"+paramToken);
//获取cookie
String cookieToken=getCookieValue(request,MiaoShaUserService.COOKIE_NAME);
System.out.println("@UserArgumentResolver-resolveArgument cookieToken:"+cookieToken);
if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken))
{
return null;
}
String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
//System.out.println("goods-token:"+token);
//System.out.println("goods-cookieToken:"+cookieToken);
MiaoshaUser user=miaoShaUserService.getByToken(token,response);
return user;
}
public String getCookieValue(HttpServletRequest request, String cookie1NameToken) {//COOKIE1_NAME_TOKEN-->"token"
//遍历request里面所有的cookie
Cookie[] cookies=request.getCookies();
if(cookies!=null) {
for(Cookie cookie :cookies) {
if(cookie.getName().equals(cookie1NameToken)) {
System.out.println("getCookieValue:"+cookie.getValue());
return cookie.getValue();
}
}
}
System.out.println("No getCookieValue!");
return null;
}
}
4.6 限制用户访问次数
- 在拦截器拦截方法执行前,去执行相关注解的逻辑,此处是对用户访问次数的访问
- 取到注解中的参数,在指定的时间内进行判断
- 首先判断用户的访问次数是否为null
- 是,则意味着用户之前未访问过,则设置count为1
- 接着判断访问的次数是否超过指定的次数,没有,访问次数加1,否则限制访问