Spring Cloud Gateway + Oauth2 搭建過程

源代碼鏈接

在SpringCloud中募强,實(shí)現(xiàn)授權(quán)功能有兩種實(shí)現(xiàn)方式:

1.網(wǎng)關(guān)授權(quán)

基于網(wǎng)關(guān)授權(quán)我們又叫【基于路徑匹配器授權(quán)】崇摄,請求在經(jīng)過網(wǎng)關(guān)的時候校驗(yàn)當(dāng)前請求的路徑是否在用戶擁有的資源路徑中逐抑。
主要利用 ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) 方法進(jìn)行校驗(yàn)。
這種方法主要是基于用戶擁有的資源路徑進(jìn)行考量汹粤。

2.微服務(wù)授權(quán)

微服務(wù)授權(quán)我們又叫【基于方法攔截】田晚,在需要進(jìn)行權(quán)限校驗(yàn)的方法上加上SpringSecurity注解贤徒,判斷當(dāng)前用戶是否有訪問此方法的權(quán)限。
1.在secruity的配置類上加上注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 啟用springsecruity前/后注解
2.在需要權(quán)限的方法或者類上添加權(quán)限注解@PreAuthorize("hasAuthority('USER')")
當(dāng)然也可以使用自定義注解或使用AOP進(jìn)行攔截校驗(yàn)踢涌,這幾種實(shí)現(xiàn)方式我們都統(tǒng)稱為基于方法攔截睁壁。
這種方法一般會基于用戶擁有的資源標(biāo)識進(jìn)行考量潘明。

書接上文 oauth2 業(yè)務(wù)模塊鑒權(quán)搭建 實(shí)現(xiàn)了在調(diào)用業(yè)務(wù)模塊的接口時進(jìn)行鑒權(quán)也就是【微服務(wù)授權(quán)】的方式钉疫。
接下來搭建【網(wǎng)關(guān)授權(quán)】

對認(rèn)證服務(wù)器的數(shù)據(jù)庫進(jìn)行修改巢价,添加路徑的配置

數(shù)據(jù)庫Sql結(jié)構(gòu)
搭建認(rèn)證服務(wù)的過程

認(rèn)證服務(wù)器增加配置壤躲,將resource path和角色對應(yīng)的內(nèi)容讀取到redis

gateway服務(wù)將從redis進(jìn)行路徑和角色的判斷

@Configuration
public class ResourceApiPathConfig {

    @Resource
    private DataSource dataSource;

    @Resource
    RedisTemplate<String, Map<String, List<String>>> redisTemplate;

    @Bean
    public RedisResourceService resourcePathDetails() throws UnknownHostException {
        return new RedisResourceService(dataSource, redisTemplate);
    }
}

public class RedisResourceService {

    private RedisTemplate<String, Map<String, List<String>>> redisTemplate;
    private DataSource dataSource;
    static final String AUTH_TO_RESOURCE = "auth_resource:";

    public RedisResourceService(DataSource _dataSource,
            RedisTemplate<String, Map<String, List<String>>> _redisTemplate) {
        this.dataSource = _dataSource;
        this.redisTemplate = _redisTemplate;
        initData();
    }

    @SuppressWarnings("unchecked")
    private void initData() {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        Map<String, List<String>> resourceRolesMap = new TreeMap<>();
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT CONCAT(\"/\",b.service,b.url) AS url,GROUP_CONCAT(c.name) AS role_names from rbac_role_resource a left join rbac_resource b ON a.resource_id = b.id left join rbac_role c ON a.role_id = c.id GROUP BY b.url");

        for (Map<String, Object> m : list) {
            System.out.print(m.get("url").toString());
            resourceRolesMap.put(m.get("url").toString(), Arrays.asList(m.get("role_names").toString().split(",")));
        }
        redisTemplate.opsForHash().putAll(AUTH_TO_RESOURCE, resourceRolesMap);
        // System.out.print("******************************set_ResourcePath");
    }
}

創(chuàng)建gateway

http -d https://start.spring.io/starter.zip javaVersion==17 groupId==com.my.demo artifactId==gatewayService name==gateway-service baseDir==gateway-service bootVersion==2.6.6.RELEASE dependencies==cloud-gateway

修改POM

<?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.6.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.my.demo</groupId>
    <artifactId>gatewayService</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway-service</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.0.6</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

修改application.properties 配置里routes[0]的project-service可以仍采用 業(yè)務(wù)服務(wù) 里搭建的服務(wù)

server.port=8501
spring.application.name=gateway-service

spring.cloud.gateway.routes[0].id=project-service
spring.cloud.gateway.routes[0].uri=http://localhost:8601
spring.cloud.gateway.routes[0].predicates[0]=Path=/project/**
spring.cloud.gateway.routes[0].filters=StripPrefix=1

spring.cloud.gateway.routes[1].id=oauth-service
spring.cloud.gateway.routes[1].uri=http://localhost:8509
spring.cloud.gateway.routes[1].predicates[0]=Path=/oauth/**

security.oauth2.client.scope=server
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=123456
security.oauth2.client.access-token-uri=http://localhost:8509/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8509/oauth/authorize
 
security.oauth2.authorization.check-token-access=http://localhost:8509/oauth/check_token

security.oauth2.resource.token-info-uri=http://localhost:8509/oauth/check_token
security.oauth2.resource.user-info-uri=http://localhost:8509/oauth/check_user
security.oauth2.resource.prefer-token-info=true
secure.ignore.urls="/actuator/**","/oauth/**"

spring.redis.database=0  
spring.redis.host=127.0.0.1
spring.redis.port=6379 
spring.redis.password=
spring.redis.timeout=2000

配置gateway的鑒權(quán)

public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private RedisTemplate redisTemplate;
    private RedisConnectionFactory redisConnectionFactory;
    static final String AUTH_TO_RESOURCE = "auth_resource:";

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {

        ServerWebExchange exchange = authorizationContext.getExchange();
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        
        String authorizationToken = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); // 從Header里取出token的值
        if (StringUtils.isBlank(authorizationToken)) {
            authorizationToken = request.getQueryParams().getFirst("access_token");
        }

        if (StringUtils.isBlank(authorizationToken)) {
            log.warn("當(dāng)前請求頭Authorization中的值不存在");
            return Mono.just(new AuthorizationDecision(false));
        }

        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        String token = authorizationToken.replace(OAuth2AccessToken.BEARER_TYPE + " ", "");
        OAuth2Authentication oAuth2Authentication = redisTokenStore.readAuthentication(token);
        Collection<GrantedAuthority> authorities = oAuth2Authentication.getAuthorities(); // 取到角色
        Map<String, List<String>> resourceRolesMap = redisTemplate.opsForHash().entries(AUTH_TO_RESOURCE);
        List<String> pathAuthorities = resourceRolesMap.get(path);
        for (GrantedAuthority authority : authorities) {
            if (pathAuthorities.contains(authority.getAuthority())) {
                return Mono.just(new AuthorizationDecision(true));
            }
        }
        return Mono.just(new AuthorizationDecision(false));

    }
}


@AllArgsConstructor
@Configuration
public class ResourceServerConfig {

    private final AuthorizationManager authorizationManager;
    private final IgnoreUrlsConfig ignoreUrlsConfig;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        http.authorizeExchange().pathMatchers(ArrayUtil.toArray(ignoreUrls, String.class)).permitAll()// 白名單配置
                .anyExchange().access(authorizationManager)// 鑒權(quán)管理器配置
                .and().exceptionHandling().and().csrf().disable();
        return http.build();
    }
}

通過 HTTPie 測試


//獲取access_token
D:\Project\oauthService>http http://127.0.0.1:8509/oauth/token client_secret==123456 grant_type==password username==Test password==123456 client_id==client scope==server
HTTP/1.1 200
Cache-Control: no-store
Connection: keep-alive
Content-Type: application/json;charset=UTF-8
Keep-Alive: timeout=60
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
    "access_token": "ec9055f9-8be7-4c40-98ad-d7f81c31137d",
    "expires_in": 43199,
    "refresh_token": "c113adb9-e192-49a2-bbf5-6cb11b8a3a3e",
    "scope": "server",
    "token_type": "bearer"
}


//通過網(wǎng)關(guān)訪問project-service的鑒權(quán)方法
D:\Project\oauthService>http -v --auth-type=bearer --auth=ec9055f9-8be7-4c40-98ad-d7f81c31137d http://127.0.0.1:8501/project/config/set
GET /project/config/set HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer ec9055f9-8be7-4c40-98ad-d7f81c31137d
Connection: keep-alive
Host: 127.0.0.1:8501
User-Agent: HTTPie/3.1.0



HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/plain;charset=UTF-8
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
content-length: 69

ec9055f9-8be7-4c40-98ad-d7f81c31137dconfig project have authorization


//通過網(wǎng)關(guān)訪問project-service的鑒權(quán)錯誤的方法
D:\Project\oauthService>http -v --auth-type=bearer --auth=ec9055f9-8be7-4c40-98ad-d7f81c31137d http://127.0.0.1:8501/project/config/get
GET /project/config/get HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Bearer ec9055f9-8be7-4c40-98ad-d7f81c31137d
Connection: keep-alive
Host: 127.0.0.1:8501
User-Agent: HTTPie/3.1.0



HTTP/1.1 500 Internal Server Error
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 144
Content-Type: application/json
Expires: 0
Pragma: no-cache
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block

{
    "error": "Internal Server Error",
    "path": "/project/config/get",
    "requestId": "b9250aa3-2",
    "status": 500,
    "timestamp": ""
}

源代碼鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捏膨,一起剝皮案震驚了整個濱河市食侮,隨后出現(xiàn)的幾起案子锯七,更是在濱河造成了極大的恐慌誉己,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異炉峰,居然都是意外死亡疼阔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門迅细,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茵典,“玉大人宾舅,你說我怎么就攤上這事筹我。” “怎么了结澄?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵麻献,是天一觀的道長猜扮。 經(jīng)常有香客問我,道長餐曼,這世上最難降的妖魔是什么鲜漩? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任孕似,我火速辦了婚禮喉祭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘理卑。我一直安慰自己蔽氨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著自赔,像睡著了一般绍妨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上津函,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天尔苦,我揣著相機(jī)與錄音行施,去河邊找鬼。 笑死稠项,一個胖子當(dāng)著我的面吹牛鲜结,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拗胜,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼埂软,長吁一口氣:“原來是場噩夢啊……” “哼勘畔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起爬立,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤懦尝,失蹤者是張志新(化名)和其女友劉穎壤圃,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踊挠,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡效床,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年剩檀,在試婚紗的時候發(fā)現(xiàn)自己被綠了旺芽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片采章。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖担租,靈堂內(nèi)的尸體忽然破棺而出奋救,到底是詐尸還是另有隱情,我是刑警寧澤演侯,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站盔粹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏轴猎。R本人自食惡果不足惜进萄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一中鼠、第九天 我趴在偏房一處隱蔽的房頂上張望援雇。 院中可真熱鬧,春花似錦具温、人聲如沸筐赔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳞绕。三九已至尸曼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冤竹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钟病,地道東北人肠阱。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓屹徘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親噪伊。 傳聞我的和親對象是個殘疾皇子鉴吹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容