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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: oauth2.0通过JdbcClientDetailsService从数据库读取相应的配置在上一节我们讲述的配置是把授权码存储在redis中,把相应的请求的路径用使用in-memory存储 ,这个是放在了内存中,但是实际开发我们的数据希望是从数据表中查询的,那应该怎么做呢?1.

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;

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

img_9ee213b13ecf480cd311bdc8178abe21.png
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
img_7c6f59dc30eed902945d205d518df7cc.png
image.png

我们可以看下获得的json:

img_9f154c70ef29893151486af60e863935.png
image.png

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

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

img_b09bfea6aeff886796c967b977ed330e.png
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携带的信息有哪些了.

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 监控 安全
数据库多实例的部署与配置方法
【10月更文挑战第23天】数据库多实例的部署和配置需要综合考虑多个因素,包括硬件资源、软件设置、性能优化、安全保障等。通过合理的部署和配置,可以充分发挥多实例的优势,提高数据库系统的运行效率和可靠性。在实际操作中,要不断总结经验,根据实际情况进行调整和优化,以适应不断变化的业务需求。
|
1月前
|
消息中间件 资源调度 关系型数据库
如何在Flink on YARN环境中配置Debezium CDC 3.0,以实现实时捕获数据库变更事件并将其传输到Flink进行处理
本文介绍了如何在Flink on YARN环境中配置Debezium CDC 3.0,以实现实时捕获数据库变更事件并将其传输到Flink进行处理。主要内容包括安装Debezium、配置Kafka Connect、创建Flink任务以及启动任务的具体步骤,为构建实时数据管道提供了详细指导。
58 9
|
1月前
|
安全 Nacos 数据库
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改。本文详细探讨了这一问题的原因及解决方案,包括限制公网访问、使用HTTPS、强化数据库安全、启用访问控制、监控和审计等步骤,帮助开发者确保服务的安全运行。
40 3
|
1月前
|
PHP 数据库 数据安全/隐私保护
布谷直播源码部署服务器关于数据库配置的详细说明
布谷直播系统源码搭建部署时数据库配置明细!
|
1月前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
50 2
|
1月前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
100 2
|
2月前
|
前端开发 Java 数据库连接
javamvc配置,增删改查,文件上传下载。
【10月更文挑战第4天】javamvc配置,增删改查,文件上传下载。
39 1
|
2月前
|
关系型数据库 MySQL Java
Django学习二:配置mysql,创建model实例,自动创建数据库表,对mysql数据库表已经创建好的进行直接操作和实验。
这篇文章是关于如何使用Django框架配置MySQL数据库,创建模型实例,并自动或手动创建数据库表,以及对这些表进行操作的详细教程。
79 0
Django学习二:配置mysql,创建model实例,自动创建数据库表,对mysql数据库表已经创建好的进行直接操作和实验。
|
2月前
|
应用服务中间件 数据库连接 网络安全
网站连接数据库配置错误
网站连接数据库配置错误
|
1月前
|
SQL 关系型数据库 数据库连接
"Nacos 2.1.0版本数据库配置写入难题破解攻略:一步步教你排查连接、权限和配置问题,重启服务轻松解决!"
【10月更文挑战第23天】在使用Nacos 2.1.0版本时,可能会遇到无法将配置信息写入数据库的问题。本文将引导你逐步解决这一问题,包括检查数据库连接、用户权限、Nacos配置文件,并提供示例代码和详细步骤。通过这些方法,你可以有效解决配置写入失败的问题。
60 0