Spring Boot 3已经发布一段时间,网上关于Spring Boot 3的资料不是很多,本着对新技术的热情,学习和研究了大量Spring Boot 3新功能和新特性,感兴趣的同学可以参考Spring官方资料全面详细的新功能/新改进介绍
新特性有很多,本文主要针对OAuth 2.0的集成,如果快速开发自己的认证授权服务、OAuth客户端以及资源服务
开发依赖 | 版本 |
---|---|
Spring Boot | 3.0.2 |
新建三个服务,分别对应认证授权服务、OAuth客户端以及资源服务
服务 | 端口 |
---|---|
认证授权服务 | 8080 |
OAuth客户端服务 | 8081 |
资源服务 | 8082 |
Spring发布了spring-security-oauth2-authorization-server项目,目前最新版是1.0版,pom.xml依赖如下
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-oauth2-authorization-server ${spring-security-oauth2-authorization-server.version}
package com.wen3.oauth.ss.authserver.authconfigure; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.UUID; @Configuration public class Oauth2ServerAutoConfiguration { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 http // Redirect to the login page when not authenticated from the // authorization endpoint .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(new AntPathRequestMatcher("/actuator/**"), new AntPathRequestMatcher("/oauth2/**"), new AntPathRequestMatcher("/**/*.json"), new AntPathRequestMatcher("/**/*.html")).permitAll() .anyRequest().authenticated() ) // Form login handles the redirect to the login page from the // authorization server filter chain .formLogin(Customizer.withDefaults()); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("test") .password("test") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("demo-client-id") .clientSecret("{noop}demo-client-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build()) .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-id-1") .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-id-2") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope("message.read") .scope("message.write") .scope("user_info") .scope("pull_requests") // 登录成功后对scope进行确认授权 .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(registeredClient); } @Bean public JWKSourcejwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private 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; } @Bean public JwtDecoder jwtDecoder(JWKSource jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } }
@SpringBootApplication public class OauthServerApplication { public static void main(String[] args) { SpringApplication.run(OauthServerApplication.class, args); } }
server: port: 8080
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-oauth2-client
package com.wen3.oauth.ss.authclient.autoconfigure; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @Configuration public class Oauth2ClientAutoConfiguration { @Bean public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests() .anyRequest().authenticated() .and().logout() .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS) .and().oauth2Client() .and().oauth2Login(); return http.build(); } }
package com.wen3.oauth.ss.authclient.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController public class OauthClientDemoController { @RequestMapping(path = "/hello") public String demo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); log.info("authentication: {}", authentication); return "hello"; } }
@SpringBootApplication public class OauthServerApplication { public static void main(String[] args) { SpringApplication.run(OauthServerApplication.class, args); } }
server: port: 8081 servlet: session: cookie: # 需要更换存放sessionId的cookie名字,否则认证服务和客户端的sessionId会相互覆盖 name: JSESSIONID-2 spring: security: oauth2: client: registration: client-id-1: provider: demo-client-id client-id: demo-client-id client-secret: demo-client-secret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}' # client-authentication-method: POST scope: user_info, pull_requests client-name: demo-client-id client-id-2: provider: demo-client-id2 client-id: demo-client-id client-secret: demo-client-secret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}' # client-authentication-method: POST scope: user_info, pull_requests client-name: demo-client-id2 provider: demo-client-id: authorization-uri: http://127.0.0.1:8080/oauth2/authorize token-uri: http://127.0.0.1:8080/oauth2/token user-info-uri: http://127.0.0.1:8082/user/info user-name-attribute: name jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks demo-client-id2: authorization-uri: http://127.0.0.1:8080/oauth2/authorize token-uri: http://127.0.0.1:8080/oauth2/token user-info-uri: http://127.0.0.1:8082/user/info user-name-attribute: name jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-oauth2-resource-server
package com.wen3.oauth.ss.resourceserver.autoconfigure; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration public class ResourceServerAutoConfiguration { @Bean public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests().anyRequest().authenticated().and() .oauth2ResourceServer().jwt(); return http.build(); } }
package com.wen3.oauth.ss.resourceserver.controller; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class UserController { @RequestMapping(path = "/user/info", method = {RequestMethod.GET,RequestMethod.POST}, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public MapgetUser(HttpServletRequest request, HttpServletResponse response) { Map map = new HashMap<>(); map.put("name", "xxx"); return map; } }
package com.wen3.oauth.ss.resourceserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ResourceServerApplication { public static void main(String[] args) { SpringApplication.run(ResourceServerApplication.class, args); } }
server: port: 8082 spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
因为配置了多个client,会让用户选择用哪个client进行OAuth登录
以上所有页面都是Spring默认的,真实业务开发会自定义这些页面
{"issuer":"http://127.0.0.1:8080","authorization_endpoint":"http://127.0.0.1:8080/oauth2/authorize","token_endpoint":"http://127.0.0.1:8080/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"jwks_uri":"http://127.0.0.1:8080/oauth2/jwks","userinfo_endpoint":"http://127.0.0.1:8080/userinfo","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token"],"revocation_endpoint":"http://127.0.0.1:8080/oauth2/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"introspection_endpoint":"http://127.0.0.1:8080/oauth2/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}
server: port: 8081 servlet: session: cookie: # 需要更换存放sessionId的cookie名字,否则认证服务和客户端的sessionId会相互覆盖 name: JSESSIONID-2 spring: security: oauth2: client: registration: client-id-1: provider: demo-client-id client-id: demo-client-id client-secret: demo-client-secret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}' # client-authentication-method: POST scope: openid, profile, user_info, pull_requests client-name: demo-client-id client-id-2: provider: demo-client-id2 client-id: demo-client-id client-secret: demo-client-secret authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}' # client-authentication-method: POST scope: openid, profile, user_info, pull_requests client-name: demo-client-id2 provider: demo-client-id: issuer-uri: http://127.0.0.1:8080 demo-client-id2: issuer-uri: http://127.0.0.1:8080
本人经过研读SpringBoot3相关源码,基本上把所有功能都体验了一遍,这篇文章主要是针对最新版的SpringBoot集成OAuth功能进行演示,背后的原理,大家有疑问的可以留言交流。