前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SSO单点登录流程源码学习

SSO单点登录流程源码学习

作者头像
六月的雨在Tencent
发布2024-03-29 08:38:39
860
发布2024-03-29 08:38:39
举报
文章被收录于专栏:CSDNCSDN
SSO单点登录流程源码学习

应用背景

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

这里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()中放在页面隐藏域中

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

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

原验证码存储

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

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

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

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

也就是当前这个方法captcha = this.generateAndStoreCaptcha(locale, ID);生成验证码和存储验证码的方法 查看当前类可以看到此处的this.store是CaptchaStore

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

而FastHashMapCaptchaStore又继承自MapCaptchaStore

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

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

点进去之后继续跟进

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

现验证码存储

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

代码语言:javascript
复制
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 引入新改造的类

上图中redisCaptchaStore的构造参数用于配置key过期时间

原验证码登录校验

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

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

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

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

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

现验证码登录校验

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

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

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

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

SSO系统LT存入redis

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

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

原LT存储及验证

首先看GenerateLoginTicketAction.java 可以看到

lt生成之后通过WebUtils.putLoginTicket(context, loginTicket);调用放入了context.getFlowScope()中, 页面表单输入用户名密码验证码后提交到达 AuthenticationViaFormAction.java 可以看到

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

现LT存储验证

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

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

改造后的生产lt的方法

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

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

补充内容(SSO系统补偿service)

现状分析

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

这又是什么原因呢?为了找到问题所在,重新切换回单台单点登录系统就能正常跳转到业务系统首页

分析问题其实还是出在nginx会话保持去掉后,两台机器之间轮询访问导致的。 继续回到SSO单点登录流程上找问题,查看login-webflow.xml,

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

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

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

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

后面需要做的就是解决login-webflow.xml中flowScope.service != null,从而让他执行到后面的generateServiceTicket为服务正确的生成ST票据完成登录授权

那么如何解决flowScope.service != null的问题呢,分析可知原来单台SSO系统,service是不会为空的,那么也就会正常执行到generateServiceTicket完成对服务授权ST票据,而多机器部署后,由于上面的改造并未考虑到service放入redis中,故而后续在失去nginx会话保持后,由于登录页面在A机器加载,此时service就会存在于A的context.getFlowScope(),而提交时可能提交到了B机器,此时通过

回去显然是获取不到service的,那么如果在A机器刷新登录页面时将service备份一份在redis中,而在登录表单提交请求到达B机器后,从redis中取出service,放入B机器的context.getFlowScope()中,那么两台机器都会拥有service,也就会完成后面对service的登录授权并分发ST票据了。

问题处理

基于上述分析,后面进行操作,修改cas-servlet.xml,在流程开始类initialFlowSetupAction中配置RedisTemplate模板

在流程开始类InitialFlowSetupAction.java中将service备用一份在redis中

在AuthenticationViaFormAction.java中的submit方法中当Service service = WebUtils.getService(context);为null时从redis中获取service并重新补偿进context.getFlowScope();

这样后面GenerateServiceTicketAction.java就会正常执行给业务系统授权ST票据信息,从而在登录完成及票据授权完成后可以跳转到正确的业务系统页面。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-03-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SSO单点登录流程源码学习
  • 应用背景
  • SSO系统验证码存入redis
    • 原验证码存储
      • 现验证码存储
        • 原验证码登录校验
          • 现验证码登录校验
          • SSO系统LT存入redis
            • 原LT存储及验证
              • 现LT存储验证
              • 补充内容(SSO系统补偿service)
                • 现状分析
                  • 问题处理
                  相关产品与服务
                  验证码
                  腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档