Spring項目使用Redis限制用戶登錄失敗的次數以及暫時鎖定用戶登錄權限

文章目錄

    • 背景
    • 環境
    • 代碼實現
      • 0. 項目結構圖(供參考)
      • 1. 數據庫中的表(供參考)
      • 2. 依賴(pom.xml)
      • 3. 配置文件(application.yml)
      • 4. 配置文件(application-dev.yml)
      • 5. UserLoginDTO
      • 6. DemoConstant
      • 7. User(后來才用的lombok,沒有統一寫法)
      • 8. UserMapper
      • 9. UserService(供參考)
      • 10. UserController
      • 11. RedisConfig
      • 12. 統一返回結果
      • 13. 啟動類LoginDemoApplication
    • 測試登錄接口(一分鐘內五次密碼全錯,觸發賬號鎖定登錄權限)
      • 第一次測試(300ms)
      • 第二次測試(32ms)
      • 第三次測試(22ms)
      • 第四次測試(12ms)
      • 第五次測試(8ms)
    • 總結

背景

前兩天被面試到這個問題,最初回答的不是很合理,登錄次數這方面記錄還一直往數據庫上面想,后來感覺在數據庫中加一個登錄日志表,來查詢一段時間內用戶登錄的次數,現在看來,自己還是太年輕了,做個登錄限制肯定是為了防止數據庫高并發,加一個表來記錄登錄日志,這就把請求都打到數據庫了,后來看了前輩們的解決方案,可以用Redis來實現,包括登錄次數和賬號鎖定,我最開始在回答這個問題的時候只回答到了賬號鎖定用Redis做管理,前者登錄次數回答的比較差勁,后來我也及時復盤了這次面試,就標題的內容做出基本實現

環境

Java8、MySQL8、Redis、IDEA,環境支持的同學可以參考這份代碼實現以下

代碼實現

0. 項目結構圖(供參考)

在這里插入圖片描述

1. 數據庫中的表(供參考)

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for login_demo_user
-- ----------------------------
DROP TABLE IF EXISTS `login_demo_user`;
CREATE TABLE `login_demo_user`  (`id` bigint NOT NULL COMMENT '主鍵',`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用戶名',`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密碼',PRIMARY KEY (`id`, `username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of login_demo_user
-- ----------------------------
INSERT INTO `login_demo_user` VALUES (1, 'hh', 'hh');SET FOREIGN_KEY_CHECKS = 1;

2. 依賴(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/></parent><groupId>com.openallzzz</groupId><artifactId>logindemo</artifactId><version>0.0.1-SNAPSHOT</version><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

3. 配置文件(application.yml)

server:port: 8080spring:profiles:active: devmain:allow-circular-references: truedatasource:druid:driver-class-name: ${datasource.driver-class-name}url: jdbc:mysql://${datasource.host}:${datasource.port}/${datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=trueusername: ${datasource.username}password: ${datasource.password}redis:host: ${redis.host}port: ${redis.port}database: ${redis.database}mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.openallzzz.logindemo.entityconfiguration:#開啟駝峰命名map-underscore-to-camel-case: truelogging:level:com:openallzzz:mapper: debugservice: infocontroller: info

4. 配置文件(application-dev.yml)

datasource:driver-class-name: com.mysql.cj.jdbc.Driverhost: localhostport: 3306database: testdbusername: rootpassword: root
redis:host: localhostport: 6379database: 1

5. UserLoginDTO

package com.openallzzz.logindemo.dto;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginDTO {private String username;private String password;}

6. DemoConstant

package com.openallzzz.logindemo.constant;public class DemoConstant {// 每分鐘限制登錄的最大次數public static final int MAX_LOGIN_TIMRS_PER_MINUTE = 5;
}

7. User(后來才用的lombok,沒有統一寫法)

package com.openallzzz.logindemo.entity;public class User {private Long id;private String username;private String password;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}
}

8. UserMapper

package com.openallzzz.logindemo.mapper;import com.openallzzz.logindemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;@Mapper
public interface UserMapper {@Select("select id, username, password from login_demo_user where username = #{username}")User selectByUsername(String username);}

9. UserService(供參考)

package com.openallzzz.logindemo.service;import com.openallzzz.logindemo.constant.DemoConstant;
import com.openallzzz.logindemo.dto.UserLoginDTO;
import com.openallzzz.logindemo.entity.User;
import com.openallzzz.logindemo.mapper.UserMapper;
import com.openallzzz.logindemo.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisTemplate redisTemplate;public Result<String> login(UserLoginDTO userLoginDTO) {if (userLoginDTO == null || userLoginDTO.getUsername() == null || userLoginDTO.getPassword() == null) {return Result.error("用戶信息不完整!");}String username = userLoginDTO.getUsername();String password = userLoginDTO.getPassword();boolean lockStatus = getLockStatus(username);if (lockStatus) {return Result.error("失敗次數過多,請稍后重試");}User user = userMapper.selectByUsername(username);if (user.getPassword().equals(password)) {clearLastCount(username);clearLockStatus(username);return Result.success();} else {int lastCount = getLastCount(username);if (lastCount + 1 == DemoConstant.MAX_LOGIN_TIMRS_PER_MINUTE) {setLockStatus(username);return Result.error("失敗次數過多,請稍后重試");} else {setLastCount(username);return Result.error("登錄失敗,請檢查用戶名或密碼,再重試");}}}private void clearLockStatus(String username) {log.info("清除用戶鎖定狀態:{}", username);String key = "Lock" + ":" + username;redisTemplate.delete(key);}private void clearLastCount(String username) {log.info("將用戶登錄次數還原:{}", username);String key = "Count" + ":" + username;redisTemplate.delete(key);}private int getLastCount(String username) {log.info("獲取用戶一分鐘內已經失敗登錄了多少次:{}", username);String key = "Count" + ":" + username;Integer count = (Integer) redisTemplate.opsForValue().get(key);return count == null ? 0 : count;}private void setLastCount(String username) {log.info("設置用戶一分鐘內已經失敗登錄了多少次:{}", username);String key = "Count" + ":" + username;redisTemplate.opsForValue().set(key, getLastCount(username) + 1, 1, TimeUnit.MINUTES);}private void setLockStatus(String username) {log.info("鎖定用戶,限制其登錄:{}", username);String key = "Lock" + ":" + username;redisTemplate.opsForValue().set(key, "lock", 2, TimeUnit.HOURS);}private boolean getLockStatus(String username) {log.info("獲取用戶是否被限制其登錄:{}", username);String key = "Lock" + ":" + username;String o = (String) redisTemplate.opsForValue().get(key);if ("lock".equals(o)) return true;return false;}}

10. UserController

package com.openallzzz.logindemo.controller;import com.openallzzz.logindemo.dto.UserLoginDTO;
import com.openallzzz.logindemo.result.Result;
import com.openallzzz.logindemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/login")public Result<String> login(@RequestBody UserLoginDTO userLoginDTO) {log.info("用戶登錄:{}", userLoginDTO);return userService.login(userLoginDTO);}}

11. RedisConfig

package com.openallzzz.logindemo.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
@EnableCaching
public class RedisConfig {@Bean(name = "redisTemplate")public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){RedisTemplate<String,Object> template = new RedisTemplate<>();//配置連接工廠template.setConnectionFactory(factory);//使用jackson序列化和反序列value的值,Jackson2JsonRedisSerializer jacksonSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();//指定需要序列化的范圍,All表示field、get和set,以及修飾符范圍,ANY表示所有范圍,包括privatemapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);jacksonSerializer.setObjectMapper(mapper);//設置template中value使用Jackson2JsonRedisSerializer序列化template.setValueSerializer(jacksonSerializer);//設置template中key使用StringRedisSerializer序列化template.setKeySerializer(new StringRedisSerializer());//這是hash中key和value的序列化方式,key采用StringRedisSerializer,value采用Jackson2JsonRedisSerializertemplate.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(jacksonSerializer);//使template屬性生效template.afterPropertiesSet();return template;}
}

12. 統一返回結果

package com.openallzzz.logindemo.result;import lombok.Data;import java.io.Serializable;/*** 后端統一返回結果* @param <T>*/
@Data
public class Result<T> implements Serializable {private Integer code; //編碼:1成功,0和其它數字為失敗private String msg; //錯誤信息private T data; //數據public static <T> Result<T> success() {Result<T> result = new Result<T>();result.code = 1;return result;}public static <T> Result<T> success(T object) {Result<T> result = new Result<T>();result.data = object;result.code = 1;return result;}public static <T> Result<T> error(String msg) {Result result = new Result();result.msg = msg;result.code = 0;return result;}}

13. 啟動類LoginDemoApplication

package com.openallzzz.logindemo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class LoginDemoApplication {public static void main(String[] args) {SpringApplication.run(LoginDemoApplication.class, args);}}

測試登錄接口(一分鐘內五次密碼全錯,觸發賬號鎖定登錄權限)

第一次測試(300ms)

在這里插入圖片描述

第二次測試(32ms)

在這里插入圖片描述

第三次測試(22ms)

在這里插入圖片描述

第四次測試(12ms)

在這里插入圖片描述

第五次測試(8ms)

在這里插入圖片描述

總結

登錄業務預先判斷了該賬號是否被鎖定,如果短期內有大量登錄請求(用戶不斷試錯、被惡意攻擊),壓力只會給到Redis,從而避免DB被大量請求打中。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/41487.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/41487.shtml
英文地址,請注明出處:http://en.pswp.cn/news/41487.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Camera Link 接口

Camera Link是一個標準的接口協議&#xff0c;用于高速的圖像數據傳輸&#xff0c;常被用在工業相機和圖像處理系統之間。這個標準由自動視覺協會&#xff08;Automated Imaging Association&#xff0c;簡稱AIA&#xff09;在2000年發布&#xff0c;旨在實現各種廠家之間的高性…

在ubuntu+cpolar+rabbitMQ環境下,實現mq服務端遠程訪問

文章目錄 前言1.安裝erlang 語言2.安裝rabbitMQ3. 內網穿透3.1 安裝cpolar內網穿透(支持一鍵自動安裝腳本)3.2 創建HTTP隧道 4. 公網遠程連接5.固定公網TCP地址5.1 保留一個固定的公網TCP端口地址5.2 配置固定公網TCP端口地址 前言 RabbitMQ是一個在 AMQP(高級消息隊列協議)基…

使用opencv4.7.0部署yolov5

yolov5原理和部署原理就不說了&#xff0c;想了解的可以看看這篇部署原理文章 #include <fstream> #include <sstream> #include <iostream> #include <opencv2/dnn.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp>/…

【Java轉Go】快速上手學習筆記(二)之基礎篇一

目錄 創建項目數據類型變量常量類型轉換計數器鍵盤交互流程控制代碼運算符 創建項目 上篇我們安裝好了Go環境&#xff0c;和用IDEA安裝Go插件來開發Go項目&#xff1a;【Java轉Go】快速上手學習筆記&#xff08;一&#xff09;之環境安裝篇 。 這篇我們開始正式學習Go語言。我…

MyBatis動態SQL:打造靈活可變的數據庫操作

目錄 if標簽trim標簽where標簽set標簽foreach標簽 動態SQL就是根據不同的條件或需求動態地生成查詢語句&#xff0c;比如動態搜索條件、動態表或列名、動態排序等。 if標簽 在我們填寫一些信息時&#xff0c;有些信息是必填字段&#xff0c;有的則是非必填的&#xff0c;這些…

淘寶API接口的實時數據和緩存數據區別

電商API接口實時數據是指通過API接口獲取到的與電商相關的實時數據。這些數據可以包括商品庫存、訂單狀態、銷售額、用戶活躍度等信息。 通過電商API接口&#xff0c;可以實時獲取到電商平臺上的各種數據&#xff0c;這些數據可以幫助企業或開發者做出及時的決策和分析。例如&…

vue動態修改audio地址

問題&#xff1a;點擊后替換url地址&#xff0c;實現了&#xff0c;但是播放器依舊沒有反應。 解決&#xff1a;vue中動態替換只是替換了地址&#xff0c;并沒有告訴audio標簽是否要執行&#xff0c;執行什么操作。要load后才能讓它知道&#xff0c;是在喊他&#xff0c;他需求…

秒懂算法 | 漢諾塔問題與木棒三角形

在數學與計算機科學中&#xff0c;遞歸(recursion)是指一個過程或函數在其定義或說明中又直接或間接調用自身的一種方法。它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解。遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算&#x…

Android性能優化——線程優化

一、線程調度原理 在任意時刻&#xff0c;CPU只能執行一條指令&#xff0c;每個線程獲取到CPU的使用權之后才可以執行指令也就是說在任意時刻&#xff0c;只有一個線程占用CPU 處于運行狀態 多線程并發&#xff0c;實際上是指多個線程輪流獲取CPU 的使用權然后分別執行各自的任…

系統安全測試要怎么做?

進行系統安全測試時&#xff0c;可以按照以下詳細的步驟進行&#xff1a; 1、信息收集和分析&#xff1a; 收集系統的相關信息&#xff0c;包括架構、部署環境、使用的框架和技術等。 分析系統的安全需求、威脅模型和安全策略等文檔。 2、威脅建模和風險評估&#xff1a; 使…

調用被fishhook的原函數

OC類如果通過runtime被hook了&#xff0c;可以通過逆序遍歷方法列表的方式調用原方法。 那系統庫的C函數被fish hook了該怎么辦呢&#xff1f; 原理和OC類異曲同工&#xff0c;即通過系統函數dlopen()獲取動態庫&#xff0c;以動態庫為參數通過系統函數dlsym()即可獲取目標系統…

leetcode292. Nim 游戲(博弈論 - java)

Nim 游戲 Nim 游戲題目描述博弈論 上期經典算法 Nim 游戲 難度 - 簡單 原題鏈接 - Nim游戲 題目描述 你和你的朋友&#xff0c;兩個人一起玩 Nim 游戲&#xff1a; 桌子上有一堆石頭。 你們輪流進行自己的回合&#xff0c; 你作為先手 。 每一回合&#xff0c;輪到的人拿掉 1 -…

494. 目標和

494. 目標和 原題鏈接&#xff1a;完成情況&#xff1a;解題思路&#xff1a;數組回溯法動態規劃 參考代碼&#xff1a;數組回溯法__494目標和__動態規劃 經驗吸取 原題鏈接&#xff1a; 494. 目標和 https://leetcode.cn/problems/target-sum/description/ 完成情況&#…

Android進階之多級列表

遇到一個需求需要顯示多級列表&#xff0c;因為界面是在平板上的&#xff0c;所以層級是從左向右往下排的&#xff0c;類似于 我當時的寫法是在xml布局里一個個RecyclerView往下排的 當然前提是已經規定好最大的層級我才敢如此去寫界面&#xff0c;如果已經明確規定只有兩級或…

69 # 強制緩存的配置

強制緩存 強制緩存&#xff1a;以后的請求都不需要訪問服務器&#xff0c;狀態碼為 200協商緩存&#xff1a;每次都判斷一下&#xff0c;告訴是否需要找緩存&#xff0c;狀態碼為 304 默認強制緩存&#xff0c;不緩存首頁&#xff08;如果已經斷網&#xff0c;那這個頁面應該…

Python發送QQ郵件

使用Python的smtplib可以發送QQ郵件&#xff0c;代碼如下 #!/usr/bin/python3 import smtplib from email.mime.text import MIMEText from email.header import Headersender 111qq.com # 發送郵箱 receivers [222qq.com] # 接收郵箱 auth_code "abc" # 授權…

Dockerfile概念、鏡像原理、制作及案例講解

1.Docker鏡像原理 Linux文件操作系統講解 2.鏡像如何制作 3.Dockerfile概念 Docker網址&#xff1a;https://hub.docker.com 3.1 Dockerfile關鍵字 4.案例

【數據結構OJ題】鏈表分割

原題鏈接&#xff1a;https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId8&&tqId11004&rp2&ru/activity/oj&qru/ta/cracking-the-coding-interview/question-ranking 目錄 1. 題目描述 2. 思路分析 3. 代碼實現 1. 題目描述 2…

AMD卡啟動Stable Diffusion AI繪畫的方法

WindowsAMD安裝法 1.安裝python 3.10.6&#xff0c;在python官網上下載安裝程序&#xff0c;***重要*** 在安裝的第一個窗口下方勾選“將python添加到path”。 2.安裝git 3.WindowsAMD使用AUTOMATIC1111的directml這一個fork&#xff0c;在這個頁面的第一段&#xff1a;https:/…

題目:2614.對角線上的質數

??題目來源&#xff1a; leetcode題目&#xff0c;網址&#xff1a;2614. 對角線上的質數 - 力扣&#xff08;LeetCode&#xff09; 解題思路&#xff1a; 遍歷對角線上的元素&#xff0c;返回最大的質數或 0 即可。 解題代碼&#xff1a; class Solution {public int dia…