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

本文涉及的产品
云数据库 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
相关文章
|
1月前
|
存储 缓存 NoSQL
深入解析Redis:一种快速、高效的键值存储系统
**Redis** 是一款高性能的键值存储系统,以其内存数据、高效数据结构、持久化机制和丰富的功能在现代应用中占有一席之地。支持字符串、哈希、列表、集合和有序集合等多种数据结构,适用于缓存、计数、分布式锁和消息队列等场景。安装Redis涉及下载、编译和配置`redis.conf`。基本操作包括键值对的设置与获取,以及哈希、列表、集合和有序集合的操作。高级特性涵盖发布/订阅、事务处理和Lua脚本。优化策略包括选择合适数据结构、配置缓存和使用Pipeline。注意安全、监控和备份策略,以确保系统稳定和数据安全。
309 1
|
2月前
|
NoSQL Linux Redis
Linux系统中安装redis+redis后台启动+常见相关配置
Linux系统中安装redis+redis后台启动+常见相关配置
|
6天前
|
存储 NoSQL 测试技术
Redis数据存储系统为什么快?
Redis的快速并非偶然,而是深思熟虑的设计理念的结果。通过将数据存储于内存、采用单线程模型、实现非阻塞I/O等独特的技术选择,Redis在高并发和低延迟方面展现了卓越的表现。
34 16
|
11天前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
15天前
|
存储 缓存 NoSQL
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
node实战——koa给邮件发送验证码并缓存到redis服务(node后端储备知识)
20 0
|
16天前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
22 0
|
23天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
185 10
|
1月前
|
NoSQL Redis
redis存入中文---的格式
redis存入中文---的格式
9 0
|
1月前
|
监控 NoSQL 测试技术
解密Redis性能:如何通过性能测试提升系统稳定性和效率
解密Redis性能:如何通过性能测试提升系统稳定性和效率
|
1月前
|
存储 NoSQL Redis
保障数据安全,保障系统稳定:Redis 数据备份与恢复全指南
保障数据安全,保障系统稳定:Redis 数据备份与恢复全指南