栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

手写单点登录

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手写单点登录

手写单点登录-同域
    • 概述
    • SSO设计的好处
    • 方案设计
    • SSO核心功能点
    • 架构设计
      • 通信流程
      • 功能演示
      • 功能实现
        • 后端
        • 前端
          • sso-client
          • sso-auth

概述

单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统 -百度百科

SSO设计的好处
  • 用户角度 :多个子应用只需登录一次,访问其他子应用无需再次登录,非常方便。
  • 系统管理员角度 : 管理员只需维护好一个统一的账号中心就可以了,方便。
  • 新系统开发角度: 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时
方案设计
方案系统架构简介
方案一前端同域+同一个redis服务共享 Cookie 同步会话
方案二前端跨域+同一个redis服务URL重定向传播会话
方案三前端跨域 + 不同Redis服务session共享会话

PS:本篇暂时只演示方案一,方案二、三目前还在设计中,目前这款后端代码适配方案一、二,前端稍作改动即可。

SSO核心功能点
功能简述
登录支持跨域
注销支持跨域
架构设计

通信流程

流程说明如下表所示:

序号内容
1打开浏览器访问系统首页->http://client1.sso.com:9001/index
2进行拦截校验是否携带了令牌,没有携带重定向到SSO登录界面->http://server.sso.com/sso/login?redirect=http://client1.sso.com:9001/index
3在登录页面输入账号密码执行登录->http://server.sso.com/sso/doLogin?redirect=http://client1.sso.com:9001/index&account=zmy&password=123
4校验账号密码,确认信息无误,生成token并标记该用户已登录,返回登录成功响应->{path:http://client1.sso.com:9001/index,token:xxxx}
5客户端接受到登录成功响应,将token拼接到回调url进行跳转->跳转至系统首页:http://client1.sso.com:9001/index?token=xxxxx
6加载系统首页之前获取url携带的token信息,存储在cookies(domain=.sso.com)中,然后重定向至系统首页->http://client1.sso.com:9001/index
7换一个域名模拟另一个子系统,打开浏览器访问系统首页->http://client2.sso.com:9001/index
8从cookie中domain=.sso.com,获取到令牌,重定向到认证中心->http://server.sso.com:8888/sso/auth?redirect=http://client2.sso.com:9001/index,进行认证
9通过令牌检测到已登录,重定向系统首页->http://client2.sso.com:9001/index
功能演示

首先在host文件中配置多个二级域名

127.0.0.1 server.sso.com
127.0.0.1 client1.sso.com
127.0.0.1 client2.sso.com
127.0.0.1 client3.sso.com

访问[http://client1.sso.com:9001],由于没有进行登录,会跳转到登录页面,登录页面地址[http://server.sso.com:9002/?redirect=http://client1.sso.com:9001#/login]

点击登录,进入到刚刚访问的[http://client1.sso.com:9001]页面,此时说明已经成功登录了

在client1已登录的情况下,访问[http://client2.sso.com:9001],由于client1已登录,client2、client3通过cookie共享会话,无需再次进行登录,可以点击注销进行统一注销

功能实现 后端

代码目录如下

演示链路如下:

前端请求-->拦截器缓存请求域-->进入控制器-->执行SSO处理器-->返回结果(重定向|REST响应)

application.yml配置登录页面地址http://server.sso.com:9002

# 端口
server:
  port: 8880

spring:
  messages:
    basename: i18n/messages

# 授权登录地址
sso:
  login:
    url: http://server.sso.com:9002 # 登录页面地址
    account: zhangmuye # 模拟账号
    password: 123456 # 模拟密码

配置拦截器SSOContextInterceptor

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private SSOContextInterceptor ssoContextInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ssoContextInterceptor)
                .addPathPatterns("
    private static final ThreadLocal contextSession = new ThreadLocal<>();

    
    private static final Map userSession = new HashMap<>();


    
    @SneakyThrows
    public void setContextSession(HttpServletRequest request, HttpServletResponse response){
        SSOContextSession ssoContextSession = new SSOContextSession();
        ssoContextSession.setRequest(request);
        ssoContextSession.setResponse(response);

        String accessToken = Optional.ofNullable(request.getHeader(SSOConstants.Properties.AUTHORIZATION))
                .orElse(request.getParameter(SSOConstants.Properties.ACCESS_TOKEN));
        String refreshToken = request.getParameter(SSOConstants.Properties.REFRESH_TOKEN);
        String account = request.getParameter(SSOConstants.Properties.ACCOUNT);
        String password = request.getParameter(SSOConstants.Properties.PASSWORD);
        String redirect = request.getParameter(SSOConstants.Properties.REDIRECT);
        String pathPostfix = request.getRequestURI();
        if ("POST".equals(request.getMethod())){
            JSONObject requestJsonObject = GetRequestJsonUtil.getRequestJsonObject(request);
            if (ObjectUtil.isNotEmpty(requestJsonObject)) {
                // 提取POST请求参数(账号、密码)
                BeanUtil.copyProperties(requestJsonObject, ssoContextSession);
                account = ssoContextSession.getAccount();
                password = ssoContextSession.getPassword();
            }
            // doLogin时 redirect可能为空,从请求头获取redirect
            if (StrUtil.isBlank(redirect)){
                String referer = request.getHeader(SSOConstants.Properties.REFERER);
                if (StrUtil.isNotBlank(referer)){
                    redirect = URLUtil.url(referer).getQuery().substring(9);
                }
            }
        }

        ssoContextSession.setAccessToken(accessToken);
        ssoContextSession.setRefreshToken(refreshToken);
        ssoContextSession.setAccount(account);
        ssoContextSession.setPassword(password);
        ssoContextSession.setRedirect(redirect);
        ssoContextSession.setPostfixPath(pathPostfix);
        contextSession.set(ssoContextSession);
    }

    
    public void setSSOUserSession(String key, SSOUserSession ssoUserSession){
        userSession.put(key, ssoUserSession);
    }

    
    public SSOContextSession getContextSession(){
        return contextSession.get();
    }

    
    public SSOUserSession getSSOUserSession(String key){
        return userSession.get(key);
    }

    
    public void removeUserSession(String key){
        userSession.remove(key);
    }

    
    public void removeContextSession(){
        contextSession.remove();
    }

}

SSOClientController用于接收来自/sso/xx的请求,通过执行ssoProcessor.ssoProcess()方法进行统一处理

@RestController
public class SSOClientController {

    @Autowired
    private SSOProcessor ssoProcessor;

    
    @RequestMapping(value = "/sso
    public Object ssoProcess() {

        log.info("[ ssoProcess ] ===>服务端SSO请求进行统一处理");
        SSOContextSession contextSession = manager.getContextSession();
        if (ObjectUtil.isEmpty(contextSession)) {
            throw new SSORunException(SSOExceptionStatus.IS_NULL_SESSION);
        }
        
        if (contextSession.isSamePostfixPath(SSOConstants.SSOUrl.SSO_AUTH)) {
            // 进行统一认证
            return ssoAuth();
        }
        if (contextSession.isSamePostfixPath(SSOConstants.SSOUrl.SSO_LOGIN)) {
            // 重定向至登录页面
            return ssoLogin();
        }
        if (contextSession.isSamePostfixPath(SSOConstants.SSOUrl.SSO_DO_LOGIN)) {
            // 执行登录:rest请求->ajax请求不支持重定向
            return ssoDoLogin();
        }
        if (contextSession.isSamePostfixPath(SSOConstants.SSOUrl.SSO_DO_LOGOUT)) {
            // 执行注销
            return ssoDoLogout();
        }

        log.error("[ ssoProcess ] ===>服务端SSO请求进行统一处理:{}",SSOExceptionStatus.INVALID_ACCESS.getMessage());
        return Result.failure(SSOExceptionStatus.INVALID_ACCESS.getCode(), SSOExceptionStatus.INVALID_ACCESS.getMessage());
    }

    
    @SneakyThrows
    private Object ssoAuth() {
        log.info("[ ssoAuth ] ===>进入认证中心");
        SSOContextSession contextSession = manager.getContextSession();
        HttpServletResponse response = contextSession.getResponse();
        // 是否合法的重定向地址
        if (!Validator.isUrl(contextSession.getRedirect())) {
            throw new SSORunException(SSOExceptionStatus.ILLEGALITY_REDIRECT);
        }
        // 校验令牌
        if (!ssoToken.verifyToken(contextSession.getAccessToken())) {
            // 认证未通过进行登录
            return ssoLogin();
        }
        // 注册客户端地址
        SSOUserSession ssoUserSession = manager.getSSOUserSession(contextSession.getAccessToken());
        Set clientUrls = Optional.ofNullable(ssoUserSession.getClientUrls()).orElse(new HashSet());
        String host = URLUtil.url(contextSession.getRedirect()).getHost();
        if (!clientUrls.contains(host)){
            clientUrls.add(host);
            ssoUserSession.setClientUrls(clientUrls);
            manager.setSSOUserSession(contextSession.getAccessToken(), ssoUserSession);
            log.info("[ ssoAuth ] ===>成功注册HOST[{}]", host);
        }
        // 重定向至客户端
        response.sendRedirect(contextSession.getRedirect());
        return null;
    }

    
    @SneakyThrows
    private Object ssoLogin() {
        log.info("[ ssoLogin ] ===>重定向至登录页面");
        SSOContextSession contextSession = manager.getContextSession();
        HttpServletResponse response = manager.getContextSession().getResponse();
        // 重定向至认证页面
        response.sendRedirect(StrUtil.format(
                "{}?{}={}",
                ssoYamlConfig.getLoginUrl(),
                SSOConstants.Properties.REDIRECT,
                contextSession.getRedirect()));
        return null;
    }

    
    @SneakyThrows
    private Object ssoDoLogin() {
        log.info("[ ssoDoLogin ] ===>执行登录");
        SSOContextSession contextSession = manager.getContextSession();
        // 进行认证
        if (!ssoYamlConfig.getAccount().equals(contextSession.getAccount())
                || !ssoYamlConfig.getPassword().equals(contextSession.getPassword())) {
            // 认证失败
            log.error("[ ssoDoLogin ] ===>执行登录:{}",SSOExceptionStatus.ACCOUNT_OR_PASSWORD_ERROR.getMessage());
            throw new SSORunException(SSOExceptionStatus.ACCOUNT_OR_PASSWORD_ERROR);
        }

        // TODO 使用三方缓存Redis代替本机缓存
        SSOUserSession ssoUserSession = new SSOUserSession();
        // 模拟生成令牌,存储到缓存中
        String accessToken = UUID.fastUUID().toString();
        String refreshToken = UUID.fromString(accessToken).toString();
        String host = URLUtil.url(contextSession.getRedirect()).getHost();
        Set clients = new HashSet<>();
        clients.add(host);
        ssoUserSession.setAccessToken(accessToken);
        ssoUserSession.setRefreshToken(refreshToken);
        ssoUserSession.setAccount(contextSession.getAccount());
        ssoUserSession.setPassword(contextSession.getPassword());
        ssoUserSession.setClientUrls(clients);
        manager.setSSOUserSession(accessToken, ssoUserSession);

        log.info("[ ssoDoLogin ] ===>成功注册HOST[{}]", host);
        log.info("[ ssoDoLogin ] ===>登录成功,下发令牌");
        // 下发令牌
        return Result.success(new Dict()
                .set(SSOConstants.Properties.PATH_POSTFIX, contextSession.getRedirect())
                .set(SSOConstants.Properties.ACCESS_TOKEN, ssoUserSession.getAccessToken())
                .set(SSOConstants.Properties.REFRESH_TOKEN, ssoUserSession.getRefreshToken())
        );
    }

    
    public Object ssoDoLogout() {
        log.info("[ ssoDoLogout ] ===>执行注销");
        SSOContextSession contextSession = manager.getContextSession();
        // 拦截非法注销
        if (!ssoToken.verifyToken(contextSession.getAccessToken())) {
            log.error("[ ssoDoLogout ] ===>执行注销:{}",SSOExceptionStatus.ILLEGALITY_TOKEN.getMessage());
            throw new SSORunException(SSOExceptionStatus.ILLEGALITY_TOKEN);
        }
        manager.removeUserSession(contextSession.getAccessToken());
        log.info("[ ssoDoLogout ] ===>已成功注销");
        return Result.success();
    }
}
前端 sso-client

核心代码,未获取到令牌进入登录页面,否则进行认证令牌是否合法,合法重定向到当前访问界面

router.beforeEach((to, from, next) => {

  let token = VueCookie.get('token');
 
  if(!token){
    let redirectUrl = window.location.origin;
    window.location.href="http://server.sso.com:8880/sso/login?redirect="+redirectUrl;
  }else{
    window.location.href="http://server.sso.com:8880/sso/auth?redirect="+redirectUrl;
  }
})
sso-auth

核心代码,进行登录,登录成功拿到响应数据redirect地址与token令牌,将token令牌存储在Cookies domain=.sso.com中,达到同域共享令牌,紧接着重定向至要访问的页面







转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1024209.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号