10 安全优化
10.1 秒杀接口地址隐藏
目的:秒杀开始之前,先去请求接口获取秒杀地址,防止脚本请求
10.1.1 SeckillController.java
@Controller
@RequestMapping("/secKill")
public class SecKillController implements InitializingBean {
@Autowired
private IGoodsService goodsService;
@Autowired
private ISeckillOrderService seckillOrderService;
@Autowired
private ISeckillGoodsService seckillGoodsService;
@Autowired
private IOrderService orderService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private MQSender mqSender;
@Autowired
private RedisScript<Long> redisScript;
private Map<Long, Boolean> EmptyStockMap = new HashMap<>();
/**
* 功能描述:秒杀
*
* windows优化前QPS:168.6
*
* windows页面静态化后QPS:568.2,
*
* windows解决库存超卖后QPS:955.6
*
* windows上使用Rabbit后QPS:1553.8
*
* windows上Redis预减库存后QPS:1345.2,1688.0
*
*
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/{path}/doSecKill", method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(@PathVariable String path, User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
ValueOperations valueOperations = redisTemplate.opsForValue();
//请求路径校验
boolean check = orderService.checkPath(user, goodsId, path);
if(!check){
return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
}
//判断是否重复抢购
SeckillOrder seckillOrder = (SeckillOrder) valueOperations.get("order:" + user.getId() + ":" + goodsId);
if(seckillOrder != null){
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
//通过内存标记,减少redis访问次数
if(EmptyStockMap.get(goodsId)){
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//预减库存
Long stock = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId), Collections.EMPTY_LIST);
if(stock < 1){
EmptyStockMap.put(goodsId, true);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
return RespBean.success(0);
}
/**
* 功能描述:获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
String str = orderService.createPath(user, goodsId);
return RespBean.success(str);
}
}
10.1.2 IOrderServiceImpl.java
/**
* 功能描述:获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@Override
public String createPath(User user, Long goodsId) {
String str = MD5Util.md5(UUIDUtil.uuid() + "123456");
redisTemplate.opsForValue().set("seckillPath:" + user.getId() + ":" +
goodsId, str, 60, TimeUnit.SECONDS);
return str;
}
/**
* 功能描述:检查秒杀地址
* @param user
* @param goodsId
* @param path
* @return
*/
@Override
public boolean checkPath(User user, Long goodsId, String path) {
if(user == null || goodsId < 0 || StringUtils.isEmpty(path)){
return false;
}
String redisPath = (String) redisTemplate.opsForValue().get("seckillPath:" + user.getId() +":" + goodsId);
return path.equals(redisPath);
}
10.1.3 goodsDetail.htm
function getSeckillPath(){
var goodsId = $("#goodsId").val;
g_showLoading();
$.ajax({
url:'/secKill/path',
type:"GET",
data:{
goodsId: $("#goodsId").val()
},
success: function(data){
if(data.code == 200){
var path = data.obj;
doSeckill(path);
}else{
layer.msg(data.message);
}
},
error: function(){
layer.msg("客户端请求错误1");
}
})
}
function doSeckill(path){
$.ajax({
url:'/secKill/' + path + '/doSecKill',
type: 'POST',
data:{
goodsId:$("#goodsId").val()
},
success:function(data){
if(data.code == 200){
// window.location.href="//www.greatytc.com/orderDetail.htm?orderId=" + data.obj.id;
getResult($("#goodsId").val());
}else{
layer.msg(data.message);
}
},
error:function (){
layer.msg("客户端请求错误");
}
})
}
10.1.4 结果
先去请求接口获取秒杀地址
秒杀真正地址
10.2 图形验证码
点击秒杀开始前,先输入验证码,分散用户请求
10.2.1 生成验证码
10.2.1.1 pom.xml
<!-- EasyCaptcha验证码-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
也可以选择其他合适的图形验证码
10.2.1.2 SeckillController.java
/**
* 功能描述:获取验证码
* @param user
* @param goodsId
* @param request
* @param response
*/
@RequestMapping(value = "/captcha", method = RequestMethod.GET)
public void verifyCode(User user, Long goodsId, HttpServletRequest request, HttpServletResponse response){
if(user == null || goodsId < 0){
throw new GlobalException(RespBeanEnum.REPEATE_ERROR);
}
//设置请求头为输出图片的类型
response.setContentType("image/jpg");
response.setHeader("Pargam", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
//生成验证码,将结果放入Redis
ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 32, 3);
redisTemplate.opsForValue().set("captcha:" + user.getId() + ":" + goodsId, captcha.text(), 300, TimeUnit.SECONDS);
try{
captcha.out(response.getOutputStream());
} catch (IOException e) {
log.error("验证码生成失败", e.getMessage());
e.printStackTrace();
}
}
10.2.1.3 goodsDetail.htm
<div class="row">
<div class="form-inline">
<img id="captchaImg" width="130" height="32" onclick="refreshCaptcha()" style="display: none" />
<input id="captcha" class="form-control" style="display: none">
<button class="btn btn-primary" type="button" id="buyButton" onclick="getSeckillPath()">
立即秒杀
<input type="hidden" name="goodsId" id="goodsId">
</button>
</div>
</div>
function refreshCaptcha(){
$("#captchaImg").attr("src", "/seckill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());
}
function countDown(){
var remainSeconds = $("#remainSeconds").val();
var timeout;
//秒杀还未开始
if(remainSeconds > 0){
$("#buyButton").attr("disabled", true);
$("#seckillTip").html("秒杀倒计时:" + remainSeconds + "秒");
timeout = setTimeout(function (){
// $("#countDown").text(remainSeconds - 1);
$("#remainSeconds").val(remainSeconds - 1);
countDown();
}, 1000);
//秒杀进行中
}else if(remainSeconds == 0){
$("#buyButton").attr("disabled", false);
if(timeout){
clearTimeout(timeout);
}
$("#seckillTip").html("秒杀进行中");
$("#captchaImg").attr("src", "/secKill/captcha?goodsId=" + $("#goodsId").val() + "&time=" + new Date());
$("#captchaImg").show()
$("#captcha").show()
}else{
$("#buyButton").attr("disabled", true);
$("#seckillTip").html("秒杀已经结束");
$("#captchaImg").hide()
$("#captcha").hide()
}
};
10.2.1.4 测试
10.2.1.5 SeckillController.java
/**
* 功能描述:获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId, String captcha){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
//验证码检查
boolean check = orderService.checkCaptcha(user, goodsId, captcha);
if(!check){
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
String str = orderService.createPath(user, goodsId);
return RespBean.success(str);
}
10.2.1.6 OrderServiceImpl.java
/**
* 功能描述:校验验证码
* @param user
* @param goodsId
* @param captcha
* @return
*/
@Override
public boolean checkCaptcha(User user, Long goodsId, String captcha) {
if(StringUtils.isEmpty(captcha) || user == null || goodsId < 0){
return false;
}
String redisCaptcha = (String) redisTemplate.opsForValue().get("captcha:" + user.getId() + ":" + goodsId);
return captcha.equals(redisCaptcha);
}
10.2.1.7 goodsDetail.htm
function getSeckillPath(){
g_showLoading();
$.ajax({
url:'/secKill/path',
type:"GET",
data:{
goodsId: $("#goodsId").val(),
captcha: $("#captcha").val()
},
success: function(data){
if(data.code == 200){
var path = data.obj;
doSeckill(path);
}else{
layer.msg(data.message);
}
},
error: function(){
layer.msg("客户端请求错误1");
}
})
}
10.2.1.8 测试
10.3 接口限流
10.3.1 简单接口限流
10.3.1.1 SeckillController.java
/**
* 功能描述:获取秒杀地址
* @param user
* @param goodsId
* @return
*/
// @AccessLimit(second=5, maxCount=5, needLogin=true)
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
/**
* 计数器算法实现,5秒内访问次数限制
*/
ValueOperations valueOperations = redisTemplate.opsForValue();
//限制访问次数,5秒内访问5次
String uri = request.getRequestURI();
// captcha = "0";
Integer count = (Integer) valueOperations.get(uri + ":" + user.getId());
if(count == null){
valueOperations.set(uri + ":" + user.getId(), 1, 5, TimeUnit.SECONDS);
}else if (count < 5){
valueOperations.increment(uri + ":" + user.getId());
}else{
return RespBean.error(RespBeanEnum.ACCESS_LIMIT_REAHCED);
}
//验证码检查
boolean check = orderService.checkCaptcha(user, goodsId, captcha);
if(!check){
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
String str = orderService.createPath(user, goodsId);
return RespBean.success(str);
}
10.3.1.2 测试
10.3.2 通用接口限流
10.3.2.1 UserContext.java
/**
* 从线程中获取用户信息
*/
public class UserContext {
private static ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user){
userHolder.set(user);
}
public static User getUser(){
return userHolder.get();
}
}
10.3.2.2 UserArgumentResolver.java
@Component
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return UserContext.getUser();
}
}
10.3.2.3 AccessInterceptor.java
@Component
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private IUserService userService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(handler instanceof HandlerMethod){
User user = getUser(request, response);
UserContext.setUser(user);
HandlerMethod hm = (HandlerMethod) handler;
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit == null){
return true;
}
int second = accessLimit.second();
int maxCount = accessLimit.maxCount();
boolean needLogin = accessLimit.needLogin();
String key = request.getRequestURI();
if(needLogin){
if(user == null){
render(response, RespBeanEnum.SESSION_ERROR);
return false;
}
key += ":" + user.getId();
}
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer count = (Integer) valueOperations.get(key);
if(count == null){
valueOperations.set(key, 1, second, TimeUnit.SECONDS);
}else if(count < maxCount){
valueOperations.increment(key);
}else{
render(response, RespBeanEnum.ACCESS_LIMIT_REAHCED);
return false;
}
}
return true;
}
/**
* 功能描述:构建返回对象
* @param response
* @param sessionError
*/
private void render(HttpServletResponse response, RespBeanEnum sessionError) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(sessionError);
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
private User getUser(HttpServletRequest request, HttpServletResponse response) {
String ticket = CookieUtil.getCookieValue(request, "userTicket");
if(StringUtils.isEmpty(ticket)){
return null;
}
return userService.getUserByCookie(ticket, request, response);
}
}
10.3.2.4 WebCofnig.java
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessLimitInterceptor);
}
10.3.2.5 AccessLimit.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
int second();
int maxCount();
boolean needLogin() default true;
}
10.3.2.6 SeckillController.java
/**
* 功能描述:获取秒杀地址
* @param user
* @param goodsId
* @return
*/
@AccessLimit(second=5, maxCount=5, needLogin=true)
@RequestMapping(value = "/path", method = RequestMethod.GET)
@ResponseBody
public RespBean getPath(User user, Long goodsId, String captcha, HttpServletRequest request){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
//验证码检查
boolean check = orderService.checkCaptcha(user, goodsId, captcha);
if(!check){
return RespBean.error(RespBeanEnum.ERROR_CAPTCHA);
}
String str = orderService.createPath(user, goodsId);
return RespBean.success(str);
}