基于Sa-Token实现微服务之前的单点登录
阅读原文时间:2023年08月12日阅读:1

修改配置文件,准备好四个域名

127.0.0.1  auth.server.com
127.0.0.1  user.server.com
127.0.0.1  third.server.com
127.0.0.1  eureka.server.com

注册中心:eureka-server服务

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>3.1.1</version>
        </dependency>
        <!--web项目驱动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot-start-version}</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.0</version>
        </dependency>


/**
 * @description: Eureka 服务端注册中心:剔除数据源操作
 * @author: GuoTong
 * @createTime: 2023-06-26 21:50
 * @since JDK 1.8 OR 11
 **/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaServer
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}


/**
 * @description: SpringBoot-Web配置
 * @author: GuoTong
 * @createTime: 2023-06-05 15:37
 * @since JDK 1.8 OR 11
 **/
@Configuration
public class SpringBootConfig implements WebMvcConfigurer {

    /**
     * Description:  添加全局跨域CORS处理
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许证书
                .allowCredentials(true)
                // 设置允许的方法
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }

    /**
     * Description: 静态资源过滤
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //ClassPath:/Static/** 静态资源释放
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        //释放swagger
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        //释放webjars
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

}


server:
  port: 10086
spring:
  application:
    name: eureka-server
  security:
    user:
      name: eureka
      password: eureka
  mvc:
    static-path-pattern: classpath:/static/**
eureka:
  client:
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
    register-with-eureka: false #自己不向注册中心注册自己
    fetch-registry: false  # 自己是注册中心
  instance:
    hostname: 127.0.0.1
    prefer-ip-address: true

准备Sa-Token认证中心server

https://sa-token.cc/doc.html#/ 可以自己参考官方网站定制

knife4j / mysql / mybatis / sa-token / commons / thymeleaf / loadbalancer / bootstrap /eureka-client / lombok

 <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--Knife4j(增强Swagger)-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>

        <!--Mysql数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-version}</version>
        </dependency>

        <!--Mybatis-plus 代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.verison}</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>${mybatisplus.verison}</version>
        </dependency>

        <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>1.33.0</version>
        </dependency>

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

        <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>1.33.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>

        <!--新版的移除了Ribbon的负载策略,所需改用新版的loadbalancer-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>${spring-cloud-starter-version}</version>
        </dependency>

        <!-- bootstrap最高级启动配置读取 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>${spring-cloud-starter-version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>3.1.1</version>
        </dependency>



/**
 * @description: 统一认证中心 SSO-Server用于对外开放接口:
 * @author: GuoTong
 * @createTime: 2022-11-26 23:03
 * @since JDK 1.8 OR 11
 **/
@RestController
public class SsoServerController {

    private AuthLoginUserService authLoginUserService;

    /**
     * Description: 构造器注入
     */
    public SsoServerController(AuthLoginUserService authLoginUserService) {
        this.authLoginUserService = authLoginUserService;
    }

    /*
     * /*
     * SSO-Server端:处理所有SSO相关请求
     *         http://{host}:{port}/sso/auth           -- 单点登录授权地址,接受参数:redirect=授权重定向地址
     *         http://{host}:{port}/sso/doLogin        -- 账号密码登录接口,接受参数:name、pwd
     *         http://{host}:{port}/sso/checkTicket    -- Ticket校验接口(isHttp=true时打开),接受参数:ticket=ticket码、ssoLogoutCall=单点注销回调地址 [可选]
     *         http://{host}:{port}/sso/signout        -- 单点注销地址(isSlo=true时打开),接受参数:loginId=账号id、secretkey=接口调用秘钥
     */
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.serverDister();
    }

    /**
     * 配置SSO相关参数
     */
    @Autowired
    private void configSso(SaSsoConfig sso) {
        // 配置:未登录时返回的View
        sso.setNotLoginView(() -> new ModelAndView("sa-login.html"));

        // 配置:登录处理函数
        sso.setDoLoginHandle((name, pwd) -> {
            QueryWrapper<AuthLoginUser> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("username", name);
            queryWrapper.eq("password", pwd);
            AuthLoginUser user = authLoginUserService.getOne(queryWrapper);
            if (user != null) {
                StpUtil.login(user.getId());
                return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
            }
            return SaResult.error("登录失败!");
        });

    }
}


/**
 * 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
 * <p>(注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)</p>
 *
 * @author kong
 */
@RestController
public class H5Controller {

    @Autowired
    private AuthLoginUserService authLoginUserService;

    /**
     * 获取 redirectUrl
     */
    @RequestMapping("/sso/getRedirectUrl")
    private Object getRedirectUrl(String redirect, String mode) {
        // 未登录情况下,返回 code=401
        if (StpUtil.isLogin() == false) {
            return SaResult.code(401);
        }
        // 已登录情况下,构建 redirectUrl
        if (SaSsoConsts.MODE_SIMPLE.equals(mode)) {
            // 模式一
            SaSsoUtil.checkRedirectUrl(SaFoxUtil.decoderUrl(redirect));
            return SaResult.data(redirect);
        } else {
            // 模式二或模式三
            String redirectUrl = SaSsoUtil.buildRedirectUrl(StpUtil.getLoginId(), redirect);
            return SaResult.data(redirectUrl);
        }
    }

    @RequestMapping("doLogin")
    public SaResult doLogin(String name, String pwd) {
        return authLoginUserService.queryUserNameAndPassword(name, pwd);
    }

    @RequestMapping(value = "isLogin", method = RequestMethod.GET)
    public SaResult isLogin() {
        return SaResult.ok("是否登录:" + StpUtil.isLogin());
    }

    @RequestMapping(value = "tokenInfo", method = RequestMethod.GET)
    public SaResult tokenInfo() {
        return SaResult.data(StpUtil.getTokenInfo());
    }

    @RequestMapping(value = "logout", method = RequestMethod.GET)
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok();
    }

}


/**
 * @description: 获取当前账号权限码集合
 * @author: GuoTong
 * @createTime: 2022-11-29 20:24
 * @since JDK 1.8 OR 11
 **/
@Component
@Slf4j
public class StpInterfaceImpl implements StpInterface {

    @Autowired
    private AuthLoginUserService authLoginUserService;

    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        AuthLoginUser user = null;
        try {
            user = authLoginUserService.getById(loginId.toString());
        } catch (Exception e) {
            log.info("Id无效--->{}", loginId);
        }
        assert user != null;
        String rule = user.getRule();
        return Collections.singletonList(rule);
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        AuthLoginUser user = null;
        try {
            user = authLoginUserService.getById(loginId.toString());
        } catch (Exception e) {
            log.info("Id无效--->{}", loginId);
        }
        assert user != null;
        String rule = user.getRule();
        return Collections.singletonList(rule);
    }

}


/**
 * @description: 授权认证:HandlerInterceptor需要注册拦截地址哦!!!
 * @author: GuoTong
 * @createTime: 2022-11-05 15:40
 * @since JDK 1.8 OR 11
 **/
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        //  获取当前会话是否已经登录,返回true=已登录,false=未登录
        String requestURI = httpServletRequest.getRequestURI();
        // 判断是否是认证中心对外认证接口
        if (requestURI.contains("/sso")) {
            return true;
        }

        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //检查是否有SkipTokenByJWT注释,有则跳过认证
        if (method.isAnnotationPresent(SkipTokenByJWT.class)) {
            SkipTokenByJWT SkipTokenByJWT = method.getAnnotation(SkipTokenByJWT.class);
            if (SkipTokenByJWT.required()) {
                return true;
            }
        }
        // 否则需要登陆
        if (!StpUtil.isLogin()) {
            throw new NotLoginException(ContextCommonMsg.ERROR_MSG_4);
        }
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(NeedTokenByJWT.class)) {
            NeedTokenByJWT needTokenByJWT = method.getAnnotation(NeedTokenByJWT.class);
            if (needTokenByJWT.required()) {
                List<String> permissionList = StpUtil.getPermissionList();
                // 查看当前用户是否包含当前接口的权限
                if (!permissionList.contains(needTokenByJWT.rule())) {
                    throw new NotLoginException(ContextCommonMsg.ERROR_MSG_3);
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }

}


/**
 * @description: 全局异常处理:
 * @author: GuoTong
 * @createTime: 2022-11-27 11:17
 * @since JDK 1.8 OR 11
 **/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 全局异常拦截
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace();
        log.error("服务执行异常---->{}", e.getMessage());
        return SaResult.error(e.getMessage());
    }

    @ExceptionHandler(value = NotLoginException.class)
    public SaResult handlerException(NotLoginException e) {
        log.error("没有登陆---->{}", e.getMessage());
        return SaResult.error(e.getMessage());
    }

    @ExceptionHandler(value = SQLException.class)
    public SaResult msgMySQLExecuteError(Exception e) {
        e.printStackTrace();
        log.error("Mysql执行异常");
        String message = e.getMessage();
        return SaResult.error(message);
    }

    @ExceptionHandler(value = HttpMessageNotReadableException.class)
    public SaResult msgNotFind(Exception e) {
        e.printStackTrace();
        log.error("请求错误");
        String message = e.getMessage();
        return SaResult.error("请求内容未传递" + message);
    }
}



/**
 * @description: SpringBoot-Web配置
 * @author: GuoTong
 * @createTime: 2023-06-05 15:37
 * @since JDK 1.8 OR 11
 **/
@Configuration
public class SpringBootConfig implements WebMvcConfigurer {

    /**
     * Description: 增加拦截器
     *
     * @param registry
     * @author: GuoTong
     * @date: 2022-11-30 13:56:44
     * @return:void
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/authLoginUser/**");
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * Description:  添加全局跨域CORS处理
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许证书
                .allowCredentials(true)
                // 设置允许的方法
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }

    /**
     * Description: 静态资源过滤
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //ClassPath:/Static/** 静态资源释放
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        //释放swagger
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        //释放webjars
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

。。。。其余的东西详见项目。。。。。

https://gitee.com/gtnotgod/sa-token-sso-system.git

认证中心登录页地址

接口管理认证中心用户

  <!--lombok-实体类简化依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-spring-boot-starter</artifactId>
            <version>${Sa-Token-version}</version>
        </dependency>
        <!-- Sa-Token 插件:整合SSO -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-sso</artifactId>
            <version>${Sa-Token-version}</version>
        </dependency>

        <!-- Sa-Token 整合redis (使用jackson序列化方式) -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-dao-redis-jackson</artifactId>
            <version>${Sa-Token-version}</version>
        </dependency>

        <!-- Sa-Token插件:权限缓存与业务缓存分离 -->
        <dependency>
            <groupId>cn.dev33</groupId>
            <artifactId>sa-token-alone-redis</artifactId>
            <version>${Sa-Token-version}</version>
        </dependency>

        <!--Open feign 服务间通讯HTTP-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>${spring-cloud-starter-version}</version>
        </dependency>

        <!--Http内置的JDKHttpURLConnection替换为OkHttp-->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>${feign-okhttp-version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastJson-version}</version>
        </dependency>
        <!--web项目驱动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot-start-version}</version>
        </dependency>
        <!--Knife4j(增强Swagger)-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>

        <!-- bootstrap最高级启动配置读取 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>${spring-cloud-starter-version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>3.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>



/**
 * @description: 创建 SSO-Client 端认证接口
 * @author: GuoTong
 * @createTime: 2022-11-27 15:32
 * @since JDK 1.8 OR 11
 **/
@RestController
public class SSOClientController {

    /*
     * SSO-Client端:处理所有SSO相关请求
     *         http://{host}:{port}/sso/login          -- Client端登录地址,接受参数:back=登录后的跳转地址
     *         http://{host}:{port}/sso/logout         -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
     *         http://{host}:{port}/sso/logoutCall     -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
     */
    @RequestMapping("/sso/*")
    public Object ssoRequest() {
        return SaSsoProcessor.instance.clientDister();
    }

    // 首页
    @RequestMapping("/")
    public String index() {
        String str = "<h2>Sa-Token SSO-Client</h2>" +
                "<p>当前会话是否登录:" + StpUtil.isLogin() + "</p>" +
                "<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">Login</a> " +
                "<a href='/sso/logout?back=self'>Logout</a></p>";
        return str;
    }
}



 @ExceptionHandler(value = NotLoginException.class)
    public Resp handlerException(NotLoginException e,
                                 HttpServletRequest request,
                                 HttpServletResponse response) {
        log.error("没有登陆---->{}", e.getMessage());
        try {
            response.sendRedirect("/sso/login?back=" + request.getRequestURL());
        } catch (IOException ex) {
            log.error("转到认证中心失败---->{}", ex.getMessage());
        }
        return Resp.error(e.getMessage());
    }


spring:
  redis:
    # Redis数据库索引(默认为0)
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 123456
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 20
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: 10000
        # 连接池中的最大空闲连接
        max-idle: 3
        # 连接池中的最小空闲连接
        min-idle: 0
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher  #Springboot2.6以上需要手动设置
    static-path-pattern: classpath:/static/**
  main:
    allow-bean-definition-overriding: true      # 重复定义bean的问题
logging:
  level:
    root: info
    org.springframework: info

# sa-token配置
sa-token:
  # SSO-相关配置
  sso:
    # SSO-Server端 统一认证地址
    auth-url: http://localhost:15001/sso/auth
    # 是否打开单点注销接口
    is-slo: true
  # 配置Sa-Token单独使用的Redis连接 (此处需要和SSO-Server端连接同一个Redis)
  alone-redis:
    # Redis数据库索引
    database: 1
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password: 123456
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0
forest:
  log-enabled: false   # 关闭 forest 请求日志打印

。。。。其余的东西详见项目。。。。。

https://gitee.com/gtnotgod/sa-token-sso-system.git

输入测试接口:http://user.server.com:13601/hello/getOne1/100011

被重定向到认证中心去了

登录访问后

同理准备好B服务THIRD-PARTY-SERVICE

注销A服务 http://user.server.com:13601/sso/logout

注销B服务http://third.server.com:14302/sso/logout

具体接口代码:

 /**
     * 通过主键查询单条数据
     *
     * @param id 主键
     * @return 单条数据
     */
    @GetMapping("/queryOne/{id}")
    public Resp<Object> selectOne(@PathVariable("id") String id) {
        return Resp.Ok(this.openFeignRPCMySQlService.selectOne(id));
    }

fegin接口

/**
 * @description: OpenFegin调用third-party-service服务
 * @FeignClient(name = "third-party-service")  name指定调用的服务名
 * @author: GuoTong
 * @createTime: 2022-12-02 19:39
 * @since JDK 1.8 OR 11
 **/
@FeignClient(name = "third-party-service")
public interface OpenFeignRPCMySQlService {

    /**
     * Description:  feign调用third-party-service服务的接口
     *
     * @author: GuoTong
     * @date: 2022-12-02 20:56:28
     * @return:
     */
    @GetMapping(value = "/hello/getOne2/{id}", produces = "application/json;charset=utf-8")
    Resp<Object> selectOne(@PathVariable("id") String id);

}

输入地址 http://user.server.com:13601/hello/queryOne/100011

重定向认证中心

登录A系统

fegin调用成功B系统

不再重定向到认证中心,A已经登录,B已完成单点,校验已持有登录状态

http://user.server.com:13601/sso/logout

已经重定向到认证中心

fegin调用A系统 :http://third.server.com:14302/hello/queryOne/100011

依旧重定向到认证中心

到此单点登录演示实现完毕;

个人项目地址,在gitee:https://gitee.com/gtnotgod/sa-token-sso-system.git

tip: 更多Sa-Token使用教程参考官方地址:https://sa-token.cc/doc.html#/