文章目录
为什么需要限制用户的登录
限制登陆其实就是在用户输入密码错误达到一定次数的时候,限制一定时间内用户的登录。
首先我们要知道为什么需要做这个功能。原因我总结有两点:
第一:保护用户账号安全,打个比方,如果有某个不怀好意的人知道了某个用户的账号,他可能会写一个程序去撞库(每次以不同的密码去登陆,撞中了就GG),当我们实现了限制用户登陆次数的功能之后,可以很大程度上拖延时间,提高用户的账号安全。
第二:保护数据库,许多人在不同的网站、APP上都有不同的密码,而这么多的密码,难免有时候会一下子想不起来,如果我们放任用户不停的去试错,当这类用户的人数达到一定程度时,我们的数据库说不定就顶不住了呀。
既然明白了为什么需要这个功能,接下来就要去考虑如何实现。
基本的思路如下图,当然,这只是一个简单的实现,比这种实现复杂的情况也有许多,这个就需要大家自己根据实际的业务需求去修改了。
有了思路我们就可以一步一步的按照上面的流程图去进行代码的编写了。
既然我们已经决定要使用redis去实现这个功能,那当然也要在SpringBoot中配置好redis
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/> <!-- lookup parent from repository -->
</parent>
<groupId>com.Lirs</groupId>
<artifactId>Spring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Spring</name>
<description>SpringBoot学习框架</description>
<properties>
<java.version>1.8</java.version>
<mysql.version>5.0.8</mysql.version>
</properties>
<!--SpringBoot集成web服务-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!--alibabaDruid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--SpringBoot整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Redis配置
RedisConfig配置类
当然,我们也可以使用SpringBoot帮我们默认配置好的RedisAutoConfiguration类
但是,SpringBoot默认配置的类不太满足我们实际的开发需求,所以这里还是建议大家自己写一个配置类
package com.Lirs.Spring.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;
}
}
mysql配置类
就算我们有了redis,用户的真实数据我们还是需要去数据库查询的,所以数据库也是必不可少的。
package com.Lirs.Spring.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = MysqlDatasourceConfig.PACKAGE,sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MysqlDatasourceConfig {
static final String PACKAGE = "com.Lirs.Spring.localMapper";
static final String MAPPER_LOCATION = "classpath:mapper/*.xml";
@Primary //当spring遇到相同类型的bean时,默认加载此注解下的bean
@Bean(name = "mysqlDataSource")
@ConfigurationProperties("spring.datasource.druid.mysql")
public DataSource mysqlDataSource(){
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return dataSource;
}
@Primary
@Bean(name = "mysqlTransactionManager")
public DataSourceTransactionManager mysqlTransactionManager(){
return new DataSourceTransactionManager(mysqlDataSource());
}
@Primary
@Bean(name = "mysqlSqlSessionFactory")
public SqlSessionFactory mysqlSqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource)
throws Exception{
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
//sessionFactory.setMapperLocations(new ClassPathResource(MysqlDatasourceConfig.MAPPER_LOCATION));
return sessionFactory.getObject();
}
}
配置文件application.yml
配置类都写好了,那接下来就是配置文件
这里我的mysql的数据源使用的是阿里的druid,如果不需要的小朋友可以将多余的部分删除哦
#tomcat中间件配置
server:
port: 8081
servlet:
context-path: /Spring
spring:
datasource:
druid:
#本地数据源
mysql:
#数据库访问配置
url: jdbc:mysql://localhost:3307/spring?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 960215@Xiaozhen
#连接池配置
initial-size: 5 #默认连接数
min-idle: 5 #最小连接数
#max-idle: 10 已经弃用
max-wait: 30000 #连接等待超时时间
time-between-eviction-runs-millis: 60000 #配置检测可以关闭的空间连接间隔时间
min-eviction-idle-time-millis: 30000 #连接在池中最小生存时间
validationQuery: select 1 #用于验证数据库连接是否有效 oracle为select 1 from dual
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 配置监控统计拦截的filters, 去掉后监控界面sql无法统计, 'wall'用于防火墙
filters: stat,wall
# Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
aop-patterns: com.Lirs.Spring.controller.*
# StatViewServlet配置 用于提供展示监控信息的html页面、JSON API
stat-view-servlet:
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台
login-username: admin
login-password: 960215@Xiaozhen
# IP白名单
# allow: 127.0.0.1
# IP黑名单(共同存在时,deny优先于allow)
# deny: 192.168.1.218
# WebStatFilter配置 用于url统计 这点之后做补充学习
web-stat-filter:
enabled: true
# 添加过滤规则
url-pattern: /*
# 忽略过滤的格式
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
#打开PSCache,并且指定每个连接上PSCache的大小 这个地方之后需要补充学习
pool-prepared-statements: true
max-open-prepared-statements: 20
max-pool-prepared-statement-per-connection-size: 20
##配置redis
redis:
#redis地址
host: 101.132.130.1
#redis密码,默认为空
password: password
#redis端口
port: 6379
#超时连接 毫秒
timeout: 3000
#redis数据库索引,默认是0号数据库
database: 0
UserContorller
因为只是简单的实现,所以一些业务我就省略了
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Object login(String username, String password){
/**
* 验证码校验等操作
*/
Map<String,Object> map = userService.login(username,password);
if(map.get("success") != null){//登陆成功
return map.get("success");
}else{
return map.get("message");
}
}
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
private final int FAILCOUNT = 5;
private final int LOCKHOURS = 2;
private final int FAILTIME = 180;
@Autowired
private UserMapper userMapper;
@Autowired
private RedisUtil redisUtil;
/**
* 用户登陆
* @param username 账号
* @param password 密码
* @return
*/
@Override
public Map<String,Object> login(String username, String password){
HashMap<String,Object> map = new HashMap<>();
long lockTime = this.getUserLoginTimeLock(username);
String key = "user:" + username + ":failCount";
if(lockTime > 0){//判断用户是否已经被锁定
map.put("message","该账号已经被锁定请在" + lockTime + "秒之后尝试");
return map;
}
User user = userMapper.selectByUserName(username);
if(user != null && password.equals(user.getPassword())){//检验用户输入的账号密码是否正确
map.put("success",user);
redisUtil.del(key);
return map;
}else{
//设置用户登陆失败次数
this.setFailCount(username);
int count =(Integer)redisUtil.get(key);
if(count == FAILCOUNT){//判断是否已经达到了最大失败次数
String lockkey = "user:" + username + ":lockTime";
redisUtil.set(lockkey,"1",LOCKHOURS * 60 * 60);//设置锁定时间为2小时
redisUtil.del(key);
map.put("message","当前账号已经被锁定,请在 " + LOCKHOURS + "小时之后再尝试");
return map;
}
//没有达到5次,返回剩余登陆次数
count = FAILCOUNT - count;
map.put("message","登陆失败,您还剩" + count +"次登陆机会");
return map;
}
}
/**
* 检查用户是否已经被锁定,如果是,返回剩余锁定时间,如果否,返回-1
* @param username username
* @return 时间
*/
private int getUserLoginTimeLock(String username) {
String key = "user:" + username + ":lockTime";
int lockTime = (int)redisUtil.getExpireSeconds(key);
if(lockTime > 0){//查询用户是否已经被锁定,如果是,返回剩余锁定时间,如果否,返回-1
return lockTime;
}else{
return -1;
}
}
/**
* 设置失败次数
* @param username username
*/
private void setFailCount(String username){
long count = this.getUserFailCount(username);
String key = "user:" + username + ":failCount";
if(count < 0){//判断redis中是否有该用户的失败登陆次数,如果没有,设置为1,过期时间为2分钟,如果有,则次数+1
redisUtil.set(key,1,FAILTIME);
}else{
redisUtil.incr(key,new Double(1));
}
}
/**
* 获取当前用户已失败次数
* @param username username
* @return 已失败次数
*/
private int getUserFailCount(String username){
String key = "user:" + username + ":failCount";
//从redis中获取当前用户已失败次数
Object object = redisUtil.get(key);
if(object != null){
return (int)object;
}else{
return -1;
}
}
}
mapper
为了方便我这里就只有一个根据用户民查找用户信息的sql
@Repository
public interface UserMapper {
@Select("select * from tb_user where username = #{username}")
User selectByUserName(String username);
}
RedisUtil
我们虽然有了redisTemplate,但是实际使用起来还是不太方便,所以我们补上一个工具类
工具类网上有很多,我这里就只列出了需要用到的一些方法,有需要的同学可以自行百度哦
package com.Lirs.Spring.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Repository
public class RedisUtil {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 设置超时时间,单位是秒
* @param key
* @param time
* @return
*/
public boolean expire(String key,long time){
try{
if(time > 0){
redisTemplate.expire(key,time, TimeUnit.SECONDS);
return true;
}else{
return false;
}
}catch (Exception ex){
ex.printStackTrace();
return false;
}
}
/**
* 获取key的过期时间,单位:秒
* @param key key
* @return 过期时间
*/
public long getExpireSeconds(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 获取key的过期时间,单位:分钟
* @param key key
* @return 过期时间
*/
public long getExpireMinutes(String key){
return redisTemplate.getExpire(key,TimeUnit.MINUTES);
}
/**
* 获取key的过期时间,单位:小时
* @param key key
* @return 过期时间
*/
public long getExpireHours(String key){
return redisTemplate.getExpire(key,TimeUnit.HOURS);
}
/**
* 获取key的过期时间,单位:天
* @param key key
* @return 过期时间
*/
public long getExpireDays(String key){
return redisTemplate.getExpire(key,TimeUnit.DAYS);
}
/**
* 判断key是否存在
* @param key
* @return
*/
public boolean hasKey(String key){
Boolean result = redisTemplate.hasKey(key);
if(result != null)
return result;
return false;
}
/**
* 删除一个或多个key
* @param key
*/
public void del(String ... key){
if(key.length < 1){
new Exception("请输入参数").printStackTrace();
}else if(key.length > 1){
redisTemplate.delete(CollectionUtils.arrayToList(key));
}else{
redisTemplate.delete(key[0]);
}
}
//************************String*********************************
/**
* 获取key的value
* @param key
* @return
*/
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 存入key-value
* @param key
* @param value
* @return
*/
public boolean set(String key,Object value){
try{
redisTemplate.opsForValue().set(key,value);
return true;
}catch (Exception ex){
ex.printStackTrace();
return false;
}
}
/**
* 存放key-value的同时设置过期时间。
* @param key
* @param value
* @param time
* @return
*/
public boolean set(String key,Object value,long time){
try{
if(time > 0){
redisTemplate.opsForValue().set(key,value,time,TimeUnit.SECONDS);
}else{
redisTemplate.opsForValue().set(key,value);
}
return true;
}catch (Exception ex){
ex.printStackTrace();
return false;
}
}
/**
* 指定key进行自增number
* @param key redis中的kye
* @param number 如果需要传入整数,可以使用new Double();
* @return 返回自增后的结果,类型为Double
*/
public Double incr(String key, Double number){
if(number < 0){
new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key,number);
}
}
效果
我们再来看看这些访问所需要的时间(第一次为登陆成功,之后都是登陆失败)
可以看到当用户登陆失败时,我们访问的时间增加了,这是因为我们再查询数据库的同时,还去对redis进行操作,想要解决这个问题,使用redis的缓存就可以啦,但是这里就不多赘述,有需求的小伙伴可以自行百度哦。
至此,大功告成!一个简单的SpringBoot使用redis实现限制用户登陆次数功能就完成了。