• 使用OAuth2实现授权服务


    综述

    OAuth 2发明之初是为了解决登录认证过程中的安全性问题,使用“委托”的形式使第三方应用获得数据权限及功能。OAuth 2.0协议中,使用访问令牌ACCESS_TOKEN代替传统的账号密码,提高了互联网环境下的安全性。

    OAuth 2共分为四种角色:

    • 授权服务:功能开放平台
    • 资源所有者:用户
    • 受保护资源:接口提供方
    • 客户端:第三方软件即接口调用方

    实则授权服务和受保护资源可以部署在同一服务器上,也可以部署在不同服务上,因为两种角色是属于同一开发团队。

    在微服务环境下使用Spring OAuth 2实现授权服务流程,需要分成三个模块:

    server端:授权服务端,配置OAuth 2授权服务器信息,负责生成授权码及访问令牌等

    resource端:接口提供方,负责解析授权令牌、鉴权、数据提供

    client端:第三方应用,负责调用第三方数据

    准备工作
    一、数据库脚本
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for authorities
    -- ----------------------------
    DROP TABLE IF EXISTS `authorities`;
    CREATE TABLE `authorities`  (
      `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      UNIQUE INDEX `ix_auth_username`(`username`, `authority`) USING BTREE,
      CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of authorities
    -- ----------------------------
    INSERT INTO `authorities` VALUES ('reader', 'READ');
    INSERT INTO `authorities` VALUES ('writer', 'READ,WRITE');
    
    -- ----------------------------
    -- Table structure for oauth_approvals
    -- ----------------------------
    DROP TABLE IF EXISTS `oauth_approvals`;
    CREATE TABLE `oauth_approvals`  (
      `userId` varchar(256) CHARACTER SET utf8 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `clientId` varchar(256) CHARACTER SET utf8 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `partnerKey` varchar(32) CHARACTER SET utf8 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `status` varchar(10) CHARACTER SET utf8 COLLATE utf8mb4_0900_ai_ci  NULL DEFAULT NULL,
      `expiresAt` datetime(0) NULL DEFAULT NULL,
      `lastModifiedAt` datetime(0) NULL DEFAULT NULL
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of oauth_approvals
    -- ----------------------------
    INSERT INTO `oauth_approvals` VALUES ('reader', 'userservice3', NULL, 'FOO', 'APPROVED', '2022-09-30 06:53:34', '2022-08-31 06:53:34');
    INSERT INTO `oauth_approvals` VALUES ('writer', 'userservice3', NULL, 'FOO', 'APPROVED', '2022-09-30 13:56:15', '2022-08-31 13:56:15');
    
    -- ----------------------------
    -- Table structure for oauth_client_details
    -- ----------------------------
    DROP TABLE IF EXISTS `oauth_client_details`;
    CREATE TABLE `oauth_client_details`  (
      `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `resource_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `scope` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `authorized_grant_types` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `web_server_redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `authorities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `access_token_validity` int(0) NULL DEFAULT NULL,
      `refresh_token_validity` int(0) NULL DEFAULT NULL,
      `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `autoapprove` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      PRIMARY KEY (`client_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of oauth_client_details
    -- ----------------------------
    INSERT INTO `oauth_client_details` VALUES ('userservice1', 'userservice', '1234', 'FOO', 'password,refresh_token', '', 'READ,WRITE', 7200, NULL, NULL, 'true');
    INSERT INTO `oauth_client_details` VALUES ('userservice2', 'userservice', '1234', 'FOO', 'client_credentials,refresh_token', '', 'READ,WRITE', 7200, NULL, NULL, 'true');
    INSERT INTO `oauth_client_details` VALUES ('userservice3', 'userservice', '1234', 'FOO', 'authorization_code,refresh_token', 'https://baidu.com,https://tev-competition-admin.qstcloud.net,http://localhost:5083/ui/login,http://localhost:8083/ui/login,http://localhost:5082/ui/remoteCall', 'READ,WRITE', 7200, NULL, NULL, 'false');
    
    -- ----------------------------
    -- Table structure for oauth_code
    -- ----------------------------
    DROP TABLE IF EXISTS `oauth_code`;
    CREATE TABLE `oauth_code`  (
      `code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
      `authentication` blob NULL
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of oauth_code
    -- ----------------------------
    
    -- ----------------------------
    -- Table structure for users
    -- ----------------------------
    DROP TABLE IF EXISTS `users`;
    CREATE TABLE `users`  (
      `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
      `enabled` tinyint(1) NOT NULL,
      PRIMARY KEY (`username`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of users
    -- ----------------------------
    INSERT INTO `users` VALUES ('reader', '$2a$04$C6pPJvC1v6.enW6ZZxX.luTdpSI/1gcgTVN7LhvQV6l/AfmzNU/3i', 1);
    INSERT INTO `users` VALUES ('writer', '$2a$04$M9t2oVs3/VIreBMocOujqOaB/oziWL0SnlWdt8hV4YnlhQrORA0fS', 1);
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    二、项目框架

    搭建项目父依赖,并创建三个模块:

    • cloud-oauth2-client
    • cloud-oauth2-server
    • cloud-oauth2-userservice
    授权服务器
    引入依赖
       <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-oauth2artifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-jdbcartifactId>
            dependency>
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-thymeleafartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    服务端配置

    授权服务配置需要继承自org.springframework.security.oauth2.config.annotation.web.configuration包下的AuthorizationServerConfigurerAdapter类,主要配置了用户信息来源、访问权限配置、Token配置。

    @Configuration
    //开启授权服务器
    @EnableAuthorizationServer 
    public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private DataSource dataSource;
        @Autowired
        private AuthenticationManager authenticationManager;
    
        /**
         * 我们配置了使用数据库来维护客户端信息。虽然在各种Demo中我们经常看到的是在内存中维护客户端信息,通过配置直接写死在这里。
         * 但是,对于实际的应用我们一般都会用数据库来维护这个信息,甚至还会建立一套工作流来允许客户端自己申请ClientID,实现OAuth客户端接入的审批。
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource);
        }
    
        /**
         * 这里干了两件事儿。首先,打开了验证Token的访问权限(以便之后我们演示)。
         * 然后,允许ClientSecret明文方式保存,并且可以通过表单提交(而不仅仅是Basic Auth方式提交),之后会演示到这个。
         * @param security
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.checkTokenAccess("permitAll()")
                    .allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance());
        }
    
        /**
         * 干了以下4件事儿:
         * 1. 配置我们的令牌存放方式为JWT方式,而不是内存、数据库或Redis方式。
         * JWT是Json Web Token的缩写,也就是使用JSON数据格式包装的令牌,由.号把整个JWT分隔为头、数据体、签名三部分。
         * JWT保存Token虽然易于使用但是不是那么安全,一般用于内部,且需要走HTTPS并配置比较短的失效时间。
         * 2. 配置JWT Token的非对称加密来进行签名
         * 3. 配置一个自定义的Token增强器,把更多信息放入Token中
         * 4. 配置使用JDBC数据库方式来保存用户的授权批准记录
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(
                    Arrays.asList(tokenEnhancer(), jwtTokenEnhancer()));
    
            endpoints.approvalStore(approvalStore())
                    .authorizationCodeServices(authorizationCodeServices())
                    .tokenStore(tokenStore())
                    .tokenEnhancer(tokenEnhancerChain)
                    .authenticationManager(authenticationManager);
        }
    
        /**
         * 使用JDBC数据库方式来保存授权码
         * @return
         */
        @Bean
        public AuthorizationCodeServices authorizationCodeServices() {
            return new JdbcAuthorizationCodeServices(dataSource);
        }
    
        /**
         * 使用JWT存储
         * @return
         */
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtTokenEnhancer());
        }
    
        /**
         * 使用JDBC数据库方式来保存用户的授权批准记录
         * @return
         */
        @Bean
        public JdbcApprovalStore approvalStore() {
            return new JdbcApprovalStore(dataSource);
        }
    
        /**
         * 自定义的Token增强器,把更多信息放入Token中
         * @return
         */
        @Bean
        public TokenEnhancer tokenEnhancer() {
            return new CustomTokenEnhancer();
        }
    
        /**
         * 配置JWT使用非对称加密方式来验证
         * @return
         */
        @Bean
        protected JwtAccessTokenConverter jwtTokenEnhancer() {
            KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
            return converter;
        }
    
        /**
         * 配置登录页面的视图信息(其实可以独立一个配置类,这样会更规范)
         */
        @Configuration
        static class MvcConfig implements WebMvcConfigurer {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("login").setViewName("login");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    自定义Token增强器

    主要用于将自定义的更多用户信息放入Token中。

    public class CustomTokenEnhancer implements TokenEnhancer {
    
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            Authentication userAuthentication = authentication.getUserAuthentication();
            if (userAuthentication != null) {
                Object principal = authentication.getUserAuthentication().getPrincipal();
                //把用户标识嵌入JWT Token中去(Key是userDetails)
                Map<String, Object> additionalInfo = new HashMap<>();
                additionalInfo.put("userDetails", principal);
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            }
            return accessToken;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    安全配置
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private DataSource dataSource;
    
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /**
         * 配置用户账户的认证方式。显然,我们把用户存在了数据库中希望配置JDBC的方式。
         * 此外,我们还配置了使用BCryptPasswordEncoder哈希来保存用户的密码(生产环境中,用户密码肯定不能是明文保存的)
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.jdbcAuthentication()
                    .dataSource(dataSource)
                    .passwordEncoder(new BCryptPasswordEncoder());
        }
    
        /**
         * 开放/login和/oauth/authorize两个路径的匿名访问。前者用于登录,后者用于换授权码,这两个端点访问的时机都在登录之前。
         * 设置/login使用表单验证进行登录。
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/login", "/oauth/authorize")
                    .permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin().loginPage("/login");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    登录页html放在templates目录下:

    <body class="uk-height-1-1">
    
    <div class="uk-vertical-align uk-text-center uk-height-1-1">
        <div class="uk-vertical-align-middle" style="width: 250px;">
            <h1>Login Formh1>
    
            <p class="uk-text-danger" th:if="${param.error}">
                用户名或密码错误...
            p>
    
            <form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/login}">
                <div class="uk-form-row">
                    <input class="uk-width-1-1 uk-form-large" type="text" placeholder="Username" name="username"
                           value="reader"/>
                div>
                <div class="uk-form-row">
                    <input class="uk-width-1-1 uk-form-large" type="password" placeholder="Password" name="password"
                           value="reader"/>
                div>
                <div class="uk-form-row">
                    <button class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Loginbutton>
                div>
            form>
    
        div>
    div>
    body>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    受保护资源服务器
    引入依赖
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-oauth2artifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    受保护资源配置

    创建核心资源服务器配置类。

    • 硬编码了资源服务器的 ID 为 userservice
    • 现在我们使用的是不落数据库的JWT方式 + 非对称加密,需要通过本地公钥进行验证,因此在这里我们配置了公钥的路径。

    公钥和密钥有多种配置方式,可以通过读取文件、读取配置文件等方式,只需要保证公钥和私钥配对即可,在本demo中可以直接复用public.cert

    @Configuration
    @EnableResourceServer //启用资源服务器
    @EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法注解方式来进行权限控制
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
        /**
         * 声明了资源服务器的ID是userservice,声明了资源服务器的TokenStore是JWT
         * @param resources
         * @throws Exception
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.resourceId("userservice").tokenStore(tokenStore());
        }
    
        /**
         * 配置TokenStore
         *
         * @return
         */
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    
        /**
         * 配置公钥
         * @return
         */
        @Bean
        protected JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            Resource resource = new ClassPathResource("public.cert");
            String publicKey = null;
            try {
                publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
            } catch (IOException e) {
                e.printStackTrace();
            }
            converter.setVerifierKey(publicKey);
            return converter;
        }
    
        /**
         * 配置了除了/user路径之外的请求可以匿名访问
         * @param http
         * @throws Exception
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .anyRequest().permitAll();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    启动项目Server、Resource。

    测试

    各大开放平台都是推荐使用授权码许可流程,无论是网页版的 Web 应用程序,还是移动应用程序。本次仅演示授权码模式登录。

    第一步,打开浏览器访问地址:

    http://localhost:8080/oauth/authorize?response_type=code&client_id=userservice3&redirect_uri=https://baidu.com
    
    • 1

    注意,客户端跳转地址需要和数据库中配置的一致(百度的 URL https://baidu.com我们之前已经在数据库中有配置了)。访问后页面会直接跳转到登录界面,我们使用用户名“reader”、密码“reader”来登录,点击批准,可以发现页面重定向到百度页,并且地址栏最后出现了授权码。

    第二步,使用授权码获取访问令牌ACCESS_TOKEN

    http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=userservice3&client_secret=1234&code=XKkHGY&redirect_uri=https://baidu.com

    redirect_uri后的参数需要在数据库oauth_client_details表中配置,不同数值间用英文逗号分隔

    Gitee地址:
    https://gitee.com/FirstMrRight/oauth2.git

  • 相关阅读:
    你不得不知的MYSQL优化——索引下推
    u盘格式化后数据能恢复吗?
    LeetCode50天刷题计划(Day 25— 旋转图像(11.20-12.30)
    FPGA的斐波那契数列Fibonacci设计verilog,代码和视频
    nodejs使用es-batis
    【资源分享】2022年第五届土木,建筑与环境工程国际会议(ICCAEE 2022)
    html实现飞机小游戏(源码)
    时间序列-ARIMA
    医院信息系统源码 HIS源码
    【QT+JS】QT和JS 中的正则表达式 、QT跑JS语言
  • 原文地址:https://blog.csdn.net/weixin_42313773/article/details/126678670