🍉Spring Authorization Server (2) 授权服务、资源服务、客户端核心配置讲解

4,563 阅读7分钟

此次搭建基于springboot3.0+,所以需要jdk17!!!

节省时间直接用 github 上基于maven已经搭建好的 sample-demo 工程直接运行即可;当然搭建直接copy 工程里面pom和核心配置更快(这部分就是cv大法)

以下先分别介绍下工程的各个模块

sample-demo

demo-authorizationserver 授权服务
messages-resource 资源服务
demo-client 客户端服务

sample-demo 这个工程的存在的作用——就是为了先感觉下 Spring Authorization Server 、Spring Security 做oauth2的认证授权流程。

demo-authorizationserver[授权服务]

核心配置

DefaultSecurityConfig

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {

   // 过滤器链
   @Bean
   public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
      http
            .authorizeHttpRequests(authorize ->//① 配置鉴权的
                  authorize
                        .requestMatchers("/assets/**", "/webjars/**", "/login","/oauth2/**","/oauth2/token").permitAll() //② 忽略鉴权的url
                        .anyRequest().authenticated()//③ 排除忽略的其他url就需要鉴权了
            )
            .csrf(AbstractHttpConfigurer::disable)
            .formLogin(formLogin ->
                  formLogin
                        .loginPage("/login")//④ 授权服务认证页面(可以配置相对和绝对地址,前后端分离的情况下填前端的url)
            )
            .oauth2Login(oauth2Login ->
                  oauth2Login
                        .loginPage("/login")//⑤ oauth2的认证页面(也可配置绝对地址)
                        .successHandler(authenticationSuccessHandler())//⑥ 登录成功后的处理 可以自定义,如果要开启oauth2认证,一定不要禁用
            );

      return http.build();
    }
    
    //...省略的不是非常关键的代码
}

@EnableWebSecurity:这是一个注解,用于启用Spring Security的功能,也就是配置Spring Security的默认安全配置。
@Configuration(proxyBeanMethods = false):这是一个注解,表示这个类是一个配置类,并且不会代理bean方法。

DefaultSecurityConfig 主要用于配置Spring Security的 默认安全设置,包括允许访问的URL、禁用CSRF保护、配置表单登录和OAuth2登录等。

AuthorizationServerConfig

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(
          HttpSecurity http, RegisteredClientRepository registeredClientRepository,
          AuthorizationServerSettings authorizationServerSettings) throws Exception {

       OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

       DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
             new DeviceClientAuthenticationConverter(
                   authorizationServerSettings.getDeviceAuthorizationEndpoint());
       DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
             new DeviceClientAuthenticationProvider(registeredClientRepository);

       // @formatter:off
       http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
             .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
                   deviceAuthorizationEndpoint.verificationUri("/activate")
             )
             .deviceVerificationEndpoint(deviceVerificationEndpoint ->
                   deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
             )
             .clientAuthentication(clientAuthentication ->
                   clientAuthentication
                         .authenticationConverter(deviceClientAuthenticationConverter)
                         .authenticationProvider(deviceClientAuthenticationProvider)
             )
             .authorizationEndpoint(authorizationEndpoint ->
                   authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
             .oidc(Customizer.withDefaults());    // Enable OpenID Connect 1.0
       // @formatter:on

       // @formatter:off
       http
             .exceptionHandling((exceptions) -> exceptions
                   .defaultAuthenticationEntryPointFor(
                         new LoginUrlAuthenticationEntryPoint("/login"),
                         new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                   )
             )
             .oauth2ResourceServer(oauth2ResourceServer ->
                   oauth2ResourceServer.jwt(Customizer.withDefaults()));
       // @formatter:on
       return http.build();
    }
}

AuthorizationServerConfig 一个Spring Security配置类,用于 配置OAuth2授权服务器的安全设置

DefaultSecurityConfig默认安全配置 ,因为是有@EnableWebSecurity 注解,默认安全配置-那可以理解为当前服务的安全配置扩展要在 DefaultSecurityConfig 里面的 SecurityFilterChain 过滤链中进行添加。-- 在后面的扩展默认安全配置篇章中可以看到。

AuthorizationServerConfig配置OAuth2授权服务器的安全设置 ,-可以理解为当前服务的 OAuth2授权服务的配置 扩展在 AuthorizationServerConfig 里面的 SecurityFilterChain 过滤链中进行添加,以上代码中就有体现。

总结

扩展OAuth2授权服务配置的class 要在 AuthorizationServerConfigSecurityFilterChain 构建过程中去添加。
扩展默认登录相关配置的class要在 DefaultSecurityConfigSecurityFilterChain 构建过程中去添加。

授权模式演示

授权码模式

/oauth2/token 接口参数说明

参数名示例值描述
grant_typeauthorization_code授权类型,固定为 "authorization_code"
codeyour_code_here授权服务器返回的授权码
redirect_uriyour_redirect_uri客户端事先注册的回调 URL
client_idyour_client_id客户端的标识

获取授权码[authorization_code] img_1.png
浏览器打开URL http://127.0.0.1:9000/oauth2/authorize?client_id=messaging-client&response_type=code&scope=message.read&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc
浏览器重定向到 /login 登录页面
img_2_1.png
登录
// 内存里初始化了一个用户,直接使用这个用户名和密码进行登录
.username("user1")
.password("password") img_2_1.png 登录成功后会进入授权页(oauth2/consent)【requireAuthorizationConsent(true)这个地方可以配置:[true 有授权页,false:无授权页] 备注:授权过一次就不会出现了,因为会持久化到数据库,目前demo基于H2重启后数据就没有了 】,进行确认授权 。 img_4.png
获取授权码-从URL上获取“code=”后面的值
http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=gMXmx2YHmwNFXMjXjgWaveNxspAEUy3q5Jc3m8h-NSjWzYbLAEmraDaUZkFhyBVTrRQlyDMWhePfEUe4Wb2g7sj1Q-mBq9HBTyCwL1qMvRNFEx-UTUtlVNFP7rZnfVWR img_5.png 获取token-POST 请求 /oauth2/token
参数值填充 img_5_1.png client信息也需要填入 img_5_2.png Token 获取成功 img_5_3.png

jwt解析网址 https://www.box3.cn/tools/jwt.html
一起看看demo img_5_4.png

刷新token

请求参数说明

参数名示例值描述
grant_typerefresh_token刷新token,固定为 "refresh_token"
refresh_tokenhn6N3M6UizLpp8MhyDcjF9qsqUixnzpZtqg_ToPcJWdVbQC4Y5n_wXQZXuchFWKHBLnD1GOBEhdusPxIAizOi7rYz1y-s8ex3bxSI0irgr8zf8QeNXvT6kz8u6ZFE8_/oauth2/token返回的refresh_token 值

refresh_token 请求URL http://127.0.0.1:9000/oauth2/token?grant_type=refresh_token&refresh_token=-hn6N3M6UizLpp8MhyDcjF9qsqUixnzpZtqg_ToPcJWdVbQC4Y5n_wXQZXuchFWKHBLnD1GOBEhdusPxIAizOi7rYz1y-s8ex3bxSI0irgr8zf8QeNXvT6kz8u6ZFE8
请求示例 img_6.png img_6_1.png img_6_2.png

设备码模式

参数说明

参数名示例值描述
grant_typedevice-messaging-client设备码类型,固定为 "device-messaging-client"
scopemessage.read作用域

请求URL http://127.0.0.1:9000/oauth2/device_authorization
img_7.png

          {
           "user_code": "VRFP-TJHW",
           "device_code": "4ZE1FZtPKBki3GVC-YBsnqhxjzQVwSTcBPchE_WKOQL4dg5qw9Z4-4NdTqLPPKknTOKPAy85_ASiKC6Ki-cBNDRLMIil9cK6Dj3HScx1CHvI3qlXDuCzsUk_0sQh-z6b",
           "verification_uri_complete": "http://127.0.0.1:9000/activate?user_code=VRFP-TJHW",
           "verification_uri": "http://127.0.0.1:9000/activate",
           "expires_in": 300
          }

verification_uri(验证user_code URL) http://127.0.0.1:9000/activate img_7_1.png 填入 user_code img_7_2.png 验证 user_code img_7_3.png
也可以直接用 verification_uri_complete 这个value ,从 http://127.0.0.1:9000/activate?user_code=VRFP-TJHW URL 上看出是携带了验证码(?user_code=VRFP-TJHW) 参数值。

messages-resource [资源服务]

携带token请求资源服务

资源服务的yml配置授权服务器的 jwt URL

   spring:
     security:
       oauth2:
         resourceserver:
            jwt:
              issuer-uri: http://127.0.0.1:9000 # 授权服务器(访问资源服务器时会到授权服务器中验>证token)

从授权服务器获取到token后,携带token访问资源服务器示例
img_8.png
用jwt工具看看“access_token”的值解析出来有什么

{
"sub": "user1",//用户名
"aud": "messaging-client",//客户端id
"nbf": 1693291796,
"scope": ["message.read"],
"iss": "http://127.0.0.1:9000",//授权服务器
"exp": 1693292096,
"iat": 1693291796
}

img_8_1.png

401异常

有一种情况,携带有效切正确的“access_token”会导致访问资源服务401,并非跨域问题导致,当资源服务器yml配置如下时

spring:
 security:
   oauth2:
     resourceserver:
        jwt:
          issuer-uri: http://localhost:9000

img_8_4.png img_8_3.png
spring.security.oauth2.resourceserver.jwt.issuer-uri配置值与“access_token”中"iss"值不一致,则会抛出 【 The iss claim is not valid】异常,最终导致401的问题。

如何解决这个问题呢?
在授权服务器获取 “access\_token” 时 请求使用的是哪一个域,spring.security.oauth2.resourceserver.jwt.issuer-uri配置值也使用同一个域

demo-client[客户端服务]

客户端配置 官方说明 URL docs.spring.io/spring-secu…

spring:
  security:
    oauth2:
      client:
        registration:
           messaging-client-oidc: # 这个是我们自己定义客户端
              provider: spring 
              client-id: messaging-client #客户端id
              client-secret: secret # 客户端id
              authorization-grant-type: authorization_code # 授权码模式
              redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}" # 这个是一个模板 {baseUrl}/login/oauth2/code/{registrationId} 官方说明:https://docs.spring.io/spring-security/reference/6.1-SNAPSHOT/servlet/oauth2/login/core.html
              scope: openid, profile
              client-name: messaging-client-oidc
        provider:
          spring:
            issuer-uri: http://localhost:9000 # 这个对应的是我们自己的授权服务器

运行sample-demo Oauth2 Demo

服务启动顺序

1. demo-authorizationserver[授权服务]
2. messages-resource[资源服务]
3. demo-client[客户端]
说明: messages-resource[资源服务]和demo-client[客户端]可以没有先后顺序;demo-client[客户端]依赖demo-authorizationserver[授权服务]的端点,启动的时候会去请求 demo-authorizationserver 的 /.well-known/openid-configuration 接口获取配置,如果请求失败时会抛出异常启动失败,所以要先把 demo-authorizationserver 启动。

举例理解【授权服务、资源服务、客户端服务】在微服务架构中担任什么样的一个角色和职责

假如我们有这样的一个微服务:【网关服务】、【认证服务】、【业务服务】
我们访问【业务服务】是通过【网关服务】进行转发的,【网关服务】判断是否登录或者有权限,如果没有登录或者没有权限,就会重定向到【认证服务】的进行登录,登录成功后,【认证服务】把token返回给【网关服务】,【网关服务】会携带token去访问【业务服务】,这样就完成了整个认证和鉴权过程,然后token是存储在【网关服务】的,也没有暴露出来,比较安全 img_10.png demo-client可以把它看作我们的【网关服务】,demo-authorizationserver可以看作我们的【认证服务】,messages-resource可以看作【业务服务】,你这样去理解是否能够理解oauth2的这个流程了呢。