3 Star 1 Fork 2

heguangchuan / spring-authorization-server

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
README-sas.md 15.60 KB
一键复制 编辑 原始数据 按行查看 历史
heguangchuan 提交于 2024-01-11 13:54 . update README-sas.md

概述

本文参考来源:

概述一节可以直接参考官方文档: https://docs.spring.io/spring-authorization-server/reference/overview.html

Spring Authorization Server 授权服务器是一个框架,提供 OAuth 2.1 和 OpenID Connect 1.0 规范及其他相关规范的实现。 它建立在 Spring Security 之上,为构建OpenID Connect 1.0 的 Identity Provider 和OAuth2 授权服务器产品提供了安全、轻量级和可定制的基础。

整合案例

https://docs.spring.io/spring-authorization-server/reference/getting-started.html

Spring Authorization Server需要Java 17或更高的运行环境,保证 Spring Boot 的版本在 3.1.0 之上,这个版本的 Spring Boot 提供了一个 Oauth2 的 Starter.

引入依赖包

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

    <!-- 手动引入该包,不然启动会报下面的错误
     java.lang.ClassNotFoundException: org.springframework.security.cas.jackson2.CasJackson2Module
    -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-cas</artifactId>
    </dependency>

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

最小化配置

https://docs.spring.io/spring-authorization-server/reference/getting-started.html#defining-required-components

根据官方提供的配置文件,稍微做了点改动,完整的配置文件如下:

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.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    /**
     * 配置认证断点的过滤器
     *
     * @param http 核心配置类
     * @return 认证过滤器链
     * @throws Exception 异常信息
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        //默认的设置,点击源码其实就是配置了认证端点的csrf校验
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 开启 OpenID Connect 1.0
                .oidc(Customizer.withDefaults());    // Enable OpenID Connect 1.0
        http
                // Redirect to the login page when not authenticated from the
                // authorization endpoint
                // 未登录时访问认证端点将被重定向至login页面
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // Accept access tokens for User Info and/or Client Registration
                // 处理使用access token访问用户信息端点和客户端注册端点
                .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

        return http.build();
    }

    /**
     * 配置认证相关的过滤器链
     *
     * @param http
     * @return
     * @throws Exception
     */
    @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();
    }

    /**
     * 配置基于内存的登录用户信息
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withUsername("abc")
                .password("{noop}abc")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(userDetails);
    }

    /**
     * 配置一个基于内存的 客户端 信息
     *
     * @return
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("first-client")
                .clientSecret("{noop}first-client-secret")
                // 基于请求头的客户端认证方式
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                //基于表单参数的客户端认证方式
                //.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                //资源服务器使用该客户端获取授权时支持的方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 授权码模式回调地址
                .redirectUri("https://www.baidu.com")
                // 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                // 自定scope
                .scope("message.read")
                //设置客户端 是否需要授权,如果为 false 将不会跳到授权页
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    /**
     *  配置jwk源,使用非对称加密
     * @return
     */
    @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;
    }

    /**
     * 配置jwt解析器
     *
     * @param jwkSource jwk源
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 添加认证服务器配置
     *
     * @return AuthorizationServerSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

}

查看授权服务配置

注意: 官方配置中开启了 oidc ,如果配置的客户端中,配置了 scope = openid 就会返回 idToken,本文默认你已了解了 oidc

开启oidc 服务配置地址
http://127.0.0.1:9000/.well-known/openid-configuration
http://127.0.0.1:9000/.well-known/oauth-authorization-server

访问上面的接口,spring authorization server 将会把对应的配置端点给返回

{
  "issuer": "http://127.0.0.1:9000",
  "authorization_endpoint": "http://127.0.0.1:9000/oauth2/authorize",
  "device_authorization_endpoint": "http://127.0.0.1:9000/oauth2/device_authorization",
  "token_endpoint": "http://127.0.0.1:9000/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:9000/oauth2/jwks",
  "userinfo_endpoint": "http://127.0.0.1:9000/userinfo",
  "end_session_endpoint": "http://127.0.0.1:9000/connect/logout",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "revocation_endpoint": "http://127.0.0.1:9000/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:9000/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"
  ]
}

访问授权接口

http://127.0.0.1:9000/oauth2/authorize?client_id=first-client&response_type=code&redirect_uri=https://www.baidu.com&scope=message.read openid

授权服务器将引导用户至登录界面进行登录,在配置文件中配置了 abc/abc 用户,登录即可,登录后授权服务器检测到尚未授权,授权服务器将引导用户至授权页面进行授权, 在配置文件中自定义了 message.read 的权限,勾选权限后点击提交将会重定向至 百度(配置文件中配置的 重定向 地址) ,并附上了授权码 code

https://www.baidu.com/?code=BuHfjOez4_GjU8kaUxK2UhkZiBgX1zjiGmaDNBMyT1AkhdxO20X_M5n2jCj8AJUv4FjMU3xwPauspQPWTt1h3o5nNWQ-5KQOulSqiRyFBywUinkS3cjLecj7XVh-Ebu3

这就完成了授权码模式的第一步,我们接下来就需要拿到这个授权码获取 accessToken, accessToken 才是我们跟资源服务器进行交易的凭证。实际上在重定向百度的时候还要携带一个 state 参数, 这个 state 参数是用来防止CSRF攻击的,正式请求需生成并携带state参数,这个参数需要自己生成,重定向时会原样返回,如下请求:

http://127.0.0.1:9000/oauth2/authorize?client_id=first-client&response_type=code&redirect_uri=https://www.baidu.com&scope=message.read openid&state=123456

获取AccessToken

使用上面授权服务器重定向时的 code 访问 access token 授权码端口,在配置文件中配置了基于请求头( ClientAuthenticationMethod.CLIENT_SECRET_BASIC )的客户端认证方式,需要在 header 中 添加 Authorization 请求头,值为 client-id:client_secret的 base64 编码,即 Basic Auth ,本文默认你已熟悉 Basic Auth

注意需要拼接上 Basic ,Basic 后面有一个空格,如:

Basic Zmlyc3QtY2xpZW50OmZpcnN0LWNsaWVudC1zZWNyZXQ=

Image

拼接好的请求 url 如下,各项参数参考授权码模式的规范即可,不做赘述

curl --location --request POST 'localhost:9000/oauth2/token?grant_type=authorization_code&redirect_uri=https://www.baidu.com&code=CmetgWfuT1cp5bDKgm27JDJtixyxeSeEhHupJ2e9Dm9-JRqG_LLbNBOhGI05_opWvAKZEDQRb7VuO_hgQYgQCCMkaUt9dlDqvaAve8aYcR4Nd86FpWTwsv8qrMoHpwpH' \
--header 'Authorization: Basic Zmlyc3QtY2xpZW50OmZpcnN0LWNsaWVudC1zZWNyZXQ='

访问成功,授权服务器将会返回授权码,之后就可以用这个授权码去交换资源了

Image

访问受限资源

使用 Bearer Auth 方式拼接上面返回的 accessToken 访问认证用户端点,本文默认你熟悉 Bearer Auth

curl --location --request GET 'http://127.0.0.1:9000/userinfo' \
--header 'Authorization: Bearer eyJraWQiOiIyY2Q4Yjg1OS03MjRjLTQyZWQtYTI3Mi05OTJhYmMxNzE3MTkiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhYmMiLCJhdWQiOiJmaXJzdC1jbGllbnQiLCJuYmYiOjE3MDIyNzI3MjgsInNjb3BlIjpbIm9wZW5pZCIsIm1lc3NhZ2UucmVhZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE3MDIyNzMwMjgsImlhdCI6MTcwMjI3MjcyOH0.ikH8etPEc198fILgVqODx4lKV1VfvvgcHXl9usT_WMnEgSo6jokN1jLdJoY90WJJY6eJfAiwa47Qy8J33SPRpGPynj7T04vCZ1LRlgG4eswaONC3rzRTug5VX5T3Sz1KlY8MmKf4AXCi339808mJtWikkjKUxpnecSl8oyoBifGOVqiD02YEwf_m8PnmYC5Nxzr_94fMtlyT2abLeXu5qjZgS4Iac56IMCCBm_GPeeoW-kKDuYPZOP_lUpu9HeTb5Knxk8cauwsNXDcK1Tcdz-QvFJO1lL7wzG7RP66Zp7FCAz7MqM2aBUQaAMx-S3fQ-2zV4YnuDBEuDvx_5cfFvA' \
--header 'Cookie: JSESSIONID=B5C8F9EB6D39EA38573E36BD8A09D811'

返回如下信息,至此,授权码模式完结。

Image

刷新Token

如果 access token 过期,可以使用未过期的 refresh token 获取新的 access token

curl --location --request POST 'localhost:9000/oauth2/token?grant_type=refresh_token&refresh_token=Dz3RBgzmPH_EvhKMJmTvnFj_mnp_-fUul132t-fDAEyGnWmgQnBv0VtrkQCvTga9JmaOPMz50bn6wLmALej38t-L7vo0JjyRCpqUWYR0WzQqCXgPZ2vZ3TxBeID5r3si' \
--header 'Authorization: Basic Zmlyc3QtY2xpZW50OmZpcnN0LWNsaWVudC1zZWNyZXQ='

Image

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/heguangchuan/spring-authorization-server.git
git@gitee.com:heguangchuan/spring-authorization-server.git
heguangchuan
spring-authorization-server
spring-authorization-server
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891