• 基于Sa-Token实现微服务之前的单点登录


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

    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服务

    pom依赖

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
                <version>3.1.1version>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <version>${spring-boot-start-version}version>
            dependency>
            <dependency>
                <groupId>org.webjarsgroupId>
                <artifactId>jqueryartifactId>
                <version>3.5.0version>
            dependency>
    

    配置web项目启动类

    /**
     * @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
    

    启动注册中心验证

    image

    准备Sa-Token认证中心server

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

    引入依赖

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

     <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            
            <dependency>
                <groupId>com.github.xiaoymingroupId>
                <artifactId>knife4j-spring-boot-starterartifactId>
                <version>${knife4j.version}version>
            dependency>
    
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>${mysql-version}version>
            dependency>
    
    
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>${mybatisplus.verison}version>
            dependency>
    
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plusartifactId>
                <version>${mybatisplus.verison}version>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-spring-boot-starterartifactId>
                <version>1.33.0version>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-ssoartifactId>
                <version>1.33.0version>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-dao-redis-jacksonartifactId>
                <version>1.33.0version>
            dependency>
    
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-thymeleafartifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-loadbalancerartifactId>
                <version>${spring-cloud-starter-version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-bootstrapartifactId>
                <version>${spring-cloud-starter-version}version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
                <version>3.1.1version>
            dependency>
    

    核心Sa-Token的接口

    
    /**
     * @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 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("登录失败!");
            });
    
        }
    }
    

    Http对外接口--简单几个示例

    /**
     * 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
     * 

    (注:如果不需要前后端分离架构下集成SSO,可删除此包下所有代码)

    * * @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(); } }

    Sa-Token的拦截器,权限

    /**
     * @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 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 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);
        }
    
    }
    

    自定义拦截器,校验Sa-Token登录

    /**
     * @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 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);
        }
    }
    
    

    Springboot配置-跨域-注册拦截器-配置静态资源过滤-RestTemplate负载均衡

    /**
     * @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

    演示

    认证中心登录页地址

    image

    接口管理认证中心用户

    image

    A系统USER-SERVICE

    依赖

      
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-spring-boot-starterartifactId>
                <version>${Sa-Token-version}version>
            dependency>
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-ssoartifactId>
                <version>${Sa-Token-version}version>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-dao-redis-jacksonartifactId>
                <version>${Sa-Token-version}version>
            dependency>
    
            
            <dependency>
                <groupId>cn.dev33groupId>
                <artifactId>sa-token-alone-redisartifactId>
                <version>${Sa-Token-version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-openfeignartifactId>
                <version>${spring-cloud-starter-version}version>
            dependency>
    
            
            <dependency>
                <groupId>io.github.openfeigngroupId>
                <artifactId>feign-okhttpartifactId>
                <version>${feign-okhttp-version}version>
            dependency>
    
            <dependency>
                <groupId>com.alibaba.fastjson2groupId>
                <artifactId>fastjson2artifactId>
                <version>${fastJson-version}version>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <version>${spring-boot-start-version}version>
            dependency>
            
            <dependency>
                <groupId>com.github.xiaoymingroupId>
                <artifactId>knife4j-spring-boot-starterartifactId>
                <version>${knife4j.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-bootstrapartifactId>
                <version>${spring-cloud-starter-version}version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
                <version>3.1.1version>
            dependency>
    
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
            dependency>
    

    核心 SSO-Client 端认证接口

    
    /**
     * @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 = "

    Sa-Token SSO-Client

    "
    + "

    当前会话是否登录:" + StpUtil.isLogin() + "

    "
    + "

    Login " + "Logout

    "
    ; return str; } }

    拦截器和认证中心的AuthenticationInterceptor一致

    核心全局异常处理

     @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

    被重定向到认证中心去了
    image

    登录访问后
    image

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

    选择注销A服务和B服务的其中一个;前提是没有登录多个账号

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

    image
    image

    A服务feign调用B服务

    具体接口代码:

     /**
         * 通过主键查询单条数据
         *
         * @param id 主键
         * @return 单条数据
         */
        @GetMapping("/queryOne/{id}")
        public Resp 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 selectOne(@PathVariable("id") String id);
    
    
    }
    
    
    

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

    重定向认证中心
    image

    登录A系统

    fegin调用成功B系统
    image

    A系统已经登录,B系统未登录,访问B系统接口fegin调用A

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

    image

    注销A系统

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

    image

    B系统再调用本系统的接口

    已经重定向到认证中心

    image

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

    依旧重定向到认证中心

    image

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

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

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

  • 相关阅读:
    HTML网页设计【足球科普】学生DW静态网页设计
    阿里核心人员年薪百万写的JAVA面试二十五大专题,不可能就这水平?
    pip 离线到内网安装包
    开发套件-开发换电脑
    20220620 面试复盘
    flutter使用getx实现路由跳转,页面没有执行dispose
    C#基础入门教程-基础教程
    Java(十)(网络编程,UDP,TCP)
    “星链”计划给国际通信运营商带来的挑战和机遇
    微信小程序怎么构建npm?
  • 原文地址:https://www.cnblogs.com/gtnotgod/p/17520543.html