SpringBoot3.X配置OAuth

news2024/11/17 5:42:41

      背景

        之前在学习OAuth2时,我就有一个疑惑,OAuth2中有太多的配置、服务类都标注了@Deprecated,如下:

        显然这些写法已经过时了,那么官方推荐的最新写法是什么样的呢?当时我没有深究这些,我以为我放过了它,它就能放过我,谁曾想不久之后,命运的大手不由分说的攥紧了我,让我不得不直面自己的困惑。

        最近我接了个大活,对公司的Java后端技术框架进行版本升级,将SpringBoot的版本从2.X升到3.X,JDK从1.8升到17,在对框架的父工程中的依赖版本进行升级之后,接下来要做的就是对已有的公共服务/组件进行升级了,比如GateWay, 流程引擎,基础平台,认证服务等。其他的服务升级都还算有惊无险,但是升级认证服务OAuth时,不夸张的说,我真是被折腾得死去活来。

        相比于SpringBoot2.X,3.X对于OAuth的配置几乎是进行了巅覆式的变更,很多之前我们熟知的配置方法,要么是换了形式,要么是换了位置,想要配得和2.X一样的效果太难了。好在经历了一番坎坷后,我终于把它给整理出来了,借着OAuth升版的机会,我也终于弄明白了最版的配置是什么样的。

      代码实践

        伴随着JDK和SpringBoot的版本升级,Spring Security也需要进行相应的升级,这直接导致了适用于SpringBoot2.X的相关OAuth配置变得不可用,甚至我们耳熟能详的配置类如AuthorizationServerConfigurerAdapter, WebSecurityConfigurerAdapter等都被删除了,下面就对比着SpringBoot2.X,详细说下3.X中对于配置做了哪些变更。

      一、依赖包的变化

        在SpringBoot2.X中要实现OAuth服务,需要引入以下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        而在SpringBoot3.X中,需要引入以下依赖包:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-oauth2-authorization-server</artifactId>
      <version>1.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-oauth2-core</artifactId>
    </dependency>

      二、支持模式的变化

        新版的spring-security-oauth2-authorization-server依赖包中,仅实现了授权码模式,要想使用之前的用户名密码模式,客户端模式等,还需要手动扩展,扩展模式需要实现这三个接口:

        AuthenticationConverter (用于将认证请求转换为标准的 Authentication 对象)

        AuthenticationProvider (用于定义如何验证用户的认证信息)

        OAuth2AuthorizationGrantAuthenticationToken(将认证对象转换为系统内部可识别的形式)

      三、数据库表的变化

        SpringBoot2.X版本时,OAuth存储客户信息的表结构如下:

create table oauth_client_details (
    client_id VARCHAR(256) PRIMARY KEY,
	resource_ids VARCHAR(256),
	client_secret VARCHAR(256),
	scope VARCHAR(256),
	authorized_grant_types VARCHAR(256),
	web_server_redirect_uri VARCHAR(256),
	authorities VARCHAR(256),
	access_token_validity INTEGER,
	refresh_token_validity INTEGER,
	additional_information VARCHAR(4096),
	autoapprove VARCHAR(256)
);

        升级为SpringBoot3.X后,客户信息表结构如下:

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,
    scopes varchar(1000) NOT NULL,
    client_settings varchar(2000) NOT NULL,
    token_settings varchar(2000) NOT NULL,
    PRIMARY KEY (id)
);

        四、链接的变化

           旧版本的OAuth服务中,相关的认证接接口的url都是/oauth/*,如/oauth/token /oauth/authorize,而升级到新版后,所有接口的url都变成了/oauth2/*,在配置客户端时需要格外注意。

      五、配置的变化

        接下来就是重头戏:配置的变化,为了更直观的展示SprinBoot在2.X和3.X对于配置的变化,我将把一套2.X的OAuth配置以及它转换成3.X的配置都贴出来,配置中涉及认证自动审批、内存模式和数据库模式,Token的过期时间,Token的JWT转换,Password的加密,自定义登陆页,客户端的授权方式等。

        1、SpringBoot2.X的配置


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Arrays;

/**
 *
 * @author leixiyueqi
 * @since 2023/12/3 22:00
 */
@EnableAuthorizationServer
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager manager;

    private final MD5PasswordEncoder encoder = new MD5PasswordEncoder();

    @Resource
    UserDetailsService service;

    @Resource
    private DataSource dataSource;

    @Resource
    TokenStore tokenStore;


    /**
     * 这个方法是对客户端进行配置,比如秘钥,唯一id,,一个验证服务器可以预设很多个客户端,
     * 之后这些指定的客户端就可以按照下面指定的方式进行验证
     * @param clients 客户端配置工具
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    /**
     * 以内存的方式设置客户端方法

     @Override
     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         clients
             .inMemory()   //这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取
             .withClient("client")   //客户端名称,随便起就行
             .secret(encoder.encode("123456"))      //只与客户端分享的secret,随便写,但是注意要加密
             .autoApprove(false)    //自动审批,这里关闭,要的就是一会体验那种感觉
             .scopes("read", "write")     //授权范围,这里我们使用全部all
             .autoApprove(true)    // 这个为true时,可以自动授权。
             .redirectUris("http://127.0.0.1:19210/leixi/login/oauth2/code/leixi-client",
             "http://127.0.0.1:8081/login/oauth2/code/client-id-1",
             "http://127.0.0.1:19210/leixi/callback")
             .authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
             //授权模式,一共支持5种,除了之前我们介绍的四种之外,还有一个刷新Token的模式
     }

     */

    // 令牌端点的安全配置,比如/oauth/token对哪些开放
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .passwordEncoder(encoder)    //编码器设定为BCryptPasswordEncoder
                .allowFormAuthenticationForClients()  //允许客户端使用表单验证,一会我们POST请求中会携带表单信息
                .checkTokenAccess("permitAll()");     //允许所有的Token查询请求
    }

    //令牌访问端点的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        endpoints
                .userDetailsService(service)
                .authenticationManager(manager)
                .tokenServices(tokenServices());
        //由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式
        endpoints.pathMapping("/oauth/confirm_access","/custom/confirm_access");
    }

    // 设置token的存储,过期时间,添加附加信息等
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setReuseRefreshToken(true);
        services.setTokenStore(tokenStore);
        services.setAccessTokenValiditySeconds(120);   // 设置令牌有效时间
        services.setRefreshTokenValiditySeconds(60*5);  //设计刷新令牌的有效时间
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new CustomTokenEnhancer(), accessTokenConverter()));
        services.setTokenEnhancer(tokenEnhancerChain);
        return services;
    }

    // 对token信息进行JWT加密
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        // 将自定义的内容封装到access_token中
        DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
        defaultAccessTokenConverter.setUserTokenConverter(new CustomerUserAuthenticationConverter());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setAccessTokenConverter(defaultAccessTokenConverter);
        converter.setSigningKey("密钥");
        return converter;
    }
}




import com.leixi.auth2.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 *
 * @author leixiyueqi
 * @since 2023/12/3 22:00
 */
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    private static final String loginUrl = "/login";

    /**
     * 注意,当在内存中获取用户信息时,就不需要创建UserDetailService的实现类了
     * 
     */
    @Autowired
    private UserDetailServiceImpl userService;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public MD5PasswordEncoder passwordEncoder() {
        return new MD5PasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // http security 要拦截的url,这里这拦截,oauth2相关和登录登录相关的url,其他的交给资源服务处理
                .authorizeRequests()
                .antMatchers( "/oauth/**","/**/*.css", "/**/*.ico", "/**/*.png", "/**/*.jpg", "/**/*.svg", "/login",
                        "/**/*.js", "/**/*.map",loginUrl, "/user/*","/base-grant.html")
                .permitAll()
                .anyRequest()
                .authenticated();
        // post请求要设置允许跨域,然后会报401
        http.csrf().ignoringAntMatchers("/login", "/logout", "/unlock/apply");

        // 表单登录
        http.formLogin()
                // 登录页面
                .loginPage(loginUrl)
                // 登录处理url
                .loginProcessingUrl("/login");
        http.httpBasic();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    /**
     *  以内存的方式载入用户信息

     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
     BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
     auth.inMemoryAuthentication()   //直接创建一个静态用户
     .passwordEncoder(encoder)
     .withUser("leixi").password(encoder.encode("123456")).roles("USER");
     }


     @Bean
     @Override
     public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
     }
     
     */

    @Bean   //这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //通过redis存储token
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

}

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;

import java.util.Map;

public class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        Map mapResp = super.convertUserAuthentication(authentication);
        try {
            UserDetails user = (UserDetails)authentication.getPrincipal();
            if (user != null) {
                mapResp.put("loginName", user.getUsername());
                mapResp.put("content", "测试在accessToken中添加附加信息");
                mapResp.put("authorities","hahahaha");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return mapResp;
    }

}


/**
 * 密码实现类,允许开发人员自由设置密码加密
 *
 * @author leixiyueqi
 * @since 2023/12/3 22:00
 */
public class MD5PasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] digest = md5.digest(rawPassword.toString().getBytes("UTF-8"));
            String pass = new String(Hex.encode(digest));
            return pass;
        } catch (Exception e) {
            throw new RuntimeException("Failed to encode password.", e);
        }
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(encode(rawPassword));
    }
}

        看得出来,SpringBoot2.X中SpringSecurityConfig的配置与OAuth2Configuration的配置有种相辅相成的感觉,但对于初学者来说,会觉得很割裂,不知道哪些东西该配在哪个文件里。

        2、Springboot3.X的配置

package com.leixi.auth2.config;

import com.leixi.auth2.custom.OAuth2PasswordAuthenticationConverter;
import com.leixi.auth2.custom.OAuth2PasswordAuthenticationProvider;
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 jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.UserDetailsService;

import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
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.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.UUID;

/**
 * OAuth的配置
 *
 * @author leixiyueqi
 * @since 2024/9/28 22:00
 */
@Configuration
@EnableWebSecurity
public class OAuth2JdbcConfiguration {


    @Autowired
    private MD5PasswordEncoder passwordEncoder;

    @Resource
    private UserDetailsService userDetailService;



    @Autowired
    private JdbcTemplate jdbcTemplate;


    @Autowired
    private CustomTokenEnhancer customTokenEnhancer;


    private static final String loginUrl = "/loginpage.html";

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        return jdbcRegisteredClientRepository;
    }


    /**
     * 在内存中获取用户信息的方式

     @Bean
     public UserDetailsService userDetailsService() {
         UserDetails userDetails = User.builder()
             .username("leixi")
             .roles("USER")
             .password(passwordEncoder.encode("123456"))
             .build();
         return new InMemoryUserDetailsManager(userDetails);
     }


     */

    /**
     * 在内存中获取客户端信息的方式,还可以用于客户端信息的入库
     *
     @Bean
     public RegisteredClientRepository registeredClientRepository() {
         JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
         RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
             .clientId("client")
             .clientSecret(passwordEncoder.encode( "123456"))
             .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
             .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
             .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
             .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
             .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
             .authorizationGrantType(AuthorizationGrantType.PASSWORD)
             .redirectUri("http://127.0.0.1:19210/leixi/login/oauth2/code/leixi-client")
             .redirectUri("http://127.0.0.1:8081/login/oauth2/code/client-id-1")
             .redirectUri("http://127.0.0.1:19210/leixi/callback")
             .scope("read")
             .scope("write")
             // 登录成功后对scope进行确认授权
             .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
             .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
             .accessTokenTimeToLive(Duration.ofHours(24))
             .refreshTokenTimeToLive(Duration.ofHours(24)).build())
             .build();
         jdbcRegisteredClientRepository.save(registeredClient);  //客户端信息入库
         return new InMemoryRegisteredClientRepository(registeredClient);
     }

     */


    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((requests) -> requests
                .requestMatchers( "/oauth/*","/*/*.css", "/*/*.ico", "/*/*.png", "/*/*.jpg", "/*/*.svg", "/login",
                        "/*/*.js", "/*/*.map",loginUrl, "/user/*","/base-grant.html").permitAll() // 允许所有用户访问这些路径
                .anyRequest().authenticated()
        );
        http.csrf(csrf -> csrf.ignoringRequestMatchers("/login", "/logout", "/unlock/apply")); // 禁用CSRF保护

        // 表单登录
        http.formLogin(formlogin -> formlogin
                        .loginPage(loginUrl)
                        .loginProcessingUrl("/login"))
                .httpBasic(httpBasic -> {})
                .authenticationProvider(daoAuthenticationProvider());
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider customerDaoAuthenticationProvider = new DaoAuthenticationProvider();
        // 设置userDetailsService
        customerDaoAuthenticationProvider.setUserDetailsService(userDetailService);
        // 禁止隐藏用户未找到异常
        customerDaoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        // 使用MD5进行密码的加密
        customerDaoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return customerDaoAuthenticationProvider;
    }

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

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        //应用了默认的安全配置,这些配置支持OAuth2授权服务器的功能。
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 自定义用户名密码的授权方式
                .tokenEndpoint((tokenEndpoint) -> tokenEndpoint
                        .accessTokenRequestConverter(new DelegatingAuthenticationConverter(Arrays.asList(
                                new OAuth2AuthorizationCodeAuthenticationConverter(),
                                new OAuth2RefreshTokenAuthenticationConverter(),
                                new OAuth2ClientCredentialsAuthenticationConverter(),
                                new OAuth2PasswordAuthenticationConverter()   //添加密码模式的授权方式
                        ))).authenticationProviders((customProviders) -> {
                            // 自定义认证提供者
                            customProviders.add(new OAuth2PasswordAuthenticationProvider(jwkSource(), userDetailService, passwordEncoder));
                        })
                )
                //启用了OpenID Connect 1.0,这是一种基于OAuth2的身份验证协议。
                .oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0
        //配置了当用户尝试访问受保护资源但未认证时的行为。设置了一个自定义的登录页面作为认证入口点。
        http.exceptionHandling((exceptions) -> exceptions
                        .authenticationEntryPoint(
                                new LoginUrlAuthenticationEntryPoint(loginUrl))
                )
                //配置了OAuth2资源服务器,指定使用JWT(JSON Web Token)进行身份验证。
                .oauth2ResourceServer(config -> config.jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
        return jwtEncoder;
    }


    @Bean
    public JwtDecoder jwtDecoder() {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource());
    }

    @Bean
    public OAuth2TokenGenerator<?> tokenGenerator() {
        JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder());
        jwtGenerator.setJwtCustomizer(customTokenEnhancer);
        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
        return new DelegatingOAuth2TokenGenerator(
                jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

    @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的方式加密token,与之前的版本有些差异,之前是采用HMAC加密
    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;
    }
}

@Service
public class CustomTokenEnhancer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    @Resource
    private UserDetailsService userDetailService;

    @Override
    public void customize(JwtEncodingContext context) {
        UserDetails user = userDetailService.loadUserByUsername(context.getPrincipal().getName());
        if (user != null) {
            context.getClaims().claims(claims -> {
                claims.put("loginName", user.getUsername());
                claims.put("name", user.getUsername());
                claims.put("content", "在accessToken中封装自定义信息");
                claims.put("authorities", "hahahaha");
            });
        }
    }
}


/**
 * Jwt工具类
 *
 * @author leixiyueqi
 * @since 2024/9/28 22:00
 */
public final class JwtUtils {

    private JwtUtils() {
    }

    public static JwsHeader.Builder headers() {
        return JwsHeader.with(SignatureAlgorithm.RS256);
    }


    public static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient,
                                                         String issuer, String subject,
                                                         Set<String> authorizedScopes) {

        Instant issuedAt = Instant.now();
        Instant expiresAt = issuedAt
                .plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

        /**
         * iss (issuer):签发人/发行人
         * sub (subject):主题
         * aud (audience):用户
         * exp (expiration time):过期时间
         * nbf (Not Before):生效时间,在此之前是无效的
         * iat (Issued At):签发时间
         * jti (JWT ID):用于标识该 JWT
         */
        // @formatter:off
        JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
        if (StringUtils.hasText(issuer)) {
            claimsBuilder.issuer(issuer);
        }
        claimsBuilder
                .subject(subject)
                .audience(Collections.singletonList(registeredClient.getClientId()))
                .issuedAt(issuedAt)
                .expiresAt(expiresAt)
                .notBefore(issuedAt);
        if (!CollectionUtils.isEmpty(authorizedScopes)) {
            claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
            claimsBuilder.claim("wangcl", "aaa");
        }
        // @formatter:on

        return claimsBuilder;
    }


}

public class OAuth2EndpointUtils {

    public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap(parameterMap.size());
        parameterMap.forEach((key, values) -> {
            if (values.length > 0) {
                String[] var3 = values;
                int var4 = values.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String value = var3[var5];
                    parameters.add(key, value);
                }
            }

        });
        return parameters;
    }

    public static void throwError(String errorCode, String parameterName, String errorUri) {
        OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
        throw new OAuth2AuthenticationException(error);
    }
}


// 注意,以下三个类是新版OAuth的密码模式的实现,不需要的可以不加
/**
 *
 * @author leixiyueqi
 * @since 2024/9/28 22:00
 */

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 从HttpServletRequest中提取username与password,传递给OAuth2PasswordAuthenticationToken
 */
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
    @Override
    public Authentication convert(HttpServletRequest request) {
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
            return null;
        }
        Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);

        String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
        if (!StringUtils.hasText(username) ||
                parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
            OAuth2EndpointUtils.throwError(
                    OAuth2ErrorCodes.INVALID_REQUEST,
                    OAuth2ParameterNames.USERNAME,"");
        }
        String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);

        Map<String, Object> additionalParameters = new HashMap<>();
        parameters.forEach((key, value) -> {
            if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
                    !key.equals(OAuth2ParameterNames.CLIENT_ID) &&
                    !key.equals(OAuth2ParameterNames.USERNAME) &&
                    !key.equals(OAuth2ParameterNames.PASSWORD)) {
                additionalParameters.put(key, value.get(0));
            }
        });

        return new OAuth2PasswordAuthenticationToken(username,password,clientPrincipal,additionalParameters);
    }
}


/**
 *
 * @author leixiyueqi
 * @since 2024/9/28 22:00
 */

import com.leixi.auth2.config.MD5PasswordEncoder;
import com.nimbusds.jose.jwk.source.JWKSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.security.Principal;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;

/**
 * 从HttpServletRequest中提取username与password,传递给OAuth2PasswordAuthenticationToken
 */
/**
 * 密码认证的核心逻辑
 */
public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {
    private static final StringKeyGenerator DEFAULT_REFRESH_TOKEN_GENERATOR =
            new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);

    private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = (context) -> {};
    private Supplier<String> refreshTokenGenerator = DEFAULT_REFRESH_TOKEN_GENERATOR::generateKey;
    private AuthorizationServerSettings authorizationServerSettings;

    public OAuth2PasswordAuthenticationProvider(JWKSource jwkSource, UserDetailsService userDetailService,
        MD5PasswordEncoder passwordEncoder) {
        this.jwkSource = jwkSource;
        this.userDetailService = userDetailService;
        this.passwordEncoder = passwordEncoder;

    }


    private final JWKSource jwkSource;


    private UserDetailsService userDetailService;

    private MD5PasswordEncoder passwordEncoder;


    public OAuth2PasswordAuthenticationProvider(JWKSource jwkSource){
        this.jwkSource = jwkSource;

    }

    public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
        Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
        this.jwtCustomizer = jwtCustomizer;
    }

    public void setRefreshTokenGenerator(Supplier<String> refreshTokenGenerator) {
        Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null");
        this.refreshTokenGenerator = refreshTokenGenerator;
    }

    @Autowired(required = false)
    void setAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
        this.authorizationServerSettings = authorizationServerSettings;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2PasswordAuthenticationToken passwordAuthentication =
                (OAuth2PasswordAuthenticationToken) authentication;

        OAuth2ClientAuthenticationToken clientPrincipal =
                getAuthenticatedClientElseThrowInvalidClient(passwordAuthentication);
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();

        // 校验账户
        String username = passwordAuthentication.getUsername();
        if (StringUtils.isEmpty(username)){
            throw new OAuth2AuthenticationException("账户不能为空");
        }
        // 校验密码
        String password = passwordAuthentication.getPassword();
        if (StringUtils.isEmpty(password)){
            throw new OAuth2AuthenticationException("密码不能为空");
        }
        // 查询账户信息
        UserDetails userDetails = userDetailService.loadUserByUsername(username);
        if (userDetails ==null) {
            throw new OAuth2AuthenticationException("账户信息不存在,请联系管理员");
        }
        // 校验密码
        if (!passwordEncoder.encode(password).equals(userDetails.getPassword())) {
            throw new OAuth2AuthenticationException("密码不正确");
        }

        // 构造认证信息
        Authentication principal = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());

        //region 直接构造一个OAuth2Authorization对象,实际场景中,应该去数据库进行校验
        OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(principal.getName())
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .attribute(Principal.class.getName(), principal)
                .attribute("scopes", registeredClient.getScopes() )
                .build();
        //endregion


        String issuer = this.authorizationServerSettings != null ? this.authorizationServerSettings.getIssuer() : null;
        Set<String> authorizedScopes = authorization.getAttribute("scopes");
        // 构造jwt token信息
        JwsHeader.Builder headersBuilder = JwtUtils.headers();
        headersBuilder.header("client-id", registeredClient.getClientId());
        headersBuilder.header("authorization-grant-type", passwordAuthentication.getGrantType().getValue());
        JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims(registeredClient, issuer, authorization.getPrincipalName(), authorizedScopes);

        // @formatter:off
        JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder)
                .registeredClient(registeredClient)
                .principal(authorization.getAttribute(Principal.class.getName()))
                .authorization(authorization)
                .authorizedScopes(authorizedScopes)
                .tokenType(OAuth2TokenType.ACCESS_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .authorizationGrant(passwordAuthentication)
                .build();
        // @formatter:on

        this.jwtCustomizer.customize(context);

        JwsHeader headers = context.getJwsHeader().build();
        JwtClaimsSet claims = context.getClaims().build();
        JwtEncoderParameters params = JwtEncoderParameters.from(headers, claims);
        NimbusJwtEncoder jwtEncoder  = new NimbusJwtEncoder(this.jwkSource);
        Jwt jwtAccessToken = jwtEncoder.encode(params);
        //Jwt jwtAccessToken = null;
        // 生成token
        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(),
                jwtAccessToken.getExpiresAt(), authorizedScopes);

        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient, clientPrincipal, accessToken);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {

        OAuth2ClientAuthenticationToken clientPrincipal = null;

        if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
            clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
        }

        if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
            return clientPrincipal;
        }

        throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
    }


}



/**
 *
 * @author 雷袭月启
 * @since 2024/9/28 22:00
 */

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;

import java.util.Map;

/**
 * 用于存放username与password
 */
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
    private static final long serialVersionUID = -559176897708927684L;
    private final String username;
    private final String password;


    public OAuth2PasswordAuthenticationToken(String username, String password, Authentication clientPrincipal, Map<String, Object> additionalParameters) {
        super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }
}

        如果不算上扩展的授权模式,SpringBoot3针对OAuth的配置要较之前精简了很多,而且一个配置文件就能搞定。从配置上也可以看出来,新版OAuth具有很高的灵活性,允许用户根据自己的需要来定义授权模式,对于安全性方面也有所增强,因此有更广阔的使用空间。

      功能测试

        配置好OAuth2后,验证配置的准确性方式就是成功启动OAuth,且相关的授权模式可以跑通。咱们借用之前几篇博客里写的client,以及PostMan,对SpringBoot3.X版的OAuth2进行测试,测试成果如下:

        1、扩展的用户名密码模式,成功

        2、授权码模式,通过该问如下链接获取code         http://127.0.0.1:19200/oauth2/authorize?response_type=code&client_id=client&scope=read&redirect_uri=http://127.0.0.1:19210/leixi/callback

        

        再利用postman,通过code来获取token

        

        接下来,咱们对token进行解析,检查封装在access_token里的信息是否存在,咱们通过之前写好的OAuth-Client对它进行解析,结果如下:

        通过以上测试,可知新版的配置完全达到了我们的要求。

      踩坑记录

        1、也不算是坑吧,SpringBoot3.X配置OAuth的方式在网上的相关资料很少,而且很难搜到,所以搜索这部分内容的资料,关键字很重要,一个是“Spring Security2.7”,一个是“spring-security-oauth2-authorization-server 配置”,可以搜到很多有用的信息。

        2、client的配置很关键,我之前在接口测试时,怎么都无法通过,结果打断点发现不同的client调用时支持不同的方法,而方法不对,就会报invalid_client,调用方法配置如下:

        3、千万不要用http://localhost:8080这种方式调用OAuth服务,但凡遇到localhost,都会报invalid_grant等bug。

        4、通过http://IP:PORT/oauth2/authorize 访问OAuth时,链接中一定要带上client_id, scope,不然无法授权,且链接中如果有redirect_uri,则redirect_uri一定要在客户端配置的redirect_uri列表内,且通过/oauth2/authorize获得code后,通过code来获取token时,请求中要有redirect_uri,且要和初始链接一致。

        5、同一个code只能用一次,之前我调试时,获取到了code,并根据code获得了token,结果在解析token时出了问题,我尝试再用那个code来获取token时就报错code过期,这算是一个常识吧,希望新上手的能吸取教训。

        6、遇到解决不了的问题,还是debug吧,通过OAuth2ClientAuthenticationFilter可以进入过滤器链,再打断点一步步的调试,耐心一点,总能找到原因的。

     后记与致谢

        最近一个月我都在死磕着OAuth,也是想凭着一鼓作气,把它的运用给一次性琢磨透彻了,然而事与愿违,越钻研下去,越发觉得它的博大精深,感觉不能靠一天两天就完全掌握,还是需要持续的学习和积累。之前的博客里我有提到,学习OAuth时感觉到一种深深的挫败感,因为我现在研究的东西,在17,18年已经被好多人研究透了。而这两天我又发现了一些变化,在SpringSecurity升级之后,很多大佬也整理了博客教新人如何使用spring-security-oauth2-authorization-server,这让我觉得前行的道路并不孤单,以下是我觉得对我帮助很大的博客,拜谢大佬,感激不尽!

        Spring Boot 最新版3.x 集成 OAuth 2.0实现认证授权服务  (首推,我就是看他的博客才配好服务端客户端的。)

        新版本Spring Security 2.7 + 用法

        SpringSecurity最新学习,spring-security-oauth2-authorization-server

        Springboot2.7 OAuth2 server使用jdbc存储RegisteredClient

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2174344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gartner 报告解读(二)| Open Telemetry可观测性解读与使用建议

上期跟大家解读了Gartner 成熟度曲线报告&#xff0c;主要分享了影响中国IT使用的4大因素--自主可控计划、AI发展趋势影响、降本增效、IT基础设施现代化程度。新来的朋友点这里&#xff0c;一键了解具体内容。 Gartner 成熟度曲线报告解读&#xff08;一&#xff09;| 2024中国…

sentinel原理源码分析系列(二)-动态规则和transport

本文是sentinel原理源码分析系列第二篇&#xff0c;分析两个组件&#xff0c;动态配置和transport 动态规则 Sentinel提供动态规则机制&#xff0c;依赖配置中心&#xff0c;如nacos&#xff0c;zookeeper&#xff0c;组件支持动态配置&#xff0c;模板类型为规则&#xff0c;支…

字节跳动青训营x豆包Marscode 技术训练营报名啦!

最近字节跳动青训营又开营了&#xff0c;作为第二次参加的我来给没有了解过的同学从几个方面简单介绍一下。 青训营是什么 青训营是字节跳动 稀土掘金 社区发起的技术系列培训 & 人才选拔项目&#xff0c;面向在校大学生&#xff0c; 课程全程免费&#xff0c;包含前端、…

mov视频怎么转换成mp4?这几种转换方法值得收藏起来!

mov视频怎么转换成mp4&#xff1f;MOV格式&#xff0c;作为苹果专属的产物&#xff0c;它在非苹果体系下的兼容性常常受限&#xff0c;导致用户可能在非苹果软件平台上遭遇播放难题&#xff0c;甚至无法顺利加载视频内容&#xff0c;而且&#xff0c;MOV格式以其独特的压缩技术…

sentinel原理源码分析系列(三)-启动和初始化

本文是sentinel原理源码分析系列第三篇&#xff0c;分析sentinel启动和初始化 启动/初始化 sentinel初始化分两块&#xff0c;静态初始和适配器(包括aop) 静态初始 1. Root EntranceNode 如果我们用一栋楼类比资源调用&#xff0c;root EntranceNode好比一栋楼的大门&…

干货 | 2024制造业数字化现状调查白皮书(免费下载)

导读&#xff1a;在这本白皮书中&#xff0c;我们询问了制造商有关数字化转型的工作情况、2024 年的优先事项和可持续性。研究结果清楚地表明&#xff0c;在数字化方面处于领先地位的制造商转型项目比那些没有规划或刚刚起步的项目实现的价值要大得多。 加入知识星球或关注下方…

windows11下vscode配置lua环境

一、lua插件的安装&#xff1a; 建议安装sumneko下的lua插件&#xff1a; 安装luadebug&#xff1a; 二、运行lua配置 安装code runner插件&#xff1a; 配置code runner 配置lua运行环境&#xff1a; 运行code&#xff0c;直接run code即可&#xff1a;

学习记录:js算法(四十八):另一棵树的子树

文章目录 另一棵树的子树我的思路网上思路 总结 另一棵树的子树 给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 二叉树 tree 的一棵子树包括 tree …

图文深入理解Oracle Network配置管理(一)

List item 本篇图文深入介绍Oracle Network配置管理。 Oracle Network概述 Oracle Net 服务 Oracle Net 监听程序 <oracle_home>/network/admin/listener.ora <oracle_home>/network/admin/sqlnet.ora建立网络连接 要建立客户机或中间层连接&#xff0c;Oracle…

【嵌入式软件-数据结构与算法】01-数据结构

摘录于老师的教学课程~~(*๓╰╯๓)~~内含链表、队列、栈、循环队列等详细介绍~~ 基础知识系列 有空再继续更~~~ 目录 【链表】 一、单链表 1、存储结构&#xff1a;带头结点的单链表 2、单链表结点类型的定义 3、创建单链表 1&#xff09;头插法 2&#xff09;尾插法 …

上交所服务器崩溃:金融交易背后的技术隐患暴露杭州BGP高防服务器43.228.71.X

一、上交所宕机事件始末 2024 年 9 月 27 日&#xff0c;上交所交易系统突发崩溃&#xff0c;这一事件犹如一颗巨石投入平静的湖面&#xff0c;引起了轩然大波。当天上午&#xff0c;众多投资者反馈券商交易出现延迟问题&#xff0c;随后上交所发布了《关于股票竞价交易出现异常…

产品管理 - 互联网产品(3) : 迭代管理

1、需求文档的每一个迭代版本号&#xff0c;都需要标识出来 根据软件文档的配置标准&#xff1a; 上线时&#xff1a;X.Y 修改时&#xff1a;X.YZ 草稿时&#xff1a;0.XY 2、每一个项目干系人&#xff0c;都可以访问到最新版本的需求。 所有角色必须要有统的一认知。这是需求…

【一文读懂】通信卫星频段探秘:从L到Ka的全面介绍(增加Q/V和UHF频段)

一、引言 背景介绍&#xff1a; 卫星通信&#xff0c;作为现代通信技术的关键一环&#xff0c;利用人造卫星作为中继&#xff0c;实现了全球范围内的即时通信。这一技术极大地拓宽了通信的边界&#xff0c;对现代社会产生了深远影响。 在广播电视领域&#xff0c;卫星通信让…

Linux——K8s pod调度

rc/rsdeployment statefulset daemonsetjob | cronjob 访问pod中的应用&#xff1a;在pod已经处于running状态之下&#xff0c;客户端的请求如何到达pod中的应用&#xff1f; K8S 平台本身的网络架构设计&#xff1a; coredns 属于K8S核心组件&#xff0c;提供K8S集群…

甄选范文“论软件的可靠性设计”,软考高级论文,系统架构设计师论文

论文真题 现代军事和商用系统中,随着系统中软件成分的不断增加,系统对软件的依赖性越来越强。软件可靠性已成为软件设计过程中不可或缺的重要组成部分。实践证明,保障软件可靠性最有效、最经济、最重要的手段是在软件设计阶段采取措施进行可靠性控制,由此提出了可靠性设计…

05-成神之路_ambari_Ambari实战-013-代码生命周期-metainfo-configFiles详解

1.Redis 集群 metainfo.xml 示例 <?xml version"1.0"?> <metainfo><schemaVersion>2.0</schemaVersion><services><service><!-- Redis 集群服务的基本信息 --><name>REDIS</name><displayName>Redi…

告别选择困难症,这些AI高效工具正改变着500万创作者的工作方式

本文背景 有个小伙子叫李光头&#xff0c;最近他为了紧跟 AI 的潮流&#xff0c;下载了不少新玩意&#xff1a;用 GPT 来写文案&#xff0c;用 Midjourney 来画图。 可当他准备开始这周的工作时&#xff0c;却发现自己陷入了一种奇怪的困境&#xff1a;虽然有了很多 AI 工具&am…

解决在vue项目中index.html中直接引入Cesium.js时候报错:Cesium is not defined

在vue项目直接引入Cesium&#xff1a; 报错&#xff1a;Cesium is not defined 原因&#xff1a;eslint报错&#xff0c;找不到Cesium 这个全局变量。 解决&#xff1a;向ESLint规则中添加全局变量&#xff1a; 找到package.json文件&#xff0c;在eslintConfig里加入 "…

【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯&#xff0c;你们的点赞收藏是我前进最大的动力&#xff01;&#xff01;希望本文内容能够帮助到你&#xff01; 目录 一&#xff1a;单例模式&#xff08;singleton&#xff09; 1&#xff1a;概念 二&#xff1a;“饿汉模…

CentOS 修改服务器登录密码的完整指南

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…