Browse Source

增加后端同一IP访问次数限制

xusl 2 years ago
parent
commit
ae00f7cab0

+ 11 - 0
backend/pom.xml

@@ -176,6 +176,17 @@
             <artifactId>commons-io</artifactId>
             <version>2.6</version>
         </dependency>
+        <!-- AOP依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!-- Map依赖 -->
+        <dependency>
+            <groupId>net.jodah</groupId>
+            <artifactId>expiringmap</artifactId>
+            <version>0.5.8</version>
+        </dependency>
     </dependencies>
     <build>
         <plugins>

+ 1 - 0
backend/src/main/java/com/jiayue/ssi/config/security/CustomAuthenticationFailureHandler.java

@@ -20,6 +20,7 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
     @Override
     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
         AuthenticationException e) throws IOException, ServletException {
+        // 清除
         response.addHeader("Access-Control-Allow-Origin", "*");
         response.setContentType("text/html;charset=UTF-8");
         response.setStatus(401);

+ 18 - 0
backend/src/main/java/com/jiayue/ssi/config/security/InterfaceLimit.java

@@ -0,0 +1,18 @@
+package com.jiayue.ssi.config.security;
+/**
+* 接口访问频率注解,默认一分钟只能访问5次
+*
+* @author xsl
+* @since 2023/02/23
+*/
+import java.lang.annotation.*;
+
+
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface InterfaceLimit {
+    long time() default 1000; // 限制时间 单位:毫秒(默认值:一分钟)
+
+    int value() default 1; // 允许请求的次数(默认值:5次)
+}

+ 68 - 0
backend/src/main/java/com/jiayue/ssi/config/security/InterfaceLimitAspect.java

@@ -0,0 +1,68 @@
+package com.jiayue.ssi.config.security;
+
+import com.jiayue.ssi.util.IPUtils;
+import com.jiayue.ssi.util.ResponseVO;
+import lombok.extern.slf4j.Slf4j;
+import net.jodah.expiringmap.ExpirationPolicy;
+import net.jodah.expiringmap.ExpiringMap;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 接口限制实现
+ *
+ * @author xsl
+ * @since 2023/02/23
+ */
+@Aspect
+@Component
+@Slf4j
+public class InterfaceLimitAspect {
+    private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();
+
+    /**
+     * 层切点
+     */
+    @Pointcut("@annotation(interfaceLimit)")
+    public void controllerAspect(InterfaceLimit interfaceLimit) {
+    }
+
+    @Around("controllerAspect(interfaceLimit)")
+    public ResponseVO doAround(ProceedingJoinPoint pjp, InterfaceLimit interfaceLimit) throws Throwable {
+        // 获得request对象
+        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
+        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
+        HttpServletRequest request = sra.getRequest();
+
+        // 获取Map value对象, 如果没有则返回默认值
+        // getOrDefault获取参数,获取不到则给默认值
+        ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
+        Integer uCount = uc.getOrDefault(IPUtils.getIpAddr(request), 0);
+        if (uCount >= interfaceLimit.value()) { // 超过次数,不执行目标方法
+            log.error("接口拦截:{} 请求超过限制频率【{}次/{}ms】,IP为{}", request.getRequestURI(), interfaceLimit.value(), interfaceLimit.time(), request.getRemoteAddr());
+            return ResponseVO.fail(null,"请求过于频繁,请稍后再试");
+        } else if (uCount == 0) { // 第一次请求时,设置有效时间
+            uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, interfaceLimit.time(), TimeUnit.MILLISECONDS);
+        } else { // 未超过次数, 记录加一
+            uc.put(request.getRemoteAddr(), uCount + 1);
+        }
+        book.put(request.getRequestURI(), uc);
+
+        // result的值就是被拦截方法的返回值
+        ResponseVO result = (ResponseVO)pjp.proceed();
+
+        return result;
+    }
+
+}

+ 2 - 1
backend/src/main/java/com/jiayue/ssi/config/security/JwtAuthenticationTokenFilter.java

@@ -1,6 +1,7 @@
 package com.jiayue.ssi.config.security;
 
 import com.jiayue.ssi.util.JwtTokenUtil;
+import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -22,7 +23,7 @@ import java.io.IOException;
  * @author: yh
  * @create: 2020-03-19 13:05
  **/
-@Component
+@RequiredArgsConstructor
 @Order(10)
 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 

+ 4 - 3
backend/src/main/java/com/jiayue/ssi/config/security/MailCodeFilter.java

@@ -2,6 +2,7 @@ package com.jiayue.ssi.config.security;
 
 import com.jiayue.ssi.constant.CacheConstants;
 import com.jiayue.ssi.util.LocalCache;
+import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.AuthenticationServiceException;
@@ -22,7 +23,7 @@ import java.io.IOException;
 * @author xsl
 * @since 2023/02/20
 */
-@Component
+@RequiredArgsConstructor
 @Order(2)
 public class MailCodeFilter extends GenericFilterBean {
     private String defaultFilterProcessUrl = "/user/login";
@@ -42,7 +43,7 @@ public class MailCodeFilter extends GenericFilterBean {
                 response.addHeader("Access-Control-Allow-Origin", "*");
                 response.setContentType("text/html;charset=UTF-8");
                 response.setStatus(401);
-                response.getWriter().write("邮箱口令效!");
+                response.getWriter().write("邮箱口令效!");
                 return;
             }
             // 页面录入的邮箱口令
@@ -60,7 +61,7 @@ public class MailCodeFilter extends GenericFilterBean {
                 response.addHeader("Access-Control-Allow-Origin", "*");
                 response.setContentType("text/html;charset=UTF-8");
                 response.setStatus(401);
-                response.getWriter().write("非法访问,邮箱口令错误!");
+                response.getWriter().write("需要6位邮箱口令!");
                 return;
             }
             if (!String.valueOf(mailCode).toLowerCase().equals(mailbox.toLowerCase())) {

+ 4 - 3
backend/src/main/java/com/jiayue/ssi/config/security/VerifyCodeFilter.java

@@ -2,6 +2,7 @@ package com.jiayue.ssi.config.security;
 
 import com.jiayue.ssi.constant.CacheConstants;
 import com.jiayue.ssi.util.LocalCache;
+import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.AuthenticationServiceException;
@@ -22,7 +23,7 @@ import java.io.IOException;
 * @author xsl
 * @since 2023/02/20
 */
-@Component
+@RequiredArgsConstructor
 @Order(1)
 public class VerifyCodeFilter extends GenericFilterBean {
     private String defaultFilterProcessUrl = "/user/login";
@@ -42,7 +43,7 @@ public class VerifyCodeFilter extends GenericFilterBean {
                 response.addHeader("Access-Control-Allow-Origin", "*");
                 response.setContentType("text/html;charset=UTF-8");
                 response.setStatus(401);
-                response.getWriter().write("验证码效!");
+                response.getWriter().write("验证码效!");
                 return;
             }
             // 校验页面验证码
@@ -57,7 +58,7 @@ public class VerifyCodeFilter extends GenericFilterBean {
                 response.addHeader("Access-Control-Allow-Origin", "*");
                 response.setContentType("text/html;charset=UTF-8");
                 response.setStatus(401);
-                response.getWriter().write("非法访问,验证码错误!");
+                response.getWriter().write("需要4位验证码!");
                 return;
             }
             if (!String.valueOf(uuidObj).toLowerCase().equals(requestCaptcha.toLowerCase())) {

+ 10 - 10
backend/src/main/java/com/jiayue/ssi/config/security/WebSecurityConfig.java

@@ -32,14 +32,14 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
     @Autowired
     CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
-    @Autowired
-    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
+//    @Autowired
+//    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
     @Autowired
     RestAccessDeniedHandler restAccessDeniedHandler;
-    @Autowired
-    VerifyCodeFilter verifyCodeFilter;
-    @Autowired
-    MailCodeFilter mailCodeFilter;
+//    @Autowired
+//    VerifyCodeFilter verifyCodeFilter;
+//    @Autowired
+//    MailCodeFilter mailCodeFilter;
 
     @Bean
     public PasswordEncoder passwordEncoder() {
@@ -54,8 +54,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Override
     protected void configure(HttpSecurity httpSecurity) throws Exception {
-        httpSecurity.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
-        httpSecurity.addFilterBefore(mailCodeFilter, UsernamePasswordAuthenticationFilter.class);
+        httpSecurity.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
+        httpSecurity.addFilterBefore(new MailCodeFilter(), UsernamePasswordAuthenticationFilter.class);
+        httpSecurity.addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
         httpSecurity
                 // 由于使用的是JWT,我们这里不需要csrf
                 .csrf().disable()
@@ -71,7 +72,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
         httpSecurity.formLogin().loginProcessingUrl("/user/login")
                 .successHandler(customAuthenticationSuccessHandler)
                 .failureHandler(customAuthenticationFailureHandler);
-        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
         httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);;
 
     }
@@ -80,6 +80,6 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     @Override
     public void configure(WebSecurity web) throws Exception {
         /*super.configure(web);*/
-        web.ignoring().antMatchers("/static/**", "/assets/**");
+        web.ignoring().antMatchers("/static/**", "/assets/**","/getVerifyCode","/getMailCode");
     }
 }

+ 4 - 2
backend/src/main/java/com/jiayue/ssi/controller/UserLoginController.java

@@ -1,6 +1,6 @@
 package com.jiayue.ssi.controller;
 
-import cn.hutool.cache.CacheUtil;
+import com.jiayue.ssi.config.security.InterfaceLimit;
 import com.jiayue.ssi.constant.CacheConstants;
 import com.jiayue.ssi.entity.SysUser;
 import com.jiayue.ssi.service.SysUserService;
@@ -46,6 +46,7 @@ public class UserLoginController {
      * @param httpServletResponse
      * @throws IOException
      */
+    @InterfaceLimit
     @GetMapping("/getVerifyCode")
     public ResponseVO getVerifyCode(HttpServletResponse httpServletResponse) throws IOException {
         // gif类型
@@ -58,7 +59,7 @@ public class UserLoginController {
         // ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48);
         // png类型
         // 三个参数分别为宽、高、位数
-        SpecCaptcha captcha = new SpecCaptcha(150, 40, 4);
+         SpecCaptcha captcha = new SpecCaptcha(150, 40, 4);
         /**
          * 验证码字符类型 TYPE_DEFAULT 数字和字母混合 TYPE_ONLY_NUMBER 纯数字 TYPE_ONLY_CHAR 纯字母 TYPE_ONLY_UPPER 纯大写字母 TYPE_ONLY_LOWER
          * 纯小写字母 TYPE_NUM_AND_UPPER 数字和大写字母
@@ -92,6 +93,7 @@ public class UserLoginController {
      * @param httpServletResponse
      * @throws IOException
      */
+    @InterfaceLimit(time = 2000)
     @PostMapping("/getMailCode")
     public ResponseVO getMailCode(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
         throws Exception {

+ 50 - 0
backend/src/main/java/com/jiayue/ssi/util/IPUtils.java

@@ -0,0 +1,50 @@
+package com.jiayue.ssi.util;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IP地址工具
+ *
+ * @author xsl
+ * @since 2023/02/23
+ */
+public class IPUtils {
+    /**
+     * 获取IP地址
+     * <p>
+     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+     */
+    public static String getIpAddr(HttpServletRequest request) {
+        String ip = null;
+        try {
+            ip = request.getHeader("x-forwarded-for");
+            if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { // 多次反向代理后会有多个ip值,第一个ip才是真实ip
+                if (ip.indexOf(",") != -1) {
+                    ip = ip.split(",")[0];
+                }
+            }
+            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.getHeader("X-Real-IP");
+            }
+            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+                ip = request.getRemoteAddr();
+            }
+        } catch (Exception e) {
+            throw e;
+        }
+        return ip;
+    }
+}

+ 14 - 19
ui/src/views/login/index.vue

@@ -34,7 +34,7 @@
         <el-input
           v-model="loginForm.verifyCode"
           auto-complete="off"
-          placeholder="验证码"
+          placeholder="4位验证码"
           style="width: 63%"
           tabindex="3"
           maxlength="4"
@@ -50,7 +50,7 @@
         <el-input
           v-model="loginForm.mailbox"
           auto-complete="off"
-          placeholder="邮箱验证码"
+          placeholder="6位邮箱口令"
           style="width: 63%"
           tabindex="4"
           maxlength="6"
@@ -108,8 +108,8 @@ export default {
           password: [{ required: true, trigger: 'blur', validator: validatePassword }]*/
         username: [{required: true, trigger: 'blur',message: '请输入用户名'}],
         password: [{required: true, trigger: 'blur',message: '请输入密码'}],
-        verifyCode: [{required: true, trigger: 'blur',message: '请输入验证码'}],
-        mailbox: [{required: true, trigger: 'blur',message: '请输入邮箱验证码'}]
+        verifyCode: [{required: true, trigger: 'blur',message: '请输入验证码'},  { min: 4, max: 4, message: '请输入4位验证码', trigger: 'blur' }],
+        mailbox: [{required: true, trigger: 'blur',message: '请输入邮箱口令'},  { min: 6, max: 6, message: '请输入6位邮箱口令', trigger: 'blur' }]
       },
       loading: false,
       redirect: undefined
@@ -199,42 +199,37 @@ export default {
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true
+          let verifycodetemp = this.loginForm.verifyCode
+          let mailboxtemp = this.loginForm.mailbox
+          if (verifycodetemp.length!=4){
+            return
+
+          }
           const param = new URLSearchParams()
           param.append('username', this.loginForm.username)
           param.append('password', this.loginForm.password)
           param.append('code', this.loginForm.verifyCode)
           param.append('verifyuuid', this.verifyuuid)
           param.append('mailbox', this.loginForm.mailbox)
-          this.$axios.post('/user/login', param,
-          ).then((res) => {
+          this.$axios.post('/user/login', param).then((res) => {
             const {data} = res
             // sessionStorage.setItem('token', data)
 
             document.cookie = "token=" + data;
             document.cookie = "user=".concat(this.loginForm.username)
             sessionStorage.setItem('user', this.loginForm.username)
-            // if (this.loginForm.username !== 'admin') {
-            //   document.cookie = "user=yw"
-            //   // sessionStorage.setItem('user', 'yw')
-            // } else {
-            //   document.cookie = "user=admin"
-            //   // sessionStorage.setItem('user', 'admin')
-            // }
             console.log('login user is :' + this.loginForm.username)
             this.$router.push('/')
             this.loading = false
           }).catch((error) => {
             // 登录失败刷新验证码
             this.updateCaptcha()
+            this.loginForm.verifyCode=''
+            this.loginForm.mailbox=''
+            this.reset()
             this.loading = false
           })
 
-          /*    this.$store.dispatch('user/login', this.loginForm).then(() => {
-            this.$router.push('/')
-            this.loading = false
-          }).catch(() => {
-            this.loading = false
-          })*/
         } else {
           console.log('error submit!!')
           return false