Spring OAuth2 開(kāi)發(fā)指南(二):OAuth2 密碼模式開(kāi)發(fā)實(shí)例

Spring OAuth2 開(kāi)發(fā)指南(二):OAuth2 密碼模式開(kāi)發(fā)實(shí)例

[TOC]

一、開(kāi)篇

本篇是《Spring OAuth2 開(kāi)發(fā)指南》系列文章的第二篇,通過(guò)代碼實(shí)例詳細(xì)介紹 OAuth2 密碼模式的開(kāi)發(fā)細(xì)節(jié)。網(wǎng)絡(luò)上關(guān)于 OAuth2 開(kāi)發(fā)的代碼示范十分多而且雜亂,基本上都是官方手冊(cè)的摘錄搬運(yùn),或者過(guò)多地受制于框架本身如 Spring Security,約束太多,缺乏系統(tǒng)性,容易造成同學(xué)們?cè)评镬F里,以至于生搬硬套。

本人主張?jiān)陂_(kāi)發(fā)落地過(guò)程中,既不能完全自己造輪子,也不應(yīng)完全依賴(lài)輪子,應(yīng)該從本質(zhì)出發(fā),在理清技術(shù)原理和細(xì)節(jié)的條件下,選擇適合的方法。從這個(gè)原則出發(fā),本文將根據(jù)“密碼模式的典型架構(gòu)層次和主要流程”(見(jiàn)《Spring OAuth2 開(kāi)發(fā)指南(一)》)中描述的流程節(jié)點(diǎn),展示其代碼實(shí)現(xiàn)。另外,文章的要點(diǎn)在于后半部分,提出了資源服務(wù)器端鑒權(quán)/權(quán)限控制,和授權(quán)服務(wù)器端鑒權(quán)/權(quán)限控制兩種實(shí)現(xiàn)方法。

需要注意的是 password 模式由于 OAuth2.1 不推薦使用所以只提供舊的組件代碼版本,具體請(qǐng)參見(jiàn) https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-02

二、 演示案例

我們繼續(xù)用相冊(cè)預(yù)覽系統(tǒng)(PAPS,Photo Album Preview System)作為演示案例。

PAPS 是一個(gè)社交平臺(tái)的子系統(tǒng),與 IBCS 類(lèi)似,采用 RESTful API 對(duì)外交互,主要功能是允許用戶(hù)預(yù)覽自己的相冊(cè),以下是 PAPS 演示項(xiàng)目的必要服務(wù):

服務(wù)名 類(lèi)別 描述 技術(shù)選型
photo-service 內(nèi)部服務(wù) 資源服務(wù)器角色,相冊(cè)預(yù)覽服務(wù) Spring Boot 開(kāi)發(fā)的 RESTful 服務(wù)
idp 內(nèi)部服務(wù) 授權(quán)服務(wù)器角色,具體指負(fù)責(zé)認(rèn)證、授權(quán)和鑒權(quán) Spring Boot 開(kāi)發(fā)
demo-h5 外部應(yīng)用 demo 應(yīng)用的前端 使用 Postman 代替

為此,我們將搭建兩個(gè)工程項(xiàng)目:photo-service 和 idp,客戶(hù)端用 Postman 代替。

三、 工程結(jié)構(gòu)

接下來(lái)演示兩個(gè)工程項(xiàng)目的框架代碼,這部分代碼包含工程的框架結(jié)構(gòu)、Spring Security 和 OAuth2 的基礎(chǔ)配置,盡量采用最精簡(jiǎn)的方式書(shū)寫(xiě)。其他項(xiàng)目可以 copy 這部分代碼作為基礎(chǔ)模板使用。

photo-service 相冊(cè)服務(wù)

  • 基礎(chǔ)工程結(jié)構(gòu)
src/main
    java
        com.example.demophoto
            config
                oauth2
                    CheckTokenAuthentication.java
                    CheckTokenFilter.java
                    CustomPermissionEvaluator.java
                    CustomRemoteTokenServices.java
                    ResourceServerConfigurer.java
            service
                PermisionEvaluatingService.java
            web
                PhotoController.java
            DemoPhotoApplication.java
    resources
        applicaton.yaml
  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>oauth2-demo-1a-photo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2-demo-1a-photo-service</name>
    <description>oauth2-demo-1a-photo-service</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • applicaton.yaml
server:
  port: 8010

security:
  oauth2:
    client:
      clientId: client2
      clientSecret: client2p
    resource:
      tokenInfoUri: http://127.0.0.1:8000/oauth/check_token
  • ResourceServerConfigurer.java
package com.example.demophoto.config.oauth2;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @param resources
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("demo-1");
    }

    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated();
    }
}

idp 授權(quán)服務(wù)

  • 基礎(chǔ)工程結(jié)構(gòu)
src/main
    java
        com.example.demoidp
            config
                oauth2
                    AuthorizationServerConfigurer.java
                    CheckTokenInterceptor.java
                    WebSecurityConfig.java
            service
                業(yè)務(wù)邏輯,如鑒權(quán)邏輯
            DemoIdpApplication.java
    resources
        applicaton.yaml
  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>oauth2-demo-1a-idp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2-demo-1a-idp</name>
    <description>oauth2-demo-1a-idp</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.8.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • applicaton.yaml
server:
  port: 8000
  • AuthorizationServerConfigurer.java
package com.example.demoidp.config.oauth2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
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;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    private AuthenticationManager authenticationManager;

    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @param authenticationManager
     */
    @Autowired
    public AuthorizationServerConfigurer(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * 配置密碼加密方法
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @param endpoints
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager);
    }

    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @param security
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                // /oauth/check_token 請(qǐng)求放行
                .checkTokenAccess("permitAll()")
                .passwordEncoder(passwordEncoder());
    }
}
  • WebSecurityConfig.java
package com.example.demoidp.config.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * spring-security-oauth2 組件一般性配置
     *
     * @return AuthenticationManager
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

四、 代碼實(shí)現(xiàn)

OAuth2 密碼模式典型架構(gòu)層次

如圖所示,是密碼模式的最精簡(jiǎn)架構(gòu)層次和主要流程。下面我們逐步實(shí)現(xiàn)該流程:

一)第一階段:認(rèn)證授權(quán)階段

1)用戶(hù)代理(demo-h5)將用戶(hù)輸入的用戶(hù)名和密碼,發(fā)送給客戶(hù)端(demo-service)

此步驟我們使用 Postman 執(zhí)行,這里不展開(kāi)介紹。

2)客戶(hù)端(demo-service)將用戶(hù)輸入的用戶(hù)名和密碼,連同 client_id + client_secret (由 idp 分配)一起發(fā)送到 idp 以請(qǐng)求令牌,如果 idp 約定了 scope 則還需要帶上 scope 參數(shù)

此步驟我們使用 Postman 執(zhí)行,這里不展開(kāi)介紹。需要注意的是,Postman 在這里仍然是一個(gè) client 角色,client_id 代表的是它自己。請(qǐng)求的 URL 為:

POST http://127.0.0.1:8000/oauth/token
3)idp 首先驗(yàn)證 client_id + client_secret 的合法性,再檢查 scope 是否無(wú)誤,最后驗(yàn)證用戶(hù)名和密碼是否正確,正確則生成 token。這一步也叫“認(rèn)證”

為了實(shí)現(xiàn)這個(gè)步驟,我們?cè)?idp 工程的 AuthorizationServerConfigurer 類(lèi)中加入以下代碼:

  • 首先是 client_id + client_secret + scope 的校驗(yàn)
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {

    ...

    /**
     * 3. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 3 步:
     *    idp 首先驗(yàn)證 client_id + client_secret 的合法性,再檢查 scope 是否無(wú)誤
     *
     *    PS: 這里為演示方便,就地創(chuàng)建了賬號(hào),生產(chǎn)環(huán)境應(yīng)自行替換成數(shù)據(jù)庫(kù)查詢(xún)等方式
     */
    private class MockJDBCClientDetailsService implements ClientDetailsService {
        @Override
        public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
            /**
             * GrantedAuthority 與 hasAuthority() 關(guān)聯(lián)
             */
            Set<GrantedAuthority> authorities = new HashSet<>();
            authorities.add(new SimpleGrantedAuthority("READ"));
            authorities.add(new SimpleGrantedAuthority("WRITE"));
    
            BaseClientDetails details1 = new BaseClientDetails();
            details1.setClientId("client1");
            details1.setClientSecret(passwordEncoder().encode("client1p"));
            details1.setAuthorizedGrantTypes(Arrays.asList("password"));
            details1.setScope(Arrays.asList("resource:write", "resource:read"));
            details1.setResourceIds(Arrays.asList("demo-1"));
            details1.setAuthorities(authorities);
    
            BaseClientDetails details2 = new BaseClientDetails();
            details2.setClientId("client2");
            details2.setClientSecret(passwordEncoder().encode("client2p"));
            details2.setAuthorizedGrantTypes(Arrays.asList("client_credentials"));
            details2.setScope(Arrays.asList("resource:write", "resource:read"));
            details2.setResourceIds(Arrays.asList("demo-1"));
            details2.setAuthorities(authorities);
    
            BaseClientDetails details3 = new BaseClientDetails();
            details3.setClientId("client3");
            details3.setClientSecret(passwordEncoder().encode("client3p"));
            details3.setAuthorizedGrantTypes(Arrays.asList("password"));
            details3.setScope(Arrays.asList("resource:write", "resource:read"));
            details3.setResourceIds(Arrays.asList("demo-1"));
            details3.setAuthorities(authorities);
    
            Map<String, ClientDetails> clients = new HashMap<>();
            clients.put("client1", details1);
            clients.put("client2", details2);
            clients.put("client3", details3);
    
            if (!clients.containsKey(clientId)) {
                throw new ClientRegistrationException("Client not found");
            }
    
            return clients.get(clientId);
        }
    }
    
    /**
     * spring-security-oauth2 組件一般性配置
     * 配置自定義 ClientDetails
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(new MockJDBCClientDetailsService());
    }
    
    ...
}
  • 然后是用戶(hù)名和密碼的校驗(yàn)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 3. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 3 步:
     *    驗(yàn)證用戶(hù)名和密碼是否正確,正確則生成 token
     *
     *    PS: 這里為演示方便,就地創(chuàng)建了賬號(hào),生產(chǎn)環(huán)境應(yīng)自行替換成數(shù)據(jù)庫(kù)查詢(xún)等方式
     */
    private class MockJDBCUserDeatilsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Map<String, String> users = new HashMap<>();
            users.put("user1", "pwd1");
            users.put("user2", "pwd2");

            if (!users.containsKey(username)) {
                throw new UsernameNotFoundException("User not found");
            }

            return User.withDefaultPasswordEncoder()
                    .username(username)
                    .password(users.get(username))
                    .roles("USER")
                    .build();
        }
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        return new MockJDBCUserDeatilsService();
    }
}

當(dāng) client_id + client_secret + scope,以及用戶(hù)名和密碼都校驗(yàn)通過(guò)后,spring-security-oauth2 會(huì)調(diào)用合適的 tokenServices 生成 token。有興趣的同學(xué)可以自行查閱源代碼追蹤整個(gè)過(guò)程,這里介紹源碼追蹤的入口方法:

我們知道 demo-h5 客戶(hù)端(Postman)首先向 http://127.0.0.1:8000/oauth/token 發(fā)起請(qǐng)求,因此我們找到 spring-security-oauth2 組件源碼中的 /oauth/token 端點(diǎn),具體路徑為:

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken()
4)idp 返回認(rèn)證結(jié)果給客戶(hù)端,認(rèn)證通過(guò)返回 token,認(rèn)證失敗返回 401。如果認(rèn)證成功則此步驟也叫“授權(quán)”

這一步 spring-security-oauth2 已經(jīng)為我們處理好了,不需要額外處理。想要追蹤源碼過(guò)程的同學(xué),可參考上一步驟介紹的入口方法。

5)客戶(hù)端收到 token 后進(jìn)行暫存,并創(chuàng)建對(duì)應(yīng)的 session

這個(gè)步驟通過(guò) Postman 演示(直接復(fù)制返回的 token 字符串即可),這里不展開(kāi)介紹。

6)客戶(hù)端頒發(fā) cookie 給用戶(hù)代理/瀏覽器

這個(gè)步驟通過(guò) Postman 演示,這里不展開(kāi)介紹。

二)第二階段:授權(quán)后請(qǐng)求資源階段

7)用戶(hù)通過(guò)用戶(hù)代理(demo-h5)訪問(wèn)“我的相冊(cè)”頁(yè)面,用戶(hù)代理攜帶 cookie 向客戶(hù)端(demo—service)發(fā)起請(qǐng)求

此步驟使用 Postman 執(zhí)行,不展開(kāi)敘述。

8)客戶(hù)端通過(guò) session 找到對(duì)應(yīng)的 token,攜帶此 token 向資源服務(wù)器(photo-service)發(fā)起請(qǐng)求

此步驟使用 Postman 執(zhí)行,我們將第 5) 步獲取的 token 作為 Bearer Token,向 photo-service 發(fā)起請(qǐng)求,請(qǐng)求的 URL 為:

GET http://127.0.0.1:8010/api/photo

該請(qǐng)求只需要攜帶 token 即可,不需要其他參數(shù)
9)資源服務(wù)器(photo-service)向 idp 請(qǐng)求驗(yàn)證 token 有效性

在介紹如何處理請(qǐng)求前,我們先在 photo-service 工程中新增相關(guān)代碼:

  • PhotoController.java
package com.example.demophoto.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/")
public class PhotoController {
    @GetMapping("/photo")
    public String fetchPhoto() {
        return "GET photo";
    }
}

此外,還有幾個(gè)關(guān)鍵配置:

  1. ResourceServerConfigurerAdapter.configure(HttpSecurity http) 方法配置了 http.authorizeRequests().anyRequest().authenticated() 使得所有請(qǐng)求都要先鑒權(quán);
  2. application.yaml 中配置了 client_id、client_secret 和 resource.tokenInfoUri,當(dāng)資源服務(wù)接受到請(qǐng)求時(shí),會(huì)攜帶 token 向 tokenInfoUri 指定的地址發(fā)起鑒權(quán)請(qǐng)求。

默認(rèn)情況下,當(dāng) demo-h5 向 photo-service 發(fā)起資源訪問(wèn)的請(qǐng)求時(shí),photo-service 會(huì)將獲取的 token 發(fā)到 idp 進(jìn)行校驗(yàn),在這個(gè)過(guò)程中 spring-security-oauth2 不會(huì)對(duì) scope 做任何處理。我們知道 scope 是用來(lái)約束 client 的權(quán)限范圍的,因此 scope 權(quán)限檢查(也視為鑒權(quán)的工作之一)這個(gè)工作需要自己編碼實(shí)現(xiàn)。

通常來(lái)說(shuō),scope 權(quán)限檢查的業(yè)務(wù)邏輯可以靈活設(shè)定,甚至可以忽略它。本文介紹兩種 scope 檢查的實(shí)現(xiàn)方法:

  1. 資源服務(wù)器端檢查;
  2. 授權(quán)服務(wù)器端檢查。

接下來(lái)的第 10) 步將拆分成兩種方式,分別對(duì)此進(jìn)行介紹。

10)【方式一:資源服務(wù)器端 scope 檢查】 idp 校驗(yàn) token 有效性,資源服務(wù)器校驗(yàn) scope

idp 校驗(yàn) token 有效性,通過(guò)則返回 client 相關(guān)信息(包含 scope )給 photo-service,photo-service 再根據(jù) scope 判斷客戶(hù)端(demo-h5)是否有權(quán)限調(diào)用此 API,如通過(guò)檢查則繼續(xù)下一步驟,否則返回 403 錯(cuò)誤給 demo-h5。這一步也叫“鑒權(quán)”

我們?cè)?photo-service 工程中添加以下代碼:

  • ResourceServerConfigurer.java
@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    ...
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/photo/**").access("#oauth2.hasScope('resource:read')")
                .antMatchers("/api/photo2/**").access("#oauth2.hasScope('resource:read')")
                .antMatchers("/api/photo3/**").access("#oauth2.hasScope('resource:write')")
                .anyRequest().authenticated();
    }
    
    ...
}

通過(guò) access("#oauth2.hasScope('resource:write')") 方法可以實(shí)現(xiàn)資源服務(wù)器端的 scope 檢查。其主要流程為:

  1. photo-service 收到客戶(hù)端請(qǐng)求后,將獲取到的 token 發(fā)往 idp 校驗(yàn);
  2. idp 校驗(yàn)通過(guò)后,將 clientDetails 信息返回給 photo-service,其中就包括 scope 參數(shù);
  3. photo-service 拿到 scope 后,根據(jù) access("#oauth2.hasScope('resource:write')") 判斷該請(qǐng)求是否在 scope 范圍內(nèi)。
10)【方式二:idp 端 scope 檢查】 idp 校驗(yàn) token + scope 有效性

idp 校驗(yàn) token 有效性,再根據(jù) scope 判斷客戶(hù)端(demo-h5)是否有權(quán)限調(diào)用此 API,最后返回校驗(yàn)結(jié)果給資源服務(wù)器。由于 spring-security-oauth2 本身沒(méi)有處理 scope 檢查,且默認(rèn)情況下,photo-service 向 idp 請(qǐng)求 token 鑒權(quán)時(shí),并未攜帶任何其他請(qǐng)求信息,因此 idp 無(wú)法知道本次請(qǐng)求的細(xì)節(jié),因此無(wú)法執(zhí)行 socpe 檢查。

所以重點(diǎn)有兩個(gè):一是 photo-service 向 idp 請(qǐng)求 token 鑒權(quán)的同時(shí)如何攜帶請(qǐng)求的細(xì)節(jié)(比如訪問(wèn)的是什么資源?請(qǐng)求的是哪個(gè)API?);二是如何攔截 token 鑒權(quán)過(guò)程使得 scope 校驗(yàn)失敗是返回 403 錯(cuò)誤?

當(dāng)然實(shí)現(xiàn)這個(gè)目的,有很多方法,本文采用了比較直觀的方法:利用 Filter。

我們?cè)?photo-service 工程中添加以下代碼:

  • ResourceServerConfigurer.java
package com.example.demophoto.config.oauth2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    private final ResourceServerProperties resource;

    @Autowired
    protected ResourceServerConfigurer(ResourceServerProperties resource) {
        this.resource = resource;
    }

    /**
     * 自定義 RemoteTokenServices 以取代資源服務(wù)器默認(rèn)使用的
     * RemoteTokenServices 向 IDP 發(fā)起 /oauth/check_token 鑒權(quán)請(qǐng)求
     *
     * @return
     */
    public CustomRemoteTokenServices customRemoteTokenServices() {
        CustomRemoteTokenServices services = new CustomRemoteTokenServices();
        services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri());
        services.setClientId(this.resource.getClientId());
        services.setClientSecret(this.resource.getClientSecret());
        return services;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("demo-1")
                .tokenServices(customRemoteTokenServices());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new CheckTokenFilter(), AbstractPreAuthenticatedProcessingFilter.class);
        http.authorizeRequests()
                .antMatchers("/api/photo/**").access("#oauth2.hasScope('resource:read')")
                .antMatchers("/api/photo2/**").access("#oauth2.hasScope('resource:read')")
                .antMatchers("/api/photo3/**").access("#oauth2.hasScope('resource:write')")
                .anyRequest().authenticated();
    }
}
  • CheckTokenFilter.java
package com.example.demophoto.config.oauth2;

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 在向 IDP 發(fā)起 /oauth/check_token 請(qǐng)求前,將請(qǐng)求細(xì)節(jié)存儲(chǔ)到 SecurityContext 中,
 * 以便 CustomRemoteTokenServices.loadAuthentication() 可以獲取到該請(qǐng)求細(xì)節(jié)
 */
public class CheckTokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {
        HttpServletRequest request = (HttpServletRequest) req;

        String uri = request.getRequestURI();
        String method = request.getMethod();

        /**
         * 僅處理 /api/**
         */
        if (!uri.startsWith("/api/")) {
            chain.doFilter(req, res);
            return;
        }

        SecurityContext sc = SecurityContextHolder.getContext();
        CheckTokenAuthentication authentication = (CheckTokenAuthentication) sc.getAuthentication();

        if (authentication == null) {
            authentication = new CheckTokenAuthentication(null);
        }

        /**
         * 將用戶(hù)代理或其他服務(wù)請(qǐng)求訪問(wèn)本資源服務(wù)器的細(xì)節(jié)(此處為 HTTP-Method + URI)
         * 存儲(chǔ)到 SecurityContext 的 authentication 對(duì)象中
         */
        Map<String, Object> details = new HashMap<>();
        details.put("uri", uri);
        details.put("method", method);

        authentication.setDetails(details);
        sc.setAuthentication(authentication);

        chain.doFilter(req, res);
    }
}
  • CustomRemoteTokenServices.java
package com.example.demophoto.config.oauth2;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

/**
 * 以 RemoteTokenServices 為模板
 * 基本思路是在向 IDP 發(fā)起 /oauth/check_token 的請(qǐng)求中,
 * 添加用戶(hù)代理或其他服務(wù)請(qǐng)求訪問(wèn)本資源服務(wù)器的 API 的細(xì)節(jié),
 * 以便 IDP 可以判斷該用戶(hù)代理或其他服務(wù)(即 client)是否可以調(diào)用此 API
 * <p>
 * (PS:也可以由 IDP 返回 ClientDetails 給資源服務(wù),由資源服務(wù)處理放行邏輯)
 */
public class CustomRemoteTokenServices implements ResourceServerTokenServices {

    protected final Log logger = LogFactory.getLog(getClass());

    private RestOperations restTemplate;

    private String checkTokenEndpointUrl;

    private String clientId;

    private String clientSecret;

    private String tokenName = "token";

    /**
     * 與 IDP 約定的存儲(chǔ) API 請(qǐng)求細(xì)節(jié)的參數(shù)
     */
    private String reqPayload = "payload";

    private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

    public CustomRemoteTokenServices() {
        restTemplate = new RestTemplate();
        ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException {
                Integer statusCode = response.getRawStatusCode();
                if (statusCode != 400) {
                    if (statusCode == 401 || statusCode == 403) {
                        HttpStatus status = HttpStatus.resolve(statusCode);
                        throw new AccessDeniedException(status.toString());
                    }
                    super.handleError(response);
                }
            }
        });
    }

    public void setRestTemplate(RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
        this.checkTokenEndpointUrl = checkTokenEndpointUrl;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }

    public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
        this.tokenConverter = accessTokenConverter;
    }

    public void setTokenName(String tokenName) {
        this.tokenName = tokenName;
    }

    /**
     * 當(dāng)使用自定義的 tokenServices 替換默認(rèn)的 tokenServices 后,
     * 原來(lái)流程中的第 9 步就變成由該方法執(zhí)行。
     *
     * 9. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 9 步:
     * 資源服務(wù)器(photo-service)向 idp 請(qǐng)求驗(yàn)證 token 有效性
     *
     * @param accessToken
     * @return
     * @throws AuthenticationException
     * @throws InvalidTokenException
     */
    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
        Map<String, Object> authDetails = new HashMap<>();

        /**
         * 取得在 CheckTokenFilter 過(guò)濾器中置入的 API 請(qǐng)求細(xì)節(jié)
         */
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            authDetails = (Map<String, Object>) authentication.getDetails();
        }

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add(tokenName, accessToken);
        if (!authDetails.isEmpty()) {
            formData.add(reqPayload, authDetails.get("method") + " " + authDetails.get("uri"));
        }
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));

        Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);

        /**
         * 11. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 11 步:
         *     如果 token 校驗(yàn)失敗則返回 401 給客戶(hù)端,如果 scope 檢查不通過(guò)則返回 403
         */
        if (map.containsKey("error")) {
            if (logger.isDebugEnabled()) {
                logger.debug("check_token returned error: " + map.get("error"));
            }
            if (map.containsKey("status")) {
                if ("403".equals(map.get("status").toString())) {
                    throw new OAuth2AccessDeniedException(map.get("error").toString());
                }
            }
            throw new InvalidTokenException(accessToken);
        }

        // gh-838
        if (map.containsKey("active") && !"true".equals(String.valueOf(map.get("active")))) {
            logger.debug("check_token returned active attribute: " + map.get("active"));
            throw new InvalidTokenException(accessToken);
        }

        return tokenConverter.extractAuthentication(map);
    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {

        if (clientId == null || clientSecret == null) {
            logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
        }

        String creds = String.format("%s:%s", clientId, clientSecret);
        try {
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Could not convert String");
        }
    }

    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map<String, Object> result = new HashMap<>();
        try {
            Map map = restTemplate.exchange(path, HttpMethod.POST,
                    new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
                    result = map;
        }
        catch (Exception e) {
            logger.error(e.getMessage());
        }

        return result;
    }

}
  • CheckTokenAuthentication.java
package com.example.demophoto.config.oauth2;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class CheckTokenAuthentication extends AbstractAuthenticationToken {

    /**
     * Creates a token with the supplied array of authorities.
     *
     * @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
     *                    represented by this authentication object.
     */
    public CheckTokenAuthentication(Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }
}

接著在 idp 工程中添加以下代碼:

  • AuthorizationServerConfigurer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    ...
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)

                // 通過(guò)插入 interceptor 來(lái)實(shí)現(xiàn)自定義的鑒權(quán)方法
                .addInterceptor(new CheckTokenInterceptor(endpoints.getTokenStore()));
    }
    
    ...
}
  • CheckTokenInterceptor.java
package com.example.demoidp.config.oauth2;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * /oauth/check_token 校驗(yàn) token 請(qǐng)求攔截器
 */
public class CheckTokenInterceptor implements HandlerInterceptor {
    private String TOKEN_NAME = "token";
    private final String TOKEN_INFO_URI = "/oauth/check_token";

    private TokenStore tokenStore;

    public CheckTokenInterceptor(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    // for test only
    private final Map<String, String> clientScopes = new HashMap<String, String>() {
        {
            put("client1[resource:read]", "GET /api/photo");
            put("client1[resource:write]", "POST /api/photo");
            put("client2[resource:read]", "GET /api/photo2");
            put("client2[resource:write]", "POST /api/photo2");
            put("client3[resource:read]", "GET /api/photo3");
            put("client3[resource:write]", "POST /api/photo3");
        }
    };

    /**
     * 10. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 10 步:
     *     idp 校驗(yàn) token 有效性和 scope 權(quán)限
     * <p>
     * 即 IDP 根據(jù) scope 判斷客戶(hù)端(demo-service)
     * 是否有權(quán)限調(diào)用此 API,最后返回校驗(yàn)結(jié)果給資源服務(wù)器
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();

        /**
         * 僅攔截 /oauth/check_token
         */
        if (!TOKEN_INFO_URI.equals(uri)) {
            return true;
        }

        /**
         * payload 是 IDP 和資源服務(wù)器角色約定的傳參格式
         * 即 client 請(qǐng)求訪問(wèn)資源服務(wù)器的 API 的細(xì)節(jié)
         * 可要求必須攜帶 payload
         *
         * 此部分可根據(jù)業(yè)務(wù)邏輯自行處理
         */
        String paylad = request.getParameter("payload");
        if (StringUtils.isEmpty(paylad)) {
            throw new AccessDeniedException("insufficient_payload");
        }

        if ("GET /error".equals(paylad)) {
            return true;
        }

        /**
         * 10. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 10 步:
         * 【方式二:idp 端 scope 檢查】 idp 校驗(yàn) token + scope 有效性
         * 
         * 根據(jù) token 查得 clientId,再根據(jù) scope 檢查該 client 是否有權(quán)限調(diào)用此 API
         * 此部分可根據(jù)業(yè)務(wù)邏輯自行處理,比如從數(shù)據(jù)庫(kù)中查詢(xún) client、API 和 scope 的關(guān)系
         */
        String token = request.getParameter(TOKEN_NAME);
        OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(token);
        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        String scopeKey = oAuth2Request.getClientId() + oAuth2Request.getScope();
        if (clientScopes.containsKey(scopeKey)) {
            if (!clientScopes.get(scopeKey).equals(paylad)) {
                throw new AccessDeniedException("insufficient_scope");
            }
        }

        return true;
    }
}

idp 端的 scope 檢查實(shí)現(xiàn)起來(lái)稍微麻煩點(diǎn),其主要思路是:

  1. 在 photo-service 向 idp 發(fā)起 /oauth/check_oauth 鑒權(quán)請(qǐng)求前,添加過(guò)濾器,將客戶(hù)端的請(qǐng)求細(xì)節(jié)保存到某個(gè)全局對(duì)象中;
  2. 替換 photo-service 默認(rèn)的 tokenServices,在向 idp 發(fā)起 /oauth/check_oauth 鑒權(quán)請(qǐng)求的過(guò)程中,將請(qǐng)求細(xì)節(jié)附加到請(qǐng)求中;
  3. idp 在 AuthorizationServerEndpointsConfigurer 中添加自定義 Interceptor,在每次 check token 前先執(zhí)行 自定義 Interceptor;
  4. idp 在自定義 Interceptor 中取出請(qǐng)求細(xì)節(jié),根據(jù)請(qǐng)求細(xì)節(jié)和 clientDetails 信息(scope),執(zhí)行 scope 檢查。

以上方法,雖然實(shí)現(xiàn)麻煩,但是定制性和靈活性很強(qiáng),不受框架約束,可以適應(yīng)各種復(fù)雜的業(yè)務(wù)邏輯。

11)資源服務(wù)器根據(jù) idp 檢驗(yàn)結(jié)果(true/false 或其他等效手段)決定是否返回用戶(hù)相冊(cè)數(shù)據(jù)給客戶(hù)端。如果 token 校驗(yàn)失敗則返回 401 給客戶(hù)端,如果 scope 檢查不通過(guò)則返回 403。這一步也叫“權(quán)限控制”

與鑒權(quán)工作中的 scope 范圍檢查類(lèi)似,實(shí)現(xiàn)權(quán)限控制的方法也有兩種:

  1. 授權(quán)服務(wù)器端的權(quán)限控制,屬于集中式權(quán)限控制;
  2. 資源服務(wù)器端的權(quán)限控制,屬于分散型權(quán)限控制。

其中,授權(quán)服務(wù)器端的權(quán)限控制比較簡(jiǎn)單,在 idp 工程的 CheckTokenInterceptor.preHandle() 方法中添加權(quán)限控制的業(yè)務(wù)代碼即可:

  • CheckTokenInterceptor.java
public class CheckTokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ...

        /**
         * 11. [密碼模式的典型架構(gòu)層次和主要流程] 中的第 11 步:
         *  授權(quán)服務(wù)器短的權(quán)限控制,即集中式權(quán)限控制
         *
         * 實(shí)現(xiàn)更細(xì)粒度的權(quán)限控制,從某種程度上來(lái)說(shuō),這個(gè)過(guò)程也可以稱(chēng)作鑒權(quán)
         */
        // 授權(quán)服務(wù)器端鑒權(quán)/權(quán)限控制業(yè)務(wù)的邏輯

        return true;
    }
}

最后來(lái)看資源服務(wù)器端的權(quán)限控制。我們使用 spring-secutity 提供的標(biāo)準(zhǔn)方法來(lái)實(shí)現(xiàn):

  1. 資源服務(wù)器端 PreAuthorize hasRole/hasAuthority
  2. 資源服務(wù)器端 PreAuthorize 自定義實(shí)現(xiàn) hasPermission

以上說(shuō)法在某種程度上也可以理解為鑒權(quán)。

首先,我們添加或修改 photo-service 工程的相關(guān)代碼:

  • PhotoController.java
package com.example.demophoto.web;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 1、 權(quán)限控制的兩種類(lèi)型:資源服務(wù)端權(quán)限控制、授權(quán)服務(wù)器端權(quán)限控制
 * 2、 權(quán)限控制的三種方法:
 *      A、 資源服務(wù)器端 PreAuthorize hasRole/hasAuthority
 *      B、 資源服務(wù)器端 HttpSecurity access 自定義實(shí)現(xiàn) hasPermission
 *      D、 授權(quán)服務(wù)器端 HandlerInterceptor
 *     以上說(shuō)法在某種程度上也可以理解為鑒權(quán)。
 */
@RestController
@RequestMapping("/api/")
public class PhotoController {
    @GetMapping("/photo")
    @PreAuthorize("hasRole('USER') and hasAuthority('WRITE')")
    public String fetchPhoto() {
        return "GET photo";
    }

    @GetMapping("/photo2")
    public String fetchPhoto2() {
        return "GET photo 2";
    }

    @GetMapping("/photo3")
    @PreAuthorize("hasPermission('PhotoController', 'read')")
    public String fetchPhoto3() {
        return "GET photo 3";
    }
}
  • ResourceServerConfigurer.java
@Configuration
@EnableResourceServer
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
    ...

    /**
     * 舊版本的 spring-security-oauth2 還需要將執(zhí)行 resources.expressionHandler(oAuth2WebSecurityExpressionHandler) 
     * 以注入自定義的 expressionHandler,當(dāng)前及以后版本不需要了
     * 
     * @return
     */
    @Bean
    public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler() {
        OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler = new OAuth2WebSecurityExpressionHandler();

        // 在新版本的 spring-security-oauth2 中,這行代碼可以不用,
        // 框架會(huì)自動(dòng)注入 customPermissionEvaluator 替換默認(rèn)的 DenyAllPermissionEvaluator
        // oAuth2WebSecurityExpressionHandler.setPermissionEvaluator(customPermissionEvaluator);
        return oAuth2WebSecurityExpressionHandler;
    }
    
    ...
}
  • CustomPermissionEvaluator.java
package com.example.demophoto.config.oauth2;

import com.example.demophoto.service.PermisionEvaluatingService;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    private PermisionEvaluatingService permisionEvaluatingService = new PermisionEvaluatingService();

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        return permisionEvaluatingService.hasPermission(authentication, targetDomainObject, permission);
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return permisionEvaluatingService.hasPermission(authentication, targetId, targetType, permission);
    }
}
  • PermisionEvaluatingService.java
package com.example.demophoto.service;

import org.springframework.security.core.Authentication;

import java.io.Serializable;

public class PermisionEvaluatingService {
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        // 業(yè)務(wù)邏輯
        return true;
    }

    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // 業(yè)務(wù)邏輯
        return true;
    }
}
  • DemoPhotoApplication.java
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 開(kāi)啟 hasRole/hasAuthority/hasPermission 支持
public class DemoPhotoApplication {
    ...
}

經(jīng)過(guò)以上配置,當(dāng)客戶(hù)端向 photo-service 發(fā)起 GET /api/photo3 請(qǐng)求時(shí),將會(huì)進(jìn)入 CustomPermissionEvaluator.hasPermission() 方法進(jìn)行判斷,因此可以實(shí)現(xiàn)非常靈活的資源服務(wù)器端權(quán)限控制。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容