• 【OAuth2】十五、客户端认证流程-自定义授权页面和客户端认证



    OAuth2AuthorizationEndpointFilter负责OAuth2AuthorizationCodeRequestAuthenticationToken的组装,而该token的认证默认则有OAuth2AuthorizationCodeRequestAuthenticationProvider负责。

    按照设计OAuth2AuthorizationCodeRequestAuthenticationToken支持的AuthenticationProvider可以有多个,目前只有一个。

    一、OAuth2AuthorizationCodeRequestAuthenticationProvider

    成员属性及其作用
    这个AuthenticationProvider的属性有:
    在这里插入图片描述
    在这里插入图片描述
    根据token的consent布尔值决定是否进入授权确认流程,分为两个分支:

    1、authenticateAuthorizationRequest

    只有GET请求会进入这个方法。下面的条件有一个不满足就会进入同意授权流程:

    客户端设置ClientSettings没有设置需要用户确认。
    授权请求的scope只有openid一个值,openid(OIDC标识)这个值本身不参与授权确认。
    如果用户确认信息OAuth2AuthorizationConsent存在而且其所有的scope已经被用户同意授权。
    在这里插入图片描述

    2、authenticateAuthorizationConsent

    只有POST请求会进入这个方法。

    这个逻辑是用户在授权确认页面进行了提交操作后,再次被OAuth2AuthorizationEndpointFilter拦截并交给OAuth2AuthorizationCodeRequestAuthenticationProvider认证。也就是说提交URI必须是授权URI而且是POST提交。这里是一个参考提交的报文:

    POST /oauth2/authorize HTTP/1.1
    Host: localhost:9000
    Content-Type: application/x-www-form-urlencoded
    client_id=felord&state=eX926CEl4pvrri0ETQ6b9MsYs8VHzdBD-X9RZmBO4vw%3D&scope=message.write&scope=message.read
    
    • 1
    • 2
    • 3
    • 4

    这里的state值是在authenticateAuthorizationRequest方法中生成的
    在这里插入图片描述
    在这里插入图片描述

    二、自定义OAuth2授权确认页面认证服务器设置

    代码在customconsent 分支
    在OAuth2AuthorizationEndpointFilter的流程分析中我们针对用户授权(consent)确认302重定向默认情况下由一个自动生成的页面承担。而大多数情况下我们希望能够自定义用户确认页面。以下是自定义用户确认授权URI的代码:

    String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
       			.queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
       			.queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
       			.queryParam(OAuth2ParameterNames.STATE, state)
       			.toUriString();
    this.redirectStrategy.sendRedirect(request, response, redirectUri);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从上面我们可以知道,如果自定义的consentPage是一个绝对URL直接拼上上面的参数,如果是一个相对路径,就根据当前请求生成一个URL。而且携带有以下几个参数:

    • scope
    • client_id
    • state
      能用绝对路径的URL,也就是说本身是支持前后端分离的,可能比前后不分离考虑的要多一些

    1 、自定义授权确认页面

    从前面DEMO的授权确认页可以看出这里需要以下几种信息:

    • RegisteredClient 注册OAuth2客户端信息,重定向携带了一个client_id可以拿到
    • Authentication 资源拥有者的信息
    • scope 用户请求的权限,这个应该302重定向的时候已经携带了
    根据前面对OAuth2AuthorizationEndpointFilter的分析,请求提交必须通过/oauth2/authorize(默认)进行POST表单提交。
    
    • 1
    • 自定义页面
      这里需要搞一个自定义页面,要素跟上面的差不多,我们可以改改样式,将来甚至你可以把上图中的①和②换成头像。页面使用了后台模板thymeleaf,授权服务器项目中引入依赖:
       <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
    • 1
    • 2
    • 3
    • 4

    然后写一个consent.html,放在Spring Boot 模板目录resources/templates下面。主要有客户端信息、用户名称、选择权限、登录按钮。
    控制器

    /**
     * 自定义用户确认页
     *
     * @author felord.cn
     */
    @Controller
    public class AuthorizationConsentController {
        private final RegisteredClientRepository registeredClientRepository;
        private final OAuth2AuthorizationConsentService authorizationConsentService;
        private final ScopeService scopeService;
    
        public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,
                                              OAuth2AuthorizationConsentService authorizationConsentService,
                                              ScopeService scopeService) {
            this.registeredClientRepository = registeredClientRepository;
            this.authorizationConsentService = authorizationConsentService;
            this.scopeService = scopeService;
        }
    
        /**
         * {@link OAuth2AuthorizationEndpointFilter} 会302重定向到{@code  /oauth2/consent}并携带入参
         *
         * @param principal 当前用户
         * @param model     视图模型
         * @param clientId  oauth2 client id
         * @param scope     请求授权的scope
         * @param state     state 值
         * @return 自定义授权确认页面 consent.html
         */
        @GetMapping(value = "/oauth2/consent")
        public String consent(Principal principal, Model model,
                              @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
                              @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
                              @RequestParam(OAuth2ParameterNames.STATE) String state) {
    
            //1、 通过client_id获取注册OAuth2客户端信息。
            RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
            String id = registeredClient.getId();
            // 2、查询当前用户在该客户端下有没有授权确认信息。
            OAuth2AuthorizationConsent currentAuthorizationConsent =
                    this.authorizationConsentService.findById(id, principal.getName());
            //3、从授权确认信息中提取已经授权的scope集合,保证结果不能为null。
            Set<String> authorizedScopes = currentAuthorizationConsent != null ?
                    currentAuthorizationConsent.getScopes() : Collections.emptySet();
            //4、scopesToApprove用来存放用户没有授权的scope,肯定不为空。
            Set<String> scopesToApprove = new HashSet<>();
            //5、previouslyApprovedScopes用来存放上次授权的scope,可能为空,授权确认流程是可以重入的。
            Set<String> previouslyApprovedScopes = new HashSet<>();
    
            for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
                if (authorizedScopes.contains(requestedScope)) {
                    previouslyApprovedScopes.add(requestedScope);
                } else {
                    scopesToApprove.add(requestedScope);
                }
            }
    
            Set<OAuth2Scope> scopesToApproves = scopeService.findByNames(scopesToApprove);
            Set<OAuth2Scope> previouslyApprovedScopesSet = scopeService.findByNames(previouslyApprovedScopes);
    
            String clientName = registeredClient.getClientName();
            model.addAttribute("clientId", clientId);// 6、 clientId原样放到页面上用于授权确认提交。
            model.addAttribute("clientName", clientName);// 7、clientName用来渲染页面,用来描述应用的,一般clientId都是乱字符,你也可以放个应用logo啥的。
            model.addAttribute("state", state);// state值原路返回用于提交。
            model.addAttribute("scopes", scopesToApproves);//scopesToApproves是对④的加强,带权限描述。
            model.addAttribute("previouslyApprovedScopes", previouslyApprovedScopesSet);//previouslyApprovedScopesSet是对⑤的加强,带权限描述
            model.addAttribute("principalName", principal.getName());//principalName当前授权用户的名称也要展示到页面上。
             // 返回自定义的consent视图。
            return "consent";
        }
    }
    
    • 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

    3、自定义页面要配置

    上面的自定义页面要配置到OAuth2AuthorizationEndpointConfigurer,非常简单:

    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                    new OAuth2AuthorizationServerConfigurer<>();
    //  把自定义的授权确认URI加入配置
    authorizationServerConfigurer.authorizationEndpoint(
        authorizationEndpointConfigurer->
                    authorizationEndpointConfigurer.consentPage("/oauth2/consent")
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4、前后端分离的思路

    首先需要用户(Resource Owner)登录返回token,然后才能用户授权确认。这里可行的流程为:

    • consentPage必须是是可以访问的全路径页面路由,例如https://auth.test.cn/consent.html。

    • 用户登录授权服务器,授权服务器返回一个登录认证凭证(例如token)给前端保存。

    • 前端携带token去GET请求授权/oauth2/authorize,注意按OAuth2要求带参数,除此之外,token的鉴权还要在这里搞定,需要你去扩展OAuth2AuthorizationCodeRequestAuthenticationConverter加入解析token的操作,否则会一直重定向的登录页面。

    • 接下来OAuth2AuthorizationEndpointFilter会重定向到你自己配置的consentPage并携带文章开头的三个参数,302重定向如何取取到这三个参数自己想办法?

    • 根据这三个参数写一个接口跟上面差不多只不过返回的是JSON数据。

    • 然后的流程跟前后不分离是一样的,切记提交的URI一定是授权请求URI,而且是POST提交。

    二、OAuth2客户端认证

    OAuth2客户端认证由另一个配置类OAuth2ClientAuthenticationConfigurer完成,
    OAuth2ClientAuthenticationConfigurer是OAuth2ClientAuthenticationFilter的配置类。
    在这里插入图片描述

    1、OAuth2 客户端类型

    根据RFC6749#section-2.1的描述,OAuth2基于客户端保护自己密码的能力定义了两种客户端类型:

    • confidential
      机密型客户端,这种OAuth2客户端有能力保护自己的client-secret,不会泄露出去并能安全地使用它。大部分承担了OAuth2客户端的后端应用都属于这一种类型。
    • public
      公开类型的客户端,这种客户端也有client-secret,但是它的client-secret很容易被外部发现,比如一个静态的前端应用,很容易被拿到这个值。

    2、客户端认证

    无论OAuth2客户端属于哪种类型,它都需要向授权服务器表明身份。OAuth2是一样的道理,要对客户端的凭据进行一个验证,以确保当前客户端的确是资源拥有者授权代理的客户端。OAuth2授权服务器约定了几种验证策略,都封装在ClientAuthenticationMethod中,后面会详细讲这个东西。

    3、OAuth2ClientAuthenticationConfigurer

    这个配置类就是配置客户端认证过滤器OAuth2ClientAuthenticationFilter的。它的配置项跟前面介绍的OAuth2AuthorizationEndpointConfigurer差不多,仅仅缺少了consentPage。所以配置上大致都一样。这里需要强调的是请求匹配规则策略RequestMatcher,目前和token相关的三个OAuth2端点都是需要被认证的,规则很简单:
    在这里插入图片描述
    主要拦截 以下post请求

    • /oauth2/token 申请令牌
    • /oauth2/revoke 令牌自省
    • /oauth2/introspect 令牌撤销

    具体的配置逻辑非常简单,只需要配置好的OAuth2ClientAuthenticationFilter被添加到AbstractPreAuthenticatedProcessingFilter之后。

    4、OAuth2ClientAuthenticationFilter

    认证逻辑
    在这里插入图片描述

    1. 先进行路径匹配,要符合OAuth2ClientAuthenticationConfigurer中分析的路径匹配规则。
    2. 通过转换器AuthenticationConverter将请求对象HttpServletRequest转换成待认证对象Authentication。
    3. 将待认证的Authentication交给AuthenticationManager去认证。
    4. 认证成功后的Authentication会被放到安全上下文SecurityContext中并保存到ThreadLocal中。
    5. 然后执行后面的过滤器链。
    6. 中间发生异常的话,会交给认证失败处理器AuthenticationFailureHandler处理,SecurityContext会被清除。

    4.1、AuthenticationConverter

    认证转换器接口负责将请求信息转换为待认证对象Authentication。OAuth2ClientAuthenticationFilter中默认为DelegatingAuthenticationConverter,它包含四种客户端认证策略:
    在这里插入图片描述

    • JwtClientAssertionAuthenticationConverter jtw客户端断言认证

      对应private_key_jwt和client_secret_jwt两种客户端认证模式。

    • ClientSecretBasicAuthenticationConverter 客户密码Basic Auth 证模式
      对应client_secret_basic客户端认证模式。

    • ClientSecretPostAuthenticationConverter 公开客户端认证
      对应none客户端认证模式,一般公开客户端才使用这种模式。

    • PublicClientAuthenticationConverter 客户密码post证模式
      对应client_secret_post客户端认证模式

    这四种策略的目的都是从请求中提取OAuth2ClientAuthenticationToken,它们都提取了OAuth2ClientAuthenticationToken所需要的四个参数:

    • clientId 客户端ID。
    • ClientAuthenticationMethod 客户端认证方式对象。
    • credentials 认证的凭据,可能是client_secret,也可能是JWT,也可能为null。
    • additionalParameters 请求携带的其它参数。
    • 这里ClientAuthenticationMethod和credentials的来源根据不同的方式而不同。

    4.1.1、JwtClientAssertionAuthenticationConverter

    请求中包含client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer的由该转换器处理:

    • ClientAuthenticationMethod为urn:ietf:params:oauth:client-assertion-type:jwt-bearer。
    • credentials 的值来自请求中的client_assertion参数,是个携带客户端的JWT。

    4.1.2、ClientSecretBasicAuthenticationConverter

    client_secret_basic方式中,它主要的逻辑就是从请求头Authorization中提取Basic Auth的值,并BASE64解码解析出client_id:client_secret值对:

    • ClientAuthenticationMethod为client_secret_basic。
    • credentials 的值为client_secret。

    4.1.3、PublicClientAuthenticationConverter

    公共客户端是没有客户端密钥client_secret的,因此它解析出client_id就完事了,不过它会校验一下是否携带PKCE的关键值code_verifier。

    • ClientAuthenticationMethod为none。
    • credentials 的值为null。

    4.1.4、ClientSecretPostAuthenticationConverter

    和client_secret_basic不同的是,client_secret_post会把client_id和client_secret放在请求体中,所以从请求体中提取这两个值就行了。

    • ClientAuthenticationMethod为client_secret_post。
    • credentials 的值为client_secret。
  • 相关阅读:
    (二十八)大数据实战——Flume数据采集之kafka数据生产与消费集成案例
    Golang 依赖注入设计哲学|12.6K 的依赖注入库 wire
    记录成功通过CSP接口获取Ukey的X509数字证书过程
    长安链Solidity智能合约调用原理分析
    主流数据库之索引及其例子
    C语言解题——指针解析(牛客网题目)
    计算机竞赛 深度学习+opencv+python实现昆虫识别 -图像识别 昆虫识别
    c语言中关于进制的一二疑惑
    ubuntu18.04 ros 安装 gazebo9
    qt波位图
  • 原文地址:https://blog.csdn.net/weixin_43333483/article/details/126089432