Spring Authorization Server 快速上手

2,534 阅读3分钟

Spring Authorization Server

Spring Authorization Server是Spring团队新开源的一个项目,它在Spring Security的基础上,提供了OAuth2.1和OpenID1.0协议的支持,开发者可以基于此定制化自己的账号认证和授权系统。

1.快速入门

下面我将展示如何快速搭建一个基于内存的简单授权系统。

1.1 环境要求

JDK版本:17

SpringBoot版本:3.2.3

Spring Authorization Server版本:1.2.2

1.2 添加依赖

新建Spring Boot项目后,添加以下依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

1.3 编写配置

添加配置类,配置最基础的组件:

/**
 * @author hundanli
 * @version 1.0.0
 * @date 2024/3/8 22:17
 */
@Configuration
@EnableWebSecurity
@ConditionalOnProperty(name = "auth.quickstart", havingValue = "true", matchIfMissing = true)
public class QuickStartConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // Enable OpenID Connect 1.0
                .oidc(Customizer.withDefaults());
        http
                // Redirect to the login page when not authenticated from the
                // authorization endpoint
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // Accept access tokens for User Info and/or Client Registration
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .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("user")
                .password("password")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("hello")
                .clientSecret("{noop}123456")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8080/authorized")
                .postLogoutRedirectUri("http://127.0.0.1:8080/")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        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<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }


}

上述配置类中的每个Bean的作用如下:

1.Spring Security过滤器链,用于协议端点。

2.用于认证的Spring Security过滤器链。

3.用于检索要认证的用户的UserDetailsService实例。

4.用于管理客户端的RegisteredClientRepository实例。

5.用于签署访问令牌的com.nimbusds.jose.jwk.source.JWKSource实例。

6.启动时生成的用于创建上述JWKSource的java.security.KeyPair实例。

7.用于解码签名访问令牌的JwtDecoder实例。

8.用于配置Spring Authorization Server的AuthorizationServerSettings实例。

1.4 配置文件

在application.properties文件中配置端口,并配置trace日志级别以便排查问题

server.port=8000
spring.application.name=auth-server
logging.level.org.springframework.security=trace

上述步骤完成后,即可启动AuthServer服务了。

你可以访问http://127.0.0.1:8000/login,并用user/password去尝试登录验证是否正常。

1.5 Client服务

在完成AuthServer后,我们还需要实现一个简单的OAuth Client服务,以便走一个完整的OAuth登录流程。

创建一个SpringBoot项目,然后实现/authorized接口,对应上述AuthServer服务中配置的redirect_uri。

1.首先需要一个DTO来分序列化AccessToken:

/**
 * @author hundanli
 * @version 1.0.0
 * @date 2024/3/8 16:04
 */
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AccessTokenDTO {

    private String accessToken;
    private String scope;
    private String tokenType;
    private Integer expiresIn;


}

2.AuthorizedController类:


/**
 * @author hundanli
 * @version 1.0.0
 * @date 2024/3/7 23:20
 */
@RestController
public class AuthorizedController {


    WebClient webClient = WebClient.create("http://127.0.0.1:8000");

    @Autowired
    private ObjectMapper objectMapper/* = new ObjectMapper()*/;


    @GetMapping("/authorized")
    public Mono<String> authorized(@RequestParam("code") String code) {
        String clientId = "hello";
        String clientSecret = "123456";
        String base64Credentials = Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8));

        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("grant_type", "authorization_code");
        formData.add("redirect_uri", "http://127.0.0.1:8080/authorized");
        formData.add("code", code);
        formData.add("client_id", clientId);
        formData.add("client_secret", clientSecret);

        return webClient.post()
                .uri("/oauth2/token")
                .header(HttpHeaders.AUTHORIZATION, "Basic " + base64Credentials)
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(BodyInserters.fromFormData(formData))
                .retrieve()
                .bodyToMono(String.class)
                .flatMap(json -> {
                    try {
                        // 解析出access_token并请求/userinfo接口获取信息
                        AccessTokenDTO accessTokenDTO = objectMapper.readValue(json, AccessTokenDTO.class);
                        String accessToken = accessTokenDTO.getAccessToken();
                        return webClient.get().uri("/userinfo")
                                .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
                                .retrieve()
                                .bodyToMono(String.class);

                    } catch (JsonProcessingException e) {
                        return Mono.just("error");
                    }
                });

    }

}

这里用到了webflux响应式编程,不熟悉的可以用常规的webmvc替代。然后启动这个OAuth Client服务。

1.6 测试OAuth认证

1.在浏览器中访问:http://127.0.0.1:8000/oauth2/authorize?response_type=code&client_id=hello&redirect_uri=http://127.0.0.1:8080/authorized&scope=openid

2.此时会跳转到:http://127.0.0.1:8000/login 页面,输入user/password进行登录,AuthServer会返回302响应和code

3.浏览器将会自动进行302请求:http://127.0.0.1:8080/authorized?code=xxx

4.然后OAuth Client将会获取这个code调用AuthServer的/oauth2/token接口获取access_token。

5.最后再使用access_token调用AuthServer的/userinfo接口获取用户信息。

至此顺利完成Spring Authorization Server的第一步,完结撒花。