• oauth2.0通过JdbcClientDetailsService从数据库读取相应的配置


    oauth2.0通过JdbcClientDetailsService从数据库读取相应的配置

     

    在上一节我们讲述的配置是把授权码存储在redis中,把相应的请求的路径用使用in-memory存储 ,这个是放在了内存中,但是实际开发我们的数据希望是从数据表中查询的,那应该怎么做呢?

    1.回顾in-memory存储

    /**
             * inMemory是存储到内存中 并未到数据库
             */
            clients.inMemory()
                    //client Id
                    .withClient("normal-app")
                    .authorizedGrantTypes("authorization_code", "implicit")
                    .authorities("ROLE_CLIENT")
                    .scopes("read","write")
                    .resourceIds(resourceId)
                    .accessTokenValiditySeconds(accessTokenValiditySeconds)//授权码存活时间
                    .and()
                    .withClient("trusted-app")
                    .authorizedGrantTypes("client_credentials", "password")
                    .authorities("ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write")
                    .resourceIds(resourceId)
                    .accessTokenValiditySeconds(accessTokenValiditySeconds)
                    .secret("secret");

    如果使用的是这种方式,我们对应的授权码的请求路径如下:

    http://localhost:8787/oauth/authorize?client_id=normal-app&response_type=code&scope=read&redirect_uri=/resources/user

    相应的参数请对照上

    然后我们使用的是jwt的令牌方式,相应的请求路径如下:

    http://localhost:8787/oauth/token?code=r8YBUL&grant_type=authorization_code&client_id=normal-app&redirect_uri=/resources/user

    这个是放在内存中的存储方式

    2.如果我需要从数据库读取相应的字段的参数 可如下配置:

      @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
            //默认值InMemoryTokenStore对于单个服务器是完全正常的(即,在发生故障的情况下,低流量和热备份备份服务器)。大多数项目可以从这里开始,也可以在开发模式下运行,以便轻松启动没有依赖关系的服务器。
            //这JdbcTokenStore是同一件事的JDBC版本,它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库,则可以使用JDBC版本,如果只有一个,则扩展同一服务器的实例,或者如果有多个组件,则授权和资源服务器。要使用JdbcTokenStore你需要“spring-jdbc”的类路径。
    
            //这个地方指的是从jdbc查出数据来存储
            clients.withClientDetails(clientDetails());
    
    
        }

    这里可以看到我们是把之前的从内存读取的方式给去掉了,取而代之的是clientDetails()这个方法,然后我们看下这个方法:

     @Bean
        public ClientDetailsService clientDetails() {
            return new JdbcClientDetailsService(dataSource);
        }

    只需配置这个bean即可 但是我们的datasource是在yml配置文件中配置好了的,只需要注入:

    import javax.sql.DataSource;
    @Resource
    private DataSource dataSource;

    但是这里还没完,我们首先要讲下JdbcClientDetailsService是如何从数据库读取的,我们可以点击进入查看相应的源码,如下所示:

    public JdbcClientDetailsService(DataSource dataSource) {
            this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
            this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?";
            this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
            this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
            this.passwordEncoder = NoOpPasswordEncoder.getInstance();
            Assert.notNull(dataSource, "DataSource required");
            this.jdbcTemplate = new JdbcTemplate(dataSource);
            this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate));
        }

    我们可以看到,他自己是有一个默认的字段的表的,里面有相应的查询的方法,所以我们需要建立一个这样的表,sql如下:

    -- ----------------------------
    -- Table structure for oauth_client_details 将请求的路径存在数据表
    -- ----------------------------
    DROP TABLE IF EXISTS `oauth_client_details`;
    CREATE TABLE `oauth_client_details` (
      `client_id` varchar(48) NOT NULL,
      `resource_ids` varchar(256) DEFAULT NULL,
      `client_secret` varchar(256) DEFAULT NULL,
      `scope` varchar(256) DEFAULT NULL,
      `authorized_grant_types` varchar(256) DEFAULT NULL,
      `web_server_redirect_uri` varchar(256) DEFAULT NULL,
      `authorities` varchar(256) DEFAULT NULL,
      `access_token_validity` int(11) DEFAULT NULL,
      `refresh_token_validity` int(11) DEFAULT NULL,
      `additional_information` varchar(4096) DEFAULT NULL,
      `autoapprove` varchar(256) DEFAULT NULL,
      PRIMARY KEY (`client_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    这个是默认的类的表,一般用它默认的即可,我们这边就需要根据以上的字段配置相关的内容,如下:

    image.png

    这里配置好了之后我们的访问路径为:

    //步骤:客户端向认证服务器申请令牌
    http://localhost:8787/oauth/token?client_id=normal-app&grant_type=authorization_code&code=1oCj8e&redirect_uri=http://localhost:8787/resources/user
    

    然后令牌的访问路径为:

    //拿到令牌后访问资源:
    http://localhost:8787/resources/user?access_token=9d62c7b0-780e-4c6a-ad5a-56d79a089342

    记得code要换成上一步生成的code

    3.掉坑回顾:

    之前我们用的jwt来存储令牌token,后来我发现怎么也不出现jwttoken,经过多次检查发现了错误,代码如下:

    package urity.demo.oauth2;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.security.SecurityProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.ClientDetailsService;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
    import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
    import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
    import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
    import urity.demo.service.RedisAuthenticationCodeServices;
    import urity.demo.support.MyUserDetailService;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
        @Resource
        private DataSource dataSource;
    
        @Value("${resource.id:spring-boot-application}")//默认是spring-boot-application
        private String resourceId;
    
    
        @Value("${access_token.validity_period:36000}")
        private int accessTokenValiditySeconds = 36000;
    
        //认证管理 很重要 如果security版本高可能会出坑哦
        @Resource
        private AuthenticationManager authenticationManager;
    
        @Resource
        private RedisAuthenticationCodeServices redisAuthenticationCodeServices;
    
        @Resource
        private MyUserDetailService myUserDetailService;
    
    
        //security
        //定义令牌端点上的安全约束。
        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
            oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
    
        }
    
        //这个是定义授权的请求的路径的Bean
        @Bean
        public ClientDetailsService clientDetails() {
            return new JdbcClientDetailsService(dataSource);
        }
    
    
    //    @Bean // 声明TokenStore实现
    //    public JdbcTokenStore jdbcTokenStore() {
    //        return new JdbcTokenStore(dataSource);
    //    }
    
        //将ClientDetailsServiceConfigurer(从您的回调AuthorizationServerConfigurer)可以用来在内存或JDBC实现客户的细节服务来定义的。客户端的重要属性是
        //clientId:(必填)客户端ID。
        //secret:(可信客户端需要)客户机密码(如果有)。没有可不填
        //scope:客户受限的范围。如果范围未定义或为空(默认值),客户端不受范围限制。read write all
        //authorizedGrantTypes:授予客户端使用授权的类型。默认值为空。
        //authorities授予客户的授权机构(普通的Spring Security权威机构)。
        //客户端的详细信息可以通过直接访问底层商店(例如,在数据库表中JdbcClientDetailsService)或通过ClientDetailsManager接口(这两种实现ClientDetailsService也实现)来更新运行的应用程序。
        //注意:JDBC服务的架构未与库一起打包(因为在实践中可能需要使用太多变体)
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
            //默认值InMemoryTokenStore对于单个服务器是完全正常的(即,在发生故障的情况下,低流量和热备份备份服务器)。大多数项目可以从这里开始,也可以在开发模式下运行,以便轻松启动没有依赖关系的服务器。
            //这JdbcTokenStore是同一件事的JDBC版本,它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库,则可以使用JDBC版本,如果只有一个,则扩展同一服务器的实例,或者如果有多个组件,则授权和资源服务器。要使用JdbcTokenStore你需要“spring-jdbc”的类路径。
    
    
    //        /**
    //         * inMemory是存储到内存中 并未到数据库
    //         */
    //        clients.inMemory()
    //                //client Id
    //                .withClient("normal-app")
    //                .authorizedGrantTypes("authorization_code", "implicit")
    //                .authorities("ROLE_CLIENT")
    //                .scopes("read","write")
    //                .resourceIds(resourceId)
    //                .accessTokenValiditySeconds(accessTokenValiditySeconds)//授权码存活时间
    //                .and()
    //                .withClient("trusted-app")
    //                .authorizedGrantTypes("client_credentials", "password")
    //                .authorities("ROLE_TRUSTED_CLIENT")
    //                .scopes("read", "write")
    //                .resourceIds(resourceId)
    //                .accessTokenValiditySeconds(accessTokenValiditySeconds)
    //                .secret("secret");
    
            //这个地方指的是从jdbc查出数据来存储
            clients.withClientDetails(clientDetails());
    
    
        }
    
    
        //AuthorizationEndpoint可以通过以下方式配置支持的授权类型AuthorizationServerEndpointsConfigurer。默认情况下,所有授权类型均受支持,除了密码(有关如何切换它的详细信息,请参见下文)。以下属性会影响授权类型:
        //authenticationManager:通过注入密码授权被打开AuthenticationManager。
        //userDetailsService:如果您注入UserDetailsService或者全局配置(例如a GlobalAuthenticationManagerConfigurer),则刷新令牌授权将包含对用户详细信息的检查,以确保该帐户仍然活动
        //authorizationCodeServices:定义AuthorizationCodeServices授权代码授权的授权代码服务(实例)。
        //implicitGrantService:在批准期间管理状态。
        //tokenGranter:(TokenGranter完全控制授予和忽略上述其他属性)
        //在XML授予类型中包含作为子元素authorization-server。
    
        /**
         * /oauth/authorize您可以从该请求中获取所有数据,
         * 然后根据需要进行渲染,
         * 然后所有用户需要执行的操作都是回复有关批准或拒绝授权的信息。
         * 请求参数直接传递给您UserApprovalHandler,
         * AuthorizationEndpoint所以您可以随便解释数据
         *
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(this.authenticationManager);
            endpoints.accessTokenConverter(accessTokenConverter());//jwt
            //从数据库查请求的路径
    //        endpoints.tokenStore(jdbcTokenStore());
            //从jwt来数据
            endpoints.tokenStore(jwtStore());
    
    
            //授权码存储
            endpoints.authorizationCodeServices(redisAuthenticationCodeServices);
    
    
            endpoints.userDetailsService(myUserDetailService);
    
    
            // 配置TokenServices参数 注意这个是默认的uuid的存储设置 与jwt无关 如果要用jwt请注释掉
    //        DefaultTokenServices tokenServices = new DefaultTokenServices();
    
            //获取令牌的是否从jdbc查 显然 这里是的
    //        tokenServices.setTokenStore(endpoints.getTokenStore());
    
            //我们可以用jwt来存放token
            // tokenServices.setTokenStore(jwtStore());
    //        tokenServices.setSupportRefreshToken(false);
    //        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    //        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
    //        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
    //        endpoints.tokenServices(tokenServices);
    
            // @Bean
            //    RedisTokenStore redisTokenStore(){
            //        return new RedisTokenStore(redisConnectionFactory);
            //    }
            // endpoints.tokenStore(redisTokenStore());
        }
    
        //定义jwttoken的某些属性
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
    
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
                /**
                 * 重写增强token的方法
                 * 自定义返回相应的信息
                 *
                 */
    
                @Override
                public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    
                    String userName = authentication.getUserAuthentication().getName();
                    // 与登录时候放进去的UserDetail实现类一直查看link{SecurityConfiguration}
                    User user = (User) authentication.getUserAuthentication().getPrincipal();
                    /** 自定义一些token属性 ***/
                    final Map<String, Object> additionalInformation = new HashMap<>();
                    additionalInformation.put("userName", userName);
                    additionalInformation.put("roles", user.getAuthorities());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                    OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                    return enhancedToken;
                }
    
            };
            // 测试用,资源服务使用相同的字符达到一个对称加密的效果,生产时候使用RSA非对称加密方式
            accessTokenConverter.setSigningKey("123");
            return accessTokenConverter;
    
        }
    
    
        @Bean
        public TokenStore jwtStore() {
    
            TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
    
            return tokenStore;
        }
    
    
        /**
         * 创建一个默认的资源服务token
         *
         * @return
         */
        @Bean
        public ResourceServerTokenServices defaultTokenServices() {
            final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setTokenEnhancer(accessTokenConverter());
            defaultTokenServices.setTokenStore(jwtStore());
            return defaultTokenServices;
        }
    
    }
    

    请注意:DefaultTokenServices与jwttoken的配置不能都存在,否则系统只找DefaultTokenServices的配置, 也就是生成的token会一直是默认的UUID,这里我们只能两者选其一配置在代码中

    / 配置TokenServices参数 注意这个是默认的uuid的存储设置 与jwt无关 如果要用jwt请注释掉
    //        DefaultTokenServices tokenServices = new DefaultTokenServices();
    
            //获取令牌的是否从jdbc查 显然 这里是的
    //        tokenServices.setTokenStore(endpoints.getTokenStore());
    
            //我们可以用jwt来存放token
            // tokenServices.setTokenStore(jwtStore());
    //        tokenServices.setSupportRefreshToken(false);
    //        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    //        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
    //        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
    //        endpoints.tokenServices(tokenServices);

    4.小技巧:

    我们从路径授权后获得了code,就可以用code请求相应对的路径换取jwttoken,我们用postman来进行测试:

    这里我们用basic Auth的方式 只需要填写name:normal-app即可,密码可以不填

    http://localhost:8787/oauth/token?client_id=normal-app&grant_type=authorization_code&code=csTjhK&redirect_uri=http://localhost:8787/resources/user

    image.png

    我们可以看下获得的json:

    image.png

    这里生成的jwttoken中携带了相应这个是jwt的信息,这个一段字符串实际上是Header和Payload加密后拼接而成的,相应的可以查看下一篇jwt的相关解析.

    我们可以访问:https://www.jsonwebtoken.io/ 来解析下这个token里面的信息:

    image.png

    这里可以看到Header和Payload的信息,Header主要存储的是type和加密算法,这里是HS256,我们主要看Payload的信息:

    {
     "aud": [
      "resourceId"
     ],
     "user_name": "test",
     "scope": [
      "read"
     ],
     "roles": [
      {
       "authority": "ROLE_USER"
      },
      {
       "authority": "admin"
      }
     ],
     "exp": 1532662701,
     "userName": "test",
     "authorities": [
      "admin",
      "ROLE_USER"
     ],
     "jti": "066cefa0-0a7a-40da-87a0-133c5a9c64d3",
     "client_id": "normal-app",
     "iat": 1532659101
    }

    这里可以看到登录的用户名,token的生命周期等.我们就可以更清晰了解生成的jwttoken携带的信息有哪些了.

  • 相关阅读:
    【Docker】Dockerfile的基本使用
    Linux nsenter 命令简介及 切换宿主机网络为docker容器网络实践
    docker+selenium搭建分布式web自动化测试环境
    docker容器与虚拟机的区别
    docker 网络模式 和 端口映射
    Java基础之数组的定义与使用详解
    Java基础之数据类型、标识符、运算符、程序流程控制结构
    docker常用命令详解
    python查询腾讯云COS存储桶目录及文件大小
    python-自动化监控进程发钉钉报警
  • 原文地址:https://www.cnblogs.com/bigben0123/p/10283810.html
Copyright © 2020-2023  润新知