SSO单点登录系统验证码、LT存入redis

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 单点登录系统无状态应用

应用背景

过去若是部署多台单点登录系统,会通过nginx配置做会话保持,从而保证不同客户端发起的登录请求会一直落在同一台机器,保证正常登录,nginx配置如图举例:

image.png

这里nginx会话保持策略采用的是ip_hash。

后随着系统的拓展,以及日常中实际工作的发现,在nginx上做会话保持有一定的弊端,比如:现在有A、B、C三台服务,不同客户端发起的请求会均衡的分布在A、B、C上,这个时候如果C宕机,nginx会把本该到C的请求均衡的分步在A、B上,此时C服务通过处理恢复正常了,这时的nginx由于会话保持,不会再给C分配请求,那么C此时就会一直处于空闲状态,因此需要去掉nginx层面的会话保持策略,这样每一次的请求均会轮询分配在每一台服务上,当宕机的服务又回来时仍然可以获取请求。

当去掉nginx会话保持时,SSO系统会出现在进入登录页面时在A上生成了验证码,默认放在了A的session,而提交时请求到了B上,而B的session中没有页面提交过来的验证码导致登录验证不通过

SSO系统验证码存入redis

如果要将验证码存入redis,那么就需要一个能够标示当前客户端的唯一的id作为key,这是就需要在流程开始类InitialFlowSetupAction.java中增加参数放在context.getFlowScope()中放在页面隐藏域中

image.png

同时在casLoginView.jsp中放置隐藏域,放入uuid

image.png

同时更改原来的获取验证码方法,传入当前隐藏域的uuid用于生成验证码后存入redis的key

image.png

原验证码存储

下面再改造生成验证码的类CaptchaImageCreateController.java

image.png

进一步跟进生成验证码的方法 java.awt.image.BufferedImage challenge = jcaptchaService.getImageChallengeForID(captchaId, request.getLocale());

image.png

进去可以看到返回 (BufferedImage)this.getChallengeForID(ID, locale);

image.png

再继续向下跟可以看到 captcha = this.generateAndStoreCaptcha(locale, ID);

image.png

也就是当前这个方法captcha = this.generateAndStoreCaptcha(locale, ID);生成验证码和存储验证码的方法

查看当前类可以看到此处的this.store是CaptchaStore

image.png

那么回到cas-servlet.xml可以看到CaptchaStore用的是FastHashMapCaptchaStore.java

image.png

FastHashMapCaptchaStore又继承自MapCaptchaStore

image.png

打开MapCaptchaStore.java 可以看到存储验证码的方法,此处的this.store用的是FastHashMap,最终原来验证码是以hashMap的形式放在服务器session中的

image.png

这里还有另一种找到验证码存储位置的入口,比如

image.png

点进去之后继续跟进

image.png

image.png

image.png

最终也是会找到原来的验证码是通过CaptchaStore存储的。

现验证码存储

找到了原始验证码实现存储的类,那么就只需要改造该类并引入即可,首先改造为新的存储类 RedisCaptchaStore.java

package org.jasig.cas.captcha;
import com.octo.captcha.Captcha;
import com.octo.captcha.service.CaptchaServiceException;
import com.octo.captcha.service.captchastore.CaptchaAndLocale;
import com.octo.captcha.service.captchastore.CaptchaStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Collection;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
 * @ClassName:RedisCaptchaStore
 * @author:dongao
 * @date 2021/12/21 13:05
 */
public class RedisCaptchaStore implements CaptchaStore {
    @NotNull
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * redis 过期时间
     */
    @Min(0)
    private final int keyTimeout;
    public RedisCaptchaStore(int keyTimeout) {
        this.keyTimeout = keyTimeout;
    }
    private final Logger logger = LoggerFactory.getLogger(RedisCaptchaStore.class);
    @Override
    public boolean hasCaptcha(String id) {
        try {
            return redisTemplate.hasKey(id);
        } catch (Exception e) {
            logger.info("redisTemplate hasKey({}) failure. error message {}",id,e.getMessage());
            return false;
        }
    }
    @Override
    public void storeCaptcha(String id, Captcha captcha) throws CaptchaServiceException {
        try {
            redisTemplate.opsForValue().set(id,new CaptchaAndLocale(captcha),keyTimeout, TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.info("redisTemplate set({}) failure. error message {}",id,e.getMessage());
        }
    }
    @Override
    public void storeCaptcha(String id, Captcha captcha, Locale locale) throws CaptchaServiceException {
        try {
            redisTemplate.opsForValue().set(id,new CaptchaAndLocale(captcha, locale),keyTimeout,TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.info("redisTemplate set({}) failure. error message {}",id,e.getMessage());
        }
    }
    @Override
    public boolean removeCaptcha(String id) {
        try {
            Object object = redisTemplate.opsForValue().get(id);
            if(object != null) {
                redisTemplate.delete(id);
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            logger.info("redisTemplate delete({}) failure. error message {}",id,e.getMessage());
            return false;
        }
    }
    @Override
    public Captcha getCaptcha(String id) throws CaptchaServiceException {
        try {
            Object captchaAndLocale = redisTemplate.opsForValue().get(id);
            return captchaAndLocale != null?((CaptchaAndLocale)captchaAndLocale).getCaptcha():null;
        } catch (Exception e) {
            logger.info("redisTemplate get({}) failure. error message {}",id,e.getMessage());
            return null;
        }
    }
    @Override
    public Locale getLocale(String id) throws CaptchaServiceException {
        try {
            Object captchaAndLocale = redisTemplate.opsForValue().get(id);
            return captchaAndLocale != null?((CaptchaAndLocale)captchaAndLocale).getLocale():null;
        } catch (Exception e) {
            logger.info("redisTemplate get({}) failure. error message {}",id,e.getMessage());
            return null;
        }
    }
    @Override
    public int getSize() {
        return 0;
    }
    @Override
    public Collection getKeys() {
        return null;
    }
    @Override
    public void empty() {
    }
    @Override
    public void initAndStart() {
    }
    @Override
    public void cleanAndShutdown() {
    }
    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

改造完成之后需要在cas-servlet.xml 引入新改造的类

image.png

上图中redisCaptchaStore的构造参数<constructor-arg index="0" value="600" />用于配置key过期时间

原验证码登录校验

查看DaAuthenticationViaFormAction.java,该类继承自 AuthenticationViaFormAction

image.png

可以看到在校验代码时 valid = captchaService.validateResponseForID(id,captcha_response).booleanValue();

传入从sessionId,

image.png

继续往下跟 AbstractCaptchaService.java 可以看到

image.png

这里点进this.store.getCaptcha(ID)可以看到

image.png

里面有两个继承自CaptchaStore 的类MapCaptchaStore 和刚才新增的用于存储和取出验证码的类 RedisCaptchaStore,此处校验通过之后会返回验证码校验结果true或false,同时执行this.store.removeCaptcha(ID); 删除session或者redis中存的验证码数据。

现验证码登录校验

首先需要修改login-webflow.xml文件的<view-state>标签内容,增加uuid属性值提交

image.png

同时修改用于接收提交参数的实体类UsernamePasswordverifyCodeCredential.java,增加参数uuid的get、set方法

image.png

再回到验证码登录校验类,更改原来的获取sessionId为通过Credentials获取uuid

image.png

后续实际校验验证码的内容无需更改,同原验证码登录校验。

总结:整体针对验证码放入redis的操作来看,只是改变了原验证码的CaptchaStore的实现类,改造实操相对简单,但是阅读原代码存储方式费力些。有了以上经验,那么后面改造LT的存储相对就简单一些了。

SSO系统LT存入redis

首先看下lt在登录页面中的位置,位于登录提交表单的隐藏域,

image.pnglt的作用简单来说就是为了应对登录用户点击退出后,在浏览器点击回退操作时,系统不会自动提交登录参数从而在操作人员无意识情况下再次登录系统。

原LT存储及验证

首先看GenerateLoginTicketAction.java 可以看到

image.png

lt生成之后通过WebUtils.putLoginTicket(context, loginTicket);调用放入了context.getFlowScope()中,

页面表单输入用户名密码验证码后提交到达 AuthenticationViaFormAction.java 可以看到

image.png

这个方法final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);会从flowScope中获取lt,通过与表单提交方法获取的lt 的final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);进行equals比较,不相同则直接返回error。

现LT存储验证

首先需要给生成验证码方法引入redisTemplate,修改cas-servlet.xml配置文件

image.png

同时在lt提交认证类中也引入redisTemplate

image.png

改造后的生产lt的方法

image.png

改造后的表单提交校验lt的方法

image.png

通过以上即可以完成SSO系统验证码、LT更改存储位置及正常业务验证的方法。

补充内容

通过上述的改造后,再配合nginx无会话保持时两台机器测试单点登录,发现每次登录成功后均不能正常跳转到业务页面,而是跳转到如下页面

33BBC5B3-3DF8-496b-8CE4-97F3C8D844ED.png

这又是什么原因呢?从新切换回单台单点登录系统就能正常跳转到权限系统首页

32D017B6-92CF-4d97-9147-527B82908EDA.png

分析问题其实还是出在nginx会话保持去掉后,两台机器之间轮询访问导致的。

继续回到SSO单点登录流程上找问题,查看login-webflow.xml,

image.png可以看到在提交登录表单验证success后应进入sendTicketGrantingTicket,同时发现在提交表单验证的submit方法中

4683095E-8ECF-45e9-9EA2-9F48ABD5A067.png

service此处不应为null,应为正确的需要跳转业务系统的地址。

image.png

继续回到sendTicketGrantingTicket,看到随后会执行serviceCheck

image.png

执行serviceCheck 时会判断flowScope.service != null 时走generateServiceTicket 如果为null,则会走viewGenericLoginSuccess,也就是上面我们看到的登录成功的页面,但是却未正常跳转到权限系统页面,那我们看一下GenerateServiceTicketAction 可以看到SSO系统会为当前service生成ST票据,而service正是我们的权限系统

C3C4D2A2-B97B-4fd3-856A-ADBC5D611C3F.png

后面需要做的就是将service也放入共享redis中保证登录正常跳转到权限系统。









相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
25天前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
57 6
|
3月前
|
JavaScript NoSQL Redis
Vue中实现修改邮箱、手机号等流程的大致过程、验证码由后端的redis生成验证(版本1.0)
这篇文章记录了在Vue中实现修改手机号和邮箱的大致流程,包括使用过滤器部分隐藏展示的手机号和邮箱,以及通过点击触发路由跳转的便捷方式。文章还描述了旧号码和新号码验证的界面实现,其中验证码由后端生成并通过弹窗展示给用户,未来可以接入真正的手机验证码接口。此外,还提供了修改邮箱的页面效果截图,并强调了学习是一个永无止境的过程。
Vue中实现修改邮箱、手机号等流程的大致过程、验证码由后端的redis生成验证(版本1.0)
|
4月前
|
存储 JSON NoSQL
JSON 存入 Redis
【7月更文挑战第8天】
106 12
|
3月前
|
缓存 NoSQL Linux
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
116 1
【Azure Redis 缓存】Windows和Linux系统本地安装Redis, 加载dump.rdb中数据以及通过AOF日志文件追加数据
|
3月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
3月前
|
存储 NoSQL Java
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
该博客文章展示了如何在Linux虚拟机上使用Redis和Jedis客户端实现手机验证码的验证功能,包括验证码的生成、存储、验证以及限制每天发送次数的逻辑,并提供了测试结果截图。
使用redis进行手机验证码的验证、每天只能发送三次验证码 (redis安装在虚拟机linux系统中)
|
3月前
|
NoSQL 数据可视化 Linux
一文教会你如何在Linux系统中使用Docker安装Redis 、以及如何使用可视化工具连接【详细过程+图解】
这篇文章详细介绍了如何在Linux系统中使用Docker安装Redis,并提供了使用可视化工具连接Redis的步骤。内容包括安装Redis镜像、创建外部配置文件、映射文件和端口、启动和测试Redis实例、配置数据持久化存储,以及使用可视化工具连接和操作Redis数据库的过程。
|
3月前
|
NoSQL JavaScript Java
SpringBoot+Vue+Redis实现验证码功能、一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis
这篇文章介绍了如何使用SpringBoot结合Vue和Redis实现验证码功能,包括验证码的有效期控制和一小时内发送次数的限制。
|
4月前
|
NoSQL Redis 数据安全/隐私保护
macos系统中redis如何设置密码
以上步骤应该可以帮助你在macOS系统的Redis服务中设置密码,确保你的数据存储更加安全。此外,确保你定期检查Redis安全性相关的最佳实践和更新,以保持你的服务安全可靠。
324 3
|
28天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
65 1