背景
前两天被面试到这个问题,最初回答的不是很合理,登录次数这方面记录还一直往数据库上面想,后来感觉在数据库中加一个登录日志表,来查询一段时间内用户登录的次数,现在看来,自己还是太年轻了,做个登录限制肯定是为了防止数据库高并发,加一个表来记录登录日志,这就把请求都打到数据库了,后来看了前辈们的解决方案,可以用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: 8080
spring:
profiles:
active: dev
main:
allow-circular-references: true
datasource:
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=true
username: ${datasource.username}
password: ${datasource.password}
redis:
host: ${redis.host}
port: ${redis.port}
database: ${redis.database}
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.openallzzz.logindemo.entity
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
logging:
level:
com:
openallzzz:
mapper: debug
service: info
controller: info
4. 配置文件(application-dev.yml)
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: testdb
username: root
password: root
redis:
host: localhost
port: 6379
database: 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 {
@Autowired
private UserMapper userMapper;
@Autowired
private 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 {
@Autowired
private 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表示所有范围,包括private
mapper.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采用Jackson2JsonRedisSerializer
template.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)
文章来源:https://www.toymoban.com/news/detail-656267.html
总结
登录业务预先判断了该账号是否被锁定,如果短期内有大量登录请求(用户不断试错、被恶意攻击),压力只会给到Redis,从而避免DB被大量请求打中。文章来源地址https://www.toymoban.com/news/detail-656267.html
到了这里,关于Spring项目使用Redis限制用户登录失败的次数以及暂时锁定用户登录权限的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!