2023-09-19  阅读(1)
原文作者:墨家巨子@俏如来 原文地址: https://blog.csdn.net/u014494148/article/details/109260840

前言

上一文章我们准备了微服务授权的环境,并对AuthServer实现了简单的认证流程,这里是接上一篇文章继续对AuthServer认证服务做Oauth2配置

1.概述Oauth2授权服务配置

我们只需要导入如下依赖即可集成JWT和Oauth2了

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

Oauth2提供了AuthorizationServerConfigurerAdapter适配器类来作为认证授权服务的配置,其中有三个方法源码如下:

    public class AuthorizationServerConfigurerAdapter  {
        //客户端详情:配置客户端请求的参数
    	public void configure(ClientDetailsServiceConfigurer clients)...	
    	//授权服务断点:配置授权码和令牌的管理/存储方式
    	public void configure(AuthorizationServerEndpointsConfigurer endpoints)...
        //授权服务安全配置:配置哪些路径放行(检查token的路径要放行)
    	public void configure(AuthorizationServerSecurityConfigurer security) ...
    }

注意,上面是源码,别乱拷贝 ,三个配置作用分别如下:

  • ClientDetailsServiceConfigurer :用来配置客户端详情服务:如配置客户端id(client_id)资源id、客户端密钥(secrect)、授权方式、scope等,可以基于内存或jdbc。(可以理解为是对浏览器向授权服务器获取授权码或令牌时需要提交的参数配置),如果你做过三方登录应该就能理解这些参数,其实就是对客户端的参数配置,在客户端获取授权码或者获取Token的URL请求中就需要带上这些客户端参数,比如:

202309192313110531.png

  • AuthorizationServerEndpointsConfigurer:配置令牌的访问端点url和令牌服务,如配置如何管理授权码(内存或jdbc),如何管理令牌(存储方式,有效时间等等)
  • AuthorizationServerSecurityConfigurer: 用来配置令牌端点的安全约束,如配置对获取授权码,检查token等某些路径进行放行

下面我整理了一个配置关系图:

202309192313117212.png

2.授权服务配置实战

2.1.客户端详情配置

第一个配置主要是通过ClientDetailsServiceConfigurer配置客户端详情,定义配置类,继承AuthorizationServerConfigurerAdapter ,复写第一个configure方法,配置类上贴注解@EnableAuthorizationServer开启授权服务配置

    //授权服务器配置
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        //密碼編碼器
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        //第一步:客户端详情配置============================
        //客户端详细信息服务配置:客戶端id,客戶端秘钥,授权方式等
        @Autowired
        private DataSource dataSource ;
        
    	//定义针对于JDBC的客户端配置详情服务
        @Bean
        public ClientDetailsService clientDetailsService(){
            JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
            //设置密码编码
            jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
            return jdbcClientDetailsService;
    	}
    	//基于jdbc的客户端详情配置方案
    	@Override
    	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    	    //配置成基于jdbc的客户端详情方案
    	    clients.withClientDetails(clientDetailsService());
    	}

JdbcClientDetailsService默认会去找数据库中的 名字为 oauth_client_details 表中的数据作为客户端详情的配置,见 JdbcClientDetailsService类的源代码,所以我们需要在数据库执行以下sql创建表:并填充好数据,如

    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;
    
    INSERT INTO `oauth_client_details` VALUES ('webapp', 'res1', '$2a$10$GPHeNpkKAUJDJcC2XafjkuTyh/P01s2ZoIu0/IsPs6WcXtnv8LNgm', 'all', 'client_credentials,password,authorization_code,refresh_token', 'http://www.baidu.com', null, '7200', '72000', null, 'true');
    INSERT INTO `oauth_client_details` VALUES ('webapp2', 'res2', '$2a$10$GPHeNpkKAUJDJcC2XafjkuTyh/P01s2ZoIu0/IsPs6WcXtnv8LNgm', 'all', 'client_credentials,password,authorization_code,refresh_token', 'http://www.baidu.com', '', '7200', '72000', '', 'true');

秘钥的明文是“secret” , 数据如下:

202309192313125103.png
因为在jdbcClientDetailsService设置了setPasswordEncoder(passwordEncoder);,所以数据库中的client_secret需要是密文加密的,加密如:BCrypt.hashpw(“secret”, BCrypt.gensalt())
oauth_client_details解释:

  • client_id :主键,必须唯一,不能为空
    用于唯一标识每一个客户端(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,实际应用也有叫app_key 案例:OaH1heR2E4eGnBr87Br8FHaUFrA2Q0kE8HqZgpdg8Sw
  • resource_ids:资源ID,不能为空,用逗号分隔
    客户端能访问的资源id集合,注册客户端时,根据实际需要可选择资源id,也可以根据不同的额注册流程,赋予对应的额资源id,案例:order-resource,pay-resource
  • client_secret:客户端秘钥,不能为空
    注册填写或者服务端自动生成,实际应用也有叫app_secret, 必须要有前缀代表加密方式,案例:{bcrypt}gY/Hauph1tqvVWiH4atxteSH8sRX03IDXRIQi03DVTFGzKfz8ZtGi
  • scope: 授权范围,不可为空
    指定client的权限范围,比如读写权限,比如移动端还是web端权限,案例:read,write / web,mobile
  • authorized_grant_types:授权方式,不可为空
    可选值 授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔,案例:implicit",“client_credentials”,“password”, “authorization_code”, “refresh_token”
  • web_server_redirect_uri:客户端重定向uri
    客户端重定向uri,authorization_code和implicit需要该值进行校验,注册时填写,案例:httt://baidu.com
  • authorities :权限,可为空
    指定用户的权限范围,如果授权的过程需要用户登陆,该字段不生效,implicit和client_credentials需要,案例:ROLE_ADMIN,ROLE_USER
  • access_token_validity:Token有效期,可空
    设置access_token的有效时间(秒),默认(606012,12小时),案例:3600
  • refresh_token_validity :刷新Token有效时期,可空
    设置refresh_token有效期(秒),默认(606024*30, 30填),案例:7200
  • additional_information: 附加数据,可空
    附加数据,值必须是json格式 ,案例:{“key”, “value”}
  • autoapprove:是否默认授权
    默认false,适用于authorization_code模式,设置用户是否自动approval操作,设置true跳过用户确认授权操作页面,直接跳到redirect_uri,案例:false
2.2.授权服务配置

第二个配置主要是通过AuthorizationServerEndpointsConfigurer 配置授权码和令牌相关的服务 ,在上面的配置类基础上增加配置内容

    //第二步:令牌服务配置=============================================
    
    //客户端详情service
    @Autowired
    private ClientDetailsService clientDetailsService;
    
    //认证管理器,在WebSecurityConfig中配置
    @Autowired
    private AuthenticationManager authenticationManager;
    
    	//令牌存储 , 基于JWT
        @Bean
        public TokenStore tokenStore(){
            //return new InMemoryTokenStore();
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
        //JWT令牌校验工具
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
            //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
            jwtAccessTokenConverter.setSigningKey("123");
            //jwtAccessTokenConverter.setKeyPair(keyPair());
            return jwtAccessTokenConverter;
        }
    
    	//配置令牌服务
        @Bean
        public AuthorizationServerTokenServices tokenService(){
            //创建默认的令牌服务
            DefaultTokenServices services = new DefaultTokenServices();
            //指定客户端详情配置
            services.setClientDetailsService(clientDetailsService());
            //支持产生刷新token
            services.setSupportRefreshToken(true);
            //token存储方式
            services.setTokenStore(tokenStore());
            
            //设置token增强 - 设置token转换器
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
    
            services.setTokenEnhancer(tokenEnhancerChain);  //jwtAccessTokenConverter()
            //token有效时间
            services.setAccessTokenValiditySeconds(72000);
            //刷新令牌默认有效时间
            services.setRefreshTokenValiditySeconds(72000);
            return services;
        }
    
    //授权码服务
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        //基于内存存储的的授权码服务
        //return new InMemoryAuthorizationCodeServices();
        //基于内存存储的的授权码服务
        return new JdbcAuthorizationCodeServices(dataSource);
    }
    
    //配置令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
            //密码授权模式需要
            .authenticationManager(authenticationManager)
            //授权码模式服务
            .authorizationCodeServices(authorizationCodeServices())
            //配置令牌管理服务
            .tokenServices(tokenService())
            //允许post方式请求
            .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

这里配置了这么几个内容:

  • AuthenticationManager
    认证管理器“password”模式会用到认证管理器,它是在上一章节的Security配置中定义的
  • TokenStore : token存储方式
    该接口常用的实现有:InMemoryTokenStore基于存储的token存储方案,JdbcTokenStore基于数据库的token存储方案,JwtToeknStore基于JWT的存储方案,RedisTokenStore基于Redis的存储方案,我上面的案例采用的是JWT的方式来实现
  • JwtAccessTokenConverter : JWT令牌转换器,JWT编码的令牌值和OAuth身份验证信息(双向)之间转换器 ,SigningKey设置的是秘钥
  • AuthorizationCodeServices
    授权码服务,提供了InMemoryAuthorizationCodeServices基于内存和基于数据库 JdbcAuthorizationCodeServices的授权码存储方案,如果是基于JDBC那么我们需要提供存储授权码的表“oauth_code”
  • AuthorizationServerTokenServices
    该接口用来配置授权服务器令牌,如配置是否支持Token,Token的存储方式(内 存,jdbc,),token加密,token过期等

由于授权码使用的是JdbcAuthorizationCodeServices基于数据库的存储方案,所以要导入授权码SQL脚本,JdbcAuthorizationCodeServices默认读取数据库中的 oauth_code 表中的数据作为授权码的存储表,所以执行以下sql创建表

    DROP TABLE IF EXISTS `oauth_code`;
    CREATE TABLE `oauth_code` (
    `code` varchar(255) DEFAULT NULL COMMENT '授权码(未加密)',
    `authentication` varbinary(5000) DEFAULT NULL COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.3.令牌端点安全配置

AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全策略,修改AuthorizationServerConfig中配置如下:

    //第三步:端点安全约束======================================
    //配置令牌安全约束
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
            //对应/oauth/token_key 公开,获取公钥需要访问该端点
            .tokenKeyAccess("permitAll()")
            //对应/oauth/check_token ,路径公开,校验Token需要请求该端点
            .checkTokenAccess("permitAll()")
            //允许客户端进行表单身份验证,使用表单认证申请令牌
            .allowFormAuthenticationForClients();
    }

到这里资源服务暂时配置完成

2.4.授权服务测试

1.登录认证中心

202309192313130714.png
2.通过浏览器获取授权码

GET访问:http://localhost:3000/oauth/authorize?client_id=webapp&response_type=code&redirect_uri=http://www.baidu.com ,操作如下:

202309192313135165.png
得到授权码

202309192313139896.png
3.获取令牌

使用Postmain发送Post请求访问Url: http://localhost:3000/oauth/token
参数:client_id=webapp&client_secret=secret&grant_type=authorization_code&code=授权码&redirect_uri=http://www.baidu.com ,操作如下:

202309192313143887.png
可以看到这里已经获取到令牌,access_token是令牌,refresh_token是刷新令牌的,expires_in是过期时间,scope是授权范围

检查令牌

检查token,访问认证服务器http://localhost:3000/oauth/check_token,Post请求如下:

202309192313150708.png
解释一下:

  • aud:里面包含的是Token可访问的资源ID
  • scope: 里面包含的是Token可访问的授权范围,这是因为我们在获取Token的时候指定了client_id =webapp,它对应了oauth_client_details表中的客户端详情配置,所以Token拥有的资源ID和授权范围都是根据client_id 加载的对应的客户端详情配置
  • user_name : Token对应的用户,也是因为在获取Token的时候使用了账号和密码进行认证。
  • authorities : Token 对应的用户的权限列表,是在获取Token的时候,Security会执行认证流程,根据用户名调用UserDeatilsService加载的权限
  • client_id : 对应的客户端ID

注意:Token的 scope授权范围,aud资源ID,以及authorities 权限列表三者决定了这个Token是否能够去访问某个资源服务器

4.密码授权模式测试

密码模式不需要获取授权码,在授权服务中我们配置了"password"密码模式,"authorization_code"授权码模式两种方式,接下来是测试“password”模式获取,将grant_type修改为“password” 添加username和password两个参数,去掉code参数

202309192313156039.png
5.刷新token

带着之前获取Token返回的刷新Token的值访问如下地址即可刷新:
http://localhost:3000/oauth/token?grant_type=refresh_token&refresh_token=刷新Token值&client_id=webapp&client_secret=secret

3.总结授权服务器配置

到这里授权服务配置告一段落,总结一下AuthServer主要做了如下事情:

  1. 集成Security和MyBatis能够实现用户的认证
  2. 集成Oauth2做授权服务配置,主要做了三个配置
  • 基于JDBC的客户端详情配置,加载oauth_client_details表中的配置
  • 基于JDBC的授权码配置和基于JWT的Token配置
  • 最后做了授权服务端点的安全配置

那么当请求到达AuthServer会发生什么事情呢?这里以授权码模式为例

  1. 首先我们要执行登录操作,Security会调用AuthenticationManager执行认证,调用UserDeatilsService加载数据库中的用户信息和权限列表保持到上下文对象中
  2. 然后我们发起一个获取授权码的请求/oauth/authorize?client_id=webapp&response_type=code&redirect_uri=http://www.baidu.com,请求到达认证服务器
  3. 认证服务器收到请求,为请求生成授权码,并返回给请求中指定的重定向地址
  4. 我们得到授权码,带着授权码去获取Token,请求/oauth/token?client_id=webapp&client_secret=secret&grant_type=authorization_code&code=授权码请求到达认证服务器
  5. 认证服务器收到请求,验证授权码后,根据client_id从oauth_client_details表加载客户端配置进行参数校验,然后认证服务器创建令牌,颁发给客户端
  6. 我们通过/oauth/check_token检查Token时,就可以看到Token对应的授权范围,资源ID,权限列表等信息,认证服务器在生成Token的时候,就已经把这些信息关联好了。

下一章节我们将对资源服务做配置,完成整个授权流程


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文