👀探秘微服务:从零开启网关 SSO 服务搭建之旅

简介: 单点登录(Single Sign-On,简称SSO)是一种认证机制,它允许用户只需一次登录就可以访问多个应用程序或系统。本文结合网关和SaToken快速搭建可用的Session管理服务。

你好,这里是专栏“SpringCloud2023实战”。

前言

单点登录(Single Sign-On,简称SSO)是一种认证机制,它允许用户只需一次登录就可以访问多个应用程序或系统。在使用SSO时,用户只需要提供一次凭据(用户名和密码等),就可以访问多个系统,而无需在每个系统中都进行登录认证。

SSO的实现通常涉及以下三个角色:

  • 服务提供商(Service Provider,SP):提供需要认证用户身份的应用程序或系统。
  • 身份提供商(Identity Provider,IdP):负责处理认证请求,验证用户身份,并返回授权票据。
  • 用户(User):需要访问多个应用程序或系统,并使用相同的凭据进行登录。

单点登录(SSO)解决用户在访问多个互相信任的系统时需要反复登录的问题。通过单点登录,用户只需在一个系统中登录一次,就可以访问所有系统,从而提高用户体验。

架构选型

不同架构下的 SSO 接入问题如下(摘自sa-token):

系统架构 采用模式 简介
前端同域 + 后端同 Redis 模式一 共享 Cookie 同步会话
前端不同域 + 后端同 Redis 模式二 URL重定向传播会话
前端不同域 + 后端不同 Redis 模式三 Http请求获取会话

根据同域与不同域和session存储中间件redis的不同分为三种模式,下文将基于最特殊的“模式三”说明springcloudGateway结合sa-token完成SSO服务的开发任务。

sa-token是一款开源好用的sso实现框架,提供开箱即用的sso服务集成。

SpringCloudGateway作为微服务的入口,用来提供sso服务是比较合适的。

SSO服务搭建

引入pom.xml

  • 引入sa-token和springcloudgateway主要是引入 spring-cloud-starter-gatewaysa-token-reactor-spring-boot3-starter
<dependencies>
        <!--gateway 网关依赖,内置webflux 依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <!--注册中心客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        </dependency>
        <!-- LB 扩展 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--caffeine 替换LB 默认缓存实现-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

        <!-- 工具包依赖 -->
        <dependency>
            <groupId>io.rainforest</groupId>
            <artifactId>banana-common-core</artifactId>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
            <version>1.37.0</version>
        </dependency>


        <!-- Sa-Token 插件:整合SSO -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-sso</artifactId>
            <version>1.37.0</version>
        </dependency>

        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-redis-jackson</artifactId>
            <version>1.37.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- 视图引擎(在前后端不分离模式下提供视图支持) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- Http请求工具(在模式三的单点注销功能下用到,如不需要可以注释掉) -->
        <dependency>
            <groupId>com.dtflys.forest</groupId>
            <artifactId>forest-spring-boot-starter</artifactId>
            <version>1.5.26</version>
        </dependency>

    </dependencies>

修改配置

  • 主要是修改Sa-Token配置和sso相关的测试账户,以及使用到的spring.redis
## 应用名称设置
spring.application.name: gateway-sso
## 微服务设置
spring:
  # Redis配置 (SSO模式一和模式二使用Redis来同步会话)
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
  cloud:
    zookeeper:
      connect-string: localhost:2181
    gateway:
      discovery:
        locator:
          enabled: false
          lowerCaseServiceId: true
      routes: ## 服务端路由设置
        - id: client1
          uri: lb://client1
          predicates:
            - Path=/client1/**
#          filters:
#            - StripPrefix=0
        - id: client2
          uri: lb://client2
          predicates:
            - Path=/client2/**
          filters:
            - StripPrefix=0
        - id: client3
          uri: lb://client3
          predicates:
            - Path=/client3/**
          filters:
            - StripPrefix=0

## springboot服务端设置
server:
  port: 10105
  servlet:
    context-path: /
## 日志级别设置
logging:
  level:
    root: info
## sso 相关配置
sso:
  account: ## 测试账号密码
    - username: yulin # 账号密码
      password: 123yl.
      userid: 10001
      permissions:
        - user.add
        - user.delete
        - user.update
        - user.query
      roles:
        - admin
        - user
    - username: sa
      password: 123456
      userid: 10002
      permissions:
        - user.add
        - user.update
        - user.query
      roles:
        - user
    - username: admin
      password: 123456
      userid: 10003
      permissions:
        - user.update
        - user.query
      roles:
        - user
    - username: test
      password: 123456
      userid: 10004
      permissions:
        - user.update
        - user.test
      roles:
        - test
# Sa-Token 配置
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: banana-token
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true
  # ------- SSO-模式一相关配置  (非模式一不需要配置)
  # cookie:
  # 配置 Cookie 作用域
  # domain: stp.com

  # ------- SSO-模式二相关配置
  sso:
    # Ticket有效期 (单位: 秒),默认五分钟
#    ticket-timeout: 300
    # 所有允许的授权回调地址
    allow-url: "*"
    # ------- SSO-模式三相关配置 (下面的配置在使用SSO模式三时打开)
    # 是否打开模式三
    is-http: true
    # SSO-Server端 ticket校验地址
    check-ticket-url: http://localhost:10105/sso/checkTicket
  sign:
    # API 接口调用秘钥
    secret-key: helloworld
    # ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明)

forest:
  # 关闭 forest 请求日志打印
  log-enabled: false

修改启动类

  • 启动类不需要特殊修改,作为网关需要启用注册中心来使用负载均衡。
package io.rainforest.banana.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Application {
   
    public static void main(String[] args) {
   
        SpringApplication.run(Application.class, args);
    }
}

开发SSO基础接口

项目地址: http://localhost:10105

接口 说明
/user/login 用户登录
/user/token 获取token信息
/user/isLogin 判断用户是否登录
/user/logout 用户登出
/user/userInfo 用户信息
/user/role 用户角色信息
/user/permission 用户权限信息
package io.rainforest.banana.gateway.sso.web.user;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import io.rainforest.banana.gateway.sso.conifg.SSOConfig;
import io.rainforest.banana.gateway.sso.dto.base.Account;
import io.rainforest.banana.gateway.sso.service.user.UserSSOServiceI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserSSOController {
   
    @Autowired
    private UserSSOServiceI userSSOServiceI;

    // 测试登录  ---- http://localhost:10105/user/doLogin?name=test&pwd=123456
    @GetMapping("login")
    public SaResult login(String name, String pwd) {
   
        if(StpUtil.isLogin()){
   
            StpUtil.logout();
        }
        // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
        Account account = userSSOServiceI.getAccount(name, pwd);
        // 此处仅做模拟登录,真实环境应该查询数据进行登录
        if(!ObjectUtils.isEmpty(account)){
   
            StpUtil.login(account.getUserid());
            return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
        }
        return SaResult.error("登录失败!");
    }

    // 查询登录状态  ---- http://localhost:10105/user/isLogin
    @GetMapping("isLogin")
    public SaResult isLogin() {
   
        return SaResult.data(StpUtil.isLogin());
    }

    // 查询 Token 信息  ---- http://localhost:10105/user/tokenInfo
    @GetMapping("token")
    public SaResult token() {
   
        return SaResult.data(StpUtil.getTokenInfo());
    }

    // 测试注销  ---- http://localhost:10105/user/logout
    @GetMapping("logout")
    public SaResult logout() {
   
        StpUtil.logout();
        return SaResult.ok();
    }

    /**
     * 获取用户信息
     * @return
     */
    @GetMapping("userInfo")
    public SaResult userInfo() {
   

        String loginId = StpUtil.getLoginIdAsString();
        if (loginId == null) {
   
            return SaResult.error("未登录");
        }
        return SaResult.data(userSSOServiceI.getUserInfo((loginId)));
    }

    /**
     * 获取权限信息
     * @return
     */
    @GetMapping("role")
    public SaResult role() {
   

        return SaResult.data(StpUtil.getRoleList());
    }
    /**
     * 获取权限信息
     * @return
     */
    @GetMapping("permission")
    public SaResult permission() {
   
        return SaResult.data(StpUtil.getPermissionList());
    }
}

也可以通过sa-token提供的开箱即用接口作为登录服务,线上环境不推荐使用。

/**
 * Sa-Token-SSO Server端 Controller 
 */
@RestController
public class SsoServerController {
   
    /*
     * SSO-Server端:处理所有SSO相关请求
     * 开放接口api说明:https://sa-token.cc/doc.html#/sso/sso-apidoc
     * 或者查看类: cn.dev33.satoken.sso.name.ApiName
     */
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
   
        return SaSsoProcessor.instance.serverDister();
    }
}

实现权限获取方法

通过实现权限获取方法可以使得用户登录的权限匹配。

package io.rainforest.banana.gateway.sso.conifg;

import cn.dev33.satoken.stp.StpInterface;
import io.rainforest.banana.gateway.sso.service.user.UserSSOServiceI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 自定义权限加载接口实现类
 * 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
 */
@Component
public class StpInterfaceImpl implements StpInterface {
   
    @Autowired
    private UserSSOServiceI userSSOServiceI;

    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
   
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        return userSSOServiceI.getPermissionsByLoginId((String) loginId);
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
   
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        return userSSOServiceI.getRolesByLoginId((String) loginId);
    }
}

权限验证说明

@Bean
public SaReactorFilter getSaReactorFilter() {
   
    return new SaReactorFilter()
            // 指定 [拦截路由]
            .addInclude("/**")    /* 拦截所有path */
            // 指定 [放行路由]
            .addExclude("/favicon.ico")
            .addExclude("/user/**")
            // 指定[认证函数]: 每次请求执行 
            .setAuth(obj -> {
   
//                    System.out.println("---------- sa全局认证");
                SaRouter.match("/**", () -> StpUtil.checkLogin());
                // 根据路由划分模块,不同模块不同鉴权
                // todo 修改为动态权限鉴权,角色权限和路径基于数据库配置
                SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
                SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
            })
            // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数 
            .setError(e -> {
   
//                    System.out.println("---------- sa全局异常 ");
                return SaResult.error(e.getMessage());
            });
}
  1. /admin/** 接口校验是否拥有admin角色
  2. /goods/** 接口校验是否拥有goods角色
  3. 实际场景并不多使用这种硬编码方式,后续修改为动态权限鉴权,角色权限和路径基于数据库配置。

例子说明

用户登录流程

## 进行用户登录
http://localhost:10105/user/doLogin?name=sa&pwd=123456
http://localhost:10105/user/doLogin?name=test&pwd=123456
## 测试接口信息
http://localhost:10105/user/tokenInfo

测试角色流程

有权限用户登录:

## 进行用户登录
http://localhost:10105/user/doLogin?name=sa&pwd=123456
## 测试接口信息
http://localhost:10105/demoUser/tokenInfo

无权限用户登录测试:

## 进行用户登录
http://localhost:10105/user/doLogin?name=test&pwd=123456
## 测试接口信息
http://localhost:10105/demoUser/tokenInfo

注: 实际测试中基于注解的权限并未生效。基于filter的权限拦截生效了。

单元测试

下面代码基于登录成功和不成功写的单元测试用例。

package io.rainforest.banana.gateway.sso.web.base;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@AutoConfigureMockMvc
public class LoginTest {
   

    @Test
    void testLoginSuccess(@Autowired WebTestClient webClient) {
   
        // 使用@Autowired注解获取WebTestClient对象,用于发送HTTP请求
        webClient
                .get().uri(url -> url.path("/user/login").queryParam("name", "test").queryParam("pwd", "123456").build())
                .accept(MediaType.APPLICATION_JSON)
                .exchange() // 发送GET请求并获取响应
                .expectStatus().isOk() // 断言响应状态码为200
                .expectBody().jsonPath("$.code").isEqualTo(200); // 断言响应体中的jsonPath("$.code")是否等于200
    }

    @Test
    void testLoginFailure(@Autowired WebTestClient webClient) {
   
        // 使用@Autowired注解获取WebTestClient对象,用于发送HTTP请求
        webClient
                .get().uri(url -> url.path("/user/login").queryParam("name", "test233").queryParam("pwd", "123456").build())
                .accept(MediaType.APPLICATION_JSON)
                .exchange() // 发送GET请求并获取响应
                .expectStatus().isOk() // 断言响应状态码为200
                .expectBody().jsonPath("$.code").isEqualTo(500); // 断言响应体中的jsonPath("$.code")是否等于200
    }

}

关于作者

来自全栈程序员nine的探索与实践,持续迭代中。

目录
相关文章
|
21小时前
|
调度 云计算 芯片
云超算技术跃进,阿里云牵头制定我国首个云超算国家标准
近日,由阿里云联合中国电子技术标准化研究院主导制定的首个云超算国家标准已完成报批,不久后将正式批准发布。标准规定了云超算服务涉及的云计算基础资源、资源管理、运行和调度等方面的技术要求,为云超算服务产品的设计、实现、应用和选型提供指导,为云超算在HPC应用和用户的大范围采用奠定了基础。
|
8天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
9天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
8623 20
|
13天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
4614 11
资料合集|Flink Forward Asia 2024 上海站
|
13天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
21天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
9天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
|
9天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
784 47
|
6天前
|
弹性计算 运维 监控
阿里云云服务诊断工具:合作伙伴架构师的深度洞察与优化建议
作为阿里云的合作伙伴架构师,我深入体验了其云服务诊断工具,该工具通过实时监控与历史趋势分析,自动化检查并提供详细的诊断报告,极大提升了运维效率和系统稳定性,特别在处理ECS实例资源不可用等问题时表现突出。此外,它支持预防性维护,帮助识别潜在问题,减少业务中断。尽管如此,仍建议增强诊断效能、扩大云产品覆盖范围、提供自定义诊断选项、加强教育与培训资源、集成第三方工具,以进一步提升用户体验。
643 243
|
3天前
|
弹性计算 运维 监控
云服务测评 | 基于云服务诊断全方位监管云产品
本文介绍了阿里云的云服务诊断功能,包括健康状态和诊断两大核心功能。作者通过个人账号体验了该服务,指出其在监控云资源状态和快速排查异常方面的优势,同时也提出了一些改进建议,如增加告警配置入口和扩大诊断范围等。