若依框架实现后端防止用户重复点击

若依框架实现后端防止用户重复点击

基于自定义注解、切面、Redis实现

1. 添加自定义注解:

代码放置位置:com/ruoyi/common/annotation/RepeatClick.java

time: 时间默认0;

unit:单位默认 秒;

key: 默认空字符串

package com.ruoyi.framework.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
 * @author yizhi
 */
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatClick {
	/**
	 * 时间
	 */
    int time() default 0;

	/**
	 * 时间单位,默认秒
	 */
	TimeUnit unit() default TimeUnit.SECONDS;

	/**
	 * 默认会校验的数据
	 */
	String key() default "";
}

2. 添加自定义切面:

代码放置位置:com/ruoyi/framework/aspectj/RepeatClickAspect.java

基于注解和Redis实现防止重复点击

package com.ruoyi.framework.aspectj;

import com.ruoyi.common.annotation.RepeatClick;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * @author yizhi
 */
@Aspect
@Component
@Log4j2
public class RepeatClickAspect {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 方式一
     *
     * @param joinPoint
     * @param repeatClick
     * @return
     * @throws Throwable
     */
    @Around("@annotation(repeatClick)")
    public Object repeatClick(ProceedingJoinPoint joinPoint, RepeatClick repeatClick) throws Throwable {
        System.out.println("进入切面了");
        Object[] args = joinPoint.getArgs();
        // 重复点击注解为空  跳过
        if (ObjectUtils.isEmpty(repeatClick)) {
            return joinPoint.proceed(args);
        }
        // 使用错误提示
        if (ObjectUtils.isEmpty(repeatClick.key()) || repeatClick.time() == 0) {
            log.error("注意:存在自定义注解,使用异常:请检查是否设置key, time, unit");
            return joinPoint.proceed(args);
        }
        String repeatClickKey = repeatClick.key() + SecurityUtils.getLoginUser().getUserId();
        if (Boolean.TRUE.equals(redisTemplate.hasKey(repeatClickKey))) {
            return AjaxResult.error("请勿重复点击");
        }
        redisTemplate.opsForValue().set(repeatClickKey, UUID.randomUUID().toString());
        redisTemplate.expire(repeatClickKey, repeatClick.time(), repeatClick.unit());
        return joinPoint.proceed(args);
    }

    /**
     * 方式二
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
//    @Around("@annotation(com.ruoyi.framework.annotation.RepeatClick)")
//    public Object repeatClick(ProceedingJoinPoint joinPoint) throws Throwable {
//        System.out.println("进入切面了");
//        Object[] args = joinPoint.getArgs();
//        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//        RepeatClick repeatClick = signature.getMethod().getAnnotation(RepeatClick.class);
//        // 重复点击注解为空  跳过
//        if (ObjectUtils.isEmpty(repeatClick)) {
//            return joinPoint.proceed(args);
//        }
//        // 使用错误提示
//        if (ObjectUtils.isEmpty(repeatClick.key()) || repeatClick.time() == 0) {
//            log.error("注意:存在自定义注解,使用异常:请检查是否设置key, time, unit");
//            return joinPoint.proceed(args);
//        }
//        String repeatClickKey = repeatClick.key() + SecurityUtils.getLoginUser().getUserId();
//        if (Boolean.TRUE.equals(redisTemplate.hasKey(repeatClickKey))) {
//            return AjaxResult.error("请勿重复点击");
//        }
//        redisTemplate.opsForValue().set(repeatClickKey, UUID.randomUUID().toString());
//        redisTemplate.expire(repeatClickKey, repeatClick.time(), repeatClick.unit());
//        return joinPoint.proceed(args);
//    }
}

3. 最后在controller中添加注解进行测试

key: 我给自己规定填写 —完整接口名称(因为唯一)

time和unit合起来一起使用,unit默认是秒,那这个就是10秒

如果unit 设置为 分钟,那这个就是十分钟

@RepeatClick(key = "bsLable.ceshi", time = 10)

@RepeatClick(key = "bsLable.ceshi", time = 10, unit = TimeUnit.MINUTES)

4. 自行查看测试结果

### 若依框架用户单点登录逻辑原理 若依框架支持单用户单点登录(Single Sign-On, SSO)功能,其核心在于限制同一用户的多个会话在同一时间内的有效性。以下是其实现机制的详细说明: #### 1. **Session管理** 若依框架通过Spring Security实现了对用户会话的严格控制。当一个用户成功登录后,系统会在内存或Redis中存储该用户的唯一标识以及对应的会话ID[^1]。 如果同一个用户尝试再次登录,系统会检测当前是否存在有效的会话。如果有,则强制注销之前的会话并创建新的会话。这种行为通常由`ConcurrentSessionControlAuthenticationStrategy`类完成,它允许开发者配置最大并发会话数,默认情况下可以设置为1以实现用户单点登录的功能[^2]。 #### 2. **Token验证与刷新** 在SSO场景下,第三方系统传递给若依系统的`token`参数会被用于身份验证。一旦验证通过,系统会生成一个新的会话并与该用户绑定。此时,任何旧的会话都会被标记为失效状态。 为了确保安全性,在每次请求时都需校验令牌的有效性和时效性。这可以通过自定义过滤器拦截HTTP请求,并调用Keycloak或其他OAuth服务进行动态授权检查来达成。 #### 3. **代码示例** 以下是一个简单的Java代码片段展示如何利用Spring Security限制单一用户的多设备登录情况: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SessionRegistry sessionRegistry; @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .maximumSessions(1) // 设置最多只有一个活动session .maxSessionsPreventsLogin(true); // 防止重复登录 super.configure(http); } @Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } } ``` 上述代码设置了每个账户仅能维持一个活跃会话,超过限额的新登录将会阻止前者的继续存在。 #### 4. **异常处理** 对于因超限而被迫登出的情况,应提供友好的提示信息告知用户原因所在。例如,“您的账号已在其他地方登录,请重新输入密码。”此类消息有助于提升用户体验同时增强安全意识。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yizhi-w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值