Redis案例

​ 使用Redis+SpringBoot2.x+Kaptcha完成验证码存储​​,共能实现主要解决在分布式系统中,网站在高并发的访问下,如何保证网站的数据安全以及性能问题


文章目录


前言

1.为什么需要使用验证码

自从进入到了web的时代,我们的生活就离不开形形色色的网站,当我们在浏览网页的时候,也免不了需要登录一些网站。但是登陆就会涉及到个人的信息安全问题。
首先:如果一个网站没有使用验证码,那么许多的黑客等不法份子,就会通过暴力侵入去破解用户的密码,从而进行非法的活动。

如果网站的密码或者用户的密码设置比较简单,比如说是6位的数字字母的随机组合,那么这是很容易被黑客或者说普通人通过数字字母的6为组合数,去暴露破解。

1.1 验证码的作用

  1. 防止恶意破解密码
  2. 防止刷票、刷页
  3. 防止论坛灌水
  4. 防止黑客​​暴力破解​​用户密码盗取信息

有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登录尝试,实际上使用验证码是现在很多网站通行的方。虽然登录麻烦一点,但是对网友的密码安全来说这个功能还是很有必要,也很重要。但我们还是 提醒大家要保护好自己的密码 ,尽量使用混杂了数字、字母、符号在内的 6 位以上密码,不要使用诸如 1234 之类的简单密码或者与用户名相同、类似的密码 ,免得你的账号被人盗用给自己带来不必要的麻烦。

验证码通常使用一些线条和一些不规则的字符组成,主要作用是为了防止一些黑客把密码数据化盗取。

2. 那又为什么要使用Redis来存储验证码

  1. Redis缓存效率极高
  2. 可以设置过期时间
  3. 考虑到分布式数据的负载均衡数据要一致

redis满足以上的所有需求,且是公用的不需要持久化的数据库,最好是一个缓存型的数据库

满足以上的缓存型数据服务器1、redis 2、Memcache

第一章、初识Kaptcha

Kaptcha 是一个可高度配置的实用验证码生成工具,可自由配置的选项如:

  • 验证码的字体
  • 验证码字体的大小
  • 验证码字体的字体颜色
  • 验证码内容的范围(数字,字母,中文汉字!)
  • 验证码图片的大小,边框,边框粗细,边框颜色
  • 验证码的干扰线
  • 验证码的样式(鱼眼样式、3D、普通模糊、…)

Kaptcha 常量配置说明

Constant

描述

默认值

kaptcha.border

验证码图片边框

yes (no|yes)

kaptcha.border.color

验证码图片边框颜色

yes (no|yes)

kaptcha.image.width

验证码图片宽度

-

kaptcha.image.height

验证码图片高度

-

kaptcha.producer.impl

验证码图片实现类

com.google.code.kaptcha.impl.DefaultKaptcha

kaptcha.textProduceer.impl

验证码文本实现类

text.impl.DefaultTextCreator

.textproducer.char.string

文本集合

验证码值从此集合中获取

textproducer.char.space

文字干扰线

.kaptcha.impl.DefaultNoise

kaptcha.border

验证码图片边框

yes (no|yes)

kaptcha.border

验证码图片边框

yes (no|yes)

kaptcha.border

验证码图片边框

yes (no|yes)

kaptcha.border

验证码图片边框

yes (no|yes)

以上的方法都是比较核心且常用的常量值。

kaptcha.noise.color 干扰 颜色,合法值: r,g,b 或者 white,black,blue. black
kaptcha.obscurificator.impl 图片样式:
水纹 com.google.code.kaptcha.impl.WaterRipple
鱼眼 com.google.code.kaptcha.impl.FishEyeGimpy
阴影 com.google.code.kaptcha.impl.ShadowGimpy com.google.code.kaptcha.impl.WaterRipple
kaptcha.background.impl 背景实现类 com.google.code.kaptcha.impl.DefaultBackground
kaptcha.background.clear.from 背景颜色渐变,开始颜色 light grey
kaptcha.background.clear.to 背景颜色渐变, 结束颜色 white
kaptcha.word.impl 文字渲染器 com.google.code.kaptcha.text.impl.DefaultWordRenderer
kaptcha.session.key session key KAPTCHA_SESSION_KEY
kaptcha.session.date session date KAPTCHA_SESSION_DATE

1.1 如何使用Kaptcha

步骤一:导入依赖

使用Kaptcha必须要引入Kaptcha的依赖文件

<dependencies>
<!--kaptcha依赖包谷歌集成的开源的验证码-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>

步骤二:编写配置

这一步就需要使用到上面所讲到的那些常量配置,通过配置来设置生成的图片的样式以及验证码的样式

  • ​需要使用一个DefaultKaptcha的一个类​​,这个类继承了Configuration的类,可以设置自定义的图片生成配置
  • ​Config类​​,用于将图片的属性通过properties设置进去,再通过Properties来完成注入
  • ​Properties​​,这个类用于设置图片的各种参数
@Configuration
public class KaptchaConfig {
/**
* 验证码配置类
* @return
*/
@Bean
@Qualifier("captchaProducer")
public DefaultKaptcha kaptcha() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "220,220,220");
// //properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "38,29,12");
properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "140");
properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "50");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "25");
// //properties.setProperty(Constants.KAPTCHA_SESSION_KEY, "code");
//验证码个数
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Courier");
//字体间隔
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_SPACE,"8");
//干扰线颜色
// properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "white");
//干扰实现类
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
//图片样式
properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
//文字来源
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789");
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}

编写一个Controller来进行测试

@RestController
public class KaptchaController {

@Autowired
private Producer captchaProducer;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 验证码过期时间 10min
*/
private static final long CAPTCHA_CODE_EXPIRED = 1000 * 10 * 60; // 600000ms = 10分钟内

/**
* 生成验证码
*
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {

String captchaText = captchaProducer.createText();
System.out.println("验证码内容为:{" + captchaText + "}");

// 存储redis,配置过期时间 , jedis/lettuce
redisTemplate.opsForValue().set(getCaptchaKey(request), captchaText, CAPTCHA_CODE_EXPIRED,
TimeUnit.MILLISECONDS);
// 创建图片验证码 将生成的验证码放入到图片容器中
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);

try (ServletOutputStream outputStream = response.getOutputStream()) {

ImageIO.write(bufferedImage, "jpg", outputStream);
outputStream.flush();
} catch (IOException e) {
System.err.println("获取流出错:{" + e.getMessage() + "}");
}
}

Redis案例【验证码】_redis

至此,验证码功能已经实现,接下来我们使用redis来进行存储验证码

1.2 使用Redis存储验证码

步骤一:导入依赖

<!--redis客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

步骤二:编写连接Redis的连接参数配置

# redis客户端的连接
spring:
redis:
host: 192.168.188.129
port: 6379
database: 1
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 100
# 连接池中的最大空闲连接
max-idle: 100
# 连接池中的最小空闲连接
min-idle: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 60000

步骤三:编写redis配置类 完成键和值的序列化

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(redisConnectionFactory);

// 配置序列化规则
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

// 设置key-value序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

// 设置hash-value序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

return redisTemplate;
}
}

步骤四:编写一个Util工具类,用于生成唯一的key

通过获取用户登陆时的浏览器User-Agent+ip地址实现对Redis存储的key的唯一性以及通过MD5加密算法进行加密操作,进一步加强安全性

public class CommonUtil {
/**
* 获取用户的IP地址
*
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) {
// "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}


/**
* MD5加密 生成唯一一个固定的Redis的Key
*
* @param data
* @return
*/
public static String MD5(String data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}

return sb.toString().toUpperCase();
} catch (Exception exception) {
}
return null;
}
}

再在之前写的Controller里面 继续完成对验证码的存储操做

private String getCaptchaKey(HttpServletRequest request) {
String ip = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
// 相当于是把用户登陆的信息进行MD5加密后生成一个固定的常量值,
// 作为Redis的键,把其设置设置成随机生成的字符串验证码
String key = "account-service:captcha:" + CommonUtil.MD5(ip + userAgent);
System.out.println("验证码:{" + key + "}");
return key;
}

Redis案例【验证码】_database_02


查看redis客户端的是否已经存入了验证码

Redis案例【验证码】_ipad_03


查看这个存入的键的剩余的存活时间

192.168.188.129:6379[1]> keys *
1) "account-service:captcha:E7E2CC56213F519895CF36BA1DE2C34B"
192.168.188.129:6379[1]> ttl "account-service:captcha:E7E2CC56213F519895CF36BA1DE2C34B"
(integer) 439
192.168.188.129:6379[1]>

Redis案例【验证码】_database_04


此时如果我们还需要发送验证码对验证码进行校验,那么此时我们就需要在编写一个方法来完成对验证码的校验

步骤五:编写一个JsonData工具类用于封装Json验证数据

//返回统一的json数据格式
public class JsonData {
private Integer code; // 表示请求成功或者失败的响应码
private String msg; // 请求成功或者失败的信息描述
private Object data; // 响应的具体数据

public JsonData(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public JsonData() {
}

// 需要向外部暴露响应成功以及失败的信息
// 响应成功
public static JsonData buildSuccess(Object data) {
return new JsonData(200, "响应成功", data);
}

public static JsonData buildSuccess(String msg) {
return new JsonData(200, msg, null);
}

// 响应失败
public static JsonData buildFail() {
return new JsonData(-100, "响应失败", null);
}

public static JsonData buildFail(String msg) {
return new JsonData(-100, msg, null);
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
}

我们可以看到这个工具类就是将获取操做成功或者失败的返回信息

步骤六:编写一个校验验证码的方法

还是在原来的controller的基础上进行扩展

/**
* 发送验证码
*
* @param to
* @param kaptcha
* @param request
* @return
*/
@GetMapping("/send_code")
public JsonData sendCode(@RequestParam(value = "to", required = true) String to,
@RequestParam(value = "kaptcha", required = true) String kaptcha,
HttpServletRequest request) {
// 根据用户标识获取唯一的key
String key = getCaptchaKey(request);
String kaptchaString = redisTemplate.opsForValue().get(key);
if (kaptchaString!=null&&kaptcha!=null&&kaptchaString.equals(kaptcha)) {
redisTemplate.delete(key);
// TODO 发送验证码

return JsonData.buildSuccess("发送成功");
}
return JsonData.buildFail("验证码错误...");
}

先生成一个验证码 使用PostMan进行模拟生成

Redis案例【验证码】_redis_05


此时我们进行校验

情况一:验证失败

Redis案例【验证码】_验证码_06


情况二:验证成功

Redis案例【验证码】_redis_07

**注意:**一旦认证成功,我这里是将原有的键就删除掉了,当然,也可以选择不删除,以为此时的键都是唯一的,再次生成新的值的时候会覆盖掉原先的值,所以影响不大。