Recharge write off card secret malicious concurrent request to prevent reuse of card secret recharge successful solution

Project scenario:

Spring boot project write off card secret recharge in concurrent test, a card secret is successfully recharged repeatedly

Problem Description:

The first time I wrote, I didn't consider so much, so I added several logic to verify the card secret status in the business layer. As long as the card secret status is OK, I can directly modify the user's balance

if(ObjectUtil.isNull(cards)){
            return failure("The card does not exist");
        }
        if(cards.getIsEnable() == false){
            return failure("The card has been disabled,Please contact the administrator");
        }
        if(cards.getStatus() == 3){
            return failure("The card has been used,If the balance is not increased,Please contact the administrator");
        }
 <update id="updateUserLeftAmount">
        UPDATE sys_user SET
		left_amount = #{add}
		where id = #{id} 
    </update>

result:

In the case of 20 concurrent requests, 100 fast flushes directly into 900 fast, because the interface is requested by a large number of duplicate data

Solution:

Since the interface is repeatedly requested, I will directly add an aop interception interface to prevent repeated requests

The slice logic verifies the number of requests for this interface from the same ip

@Aspect
@Component
public class LimitRequestAspect {

    private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();

    // Define tangent point
    // Let all methods annotated with @ LimitRequest execute facet methods
    @Pointcut("@annotation(limitRequest)")
    public void excudeService(LimitRequest limitRequest) {
    }

    @Around("excudeService(limitRequest)")
    public Object doAround(ProceedingJoinPoint pjp, LimitRequest limitRequest) throws Throwable {

        // Get request object
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();

        // Gets the Map object. If not, the default value is returned
        // The first parameter is key, and the second parameter is the default value
        ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
        Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0);



        if (uCount >= limitRequest.count()) { // If the number of times is exceeded, the target method will not be executed
            throw new BaseException("Do not request frequently");
//            return "the number of interface requests exceeded";
        } else if (uCount == 0){ // Set the effective time on the first request
//            /** Expires entries based on when they were last accessed */
//            ACCESSED,
//            /** Expires entries based on when they were created */
//            CREATED;
            uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, limitRequest.time(), TimeUnit.MILLISECONDS);
        } else { // If the number of times is not exceeded, add one to the record
            uc.put(request.getRemoteAddr(), uCount + 1);
        }
        book.put(request.getRequestURI(), uc);

        // The value of result is the return value of the intercepted method
        Object result = pjp.proceed();

        return result;
    }

Annotation

@Documented
@Target(ElementType.METHOD) // Note that the annotation can only be placed on the method
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitRequest {
    long time() default 6000; // Limit time unit: milliseconds
    int count() default 1; // Number of requests allowed
}

result:

Although the number of interface requests is intercepted, it does not prevent the problem of repeated recharge of the card secret from being used concurrently. Then the boss told me to add a lock and sent me a picture

Then I added a version field according to the description of the picture and modified my sql

<update id="updateUserLeftAmount">
        UPDATE sys_user SET
		left_amount = #{add},version = version+1
		where left_amount = #{leftAmount} and id = #{id} and version=#{version}
    </update>

Retest:

Or will there be repeated recharge

Solution:

Ask the boss. The boss sent me two more pictures about redis. Let me use the single thread mechanism of redis to verify

Then I integrated jedis and modified the verification logic code as follows

Before modifying the user's balance, use the setnx operation of redis to save the user's unique id, set an expiration time, and then expire to query whether it exists. If it exists, return

 JedisUtil jedisUtil = JedisUtil.getInstance();
            Long verifyCarmi = jedisUtil.setnxWithTimeOut("verifyCarmi", user.getId(), 10);
            if(verifyCarmi == 0){
                map.put("msg","Do not recharge repeatedly");
                map.put("bool",false);
                return map;
            }

jedisUtil. Code for setnxwithtimeout

   /**
     * Add a key value pair. If the key exists, it will not be added. If it does not exist, set the validity period of the key after adding
     * @param key
     * @param value
     * @param timeOut
     */
    public Long setnxWithTimeOut(String key,String value,int timeOut){
        Jedis jedis = getJedis();
        long expire = 2;
        if(0!=jedis.setnx(key, value)){
            expire = jedis.expire(key, timeOut);
        }
        returnJedis(jedis);
        return expire;
    }

Retest:

No problem. You can only recharge it once under concurrent requests. It's all for redis's verification return
redis is very useful 😊
Thank you for learning

Keywords: Java MySQL Redis

Added by rReLmy on Mon, 20 Dec 2021 05:01:03 +0200