【oauth2 客戶端模式】Spring Authorization Server + Resource + Client 資源服務間的相互訪問

1、概述

上一節中介紹了項目的搭建,並實現了授權碼模式的訪問。在上一節的基礎上,再來實現客戶端模式。【圖文詳解】搭建 Spring Authorization Server + Resource + Client 完整Demo

  • 用戶通過客戶端訪問資源是 授權碼模式
  • 微服務(資源)間的訪問是 客戶端模式;客戶端模式下,只需要提供註冊客戶端的ID和密鑰,就可以向授權服務器申請令牌,授權服務器覈實ID和密鑰後,會直接發放令牌,無須再認證/授權,特別適合項目內部模塊間的調用。

在這裏插入圖片描述

2、授權服務器中註冊新客戶端

爲了讓請求資源的主體更加清晰,再註冊一個客戶端micro_service,專門供資源服務器之間的相互調用。也可以用原來客戶端 my_client ,不過要在授權模式GrantType中添加 CLIENT_CREDENTIALS

  • 客戶端模式直接返回token;不需要回調地址
  • 在授權服務器的授權服務配置類 AuthorizationServerConfiguration.java 中添加
    /**
     * 定義客戶端(令牌申請方式:客戶端模式)
     *
     * @param clientId 客戶端ID
     * @return
     */
    private RegisteredClient createRegisteredClient(final String clientId) {
        // JWT(Json Web Token)的配置項:TTL、是否複用refrechToken等等
        TokenSettings tokenSettings = TokenSettings.builder()
                // 令牌存活時間:1年
                .accessTokenTimeToLive(Duration.ofDays(365))
                // 令牌不可以刷新
                //.reuseRefreshTokens(false)
                .build();
        // 客戶端相關配置
        ClientSettings clientSettings = ClientSettings.builder()
                // 是否需要用戶授權確認
                .requireAuthorizationConsent(false)
                .build();

        return RegisteredClient
                // 客戶端ID和密碼
                .withId(UUID.randomUUID().toString())
                //.withId(id)
                .clientId(clientId)
                //.clientSecret("{noop}123456")
                .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
                // 客戶端名稱:可省略
                .clientName("micro_service")
                // 授權方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授權模式
                // ---- 【客戶端模式】
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 客戶端模式直接返回token;不需要回調地址
                //.redirectUri("...")
                // 授權範圍(當前客戶端的角色)
                .scope("all")
                // JWT(Json Web Token)配置項
                .tokenSettings(tokenSettings)
                // 客戶端配置項
                .clientSettings(clientSettings)
                .build();
    }
  • 修改註冊方法:該方法僅注重功能,結構不夠優雅,可以自行修改
    /**
     * 註冊客戶端
     *
     * @param jdbcTemplate 操作數據庫
     * @return 客戶端倉庫
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // ---------- 1、檢查當前客戶端是否已註冊
        // 操作數據庫對象
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        /*
         客戶端在數據庫中記錄的區別
         ------------------------------------------
         id:僅表示客戶端在數據庫中的這個記錄
         client_id:唯一標示客戶端;請求token時,以此作爲客戶端的賬號
         client_name:客戶端的名稱,可以省略
         client_secret:密碼
         */
        String clientId_1 = "my_client";
        String clientId_2 = "micro_service";
        // 查詢客戶端是否存在
        RegisteredClient registeredClient_1 = registeredClientRepository.findByClientId(clientId_1);
        RegisteredClient registeredClient_2 = registeredClientRepository.findByClientId(clientId_2);

        // ---------- 2、添加客戶端
        // 數據庫中沒有
        if (registeredClient_1 == null) {
            registeredClient_1 = this.createRegisteredClientAuthorizationCode(clientId_1);
            registeredClientRepository.save(registeredClient_1);
        }
        // 數據庫中沒有
        if (registeredClient_2 == null) {
            registeredClient_2 = this.createRegisteredClient(clientId_2);
            registeredClientRepository.save(registeredClient_2);
        }

        // ---------- 3、返回客戶端倉庫
        return registeredClientRepository;
    }

3、資源服務器之間訪問

3.1、案例說明

用 資源服務器B 調用 資源服務器A 中的資源;

具體:服務B/res1 --> 服務A/res2

服務A/res2 接口在前面用 my_client 是無法訪問的;

在這裏插入圖片描述

當前 資源服務器B 無安全策略,可以直接訪問

在這裏插入圖片描述

3.2、令牌申請與使用 處理邏輯

在這裏插入圖片描述

3.3、改造資源服務器B

  • 配置RestTemplat
@Configuration(proxyBeanMethods = false)
public class RestTemplateConfiguration {
    @Bean
    public RestTemplate oauth2ClientRestTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
}
  • 修改API接口類
@RestController
public class ResourceController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/res1")
    public String getRes1(HttpServletRequest request) {
        // 調用資源服務器A中的資源res2
        return getServer("http://127.0.0.1:8001/res2", request);
        //return JSON.toJSONString(new Result(200, "服務B -> 資源1"));
    }

    @GetMapping("/res2")
    public String getRes2() {
        return JSON.toJSONString(new Result(200, "服務B -> 資源2"));
    }

    /**
     * 請求資源
     *
     * @param url
     * @param request
     * @return
     */
    private String getServer(String url,
                             HttpServletRequest request) {
        // ======== 1、從session中取token ========
        HttpSession session = request.getSession();
        String token = (String) session.getAttribute("micro-token");

        // ======== 2、請求token ========
        // 先查session中是否有token;session中沒有
        if (StringUtils.isEmpty(token)) {
            // ===== 去認證中心申請 =====
            // 對id及密鑰加密
            byte[] userpass = Base64.encodeBase64(("micro_service:123456").getBytes());
            String str = "";
            try {
                str = new String(userpass, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            // 請求頭
            HttpHeaders headers1 = new HttpHeaders();
            // 組裝請求頭
            headers1.add("Authorization", "Basic " + str);
            // 請求體
            HttpEntity<Object> httpEntity1 = new HttpEntity<>(headers1);
            // 響應體
            ResponseEntity<String> responseEntity1 = null;
            try {
                // 發起申請令牌請求
                responseEntity1 = restTemplate.exchange("http://os.com:9000/oauth2/token?grant_type=client_credentials", HttpMethod.POST, httpEntity1, String.class);
            } catch (RestClientException e) {
                //
                System.out.println("令牌申請失敗");
            }

            // 令牌申請成功
            if (responseEntity1 != null) {
                // 解析令牌
                // String t = JSON.parseObject(responseEntity1.getBody(), MyAuth.class).getAccess_token();
                Map<String, String> resMap = JSON.parseObject(responseEntity1.getBody(), HashMap.class);
                String t = resMap.get("access_token");
                // 存入session
                session.setAttribute("micro-token", t);
                // 賦於token變量
                token = t;
            }
        }

        // ======== 3、請求資源 ========
        // 請求頭
        HttpHeaders headers2 = new HttpHeaders();
        // 組裝請求頭
        headers2.add("Authorization", "Bearer " + token);
        // 請求體
        HttpEntity<Object> httpEntity2 = new HttpEntity<>(headers2);
        // 響應體
        ResponseEntity<String> responseEntity2;
        try {
            // 發起訪問資源請求
            responseEntity2 = restTemplate.exchange(url, HttpMethod.GET, httpEntity2, String.class);
        } catch (RestClientException e) {
            // 令牌失效(認證失效401) --> 清除session
            // e.getMessage() 信息格式:
            // 401 : "{"msg":"認證失敗","uri":"/res2"}"   
            String str = e.getMessage();
            // 判斷是否含有 401
            if(StringUtils.contains(str, "401")){
                // 如果有401,把session中 micro-token 的值設爲空
                session.setAttribute("micro-token","");
            }            
            // 取兩個括號中間的部分(包含兩個括號)
            return str.substring(str.indexOf("{"), str.indexOf("}") + 1);
        }
        // 返回
        return responseEntity2.getBody();
    }
}

// 用於解析申請到的令牌數據
/*@Data
class MyAuth {
    private String access_token;
    private String scope;
    private String token_type;
    private long expires_in;
}*/

3.4、測試

  • 啓動server、resource,無須啓動client

在這裏插入圖片描述

  • 直接訪問resource_b

在這裏插入圖片描述

3.5、資源服務器B添加安全策略

  • 添加依賴
<!-- 資源服務器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
  • 再次啓動測試,已經無法直接訪問;需要通過客戶端去訪問

在這裏插入圖片描述

3.6、繼續改造資源服務器B

複製 資源服務器A 中配置策略到 資源服務器B 中來

  • 複製cer公鑰文件
  • appliction.yml中添加jtw配置
# 自定義 jwt 配置(校驗jwt)
jwt:
  cert-info:
    # 公鑰證書存放位置
    public-key-location: myjks.cer
  claims:
    # 令牌的鑑發方:即授權服務器的地址
    issuer: http://os.com:9000
  • 複製 oauth2 配置包;如下圖

在這裏插入圖片描述

在這裏插入圖片描述

3.7、客戶端client訪問測試

  • 用 maven 的 clean 清理項目
  • 啓動 server、resource (a和b)、client

在這裏插入圖片描述

  • 登錄客戶端

在這裏插入圖片描述

  • 訪問資源服務A

在這裏插入圖片描述

在這裏插入圖片描述

  • 訪問資源服務B

在這裏插入圖片描述
在這裏插入圖片描述

如果需要 資源服務器A 調用 B 中資源;可以把 B 中的實現邏輯複製過去就行。

後期會把資源服務器中的公共部分抽離出來,製成starter

2023-4-16:用starter實現Oauth2中資源服務的統一配置

【oauth2 客戶端模式】Spring Authorization Server + Resource + Client 資源服務間的相互訪問_registeredclientrepository-CSDN博客

一、前言

Oauth2中的資源服務Resource需要驗證令牌,就要配置令牌的解碼器JwtDecoder,認證服務器的公鑰等等。如果有多個資源服務Resource,就要重複配置,比較繁鎖。把公共的配置信息抽取出來,製成starter,可以極大地簡化操作。

  • 未使用starter的原來配置

在這裏插入圖片描述

在這裏插入圖片描述

二、製作starter

詳細步驟參考:自定義啓動器 Starter【保姆級教程】

1、完整結構圖

在這裏插入圖片描述

2、外部引用模塊

  • 名稱:tuwer-oauth2-config-spring-boot-starter

  • 普通的 maven 項目

  • 資源服務中引入該模塊的依賴即可

  • 模塊中只有一個pom.xml文件,其餘的都可刪除

  • 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tuwer</groupId>
    <artifactId>tuwer-oauth2-config-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <description>oauth2-config啓動器</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <!-- 編譯編碼 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 自動配置模塊 -->
        <dependency>
            <groupId>com.tuwer</groupId>
            <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

3、自動配置模塊

  • 核心模塊

  • 名稱:tuwer-oauth2-config-spring-boot-starter-autoconfigure

  • spring boot 項目

  • 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.7</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tuwer</groupId>
    <artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId>
    <version>1.0-SNAPSHOT</version>
    <description>oauth2-config啓動器自動配置模塊</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <!-- 編譯編碼 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 基礎啓動器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 資源服務器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
    </dependencies>
</project>
  • handler(拒絕訪問、認證失敗)處理類
package com.tuwer.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * <p>拒絕訪問處理器</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
    @SneakyThrows
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException
    ) throws IOException, ServletException {
        //todo your business
        HashMap<String, String> map = new HashMap<>(2);
        map.put("uri", request.getRequestURI());
        map.put("msg", "拒絕訪問");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}
package com.tuwer.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

/**
 * <p>認證失敗處理器</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @SneakyThrows
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException
    ) throws IOException, ServletException {
        HashMap<String, String> map = new HashMap<>(2);
        if (authException instanceof InvalidBearerTokenException) {
            // 令牌失效
            System.out.println("token失效");
            //todo token處理邏輯
        }
        map.put("uri", request.getRequestURI());
        map.put("msg", "認證失敗");
        if (response.isCommitted()) {
            return;
        }
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setStatus(HttpServletResponse.SC_ACCEPTED);
        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ObjectMapper objectMapper = new ObjectMapper();
        String resBody = objectMapper.writeValueAsString(map);
        PrintWriter printWriter = response.getWriter();
        printWriter.print(resBody);
        printWriter.flush();
        printWriter.close();
    }
}
  • property(權限、令牌)屬性類

    權限屬性類:AuthProperty,通過application.yml來配置權限,避免在自動配置類中以硬編碼的形式寫入權限

package com.tuwer.config.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.CollectionUtils;

import java.util.Set;

/**
 * <p>權限屬性類</p>
 *
 * @author 土味兒
 * Date 2022/9/2
 * @version 1.0
 */
@Data
@ConfigurationProperties(prefix = "resource-auth")
public final class AuthProperty {
    private Authority authority;

    /**
     * 權限
     */
    @Data
    public static class Authority {
        private Set<String> roles;
        private Set<String> scopes;
        private Set<String> auths;
    }

    /**
     * 組裝權限字符串
     * 目的:給 hasAnyAuthority() 方法生成參數
     * @return
     */
    public String getAllAuth() {
        StringBuilder res = new StringBuilder();

        // 角色
        Set<String> roles = this.authority.roles;
        // 角色非空時
        if (!CollectionUtils.isEmpty(roles)) {
            for (String role : roles) {
                res.append(role).append("','");
            }
            // 循環結果後,生成類似:x ',' y ',' z ','
        }

        // 範圍
        Set<String> scopes = this.authority.scopes;
        // 非空時
        if (!CollectionUtils.isEmpty(scopes)) {
            for (String scope : scopes) {
                res.append("SCOPE_" + scope).append("','");
            }
            // 循環結果後,生成類似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ','
        }

        // 細粒度權限
        Set<String> auths = this.authority.auths;
        // 非空時
        if (!CollectionUtils.isEmpty(auths)) {
            for (String auth : auths) {
                res.append(auth).append("','");
            }
            // 循環結果後,生成類似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' l ',' m ',' n ','
        }

        // 如果res不爲空,去掉最後多出的三個字符 ','
        int len = res.length();
        if (len > 3) {
            res.delete(len - 3, len);
        }

        return res.toString();
    }
}

在這裏插入圖片描述

在這裏插入圖片描述

package com.tuwer.config.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * <p>屬性配置類</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperty {
    /*
    ======= 配置示例 ======
    # 自定義 jwt 配置
    jwt:
        cert-info:
            # 證書存放位置
            public-key-location: myKey.cer
        claims:
            # 令牌的鑑發方:即授權服務器的地址
            issuer: http://os:9000
    */
    /**
     * 證書信息(內部靜態類)
     * 證書存放位置...
     */
    private CertInfo certInfo;

    /**
     * 證書聲明(內部靜態類)
     * 發證方...
     */
    private Claims claims;

    @Data
    public static class Claims {
        /**
         * 發證方
         */
        private String issuer;
        /**
         * 有效期
         */
        //private Integer expiresAt;
    }

    @Data
    public static class CertInfo {
        /**
         * 證書存放位置
         */
        private String publicKeyLocation;
    }
}
  • 解碼器自動配置類
package com.tuwer.config;

import com.nimbusds.jose.jwk.RSAKey;
import com.tuwer.config.property.JwtProperty;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

import javax.annotation.Resource;
import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;

/**
 * <p>自定義jwt解碼器</p>
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@EnableConfigurationProperties(JwtProperty.class)
@Configuration
public class JwtDecoderConfiguration {
    /**
     * 注入 JwtProperties 屬性配置類
     */
    @Resource
    private JwtProperty jwtProperty;

    /**
     *  校驗jwt發行者 issuer 是否合法
     *
     * @return the jwt issuer validator
     */
    @Bean
    JwtIssuerValidator jwtIssuerValidator() {
        return new JwtIssuerValidator(this.jwtProperty.getClaims().getIssuer());
    }

/*
    *
     *  校驗jwt是否過期
     *
     * @return the jwt timestamp validator
*/
/*    @Bean
    JwtTimestampValidator jwtTimestampValidator() {
        System.out.println("檢測令牌是否過期!"+ LocalDateTime.now());
        return new JwtTimestampValidator(Duration.ofSeconds((long) this.jwtProperties.getClaims().getExpiresAt()));
    }*/

    /**
     * jwt token 委託校驗器,集中校驗的策略{@link OAuth2TokenValidator}
     *
     * // @Primary:自動裝配時當出現多個Bean候選者時,被註解爲@Primary的Bean將作爲首選者,否則將拋出異常
     * @param tokenValidators the token validators
     * @return the delegating o auth 2 token validator
     */
    @Primary
    @Bean({"delegatingTokenValidator"})
    public DelegatingOAuth2TokenValidator<Jwt> delegatingTokenValidator(Collection<OAuth2TokenValidator<Jwt>> tokenValidators) {
        return new DelegatingOAuth2TokenValidator<>(tokenValidators);
    }

    /**
     * 基於Nimbus的jwt解碼器,並增加了一些自定義校驗策略
     *
     * // @Qualifier 當有多個相同類型的bean存在時,指定注入
     * @param validator DelegatingOAuth2TokenValidator<Jwt> 委託token校驗器
     * @return the jwt decoder
     */
    @SneakyThrows
    @Bean
    public JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator")
                                         DelegatingOAuth2TokenValidator<Jwt> validator) {
        // 指定 X.509 類型的證書工廠
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        // 讀取cer公鑰證書來配置解碼器
        String publicKeyLocation = this.jwtProperty.getCertInfo().getPublicKeyLocation();
        // 獲取證書文件輸入流
        ClassPathResource resource = new ClassPathResource(publicKeyLocation);
        InputStream inputStream = resource.getInputStream();
        // 得到證書
        X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
        // 解析
        RSAKey rsaKey = RSAKey.parse(certificate);
        // 得到公鑰
        RSAPublicKey key = rsaKey.toRSAPublicKey();
        // 構造解碼器
        NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(key).build();
        // 注入自定義JWT校驗邏輯
        nimbusJwtDecoder.setJwtValidator(validator);
        return nimbusJwtDecoder;
    }
}
  • 主自動配置類:安全權限等
package com.tuwer.config;

import com.tuwer.config.handler.SimpleAccessDeniedHandler;
import com.tuwer.config.handler.SimpleAuthenticationEntryPoint;
import com.tuwer.config.property.AuthProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

import javax.annotation.Resource;

/**
 * <p>資源服務器配置</p>
 * 當解碼器JwtDecoder存在時生效
 *
 * @author 土味兒
 * Date 2022/5/11
 * @version 1.0
 */
@ConditionalOnBean(JwtDecoder.class)
@EnableConfigurationProperties(AuthProperty.class)
@Configuration
public class AutoConfiguration {
    @Resource
    private AuthProperty authProperty;
    /**
     * 資源管理器配置
     *
     * @param http the http
     * @return the security filter chain
     * @throws Exception the exception
     */
    @Bean
    SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
        // 拒絕訪問處理器 401
        SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler();
        // 認證失敗處理器 403
        SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();

        return http
                // security的session生成策略改爲security不主動創建session即STALELESS
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                // 允許【pc客戶端】或【其它微服務】訪問
                .authorizeRequests()
                //.antMatchers("/**").hasAnyAuthority("SCOPE_client_pc","SCOPE_micro_service")
                // 從配置文件中讀取權限信息
                .antMatchers("/**").hasAnyAuthority(authProperty.getAllAuth())
                // 其餘請求都需要認證
                .anyRequest().authenticated()
             .and()
                // 異常處理
                .exceptionHandling(exceptionConfigurer -> exceptionConfigurer
                        // 拒絕訪問
                        .accessDeniedHandler(accessDeniedHandler)
                        // 認證失敗
                        .authenticationEntryPoint(authenticationEntryPoint)
                )
                // 資源服務
                .oauth2ResourceServer(resourceServer -> resourceServer
                        .accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .jwt()
                )
                .build();
    }


    /**
     * JWT個性化解析
     *
     * @return
     */
    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
//        如果不按照規範  解析權限集合Authorities 就需要自定義key
//        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
//        OAuth2 默認前綴是 SCOPE_     Spring Security 是 ROLE_
//        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        // 用戶名 可以放sub
        jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);
        return jwtAuthenticationConverter;
    }

    /**
     * 開放一些端點的訪問控制
     * 不需要認證就可以訪問的端口
     * @return
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().antMatchers(
                "/actuator/**"
        );
    }
}
  • spring.factories

指明自動配置類的地址,在 resources 目錄下編寫一個自己的 META-INF\spring.factories;有兩個自動配置類,中間用逗號分開

注意點

如果同一個組中有多個starter,自動配置類名稱不要相同;如果相同,將只有一個配置類生效,其餘的將失效。

如:

  • starterA中:com.tuwer.config.AutoConfiguration

  • starterB中:就不要再用 com.tuwer.config.AutoConfiguration 名稱,可以改爲 com.tuwer.config.AutoConfigurationB

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.tuwer.config.JwtDecoderConfiguration,\
  com.tuwer.config.AutoConfiguration
  • application.yml

令牌、權限的配置可以放在引用starter的資源服務中;如果每個資源服務的配置都一樣,可以放在starter中

# 自定義 jwt 配置(校驗jwt)
jwt:
  cert-info:
    # 公鑰證書存放位置
    public-key-location: myjks.cer
  claims:
    # 令牌的鑑發方:即授權服務器的地址
    issuer: http://os.com:9000

# 自定義權限配置
resource-auth:
  # 權限
  authority:
    # 角色名稱;不用加ROlE_,提取用戶角色權限時,自動加
    roles:
    # 授權範圍;不用加SCOPE_,保持與認證中心中定義的一致即可;
    # 後臺自動加 SCOPE_
    scopes:
      - client_pc
      - micro_service
    # 細粒度權限
    auths:
  • 公鑰

把認證中心的公鑰文件myjks.cer放到resources目錄下

4、install

把starter安裝install到本地maven倉庫中

在這裏插入圖片描述

在這裏插入圖片描述

三、使用starter

1、引入starter依賴

在資源服務中引入 tuwer-oauth2-config-spring-boot-starter

在這裏插入圖片描述

2、application.yml

在這裏插入圖片描述

3、刪除資源服務中原文件

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章