22-Spring Authorization Server初体验

网友投稿 414 2022-11-15

22-Spring Authorization Server初体验

在上一篇中我对授权服务器项目Spring Authorization Server进行了介绍,这一篇我们来一起看看它是如何搭建、如何使用。先不要纠结为什么要这样配置,先跑起来,后面会去深入探究原因。

本文DEMO:springauthserver 分支。

项目环境依赖

本文Spring Authorization Server版本为​​0.2.2​​。

像OAuth2 Client、Resource Server一样,Spring Authorization Server也是以插件的形式接入Spring Security的体系中。下面列举了目前必备的环境依赖:

org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-oauth2-authorization-server 0.2.2 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc com.h2database h2

这里要注意几点,环境要求参考​​Spring Authorization Server介绍​​一文。此外,Spring Authorization Server是需要数据库支持的,这里我依然使用了小巧的H2数据库。

在实际开发中,你可以通过更换驱动的手段切换到其它类型的数据库,可能会涉及到简单的数据库方言修改或者配置变化。

授权服务器过滤器链

OAuth2授权服务器专门处理OAuth2客户端的授权请求流程,授权端点、Token端点、用户信息端点等等都需要对应的过滤器支持,这些过滤器由Spring Authorization Server中的​​OAuth2AuthorizationServerConfigurer​​负责初始化和配置。我们只需要定义一个优先级最高的过滤器链,把授权服务器配置类初始化并激活即可。

@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); // TODO 你可以根据需求对authorizationServerConfigurer进行一些个性化配置 RequestMatcher authorizationServerEndpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); // ① .authorizeRequests().anyRequest().authenticated() .and() // ②忽略掉相关端点的csrf .csrf(csrf -> csrf .ignoringRequestMatchers(authorizationServerEndpointsMatcher)) // 开启form登录 .formLogin() .and() // ③应用 授权服务器的配置 .apply(authorizationServerConfigurer); return }

上面是一个基本的配置,关键的步骤为:

配置拦截授权服务器相关的请求端点。由于是接口调用,同时关闭相关端点的CSRF功能。将配置类加入​​HttpSecurity​​激活配置。

客户端的注册和持久化管理

按照OAuth2协议,所有的OAuth2客户端都应该在授权服务器中进行信息注册。你去申请接入第三方开放平台,都要提交一些信息,第三方平台审核通过后会把一些OAuth2客户端信息发给你,这些信息你不会陌生,大部分都包含在OAuth2客户端类库的​​OAuth2ClientProperties.Registration​​中,对应Spring Authorization Server授权服务器的实体为​​RegisteredClient​​:

public class RegisteredClient implements Serializable { private static final long serialVersionUID = Version.SERIAL_VERSION_UID; private String id; private String clientId; private Instant clientIdIssuedAt; private String clientSecret; private Instant clientSecretExpiresAt; private String clientName; private Set clientAuthenticationMethods; private Set authorizationGrantTypes; private Set redirectUris; private Set scopes; private ClientSettings clientSettings; private TokenSettings tokenSettings;// 略……}

这些属性多数在前面的章节中已经介绍了,​​redirect_uri​​​变成了复数以适应多个OAuth2客户端,另外​​redirect_uri​​​还有一些隐含规则和操作 ,相关源码:

private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) { UriComponents requestedRedirect; try { requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build(); if (requestedRedirect.getFragment() != null) { return false; } } catch (Exception ex) { return false; } String requestedRedirectHost = requestedRedirect.getHost(); if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { // As per // While redirect URIs using localhost (i.e., // "function similarly to loopback IP // redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED. return false; } if (!isLoopbackAddress(requestedRedirectHost)) { // As per // When comparing client redirect URIs against pre-registered URIs, // authorization servers MUST utilize exact string matching. return registeredClient.getRedirectUris().contains(requestedRedirectUri); } // As per // The authorization server MUST allow any port to be specified at the // time of the request for loopback IP redirect URIs, to accommodate // clients that obtain an available ephemeral port from the operating // system at the time of the request. for (String registeredRedirectUri : registeredClient.getRedirectUris()) { UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri); registeredRedirect.port(requestedRedirect.getPort()); if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) { return true; } } return false; }

这里简单总结一个要点:

如果OAuth2授权服务器是Spring Authorization Server,目前必须严格按照这个规则配置​​redirect_uri​​。

ClientSettings

该OAuth2客户端的一些规则配置,包括:

​​private_key_jwt​​​和​​client_secret_jwt​​​参见​​ClientAuthenticationMethod​​。

TokenSettings

注册OAuth2客户端时对该客户端令牌的通用规则配置,包含了:

​​ACCESS_TOKEN_TIME_TO_LIVE​​​ 访问令牌生存时间,默认​​5​​分钟。​​REUSE_REFRESH_TOKENS​​​ 是否可以复用刷新令牌,默认​​true​​。​​ID_TOKEN_SIGNATURE_ALGORITHM​​OIDC ID Token使用的签名算法,默认​​RS256​​

你可以通过​​TokenSettings.withSettings​​添加额外的自定义属性或者覆盖已有的属性。

我们来初始化一个OAuth2客户端,这里我们使用的客户端授权方法​​ClientAuthenticationMethod​​​是​​client_secret_basic​​​,因为之前对应的​​basic​​已经不建议使用了:

private RegisteredClient createRegisteredClient(final String id) { return RegisteredClient.withId(UUID.randomUUID().toString())// 客户端ID .clientId("felord")// 此处为了避免频繁启动重复写入仓库 .id(id)// client_secret_basic 模式下的密码 在客户端需要存明文 在授权服务器存密文 .clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder() .encode("secret"))// 名称可不定义 .clientName("felord")// 授权方法 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)// 支持的授权类型 .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)// 回调地址名单,不在此列将被拒绝 而且只能使用IP或者域名 不能使用 localhost .redirectUri(" .redirectUri(" .redirectUri(" .redirectUri(" .redirectUri(" OIDC支持 .scope(OidcScopes.OPENID)// 其它Scope .scope("message.read") .scope("userinfo") .scope("message.write")// JWT的配置项 包括TTL 是否复用refreshToken等等 .tokenSettings(TokenSettings.builder().build())// 配置客户端相关的配置项,包括验证密钥或者 是否需要授权页面 .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(true).build()) .build(); }

上面注册的OAuth2客户端信息需要持久化到数据库,​​RegisteredClientRepository​​​接口抽象了对​​RegisteredClient​​的持久化操作,这里我们直接启用内置的JDBC实现以代替默认的内存实现:

@SneakyThrows @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { // 每次都会初始化 生产的话 只初始化JdbcRegisteredClientRepository JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); // TODO 生产上 注册客户端需要使用接口 不应该采用下面的方式 // only@test begin final String id = "10000"; RegisteredClient registeredClient = registeredClientRepository.findById(id); if (registeredClient == null) { registeredClient = this.createRegisteredClient(id); registeredClientRepository.save(registeredClient); } // only@test end return registeredClientRepository; }

这里为了测试,我们在初始化​​JdbcRegisteredClientRepository​​的时候保存了一个OAuth2客户端信息。

授权状态信息持久化

资源拥有者的OAuth2授权状态信息​​OAuth2Authorization​​也需要持久化管理,Spring Authorization Server提供了​​OAuth2AuthorizationService​​来负责这个工作,我们同样需要启用内置的JDBC实现以代替默认的内存实现:

@Bean public OAuth2AuthorizationService authorizationService( JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); }

授权确认状态持久化

如果该客户端配置​​ClientSettings​​​开启了授权确认​​REQUIRE_AUTHORIZATION_CONSENT​​ ,授权确认的信息也要持久化管理,需要启用内置的JDBC实现以代替默认的内存实现:

@Bean public OAuth2AuthorizationConsentService authorizationConsentService( JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); }

OAuth2客户端注册到授权服务器的注册信息中配置了授权确认功能才有用。

JWK源配置

授权服务器公私钥都需要,参考​​Spring Security中的JOSE类库​​中的方法,结合Spring Authorization Server提供的方案,我们只需要定义一个​​JWKSource​​类型的Spring Bean即可:

/** * 加载JWK资源 * * @return the jwk source */ @SneakyThrows @Bean public JWKSource jwkSource() { //TODO 这里优化到配置 // jks classpath路径 String path = "jose.jks"; // key alias String alias = "jose"; // password String pass = "felord.cn"; ClassPathResource resource = new ClassPathResource(path); KeyStore jks = KeyStore.getInstance("jks"); char[] pin = pass.toCharArray(); jks.load(resource.getInputStream(), pin); RSAKey rsaKey = RSAKey.load(jks, alias, pin); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); }

到这里就配置完了。启动项目,访问下面的issue端点:

"issuer": " "authorization_endpoint": " "token_endpoint": " "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ], "jwks_uri": " "response_types_supported": [ "code" ], "grant_types_supported": [ "authorization_code", "client_credentials", "refresh_token" ], "revocation_endpoint": " "revocation_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ], "introspection_endpoint": " "introspection_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ], "code_challenge_methods_supported": [ "plain", "S256" ]}

这些配置是提供给OAuth2客户端的,里面也有不少的端点,比如​​jwks_uri​​你可以访问一下,看看能否获取公钥JWK。

授权服务器尽量配置域名避免使用​​localhost​​。

附数据库DDL脚本

Spring Authorization Server的类库内置了数据库DDL脚本,在​​org/springframework/security/oauth2/server/authorization​​下,分别是

​​oauth2-authorization-schema.sql​​​​oauth2-authorization-consent-schema.sql​​​​oauth2-registered-client-schema.sql​​

你可以手动或者借助于​​spring.sql.init​​系列命令进行初始化。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:使用Qt+OpenGL创建球体+简单交互
下一篇:JLink和JTAG接口详细及接线
相关文章

 发表评论

暂时没有评论,来抢沙发吧~