百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

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

liebian365 2025-02-16 21:52 7 浏览 0 评论

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

前言

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

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

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

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

架构选型

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



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

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

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

SSO服务搭建

引入pom.xml

  • 引入sa-token和springcloudgateway主要是引入 spring-cloud-starter-gateway 和 sa-token-reactor-spring-boot3-starter。

        
        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
        
            org.springframework.boot
            spring-boot-starter-data-redis-reactive
        
        
        
            org.springframework.cloud
            spring-cloud-starter-zookeeper-discovery
        
        
        
            org.springframework.cloud
            spring-cloud-starter-loadbalancer
        
        
        
            com.github.ben-manes.caffeine
            caffeine
        

        
        
            io.rainforest
            banana-common-core
        
        
        
            org.projectlombok
            lombok
            provided
        
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
        
            cn.dev33
            sa-token-reactor-spring-boot3-starter
            1.37.0
        


        
        
            cn.dev33
            sa-token-sso
            1.37.0
        

        
        
            cn.dev33
            sa-token-redis-jackson
            1.37.0
        
        
            org.apache.commons
            commons-pool2
        

        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        

        
        
            com.dtflys.forest
            forest-spring-boot-starter
            1.5.26
        

    

修改配置

  • 主要是修改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



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 getPermissionList(Object loginId, String loginType) {
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        return userSSOServiceI.getPermissionsByLoginId((String) loginId);
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List 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的探索与实践,持续迭代中。

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: