<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    擁抱 Spring 全新 OAuth 解決方案:spring-authorization-server 該怎么玩?

    VSole2023-01-11 10:22:09

    前言

    為什么使用spring-authorization-server?

    真實原因:原先是因為個人原因,需要研究新版鑒權服務,看到了spring-authorization-server,使用過程中,想著能不能整合新版本cloud,因此此處先以springboot搭建spring-authorization-server,后續再替換為springcloud2021。

    官方原因:原先使用Spring Security OAuth,而該項目已經逐漸被淘汰,雖然網上還是有不少該方案,但秉著技術要隨時代更新,從而使用spring-authorization-server

    Spring 團隊正式宣布 Spring Security OAuth 停止維護,該項目將不會再進行任何的迭代

    項目構建

    以springboot搭建spring-authorization-server(即認證與資源服務器)

    數據庫相關表結構構建

    需要創建3張表,sql分別如下

    CREATE TABLE `oauth2_authorization`  (
      `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `attributes` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `authorization_code_value` blob NULL,
      `authorization_code_issued_at` timestamp(0) NULL DEFAULT NULL,
      `authorization_code_expires_at` timestamp(0) NULL DEFAULT NULL,
      `authorization_code_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `access_token_value` blob NULL,
      `access_token_issued_at` timestamp(0) NULL DEFAULT NULL,
      `access_token_expires_at` timestamp(0) NULL DEFAULT NULL,
      `access_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `oidc_id_token_value` blob NULL,
      `oidc_id_token_issued_at` timestamp(0) NULL DEFAULT NULL,
      `oidc_id_token_expires_at` timestamp(0) NULL DEFAULT NULL,
      `oidc_id_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `refresh_token_value` blob NULL,
      `refresh_token_issued_at` timestamp(0) NULL DEFAULT NULL,
      `refresh_token_expires_at` timestamp(0) NULL DEFAULT NULL,
      `refresh_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
     
     
    CREATE TABLE `oauth2_authorization_consent`  (
      `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
     
     
     
    CREATE TABLE `oauth2_registered_client`  (
      `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `client_id_issued_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
      `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `client_secret_expires_at` timestamp(0) NULL DEFAULT NULL,
      `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
      `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
    
    先進行認證服務器相關配置

    pom.xml引入依賴

    注意!!!spring boot版本需2.6.x以上,是為后面升級成cloud做準備
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <version>1.18.22version>
    dependency>
     
    <dependency>
        <groupId>com.xxxx.iovgroupId>
        <artifactId>iov-cloud-framework-webartifactId>
        <version>2.0.0-SNAPSHOTversion>
        <exclusions>
            
            <exclusion>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            exclusion>
        exclusions>
    dependency>
     
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
        <version>2.6.6version>
    dependency>
     
    <dependency>
        <groupId>cn.hutoolgroupId>
        <artifactId>hutool-allartifactId>
        <version>5.8.0version>
    dependency>
     
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>fastjsonartifactId>
        <version>1.2.39version>
    dependency>
     
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
     
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-oauth2-authorization-serverartifactId>
        <version>0.2.3version>
    dependency>
     
    <dependency>
        <groupId>org.springframework.securitygroupId>
        <artifactId>spring-security-casartifactId>
    dependency>
     
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-thymeleafartifactId>
    dependency>
     
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druid-spring-boot-starterartifactId>
        <version>1.2.9version>
    dependency>
     
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>8.0.28version>
    dependency>
     
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.5.1version>
    dependency>
     
    <dependency>
        <groupId>com.google.guavagroupId>
        <artifactId>guavaartifactId>
        <version>31.1-jreversion>
    dependency>
    

    創建自定義登錄頁面 login.html (可不要,使用自帶的登錄界面)

    html>
    <html lang="en"
          xmlns:th="https://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
    <head>
        <meta charset="utf-8">
        <meta name="author" content="test">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <meta name="description" content="This is a login page template based on Bootstrap 5">
        <title>Login Pagetitle>
        <style>
            .is-invalid {
                color: red;
            }
     
            .invalid-feedback {
                color: red;
            }
     
            .mb-3 {
                margin-bottom: 3px;
            }
        style>
        <script th:inline="javascript">
            /*        // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/
            if (window !== top) {
                top.location.href = location.href;
            }
        script>
    head>
    <body class="hold-transition login-page">
    <div class="login-box">
        <div class="card">
            <div class="card-body login-card-body">
                <p class="login-box-msg">Sign in to start your sessionp>
                <div th:if="${param.error}" class="alert alert-error">
                    Invalid username and password.
                div>
                <div th:if="${param.logout}" class="alert alert-success">
                    You have been logged out.
                div>
                <form th:action="@{/login}" method="post" id="loginForm">
                    <div class="input-group mb-3">
                        <input type="text" class="form-control" value="zxg" name="username" placeholder="Email"
                               autocomplete="off">
                    div>
                    <div class="input-group mb-3">
                        <input type="password" id="password" name="password" value="123" class="form-control"
                               maxlength="25" placeholder="Password"
                               autocomplete="off">
                    div>
                    <div class="row">
                        <div class="col-4">
                            <button type="submit" id="submitBtn">Sign Inbutton>
                        div>
                    div>
                form>
                <p class="mb-1">
                    <a href="javascript:void(0)">I forgot my passworda>
                p>
                <p class="mb-0">
                    <a href="javascript:void(0)" class="text-center">Register a new membershipa>
                p>
            div>
        div>
    div>
     
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js">script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js">script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js">script>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js">script>
     
    <script th:inline="javascript">
     
        $(function () {
            var encrypt = new JSEncrypt();
     
            $.validator.setDefaults({
                submitHandler: function (form) {
                    console.log("Form successful submitted!");
                    form.submit();
                }
            });
     
        });
    script>
    body>
    html>
    

    創建自定義授權頁面 consent.html(可不要,可使用自帶的授權頁面)

    html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" 
              integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
        <title>授權頁面title>
        <style>
            body {
                background-color: aliceblue;
            }
        style>
       <script>
          function cancelConsent() {
             document.consent_form.reset();
             document.consent_form.submit();
          }
       script>
    head>
    <body>
    <div class="container">
        <div class="py-5">
            <h1 class="text-center text-primary">用戶授權確認h1>
        div>
        <div class="row">
            <div class="col text-center">
                <p>
                    應用
                    <a >span>a>
                    想要訪問您的賬號
                    <span class="font-weight-bold" th:text="${principalName}">span>
                p>
            div>
        div>
        <div class="row pb-3">
            <div class="col text-center"><p>上述應用程序請求以下權限<br/>請審閱以下選項并勾選您同意的權限p>div>
        div>
        <div class="row">
            <div class="col text-center">
                <form name="consent_form" method="post" action="/oauth2/authorize">
                    <input type="hidden" name="client_id" th:value="${clientId}">
                    <input type="hidden" name="state" th:value="${state}">
     
                    <div th:each="scope: ${scopes}" class="form-group form-check py-1">
                        <input class="form-check-input"
                               type="checkbox"
                               name="scope"
                               th:value="${scope.scope}"
                               th:id="${scope.scope}">
                        <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label>
                        <p class="text-primary" th:text="${scope.description}">p>
                    div>
     
                    <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已對上述應用授予以下權限:p>
                    <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1">
                        <input class="form-check-input"
                               type="checkbox"
                               th:id="${scope.scope}"
                               disabled
                               checked>
                        <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label>
                        <p class="text-primary" th:text="${scope.description}">p>
                    div>
     
                    <div class="form-group pt-3">
                        <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
                            同意授權
                        button>
                    div>
                    <div class="form-group">
                        <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();">
                            取消授權
                        button>
                    div>
                form>
            div>
        div>
        <div class="row pt-4">
            <div class="col text-center">
                <p>
                    <small>
                        需要您同意并提供訪問權限。
                        <br/>如果您不同意,請單擊<span class="font-weight-bold text-primary">取消授權span>,將不會為上述應用程序提供任何您的信息。
                    small>
                p>
            div>
        div>
    div>
    body>
    html>
    

    修改配置文件 application.yml(配置內容可自行簡略)

    server:
      port: 9000
     
    spring:
      application:
        name: authorization-server
      thymeleaf:
        cache: false
      datasource:
        url: jdbc:mysql://192.168.1.69:3306/test
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: http://127.0.0.1:9000  #認證中心端點,作為資源端的配置
              
    application:
      security:
        excludeUrls: #excludeUrls中存放白名單地址
          - "/favicon.ico" 
     
    # mybatis plus配置
    mybatis-plus:
      mapper-locations: classpath:/mapper/*Mapper.xml
      global-config:
        # 關閉MP3.0自帶的banner
        banner: false
        db-config:
          #主鍵類型  0:"數據庫ID自增", 1:"不操作", 2:"用戶輸入ID",3:"數字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake";
          id-type: AUTO
          #字段策略
          insert-strategy: not_null
          update-strategy: not_null
          select-strategy: not_null
          #駝峰下劃線w轉換
          table-underline: true
          # 邏輯刪除配置
          # 邏輯刪除全局值(1表示已刪除,這也是Mybatis Plus的默認配置)
          logic-delete-value: 1
          # 邏輯未刪除全局值(0表示未刪除,這也是Mybatis Plus的默認配置)
          logic-not-delete-value: 0
      configuration:
        #駝峰
        map-underscore-to-camel-case: true
        #打開二級緩存
        cache-enabled: true
        # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #開啟sql日志
    

    新增認證服務器配置文件 AuthorizationServerConfig

    @Configuration(proxyBeanMethods = false)
    public class AuthorizationServerConfig {
        /**
         * 自定義授權頁面
         * 使用系統自帶的即不用
         */
        private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
     
        /**
         * 自定義UserDetailsService
         */
        @Autowired
        private UserService userService;
     
     
        /**
         *
         * 使用默認配置進行form表單登錄
         * OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
         */
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
            OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
     
            authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));
     
            RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
     
            http
                    .requestMatcher(endpointsMatcher)
                    .userDetailsService(userService)
                    .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
                    .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                    .apply(authorizationServerConfigurer);
            return http.formLogin(Customizer.withDefaults()).build();
        }
     
        /**
         * 注冊客戶端應用
         */
        @Bean
        public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
            // Save registered client in db as if in-jdbc
            RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                    .clientId("zxg")
                    .clientSecret("123")
                    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                    .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                    .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                    // 回調地址
                    .redirectUri("http://www.baidu.com")
                    // scope自定義的客戶端范圍
                    .scope(OidcScopes.OPENID)
                    .scope("message.read")
                    .scope("message.write")
                    // client請求訪問時需要授權同意
                    .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                    // token配置項信息
                    .tokenSettings(TokenSettings.builder()
                            // token有效期100分鐘
                            .accessTokenTimeToLive(Duration.ofMinutes(100L))
                            // 使用默認JWT相關格式
                            .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                            // 開啟刷新token
                            .reuseRefreshTokens(true)
                            // refreshToken有效期120分鐘
                            .refreshTokenTimeToLive(Duration.ofMinutes(120L))
                            .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
                    )
                    .build();
     
            // Save registered client in db as if in-memory
            JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
            registeredClientRepository.save(registeredClient);
            return registeredClientRepository;
        }
     
        /**
         * 授權服務:管理OAuth2授權信息服務
         */
        @Bean
        public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
            return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
        }
     
        /**
         * 授權確認信息處理服務
         */
        @Bean
        public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
            return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
        }
     
        /**
         * 加載JWK資源
         * JWT:指的是 JSON Web Token,不存在簽名的JWT是不安全的,存在簽名的JWT是不可竄改的
         * JWS:指的是簽過名的JWT,即擁有簽名的JWT
         * JWK:既然涉及到簽名,就涉及到簽名算法,對稱加密還是非對稱加密,那么就需要加密的 密鑰或者公私鑰對。此處我們將 JWT的密鑰或者公私鑰對統一稱為 JSON WEB KEY,即 JWK。
         */
        @Bean
        public JWKSource jwkSource() {
            RSAKey rsaKey = JwksUtils.generateRsa();
            JWKSet jwkSet = new JWKSet(rsaKey);
            return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
        }
     
        /**
         * 配置 OAuth2.0 提供者元信息
         */
        @Bean
        public ProviderSettings providerSettings() {
            return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
        }
     
    }
    

    新增Security的配置文件WebSecurityConfig

    @Configuration
    @EnableWebSecurity(debug = true) //開啟Security
    public class WebSecurityConfig {
        @Autowired
        private ApplicationProperties properties;
     
        /**
         * 設置加密方式
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
    //        // 將密碼加密方式采用委托方式,默認以BCryptPasswordEncoder方式進行加密,兼容ldap,MD4,MD5等方式
    //        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
     
            // 此處我們使用明文方式 不建議這樣
            return NoOpPasswordEncoder.getInstance();
        }
     
        /**
         * 使用WebSecurity.ignoring()忽略某些URL請求,這些請求將被Spring Security忽略
         */
        @Bean
        WebSecurityCustomizer webSecurityCustomizer() {
            return new WebSecurityCustomizer() {
                @Override
                public void customize(WebSecurity web) {
                    // 讀取配置文件application.security.excludeUrls下的鏈接進行忽略
                    web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{}));
                }
            };
        }
     
        /**
         * 針對http請求,進行攔截過濾
         *
         * CookieCsrfTokenRepository進行CSRF保護的工作方式:
         *      1.客戶端向服務器發出GET請求,例如請求主頁
         *      2.Spring發送 GET 請求的響應以及 Set-cookie 標頭,其中包含安全生成的XSRF令牌
         */
        @Bean
        public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                    .authorizeRequests(authorizeRequests ->
                            authorizeRequests.antMatchers("/login").permitAll()
                                    .anyRequest().authenticated()
                    )
     
                    //使用默認登錄頁面
                    //.formLogin(withDefaults())
     
                    //設置form登錄,設置且放開登錄頁login
                    .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())
     
                    // Spring Security CSRF保護
                    .csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository()))
                    
    //                 //開啟認證服務器的資源服務器相關功能,即需校驗token
    //                .oauth2ResourceServer()
    //                .accessDeniedHandler(new SimpleAccessDeniedHandler())
    //                .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
    //                .jwt()
            ;
            return httpSecurity.build();
        }
     
    }
    

    新增讀取application配置的類 ApplicationProperties

    /**
    * 此步主要是獲取配置文件中配置的白名單,可自行舍去或自定義實現其他方式
    **/
    @Data
    @Component
    @ConfigurationProperties("application")
    public class ApplicationProperties {
        private final Security security = new Security();
     
        @Data
        public static class Security {
            private Oauth2 oauth2;
            private List excludeUrls = new ArrayList<>();
     
            @Data
            public static class Oauth2 {
                private String issuerUrl;
     
            }
        }
    }
    

    新增 JwksUtils 類和 KeyGeneratorUtils,這兩個類作為JWT對稱加密

    public final class JwksUtils {
     
        private JwksUtils() {
        }
     
        /**
         * 生成RSA加密key (即JWK)
         */
        public static RSAKey generateRsa() {
            // 生成RSA加密的key
            KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
            // 公鑰
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
            // 私鑰
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
            // 構建RSA加密key
            return new RSAKey.Builder(publicKey)
                    .privateKey(privateKey)
                    .keyID(UUID.randomUUID().toString())
                    .build();
        }
     
        /**
         * 生成EC加密key (即JWK)
         */
        public static ECKey generateEc() {
            // 生成EC加密的key
            KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
            // 公鑰
            ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
            // 私鑰
            ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
            // 根據公鑰參數生成曲線
            Curve curve = Curve.forECParameterSpec(publicKey.getParams());
            // 構建EC加密key
            return new ECKey.Builder(curve, publicKey)
                    .privateKey(privateKey)
                    .keyID(UUID.randomUUID().toString())
                    .build();
        }
     
        /**
         * 生成HmacSha256密鑰
         */
        public static OctetSequenceKey generateSecret() {
            SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
            return new OctetSequenceKey.Builder(secretKey)
                    .keyID(UUID.randomUUID().toString())
                    .build();
        }
    }
     
     
    class KeyGeneratorUtils {
     
        private KeyGeneratorUtils() {
        }
     
        /**
         * 生成RSA密鑰
         */
        static KeyPair generateRsaKey() {
            KeyPair keyPair;
            try {
                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                keyPairGenerator.initialize(2048);
                keyPair = keyPairGenerator.generateKeyPair();
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
            return keyPair;
        }
     
        /**
         * 生成EC密鑰
         */
        static KeyPair generateEcKey() {
            EllipticCurve ellipticCurve = new EllipticCurve(
                    new ECFieldFp(
                            new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
                    new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
                    new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
            ECPoint ecPoint = new ECPoint(
                    new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
                    new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
            ECParameterSpec ecParameterSpec = new ECParameterSpec(
                    ellipticCurve,
                    ecPoint,
                    new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
                    1);
     
            KeyPair keyPair;
            try {
                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
                keyPairGenerator.initialize(ecParameterSpec);
                keyPair = keyPairGenerator.generateKeyPair();
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
            return keyPair;
        }
     
        /**
         * 生成HmacSha256密鑰
         */
        static SecretKey generateSecretKey() {
            SecretKey hmacKey;
            try {
                hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
            return hmacKey;
        }
    }
    

    新建 ConsentController,編寫登錄和認證頁面的跳轉

    如果在上面沒有使用自定義的登錄和授權頁面,下面的跳轉方法按需舍去

    @Slf4j
    @Controller
    public class ConsentController {
     
        private final RegisteredClientRepository registeredClientRepository;
        private final OAuth2AuthorizationConsentService authorizationConsentService;
     
        public ConsentController(RegisteredClientRepository registeredClientRepository,
                                 OAuth2AuthorizationConsentService authorizationConsentService) {
            this.registeredClientRepository = registeredClientRepository;
            this.authorizationConsentService = authorizationConsentService;
        }
     
        @ResponseBody
        @GetMapping("/favicon.ico")
        public String faviconico(){
            return "favicon.ico";
        }
     
        @GetMapping("/login")
        public String loginPage(){
            return "login";
        }
     
        @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) {
     
            // Remove scopes that were already approved
            Set scopesToApprove = new HashSet<>();
            Set previouslyApprovedScopes = new HashSet<>();
            RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
            OAuth2AuthorizationConsent currentAuthorizationConsent =
                    this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
            Set authorizedScopes;
            if (currentAuthorizationConsent != null) {
                authorizedScopes = currentAuthorizationConsent.getScopes();
            } else {
                authorizedScopes = Collections.emptySet();
            }
            for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
                if (authorizedScopes.contains(requestedScope)) {
                    previouslyApprovedScopes.add(requestedScope);
                } else {
                    scopesToApprove.add(requestedScope);
                }
            }
     
            model.addAttribute("clientId", clientId);
            model.addAttribute("state", state);
            model.addAttribute("scopes", withDescription(scopesToApprove));
            model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
            model.addAttribute("principalName", principal.getName());
     
            return "consent";
        }
     
        private static Set withDescription(Set scopes) {
            Set scopeWithDescriptions = new HashSet<>();
            for (String scope : scopes) {
                scopeWithDescriptions.add(new ScopeWithDescription(scope));
     
            }
            return scopeWithDescriptions;
        }
     
        public static class ScopeWithDescription {
            private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
            private static final Map scopeDescriptions = new HashMap<>();
            static {
                scopeDescriptions.put(
                        "message.read",
                        "This application will be able to read your message."
                );
                scopeDescriptions.put(
                        "message.write",
                        "This application will be able to add new messages. It will also be able to edit and delete existing messages."
                );
                scopeDescriptions.put(
                        "other.scope",
                        "This is another scope example of a scope description."
                );
            }
     
            public final String scope;
            public final String description;
     
            ScopeWithDescription(String scope) {
                this.scope = scope;
                this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
            }
        }
     
    }
    

    新建 UserController,User,UserService等標準的自定義用戶業務,此處僅放出UserServiceImpl

    @RequiredArgsConstructor
    @Slf4j
    @Component
    class UserServiceImpl implements UserService {
        private final UserMapper userMapper;
     
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getUsername,username));
            return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
        }
    }
    

    啟動項目,如下圖

    認證服務器整體結構圖

    資源服務器相關配置

    pom.xml引入資源服務器相關依賴

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
    dependency>
     
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    

    新增配置文件 application.yaml

    server:
      port: 9003
    spring:
      application:
        name: resource
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: http://127.0.0.1:9000
    feign:
      client:
        config:
          default: #配置超時時間
            connect-timeout: 10000
            read-timeout: 10000
    

    新增資源服務器配置文件 ResourceServerConfiguration

    @Configuration
    @EnableWebSecurity(debug = true)
    @EnableGlobalMethodSecurity(prePostEnabled = true) //開啟鑒權服務
    public class ResourceServerConfiguration {
     
        @Bean
        public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
            // 所有請求都進行攔截
            httpSecurity.authorizeRequests().anyRequest().authenticated();
            // 關閉session
            httpSecurity.sessionManagement().disable();
            // 配置資源服務器的無權限,無認證攔截器等 以及JWT驗證
            httpSecurity.oauth2ResourceServer()
                    .accessDeniedHandler(new SimpleAccessDeniedHandler())
                    .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
                    .jwt();
            return httpSecurity.build();
        }
     
    }
    

    新增相關無認證無權限統一攔截回復 SimpleAccessDeniedHandlerSimpleAuthenticationEntryPoint

    /**
     * 攜帶了token 而且token合法 但是權限不足以訪問其請求的資源 403
     * @author zxg
     */
    public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
     
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setCharacterEncoding("utf-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            ObjectMapper objectMapper = new ObjectMapper();
            String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("無權訪問"));
            PrintWriter printWriter = response.getWriter();
            printWriter.print(resBody);
            printWriter.flush();
            printWriter.close();
        }
    }
     
     
    /**
     * 在資源服務器中 不攜帶token 或者token無效  401
     * @author zxg
     */
    @Slf4j
    public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            if (response.isCommitted()){
                return;
            }
     
            Throwable throwable = authException.fillInStackTrace();
     
            String errorMessage = "認證失敗";
     
            if (throwable instanceof BadCredentialsException){
                errorMessage = "錯誤的客戶端信息";
            }else {
                Throwable cause = authException.getCause();
     
                if (cause instanceof JwtValidationException) {
                    log.warn("JWT Token 過期,具體內容:" + cause.getMessage());
                    errorMessage = "無效的token信息";
                } else if (cause instanceof BadJwtException){
                    log.warn("JWT 簽名異常,具體內容:" + cause.getMessage());
                    errorMessage = "無效的token信息";
                } else if (cause instanceof AccountExpiredException){
                    errorMessage = "賬戶已過期";
                } else if (cause instanceof LockedException){
                    errorMessage = "賬戶已被鎖定";
    //            } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){
    //                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"無效的客戶端")));
    //            } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){
    //                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("無效的類型")));
    //            } else if (cause instanceof UnauthorizedClientException) {
    //                response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未經授權的客戶端")));
                } else if (throwable instanceof InsufficientAuthenticationException) {
                    String message = throwable.getMessage();
                    if (message.contains("Invalid token does not contain resource id")){
                        errorMessage = "未經授權的資源服務器";
                    }else if (message.contains("Full authentication is required to access this resource")){
                        errorMessage = "缺少驗證信息";
                    }
                }else {
                    errorMessage = "驗證異常";
                }
            }
     
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setCharacterEncoding("utf-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            ObjectMapper objectMapper = new ObjectMapper();
            String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage));
            PrintWriter printWriter = response.getWriter();
            printWriter.print(resBody);
            printWriter.flush();
            printWriter.close();
        }
    }
    

    新增 ResourceController 進行接口測試

    @Slf4j
    @RestController
    public class ResourceController {
     
        /**
         * 測試Spring Authorization Server,測試權限
         */
        @PreAuthorize("hasAuthority('SCOPE_message.read')")
        @GetMapping("/getTest")
        public String getTest(){
            return "getTest";
        }
     
        /**
         * 默認登錄成功跳轉頁為 /  防止404狀態
         *
         * @return the map
         */
        @GetMapping("/")
        public Map index() {
            return Collections.singletonMap("msg", "login success!");
        }
     
        @GetMapping("/getResourceTest")
        public SingleResultBundle getResourceTest(){
            return SingleResultBundle.success("這是resource的測試方法 getResourceTest()");
        }
    }
    

    啟動項目,效果如下

    項目總體結構如下

    測試認證鑒權
    #調用 /oauth2/authorize ,獲取code
    http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com
    #會判斷是否登錄,若沒有,則跳轉到登錄頁面,如下圖1
    #登錄完成后,會提示是否授權,若沒有,則跳轉到授權界面,如下圖2
    #授權成功后,跳轉到回調地址,并帶上code,如圖3
    

    打開postman,進行獲取access_token

    #訪問 /oauth2/token 地址
    #在Authorization中選擇Basic Auth模式,填入對應客戶端,其會在header中生成Authorization,如下圖右側
    

    返回結果如下

    調用ResourceController中的接口,測試token是否生效

    源碼下載地址

    • https://gitee.com/rjj521/authorization-server-learn

    總結

    至此,spring-authorization-server的基礎使用已完成,總體上和原Spring Security OAuth大差不差,個別配置項不同。期間在網上搜尋了很多資料,然后進行整合,因此文中存在與其他網上教程相同代碼,如有爭議,請聯系我刪除改正,謝謝。

    來源:blog.csdn.net/qq_37182370/article/
    details/124822587
    
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    前言為什么使用spring-authorization-server?真實原因:原先是因為個人原因,需要研究新版鑒權服務,看到了spring-authorization-server,使用過程中,想著能不能整合新版本cloud,因此此處先以springboot搭建spring-authorization-server,后續再替換為springcloud2021。官方原因:原先使用Spring Security OAuth,而項目已經逐漸被淘汰,雖然網上還是有不少方案,但秉著技術要隨時代更新,從而使用spring-authorization-serverSpring 團隊正式宣布 Spring Security OAuth 停止維護,項目將不會再進行任何的迭代項目構建以springboot搭建spring-authorization-server數據庫相關表結構構建需要創建3張表,sql分別如下CREATE?
    用戶名:加密密碼:密碼最后一次修改日期:兩次密碼的修改時間間隔:密碼有效期:密碼修改到期到的警告天數:密碼過期之后的寬限天數:賬號失效時間:保留。查看下pid所對應的進程文件路徑,
    GoldPickaxe.iOS 采用了一種全新的分發方式,利用 Apple 的移動應用測試平臺 TestFlight 進行傳播。平臺將其刪除后,攻擊者采用多階段的社會工程學方式說服受害者安裝移動設備管理(MDM)配置文件,借此完全控制受害者的設備。
    十大網絡暴力事件
    2022-07-26 11:11:34
    ColonialPipeline在當地時間周五 因受到勒索軟件攻擊,被迫關閉其美國東部沿海各州供油的關鍵燃油網絡。事件涉事的黑客團隊DarkSide索要高達數百萬美元虛擬幣。事件也是2021年造成實質影響最大的網絡安全事件。否則一旦遭到破壞或數據泄露,將會造成嚴重后果。案件是我國迄今為止查獲單體數據泄露最大案件。
    Spring Cloud 突發漏洞 Log4j2 的核彈級漏洞剛告一段落,Spring Cloud Gateway 又突發高危漏洞,又得折騰了。。。 2022年3月1日,Spring官方發布了關于Spring Cloud Gateway的兩個CVE漏洞,分別為CVE-2022-22946與CVE-2022-22947: 版本/分支/tag:3.4.X
    Log4j2 的核彈級漏洞剛告一段落,Spring Cloud Gateway 又突發高危漏洞,又得折騰了。。。 2022年3月1日,Spring官方發布了關于Spring Cloud Gateway的兩個CVE漏洞,分別為CVE-2022-22946與CVE-2022-22947: 版本/分支/tag:3.4.X 問題描述:
    近日,四葉草安全團隊監測發現 Spring框架曝出0day漏洞 , 漏洞可能已被遠程攻擊者利用,漏洞威脅等級: 高危、非常緊急。 Spring作為目前全球最受歡迎的Java輕量級開源框架,Spring開發人員專注于業務邏輯,簡化Java企業級應用的開發周期。 在Spring框架的JDK9版本(及以上版本)中,由于未對傳輸的數據進行有效的驗證,攻擊者可利用漏洞在未授權的情況下,構造惡意
    關于 Apache ActiveMQ 代碼問題漏洞情況的通報
    Java審計其實和Php審計的思路一樣,唯一不同的可能是復雜的框架和代碼。
    端口掃描開始,發現驚喜
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类