• (一)学习spring-cloud2021之spring-authorization-server


    1. 前言

    1.1为什么使用spring-authorization-server?

    真实原因:原先是因为个人原因,需要研究新版鉴权服务,看到了spring-authorization-server,使用过程中,想着能不能整合新版本cloud,因此此处先以springboot搭建spring-authorization-server,后续再替换为springcloud2021。

    官方原因:原先使用Spring Security OAuth,而该项目已经逐渐被淘汰,虽然网上还是有不少该方案,但秉着技术要随时代更新,从而使用spring-authorization-server

    2.项目迭代历程

    1. 以springboot搭建spring-authorization-server(即认证与资源服务器)

    2. 升级为springcloud2021.0.x框架,引入nacos作为注册中心

    3. 引入sentinel,进行限流降级熔断配置

    4. 引入nacos配置中心

    5. 升级sentinel,配合nacos,使项目能动态配置落地所有Feign的降级配置

    6. 引入gateway网关,swagger文档工具

    7. 待续

    3.项目构建

    3.1 以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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    先进行认证服务器相关配置

    pom.xml引入依赖

    注意!!!spring boot版本需2.6.x以上,是为后面升级成cloud做准备

    
        org.projectlombok
        lombok
        1.18.22
    
    
    
    
        com.xxxx.iov
        iov-cloud-framework-web
        2.0.0-SNAPSHOT
        
            
            
                org.springframework.boot
                spring-boot-starter-web
            
        
    
    
    
        org.springframework.boot
        spring-boot-starter-web
        2.6.6
    
    
    
    
        cn.hutool
        hutool-all
        5.8.0
    
    
    
    
        com.alibaba
        fastjson
        1.2.39
    
    
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
    
    
        org.springframework.security
        spring-security-oauth2-authorization-server
        0.2.3
    
    
    
    
        org.springframework.security
        spring-security-cas
    
    
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
    
    
    
    
        com.alibaba
        druid-spring-boot-starter
        1.2.9
    
    
    
    
        mysql
        mysql-connector-java
        8.0.28
    
    
    
    
        com.baomidou
        mybatis-plus-boot-starter
        3.5.1
    
    
    
    
        com.google.guava
        guava
        31.1-jre
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    创建自定义登录页面 login.html (可不要,使用自带的登录界面)

    
    
    
        
        
        
        
        Login Page
        
        
    
    
    
    
    
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    创建自定义授权页面 consent.html(可不要,可使用自带的授权页面)

    
    
    
        
        
        
        授权页面
        
       
    
    
    

    用户授权确认

    应用 想要访问您的账号

    上述应用程序请求以下权限
    请审阅以下选项并勾选您同意的权限

    您已对上述应用授予以下权限:

    需要您同意并提供访问权限。
    如果您不同意,请单击取消授权,将不会为上述应用程序提供任何您的信息。

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    修改配置文件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日志
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    新增认证服务器配置文件 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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    新增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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    新增读取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;
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    新增 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    新建 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);
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    新建 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<>());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    启动项目,如下图

    认证服务器整体结构图

    ----------------------------------------------------------------------------------------------------------------------------------------------------------

    资源服务器相关配置

    pom.xml引入资源服务器相关依赖

    
    
        org.springframework.boot
        spring-boot-starter-oauth2-resource-server
    
    
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    新增配置文件 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    新增资源服务器配置文件 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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    新增相关无认证无权限统一拦截回复 SimpleAccessDeniedHandler 和 SimpleAuthenticationEntryPoint

    /**
     * 携带了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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    新增 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()");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    启动项目,效果如下

    项目总体结构如下

    测试认证鉴权

    #调用 /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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    打开postman,进行获取access_token

    #访问 /oauth2/token 地址
    #在Authorization中选择Basic Auth模式,填入对应客户端,其会在header中生成Authorization,如下图右侧
    
    • 1
    • 2

    返回结果如下

    调用ResourceController中的接口,测试token是否生效

    源码下载地址

    应多位网友反应,上传源码,部分地方与上文代码有所出入
    authorization-server-demo: 学习authorization-server总结

    总结

    至此,spring-authorization-server的基础使用已完成,总体上和原Spring Security OAuth大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。由于不太会写文章,我就直接贴出代码,代码中我有加上注释,所以上述文章中,没有很具体的描述,基本就是个人开发流程,若文中有那里写不对,欢迎指教,不喜勿喷。

    关于升级springcloud2021.0.x部分,请查看后续相关文章

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    debian 11.3 国内最快镜像源
    前后端分离项目,vue+uni-app+php+mysql在线教育视频点播小程序系统 开题报告
    六、互联网技术——数据存储
    了解 Flutter 开发者们的 IDE 使用情况
    【一起入门DeepLearning】中科院深度学习_期末总复习
    《代码大全2》第14章 组织直线型代码
    金仓数据库KingbaseES ksql工具用户指南及参考--5. Ksql 命令参考
    Jmeter —— 自动录制脚本
    机车外套哈哈我真的是因为
    论基于架构的软件设计方法及应用
  • 原文地址:https://blog.csdn.net/m0_67402125/article/details/126066990