Spring Authorization Server (二)认证服务器搭建

奋斗吧
奋斗吧
擅长邻域:未填写

标签: Spring Authorization Server (二)认证服务器搭建 架构博客 51CTO博客

2023-07-18 18:24:26 220浏览

Spring Authorization Server (二)认证服务器搭建,本篇介绍了SpringAuthorizationServer认证服务器的搭建,实现了授权码模式,也演示了refresh_token模式,最后对客户端和用户信息改造成基于数据库的存储。

在上一篇中,我们对Spring Authorization Server相关理论进行了一些介绍,这一篇开始,我们进入实操环节。首先,我们来搭建认证服务器。

版本要求

目前Spring Authorization Server官方最新的版本为1.1.1。

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1

官方要求Java版本要17及以上。

Spring Authorization Server (二)认证服务器搭建_认证服务器_02

官网推荐的配置如下。

Spring Authorization Server (二)认证服务器搭建_Spring Security_03

按照官网推荐,Spring Authorization Server工程配置如下:

Spring Authorization Server版本:1.1.1

JDK版本:17

Spring Boot版本:3.1.1

我们选择Project为Maven,本人的Maven版本为3.8.8。

认证服务器搭建

先创建一个名称为spring-oauth-parent的父工程,然后在spring-oauth-parent下创建名称为spring-oauth-server的子工程作为认证服务器,在spring-oauth-server子工程添加Spring Authorization Server配置。

工程结构如下。

Spring Authorization Server (二)认证服务器搭建_Spring Boot3_04

父级工程spring-oauth-parent就一个pom.xml配置如下。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.1.1</version>
  </parent>
  <packaging>pom</packaging>

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.oauth</groupId>
  <artifactId>spring-oauth-parent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>父级工程</name>

  <modules>
    <module>spring-oauth-server</module>
  </modules>

</project>

下面都是spring-oauth-server认证服务器的代码了。

pom.xml配置如下。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.oauth</groupId>
        <artifactId>spring-oauth-parent</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>spring-oauth-server</artifactId>
    <name>认证服务器</name>

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

</project>

application.yml配置如下。

server:
  port: 9000

logging:
  level:
    org.springframework.security: trace

spring:
  application:
    name: spring-oauth-server

启动类SpringOauthServerApplication包路径为org.oauth.server,代码如下。

package org.oauth.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Rommel
 * @version 1.0
 * @date 2023/7/13-17:08
 * @description TODO
 */
@SpringBootApplication
public class SpringOauthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringOauthServerApplication.class,args);
    }
}

在org.oauth.server包路径下增加config目录,直接从官网(https://docs.spring.io/spring-authorization-server/docs/current/reference/html/getting-started.html)将SecurityConfig拷贝放到org.oauth.server.config路径下,代码如下。

package org.oauth.server.config;

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;

/**
 * @author Rommel
 * @version 1.0
 * @date 2023/7/10-16:34
 * @description TODO
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * Spring Authorization Server 相关配置
     * 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置,配置的内容有点区别,
     * 因为Spring Authorization Server是建立在Spring Security 基础上的,defaultSecurityFilterChain方法主要
     * 配置Spring Security相关的东西,而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                //开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。
                .oidc(Customizer.withDefaults());
        http
                //将需要认证的请求,重定向到login页面行登录认证。
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // 使用jwt处理接收到的access token
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }

    /**
     *Spring Security 过滤链配置(此处是纯Spring Security相关配置)
     */
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                //设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                // 由Spring Security过滤链中UsernamePasswordAuthenticationFilter过滤器拦截处理“login”页面提交的登录信息。
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

    /**
     *设置用户信息,校验用户名、密码
     * 这里或许有人会有疑问,不是说OAuth 2.1已经移除了密码模式了码?怎么这里还有用户名、密码登录?
     * 例如:某平台app支持微信登录,用户想使用微信账号登录登录该平台app,则用户需先登录微信app,
     * 此处代码的操作就类似于某平台app跳到微信登录界面让用户先登录微信,然后微信校验用户提交的用户名、密码,
     * 登录了微信才对某平台app进行授权,对于微信平台来说,某平台的app就是OAuth 2.1中的客户端。
     * 其实,这一步是Spring Security的操作,纯碎是认证平台的操作,是脱离客户端(第三方平台)的。
     */
    @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("oidc-client")
                //{noop}开头,表示“secret”以明文存储
                .clientSecret("{noop}secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                //.redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client")
                //将上面的redirectUri地址注释掉,改成下面的地址,是因为我们暂时还没有客户端服务,以免重定向跳转错误导致接收不到授权码
                .redirectUri("http://www.baidu.com")
                //退出操作,重定向地址,暂时也没遇到
                .postLogoutRedirectUri("http://127.0.0.1:8080/")
                //设置客户端权限范围
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                //客户端设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();
        //配置基于内存的客户端信息
        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    /**
     *配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
     * JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
     */
    @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);
    }

    /**
     *生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
     */
    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解析器
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     *配置认证服务器请求地址
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        //什么都不配置,则使用默认地址
        return AuthorizationServerSettings.builder().build();
    }

}

在上面SecurityConfig代码中,我们已经开启了OpenID Connect 1.0,于是我们就可以使用http://127.0.0.1:9000/.well-known/openid-configuration地址请求查看认证服务器信息了。postman请求结果如下。

Spring Authorization Server (二)认证服务器搭建_Spring Boot3_05

认证服务器返回完整的json信息如下。

{
    "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"
    ]
}

上面的配置信息中,其中authorization_endpoint为授权码的授权地址,device_authorization_endpoint为设备授权码的授权地址,token_endpoint为获取token的地址。

这里可能有人会觉得奇怪,“/.well-known/openid-configuration”这个地址是哪里来的?打开源码中OidcProviderConfigurationEndpointFilter这个类就可以看到了。

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_06

该类是Spring Security过滤链中的一个过滤器,我们发起“/.well-known/openid-configuration”请求时,会被OidcProviderConfigurationEndpointFilter拦截。

接下来,我们使用http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com请求获取授权码,则跳转至下面登录页面。

Spring Authorization Server (二)认证服务器搭建_OpenID Connect 1.0_07

输入用户名:user,密码:password,则跳转至授权页面,

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_08

勾选授权信息profile,点击提交按钮,则返回如下结果。

Spring Authorization Server (二)认证服务器搭建_认证服务器_09

从浏览器地址栏中,我们看到授权服务器已经返回了授权码code,接下来,我们使用授权code向“http://localhost:9000/oauth2/token?grant_type=authorization_code&code=FqecKQ_0zhQMtDGo3qjM7VwbOGxPONArb3lYhFH5_suZ3t_tybkuWos_mxjKhTZhhI1d1EOVJooDKKwkqtecctzQmQfA_k3eS2MsbuHs42acsG1uR6yHpI1auY2Nd9SU&redirect_uri=http://www.baidu.com”地址请求获取令牌,结果如下。

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_10

到此,授权码模式的流程就完成了。

token刷新

还是http://localhost:9000/oauth2/token地址,参数授权类型grant_type的值改为refresh_token,传入上面返回的refresh_token,请求结果如下所示。

Spring Authorization Server (二)认证服务器搭建_Spring Boot3_11

Spring Authorization Server (二)认证服务器搭建_认证服务器_12

客户端注册信息存储改造

在上面的认证过程中,客户端信息是基于内存的,写死在代码中,现在我们将他改造从数据库中读取。

我们在org.springframework.security.oauth2.server.authorization包下,可以看到oauth2-registered-client-schema.sql、oauth2-authorization-consent-schema.sql、oauth2-authorization-schema.sql三个sql文件。

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_13

上面三个sql文件整理如下。

/**
 * 客户端信息表
 */
CREATE TABLE oauth2_registered_client (
    id varchar(100) NOT NULL,
    client_id varchar(100) NOT NULL,
    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret varchar(200) DEFAULT NULL,
    client_secret_expires_at timestamp DEFAULT NULL,
    client_name varchar(200) NOT NULL,
    client_authentication_methods varchar(1000) NOT NULL,
    authorization_grant_types varchar(1000) NOT NULL,
    redirect_uris varchar(1000) DEFAULT NULL,
    post_logout_redirect_uris varchar(1000) DEFAULT NULL,
    scopes varchar(1000) NOT NULL,
    client_settings varchar(2000) NOT NULL,
    token_settings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

/**
 * 授权确认表
 */
CREATE TABLE oauth2_authorization_consent (
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorities varchar(1000) NOT NULL,
    PRIMARY KEY (registered_client_id, principal_name)
);

/**
 * 授权信息表
 */
CREATE TABLE oauth2_authorization (
    id varchar(100) NOT NULL,
    registered_client_id varchar(100) NOT NULL,
    principal_name varchar(200) NOT NULL,
    authorization_grant_type varchar(100) NOT NULL,
    authorized_scopes varchar(1000) DEFAULT NULL,
    attributes blob DEFAULT NULL,
    state varchar(500) DEFAULT NULL,
    authorization_code_value blob DEFAULT NULL,
    authorization_code_issued_at timestamp DEFAULT NULL,
    authorization_code_expires_at timestamp DEFAULT NULL,
    authorization_code_metadata blob DEFAULT NULL,
    access_token_value blob DEFAULT NULL,
    access_token_issued_at timestamp DEFAULT NULL,
    access_token_expires_at timestamp DEFAULT NULL,
    access_token_metadata blob DEFAULT NULL,
    access_token_type varchar(100) DEFAULT NULL,
    access_token_scopes varchar(1000) DEFAULT NULL,
    oidc_id_token_value blob DEFAULT NULL,
    oidc_id_token_issued_at timestamp DEFAULT NULL,
    oidc_id_token_expires_at timestamp DEFAULT NULL,
    oidc_id_token_metadata blob DEFAULT NULL,
    refresh_token_value blob DEFAULT NULL,
    refresh_token_issued_at timestamp DEFAULT NULL,
    refresh_token_expires_at timestamp DEFAULT NULL,
    refresh_token_metadata blob DEFAULT NULL,
    user_code_value blob DEFAULT NULL,
    user_code_issued_at timestamp DEFAULT NULL,
    user_code_expires_at timestamp DEFAULT NULL,
    user_code_metadata blob DEFAULT NULL,
    device_code_value blob DEFAULT NULL,
    device_code_issued_at timestamp DEFAULT NULL,
    device_code_expires_at timestamp DEFAULT NULL,
    device_code_metadata blob DEFAULT NULL,
    PRIMARY KEY (id)
);

       新建mysql数据库oauth-server,执行上面的sql语句。

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_14

将上面代码中注册的客户端信息整理成sql语句插入数据库表。

INSERT INTO `oauth-server`.`oauth2_registered_client` (`id`, `client_id`, `client_id_issued_at`, `client_secret`, `client_secret_expires_at`, `client_name`, `client_authentication_methods`, `authorization_grant_types`, `redirect_uris`, `post_logout_redirect_uris`, `scopes`, `client_settings`, `token_settings`) VALUES ('3eacac0e-0de9-4727-9a64-6bdd4be2ee1f', 'oidc-client', '2023-07-12 07:33:42', '{noop}secret', NULL, '3eacac0e-0de9-4727-9a64-6bdd4be2ee1f', 'client_secret_basic', 'refresh_token,authorization_code', 'http://www.baidu.com', 'http://127.0.0.1:8080/', 'openid,profile', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.client.require-proof-key\":false,\"settings.client.require-authorization-consent\":true}', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.token.reuse-refresh-tokens\":true,\"settings.token.id-token-signature-algorithm\":[\"org.springframework.security.oauth2.jose.jws.SignatureAlgorithm\",\"RS256\"],\"settings.token.access-token-time-to-live\":[\"java.time.Duration\",300.000000000],\"settings.token.access-token-format\":{\"@class\":\"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat\",\"value\":\"self-contained\"},\"settings.token.refresh-token-time-to-live\":[\"java.time.Duration\",3600.000000000],\"settings.token.authorization-code-time-to-live\":[\"java.time.Duration\",300.000000000],\"settings.token.device-code-time-to-live\":[\"java.time.Duration\",300.000000000]}');

注意:此时将客户端密钥{noop}secret,改为密文存储。

添加数据库连接,application.yml配置如下。

server:
  port: 9000

logging:
  level:
    org.springframework.security: trace

spring:
  application:
    name: spring-oauth-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth-server?serverTimezone=UTC&userUnicode=true&characterEncoding=utf-8
    username: root
    password: root

     添加maven依赖,pom.xml配置如下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.oauth</groupId>
        <artifactId>spring-oauth-parent</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>spring-oauth-server</artifactId>
    <name>认证服务器</name>

    <dependencies>
        <!--Spring Authorization Server-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!-- mybatis-plus 3.5.3及以上版本 才支持 spring boot 3-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <!-- 添加spring security cas支持 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-cas</artifactId>
        </dependency>
    </dependencies>
</project>

      注意:这里需添加spring-security-cas依赖,否则启动时报java.lang.ClassNotFoundException: org.springframework.security.cas.jackson2.CasJackson2Module错误。

在SecurityConfig中把RegisteredClientRepository、OAuth2AuthorizationService、OAuth2AuthorizationConsentService分别注入。

package org.oauth.server.config;

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.jdbc.core.JdbcTemplate;
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.oidc.OidcUserInfo;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
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;

/**
 * @author Rommel
 * @version 1.0
 * @date 2023/7/10-16:34
 * @description TODO
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {


    /**
     * Spring Authorization Server 相关配置
     * 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置,配置的内容有点区别,
     * 因为Spring Authorization Server是建立在Spring Security 基础上的,defaultSecurityFilterChain方法主要
     * 配置Spring Security相关的东西,而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                //开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。
                .oidc(Customizer.withDefaults());
        http
                //将需要认证的请求,重定向到login页面行登录认证。
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // 使用jwt处理接收到的access token
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }


    /**
     *Spring Security 过滤链配置(此处是纯Spring Security相关配置)
     */
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                //设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                // 由Spring Security过滤链中UsernamePasswordAuthenticationFilter过滤器拦截处理“login”页面提交的登录信息。
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

    /**
     * 客户端信息
     * 对应表:oauth2_registered_client
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }


    /**
     * 授权信息
     * 对应表:oauth2_authorization
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 授权确认
     *对应表:oauth2_authorization_consent
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }


    /**
     *设置用户信息,校验用户名、密码
     * 这里或许有人会有疑问,不是说OAuth 2.1已经移除了密码模式了码?怎么这里还有用户名、密码登录?
     * 例如:某平台app支持微信登录,用户想使用微信账号登录登录该平台app,则用户需先登录微信app,
     * 此处代码的操作就类似于某平台app跳到微信登录界面让用户先登录微信,然后微信校验用户提交的用户名、密码,
     * 登录了微信才对某平台app进行授权,对于微信平台来说,某平台的app就是OAuth 2.1中的客户端。
     * 其实,这一步是Spring Security的操作,纯碎是认证平台的操作,是脱离客户端(第三方平台)的。
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        //基于内存的用户数据校验
        return new InMemoryUserDetailsManager(userDetails);
    }



    /**
     *配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
     * JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
     */
    @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);
    }

    /**
     *生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
     */
    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解析器
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     *配置认证服务器请求地址
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        //什么都不配置,则使用默认地址
        return AuthorizationServerSettings.builder().build();
    }

}

重启服务,再次访问http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com地址,

Spring Authorization Server (二)认证服务器搭建_认证服务器_15

输入用户名:user,密码:password,则跳转至授权页面,

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_16

勾选授权信息profile,点击提交按钮,则返回如下结果。

Spring Authorization Server (二)认证服务器搭建_Spring Security_17

可以看一下oauth2_authorization_consent、oauth2_authorization这两个表也有数据了。

表:oauth2_authorization_consent

Spring Authorization Server (二)认证服务器搭建_Spring Security_18

表:oauth2_authorization

Spring Authorization Server (二)认证服务器搭建_Spring Boot3_19

用户信息存储改造

在SecurityConfig类中的UserDetailsService也是基于内存的,用户信息在代码中写死,我们也把他改成从数据库中读取,新建sys_user表如下。

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '姓名',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '描述',
  `status` tinyint DEFAULT NULL COMMENT '状态(1:正常 0:停用)',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `idx_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表';

INSERT INTO `oauth-server`.`sys_user` (`id`, `username`, `password`, `name`, `description`, `status`) VALUES (1, 'user', '$2a$10$8fyY0WbNAr980e6nLcPL5ugmpkLLH3serye5SJ3UcDForTW5b0Sx.', '测试用户', 'Spring Security 测试用户', 1);

       给sys_user增加对应的对应的实体和功能实现类,SysUserEntity、SysUserMapper、SysUserService、SysUserServiceImpl代码如下。

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class SysUserEntity implements Serializable {

    /**
     * 主键
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 名字
     */
    private String name;

    /**
     * 描述
     */
    private String description;

    /**
     * 状态
     */
    private Integer status;

}

@Mapper
public interface SysUserMapper extends BaseMapper<SysUserEntity> {

}

public interface SysUserService {

    /**
     *
     * @param username
     * @return
     * @author  Rommel
     * @date    2023/7/12-23:48
     * @version 1.0
     * @description  根据用户名查询用户信息
     */
    SysUserEntity selectByUsername(String username);
}

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUserEntity> implements SysUserService {
    @Override
    public SysUserEntity selectByUsername(String username) {
        LambdaQueryWrapper<SysUserEntity> lambdaQueryWrapper = new LambdaQueryWrapper();
        lambdaQueryWrapper.eq(SysUserEntity::getUsername,username);
        return this.getOne(lambdaQueryWrapper);
    }
}

 SecurityConfig类,将UserDetailsService基于内存实现的代码注释掉,增加PasswordEncoder配置,SecurityConfig改造后代码如下。

package org.oauth.server.config;

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.jdbc.core.JdbcTemplate;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.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;

/**
 * @author Rommel
 * @version 1.0
 * @date 2023/7/10-16:34
 * @description TODO
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {


    /**
     * Spring Authorization Server 相关配置
     * 此处方法与下面defaultSecurityFilterChain都是SecurityFilterChain配置,配置的内容有点区别,
     * 因为Spring Authorization Server是建立在Spring Security 基础上的,defaultSecurityFilterChain方法主要
     * 配置Spring Security相关的东西,而此处authorizationServerSecurityFilterChain方法主要配置OAuth 2.1和OpenID Connect 1.0相关的东西
     */
    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                //开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。
                .oidc(Customizer.withDefaults());
        http
                //将需要认证的请求,重定向到login页面行登录认证。
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                // 使用jwt处理接收到的access token
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(Customizer.withDefaults()));

        return http.build();
    }


    /**
     *Spring Security 过滤链配置(此处是纯Spring Security相关配置)
     */
    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                //设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                // 由Spring Security过滤链中UsernamePasswordAuthenticationFilter过滤器拦截处理“login”页面提交的登录信息。
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

    /**
     * 客户端信息
     * 对应表:oauth2_registered_client
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }


    /**
     * 授权信息
     * 对应表:oauth2_authorization
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 授权确认
     *对应表:oauth2_authorization_consent
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }


    /**
     *设置用户信息,校验用户名、密码
     * 这里或许有人会有疑问,不是说OAuth 2.1已经移除了密码模式了码?怎么这里还有用户名、密码登录?
     * 例如:某平台app支持微信登录,用户想使用微信账号登录登录该平台app,则用户需先登录微信app,
     * 此处代码的操作就类似于某平台app跳到微信登录界面让用户先登录微信,然后微信校验用户提交的用户名、密码,
     * 登录了微信才对某平台app进行授权,对于微信平台来说,某平台的app就是OAuth 2.1中的客户端。
     * 其实,这一步是Spring Security的操作,纯碎是认证平台的操作,是脱离客户端(第三方平台)的。
     */
//    @Bean
//    public UserDetailsService userDetailsService() {
//        UserDetails userDetails = User.withDefaultPasswordEncoder()
//                .username("user")
//                .password("password")
//                .roles("USER")
//                .build();
//        //基于内存的用户数据校验
//        return new InMemoryUserDetailsManager(userDetails);
//    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     *配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
     * JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
     */
    @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);
    }

    /**
     *生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
     */
    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解析器
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     *配置认证服务器请求地址
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        //什么都不配置,则使用默认地址
        return AuthorizationServerSettings.builder().build();
    }

}

      创建UserDetailsServiceImpl类继承UserDetailsService接口,添加@Service注解交给Spring容器管理,重写loadUserByUsername(String username)方法,实现用户信息从数据库查询,UserDetailsServiceImpl代码如下。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUserEntity sysUserEntity = sysUserService.selectByUsername(username);
        List<SimpleGrantedAuthority> grantedAuthorityList = Arrays.asList("USER").stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());

        return new User(username,sysUserEntity.getPassword(),grantedAuthorityList);
    }
}

        此时,项目代码结构如下。

Spring Authorization Server (二)认证服务器搭建_认证服务器_20

重启服务,,再次访问http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com地址,跳转到登录页面,

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_21

输入用户名:user,密码:123456,则跳转至授权页面,

Spring Authorization Server (二)认证服务器搭建_OAuth 2.1_22

勾选授权信息profile,点击提交按钮,则返回如下结果。

Spring Authorization Server (二)认证服务器搭建_Spring Boot3_23

至于上面是如何校验用户名user和密码123456的,我在《Spring Security+JWT 之心诚则灵》一篇中有比较详细的讲解,感兴趣的可以去看一下。

总结

本篇,我们介绍了认证服务器的搭建,授权码模式的实现,对客户端和用户信息改造成基于数据库的存储,到此一个基础版的认证服务器就已经搭建完成了。

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695