当前位置: 首页 > news >正文

Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战

目录

    • 前言
    • 无图无真相
    • 创建数据库
    • 授权服务器
      • maven 依赖
      • application.yml
      • 授权服务器配置
        • AuthorizationServierConfig
        • DefaultSecutiryConfig
      • 密码模式扩展
        • PasswordAuthenticationToken
        • PasswordAuthenticationConverter
        • PasswordAuthenticationProvider
      • JWT 自定义字段
      • 自定义认证响应
        • 认证成功响应
        • 认证失败响应
        • 配置自定义处理器
      • 密码模式测试
        • 单元测试
        • Postman 测试
    • 资源服务器
      • maven 依赖
      • application.yml
      • 资源服务器配置
    • 认证流程测试
      • 登录认证授权
      • 获取用户信息
    • 结语
    • 源码
    • 参考文档

前言

Spring Security OAuth2 的最终版本是2.5.2,并于2022年6月5日正式宣布停止维护。Spring 官方为此推出了新的替代产品,即 Spring Authorization Server。然而,出于安全考虑,Spring Authorization Server 不再支持密码模式,因为密码模式要求客户端直接处理用户的密码。但对于受信任的第一方系统(自有APP和管理系统等),许多情况下需要使用密码模式。在这种情况下,需要在 Spring Authorization Server 的基础上扩展密码模式的支持。本文基于开源微服务商城项目 youlai-mall、Spring Boot 3 和 Spring Authorization Server 1.1 版本,演示了如何扩展密码模式,以及如何将其应用于 Spring Cloud 微服务实战。

无图无真相

通过 Spring Cloud Gateway 访问认证中心认证成功获取到访问令牌。完整源码:youlai-mall

创建数据库

Spring Authorization Server 官方提供的授权服务器示例 demo-authorizationserver 初始化数据库所使用的3个SQL脚本路径如下:

根据路径找到3张表的SQL脚本

  • 令牌发放记录表: oauth2-authorization-schema.sql
  • 授权记录表: oauth2-authorization-consent-schema.sql
  • 客户端信息表: oauth2-registered-client-schema.sql

整合后的完整数据库 SQL 脚本如下:

-- ----------------------------
-- 1. 创建数据库
-- ----------------------------
CREATE DATABASE IF NOT EXISTS oauth2_server DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;-- ----------------------------
-- 2. 创建表
-- ----------------------------
use oauth2_server;SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 2.1 oauth2_authorization 令牌发放记录表
-- ----------------------------
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)
);-- ----------------------------
-- 2.2 oauth2_authorization_consent 授权记录表
-- ----------------------------
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)
);-- ----------------------------
-- 2.3 oauth2-registered-client OAuth2 客户端信息表
-- ----------------------------
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)
);

授权服务器

youlai-auth 模块作为认证授权服务器

maven 依赖

在 youlai-auth 模块的 pom.xml 添加授权服务器依赖

<!-- Spring Authorization Server 授权服务器依赖 -->
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.1.1</version>
</dependency>

application.yml

认证中心配置 oauth2_server 数据库连接信息

spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2_server?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=trueusername: rootpassword: 123456

授权服务器配置

参考 Spring Authorization Server 官方示例 demo-authorizationserver

AuthorizationServierConfig

参考: Spring Authorization Server 官方示例 demo-authorizationserver 下的 AuthorizationServerConfig.java 进行授权服务器配置

package com.youlai.auth.config;/*** 授权服务器配置** @author haoxr* @since 3.0.0*/
@Configuration
@RequiredArgsConstructor
@Slf4j
public class AuthorizationServerConfig {private final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;/*** 授权服务器端点配置*/@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,AuthenticationManager authenticationManager,OAuth2AuthorizationService authorizationService,OAuth2TokenGenerator<?> tokenGenerator) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();authorizationServerConfigurer.tokenEndpoint(tokenEndpoint ->tokenEndpoint.accessTokenRequestConverters(authenticationConverters ->// <1>authenticationConverters.addAll(// 自定义授权模式转换器(Converter)List.of(new PasswordAuthenticationConverter()))).authenticationProviders(authenticationProviders ->// <2>authenticationProviders.addAll(// 自定义授权模式提供者(Provider)List.of(new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator)))).accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应);RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();http.securityMatcher(endpointsMatcher).authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)).apply(authorizationServerConfigurer);return http.build();}@Bean // <5>public JWKSource<SecurityContext> jwkSource() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();// @formatter:offRSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();// @formatter:onJWKSet jwkSet = new JWKSet(rsaKey);return new ImmutableJWKSet<>(jwkSet);}private static KeyPair generateRsaKey() { // <6>KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();} catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}@Beanpublic PasswordEncoder passwordEncoder() {return PasswordEncoderFactories.createDelegatingPasswordEncoder();}@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);// 初始化 OAuth2 客户端initMallAppClient(registeredClientRepository);initMallAdminClient(registeredClientRepository);return registeredClientRepository;}@Beanpublic OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,RegisteredClientRepository registeredClientRepository) {JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);rowMapper.setLobHandler(new DefaultLobHandler());ObjectMapper objectMapper = new ObjectMapper();ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);objectMapper.registerModules(securityModules);objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());// 使用刷新模式,需要从 oauth2_authorization 表反序列化attributes字段得到用户信息(SysUserDetails)objectMapper.addMixIn(SysUserDetails.class, SysUserMixin.class);objectMapper.addMixIn(Long.class, Object.class);rowMapper.setObjectMapper(objectMapper);service.setAuthorizationRowMapper(rowMapper);return service;}@Beanpublic OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,RegisteredClientRepository registeredClientRepository) {// Will be used by the ConsentControllerreturn new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);}@BeanOAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));jwtGenerator.setJwtCustomizer(jwtCustomizer);OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}/*** 初始化创建商城管理客户端** @param registeredClientRepository*/private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) {String clientId = "mall-admin";String clientSecret = "123456";String clientName = "商城管理客户端";/*如果使用明文,客户端认证时会自动升级加密方式,换句话说直接修改客户端密码,所以直接使用 bcrypt 加密避免不必要的麻烦官方ISSUE: https://github.com/spring-projects/spring-authorization-server/issues/1099*/String encodeSecret = passwordEncoder().encode(clientSecret);RegisteredClient registeredMallAdminClient = registeredClientRepository.findByClientId(clientId);String id = registeredMallAdminClient != null ? registeredMallAdminClient.getId() : UUID.randomUUID().toString();RegisteredClient mallAppClient = RegisteredClient.withId(id).clientId(clientId).clientSecret(encodeSecret).clientName(clientName).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).authorizationGrantType(AuthorizationGrantType.PASSWORD) // 密码模式.authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 验证码模式.redirectUri("http://127.0.0.1:8080/authorized").postLogoutRedirectUri("http://127.0.0.1:8080/logged-out").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();registeredClientRepository.save(mallAppClient);}/*** 初始化创建商城APP客户端** @param registeredClientRepository*/private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) {String clientId = "mall-app";String clientSecret = "123456";String clientName = "商城APP客户端";// 如果使用明文,在客户端认证的时候会自动升级加密方式,直接使用 bcrypt 加密避免不必要的麻烦String encodeSecret = passwordEncoder().encode(clientSecret);RegisteredClient registeredMallAppClient = registeredClientRepository.findByClientId(clientId);String id = registeredMallAppClient != null ? registeredMallAppClient.getId() : UUID.randomUUID().toString();RegisteredClient mallAppClient = RegisteredClient.withId(id).clientId(clientId).clientSecret(encodeSecret).clientName(clientName).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).authorizationGrantType(WxMiniAppAuthenticationToken.WECHAT_MINI_APP) // 微信小程序模式.authorizationGrantType(SmsCodeAuthenticationToken.SMS_CODE) // 短信验证码模式.redirectUri("http://127.0.0.1:8080/authorized").postLogoutRedirectUri("http://127.0.0.1:8080/logged-out").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();registeredClientRepository.save(mallAppClient);}
}
DefaultSecutiryConfig
  • 参考 Spring Authorization Server 官方示例 demo-authorizationserver 下的 DefaultSecurityConfig.java 进行安全配置
package com.youlai.auth.config;/*** 授权服务器安全配置** @author haoxr* @since 3.0.0*/
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {/*** Spring Security 安全过滤器链配置*/@Bean@Order(0)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(requestMatcherRegistry ->{requestMatcherRegistry.anyRequest().authenticated();}).csrf(AbstractHttpConfigurer::disable).formLogin(Customizer.withDefaults());return http.build();}/*** Spring Security 自定义安全配置*/@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return (web) ->// 不走过滤器链(场景:静态资源js、css、html)web.ignoring().requestMatchers("/webjars/**","/doc.html","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**");}
}

密码模式扩展

PasswordAuthenticationToken
package com.youlai.auth.authentication.password;/*** 密码授权模式身份验证令牌(包含用户名和密码等)** @author haoxr* @since 3.0.0*/
public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");/*** 令牌申请访问范围*/private final Set<String> scopes;/*** 密码模式身份验证令牌** @param clientPrincipal      客户端信息* @param scopes               令牌申请访问范围* @param additionalParameters 自定义额外参数(用户名和密码)*/public PasswordAuthenticationToken(Authentication clientPrincipal,Set<String> scopes,@Nullable Map<String, Object> additionalParameters) {super(PASSWORD, clientPrincipal, additionalParameters);this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());}/*** 用户凭证(密码)*/@Overridepublic Object getCredentials() {return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);}public Set<String> getScopes() {return scopes;}
}
PasswordAuthenticationConverter
package com.youlai.auth.authentication.password;/*** 密码模式参数解析器* <p>* 解析请求参数中的用户名和密码,并构建相应的身份验证(Authentication)对象** @author haoxr* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter* @since 3.0.0*/
public class PasswordAuthenticationConverter implements AuthenticationConverter {@Overridepublic 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 scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);if (StringUtils.hasText(scope) &&parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.SCOPE,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}Set<String> requestedScopes = null;if (StringUtils.hasText(scope)) {requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));}// 用户名验证(必需)String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);if (StrUtil.isBlank(username)) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.USERNAME,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 密码验证(必需)String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);if (StrUtil.isBlank(password)) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.PASSWORD,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用于身份认证)Map<String, Object> additionalParameters = parameters.entrySet().stream().filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&!e.getKey().equals(OAuth2ParameterNames.SCOPE)).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));return new PasswordAuthenticationToken(clientPrincipal,requestedScopes,additionalParameters);}}
PasswordAuthenticationProvider
package com.youlai.auth.authentication.password;/*** 密码模式身份验证提供者* <p>* 处理基于用户名和密码的身份验证** @author haoxr* @since 3.0.0*/
@Slf4j
public class PasswordAuthenticationProvider implements AuthenticationProvider {private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";private final AuthenticationManager authenticationManager;private final OAuth2AuthorizationService authorizationService;private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;/*** Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.** @param authenticationManager the authentication manager* @param authorizationService  the authorization service* @param tokenGenerator        the token generator* @since 0.2.3*/public PasswordAuthenticationProvider(AuthenticationManager authenticationManager,OAuth2AuthorizationService authorizationService,OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {Assert.notNull(authorizationService, "authorizationService cannot be null");Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");this.authenticationManager = authenticationManager;this.authorizationService = authorizationService;this.tokenGenerator = tokenGenerator;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {PasswordAuthenticationToken resourceOwnerPasswordAuthentication = (PasswordAuthenticationToken) authentication;OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient(resourceOwnerPasswordAuthentication);RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();// 验证客户端是否支持授权类型(grant_type=password)if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);}// 生成用户名密码身份验证令牌Map<String, Object> additionalParameters = resourceOwnerPasswordAuthentication.getAdditionalParameters();String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);// 用户名密码身份验证,成功后返回带有权限的认证信息Authentication usernamePasswordAuthentication;try {usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);} catch (Exception e) {// 需要将其他类型的异常转换为 OAuth2AuthenticationException 才能被自定义异常捕获处理,逻辑源码 OAuth2TokenEndpointFilter#doFilterInternalthrow new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());}// 验证申请访问范围(Scope)Set<String> authorizedScopes = registeredClient.getScopes();Set<String> requestedScopes = resourceOwnerPasswordAuthentication.getScopes();if (!CollectionUtils.isEmpty(requestedScopes)) {Set<String> unauthorizedScopes = requestedScopes.stream().filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)).collect(Collectors.toSet());if (!CollectionUtils.isEmpty(unauthorizedScopes)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);}authorizedScopes = new LinkedHashSet<>(requestedScopes);}// 访问令牌(Access Token) 构造器DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(usernamePasswordAuthentication) // 身份验证成功的认证信息(用户名、权限等信息).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(authorizedScopes).authorizationGrantType(AuthorizationGrantType.PASSWORD) // 授权方式.authorizationGrant(resourceOwnerPasswordAuthentication) // 授权具体对象;// 生成访问令牌(Access Token)OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType((OAuth2TokenType.ACCESS_TOKEN)).build();OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);if (generatedAccessToken == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the access token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());// 权限数据(perms)比较多通过反射移除,不随令牌一起持久化至数据库ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null);OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName()).authorizationGrantType(AuthorizationGrantType.PASSWORD).authorizedScopes(authorizedScopes).attribute(Principal.class.getName(), usernamePasswordAuthentication); // attribute 字段if (generatedAccessToken instanceof ClaimAccessor) {authorizationBuilder.token(accessToken, (metadata) ->metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));} else {authorizationBuilder.accessToken(accessToken);}// 生成刷新令牌(Refresh Token)OAuth2RefreshToken refreshToken = null;if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&// Do not issue refresh token to public client!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the refresh token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}refreshToken = (OAuth2RefreshToken) generatedRefreshToken;authorizationBuilder.refreshToken(refreshToken);}OAuth2Authorization authorization = authorizationBuilder.build();// 持久化令牌发放记录到数据库this.authorizationService.save(authorization);additionalParameters = Collections.emptyMap();return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);}/*** 判断传入的 authentication 类型是否与当前认证提供者(AuthenticationProvider)相匹配--模板方法* <p>* ProviderManager#authenticate 遍历 providers 找到支持对应认证请求的 provider-迭代器模式** @param authentication* @return*/@Overridepublic boolean supports(Class<?> authentication) {return PasswordAuthenticationToken.class.isAssignableFrom(authentication);}}

JWT 自定义字段

参考官方 ISSUE :Adds how-to guide on adding authorities to access tokens

package com.youlai.auth.config;/*** JWT 自定义字段** @author haoxr* @since 3.0.0*/
@Configuration
@RequiredArgsConstructor
public class JwtTokenClaimsConfig {private final RedisTemplate redisTemplate;@Beanpublic OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {return context -> {if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) {// Customize headers/claims for access_tokenOptional.ofNullable(context.getPrincipal().getPrincipal()).ifPresent(principal -> {JwtClaimsSet.Builder claims = context.getClaims();if (principal instanceof SysUserDetails userDetails) { // 系统用户添加自定义字段Long userId = userDetails.getUserId();claims.claim("user_id", userId);  // 添加系统用户ID// 角色集合存JWTvar authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities()).stream().collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));claims.claim(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY, authorities);// 权限集合存Redis(数据多)Set<String> perms = userDetails.getPerms();redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userId, perms);} else if (principal instanceof MemberDetails userDetails) { // 商城会员添加自定义字段claims.claim("member_id", String.valueOf(userDetails.getId())); // 添加会员ID}});}};}}

自定义认证响应

🤔 如何自定义 OAuth2 认证成功或失败的响应数据结构符合当前系统统一的规范?

下图左侧部份是 OAuth2 原生返回(⬅️ ),大多数情况下,我们希望返回带有业务码的数据(➡️),以方便前端进行处理。

OAuth2 处理认证成功或失败源码坐标 OAuth2TokenEndpointFilter#doFilterInternal ,如下图:

根据源码阅读,发现只要重写✅ AuthenticationSuccessHandler 和❌ AuthenticationFailureHandler 的逻辑,就能够自定义认证成功和认证失败时的响应数据格式。

认证成功响应
package com.youlai.auth.handler;/*** 认证成功处理器** @author haoxr* @since 3.0.0*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {/*** MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换*/private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();/*** 自定义认证成功响应数据结构** @param request the request which caused the successful authentication* @param response the response* @param authentication the <tt>Authentication</tt> object which was created during* the authentication process.* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =(OAuth2AccessTokenAuthenticationToken) authentication;OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();OAuth2AccessTokenResponse.Builder builder =OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()).tokenType(accessToken.getTokenType());if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));}if (refreshToken != null) {builder.refreshToken(refreshToken.getTokenValue());}if (!CollectionUtils.isEmpty(additionalParameters)) {builder.additionalParameters(additionalParameters);}OAuth2AccessTokenResponse accessTokenResponse = builder.build();Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter.convert(accessTokenResponse);ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);}
}
认证失败响应
package com.youlai.auth.handler;/*** 认证失败处理器** @author haoxr* @since 2023/7/6*/
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {/*** MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换*/private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);Result result = Result.failed(error.getErrorCode());accessTokenHttpResponseConverter.write(result, null, httpResponse);}
}
配置自定义处理器

AuthorizationServierConfig

public SecurityFilterChain authorizationServerSecurityFilterChain() throws Exception {// ...authorizationServerConfigurer.tokenEndpoint(tokenEndpoint ->tokenEndpoint// ....accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定义成功响应.errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定义失败响应);}

密码模式测试

单元测试

启动 youlai-system 模块,需要从其获取系统用户信息(用户名、密码)进行认证

package com.youlai.auth.authentication;/*** OAuth2 密码模式单元测试*/
@SpringBootTest
@AutoConfigureMockMvc
@Slf4j
public class PasswordAuthenticationTests {@Autowiredprivate MockMvc mvc;/*** 测试密码模式登录*/@Testvoid testPasswordLogin() throws Exception {HttpHeaders headers = new HttpHeaders();// 客户端ID和密钥headers.setBasicAuth("mall-admin", "123456");this.mvc.perform(post("/oauth2/token").param(OAuth2ParameterNames.GRANT_TYPE, "password") // 密码模式.param(OAuth2ParameterNames.USERNAME, "admin") // 用户名.param(OAuth2ParameterNames.PASSWORD, "123456") // 密码.headers(headers)).andDo(print()).andExpect(status().isOk()).andExpect(jsonPath("$.data.access_token").isNotEmpty());}
}

单元测试通过,打印响应数据可以看到返回的 access_token 和 refresh_token

Postman 测试
  • 请求参数

  • 认证参数

    Authorization Type 选择 Basic Auth , 填写客户端ID(mall-admin)和密钥(123456),

资源服务器

youlai-system 系统管理模块也作为资源服务器

maven 依赖

<!-- Spring Authorization Server 授权服务器依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

application.yml

通过 Feign 请求 youlai-system 服务以获取系统用户认证信息(用户名和密码),在用户尚未登录的情况下,需要将此请求的路径配置到白名单中以避免拦截。

security:# 允许无需认证的路径列表whitelist-paths:# 获取系统用户的认证信息用于账号密码判读- /api/v1/users/{username}/authInfo

资源服务器配置

配置 ResourceServerConfig 位于资源服务器公共模块 common-security 中

package com.youlai.common.security.config;import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONUtil;
import com.youlai.common.constant.SecurityConstants;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;import java.util.List;/*** 资源服务器配置** @author haoxr* @since 3.0.0*/
@ConfigurationProperties(prefix = "security")
@Configuration
@EnableWebSecurity
@Slf4j
public class ResourceServerConfig {/*** 白名单路径列表*/@Setterprivate List<String> ignoreUrls;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {log.info("whitelist path:{}", JSONUtil.toJsonStr(ignoreUrls));http.authorizeHttpRequests(requestMatcherRegistry ->{if (CollectionUtil.isNotEmpty(ignoreUrls)) {requestMatcherRegistry.requestMatchers(Convert.toStrArray(ignoreUrls)).permitAll();}requestMatcherRegistry.anyRequest().authenticated();}).csrf(AbstractHttpConfigurer::disable);http.oauth2ResourceServer(resourceServerConfigurer ->resourceServerConfigurer.jwt(jwtConfigurer -> jwtAuthenticationConverter())) ;return http.build();}/*** 不走过滤器链的放行配置*/@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return (web) -> web.ignoring().requestMatchers("/webjars/**","/doc.html","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**");}/*** 自定义JWT Converter** @return Converter* @see JwtAuthenticationProvider#setJwtAuthenticationConverter(Converter)*/@Beanpublic Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY);jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY);JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);return jwtAuthenticationConverter;}
}

认证流程测试

分别启动 youlai-mall 的 youai-auth (认证中心)、youlai-system(系统管理模块)、youali-gateway(网关)

登录认证授权

  • 请求参数

  • 认证参数

    Authorization Type 选择 Basic Auth , 填写客户端ID(mall-admin)和密钥(123456),

  • 成功响应

    认证成功,获取到访问令牌(access_token )

获取用户信息

使用已获得的访问令牌 (access_token) 向资源服务器发送请求以获取登录用户信息

在这里插入图片描述

成功地获取登录用户信息的响应,而不是出现未授权的401错误。

结语

关于 Spring Authorization Server 1.1 版本的密码模式扩展和在 Spring Cloud 中使用新的授权方式,可以说与 Spring Security OAuth2 的代码相似度极高。如果您已经熟悉 Spring Security OAuth2,那么学习 Spring Authorization Server 将变得轻而易举。后续文章会更新其他常见授权模式的扩展,敬请期待~

源码

本文完整源码: youlai-mall

参考文档

  • Spring Security 弃用 授权服务器和资源服务器

  • Spring Security OAuth 生命周期终止通知

    Spring Security OAuth 2.0 更新路线图

相关文章:

Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战

目录 前言无图无真相创建数据库授权服务器maven 依赖application.yml授权服务器配置AuthorizationServierConfigDefaultSecutiryConfig 密码模式扩展PasswordAuthenticationTokenPasswordAuthenticationConverterPasswordAuthenticationProvider JWT 自定义字段自定义认证响应认…...

第二证券:AIGC概念活跃,焦点科技、三维通信涨停,万兴科技大涨

AIGC概念24日盘中走势生动&#xff0c;到发稿&#xff0c;万兴科技、三态股份涨超10%&#xff0c;焦点科技、三维通讯、我国科传等涨停&#xff0c;中文在线涨超9%&#xff0c;果麦文明、新国都涨约7%。 消息面上&#xff0c;各大电商途径于10月18-24日先后发动“双11”大促或…...

7-4、S加减速转动实现【51单片机控制步进电机-TB6600系列】

摘要&#xff1a;本节介绍实现步进电机S曲线运动的代码 一、目标功能 实现步进电机转动总角度720&#xff0c;其中加减速各90 加速段&#xff1a;加速类型&#xff1a;S曲线   加速角度&#xff1a;角度为90   起步速度&#xff1a;30RPM&#xff0c;   终止速度&#x…...

RK3568-pcie接口

pcie接口与sata接口 pcie总线pcie总线pcie控制器sata控制器nvme设备sata设备nvme协议ahci协议m-key接口b-key接口RC模式和EP模式 RC和EP分别对应主模式和从模式,普通的PCI RC主模式可以用于连接PCI-E以太网芯片或PCI-E的硬盘等外设。 RC模式使用外设一般都有LINUX驱动程序,安…...

spring监听请求执行结束,移除当前ThreadLocal数据两种方法

在开发过程中&#xff0c;很多时候我们会使用ThreadLocal来临时缓存数据&#xff0c;当一次数据请求执行完成后需要主动执行释放当前ThreadLocal缓存数据资源&#xff0c;防止未能及时释放导致下一次访问时候ThreadLocal依然保持上一次缓存的数据。 spring提供两种方式去监听一…...

知识图谱--Jena基础操作和检索推理应用

在上一篇读书笔记中讲到知识图谱存储主要有基于开源的Jena方式和基于图数据库(Neo4j)方式,本次主要对Jena的基础操作和如何应用进行了实践总结,同时结合了D2R,将结构化数据转换成Jena可以加载的格式(即RDF,后缀是.nt),Apache Jena作为一种开源的Java语义网框架,主要功…...

GEE python——将GEE ASSETS中存储的影像或者矢量转化为数据格式XEE()

数据转换器是内置于 getPixels、computePixels、listFeatures 和 computeFeatures 中的客户端转换能力。通过指定兼容的文件格式,这些方法可以返回 Python 原生格式的数据,如用于栅格的结构化 NumPy 数组和用于矢量的 Pandas DataFrames 或 GeoPandas GeoDataFrames。对于矢量…...

Java集合框架:List、Set、Map类型及泛型详解

文章目录 &#x1f4d5;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作者、产品软文创造者、技术文章评审老师、问卷调查设计师、个人社区创始人、开源项目贡献者。&#x1f30e;跑过十五…...

Ubuntu 安装 docker

一.添加Docker官方GPG密钥 curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - 如果出现&#xff1a; The program curl is currently not installed. You can install it by typing: sudo apt install curl 先安装 curl : sudo apt inst…...

BUUCTF zip伪加密 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 下载附件&#xff0c;得到一个zip压缩包。 密文&#xff1a; 解题思路&#xff1a; 1、刚开始尝试解压&#xff0c;看到了flag.txt文件&#xff0c;但需要解压密码。结合题目&#xff0c;确认这是zip伪加密&#…...

p5.js 到底怎么设置背景图?

本文简介 点赞 关注 收藏 学会了 在 《p5.js 光速入门》 里我们学过加载图片元素&#xff0c;学过过背景色的用法&#xff0c;但当时没提到背景图要怎么使用。 本文就把背景图这部分内容补充完整&#xff0c;并且会提到在 p5.js 里使用背景图的一些注意点。 背景图的用法…...

python+unittest+requests+HTMLRunner编写接口自动化测试集

问题描述&#xff1a; 搭建接口测试框架&#xff0c;执行用例请求多个不同请求方式的接口 实现步骤&#xff1a; ① 创建配置文件config.ini&#xff0c;写入部分公用参数&#xff0c;如接口的基本url、测试报告文件路径、测试数据文件路径等配置项 1 [DATABASE] 2 data_addre…...

Vue---监听div元素宽高改变时echart图表重新resize

一、需求描述 当点击上图的红色框时&#xff0c;echart的div元素宽会改变但是无法触发echarts图表的resize重新渲染&#xff0c;对于浏览器而言&#xff0c;浏览器具有window.resize方法监听浏览器窗口大小的改变&#xff0c;而div元素没有监听宽高改变的方法。 二、解决方案 …...

Kubernetes Etcd不可用日志:NOSPACE 的问题修复

两套k8s集群同一天同时出现etcd集群空间超过配额&#xff0c;kubectl get cs时发现所有的etcd均返回503报错&#xff0c;查看etcd的告警发现有NO SPACE的信息且 etcdctl --endpoints${ETCD_ENDPOINT} --cert${ETCD_CERTFILE} --key${ETCD_KEYFILE} --cacert${ETCD_CAFILE} endp…...

分组卷积的思想神了

大家好啊&#xff0c;我是董董灿。 最近&#xff0c;分组卷积帮我解决了一个大忙&#xff0c;事情是这样的。 这几天遇到一个头疼的问题&#xff0c;就是要在某一芯片上完成一个神经网络的适配&#xff0c;这个神经网络中卷积居多&#xff0c;并且有一些卷积的通道数很大&…...

北邮22级信通院数电:Verilog-FPGA(7)第七周实验(2):BCD七段显示译码器(关注我的uu们加群咯~)

北邮22信通一枚~ 跟随课程进度更新北邮信通院数字系统设计的笔记、代码和文章 持续关注作者 迎接数电实验学习~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院数电实验_青山如墨雨如画的博客-CSDN博客 关注作者的uu们可以进群啦~ 一.verilog代码 1.1 …...

LVS集群-DR模式

概念&#xff1a; LVS-DR模式&#xff0c;也是最常用的lVS负载方式&#xff0c;DR DIRECT ROUTING 直接路由模式 负载均衡器lVS调度器&#xff0c;只负责请求和转发到后端的真实服务器&#xff0c;但是影响结果&#xff0c;由后端服务器直接转发给客户端&#xff0c;不需要经…...

行为型模式-状态模式

在状态模式中&#xff0c;类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。 在状态模式中&#xff0c;我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。 意图&#xff1a;允许对象在内部状态发生改变时改变它的行为&#xff0…...

html/css/javascript/js实现的简易打飞机游戏

源码下载地址 支持&#xff1a;远程部署/安装/调试、讲解、二次开发/修改/定制 视频浏览地址...

ubuntu 22.04安装百度网盘

百度网盘 客户端下载 (baidu.com) 下载地址 sudo dpkg -i baidunetdisk_4.17.7_amd64.deb...

关于路由转发

路由表的作用 路由表的作用&#xff1a;目标网络匹配路由表&#xff0c;从相应网络转发&#xff1b;不匹配路由表&#xff0c;丢弃或转发至默认路由器。 路由转发的原理 根据IP地址找到目标网络&#xff0c;由应路由器解封装查看目标网络是否可达&#xff0c;重新封装进行转…...

oradebug current_sql

CDB 或者PDB都可以&#xff0c;但是数据库sid不对找不到SQL TEXT ps -ef|grep 3485015 oracle 3485015 1 51 21:19 ? 00:01:10 oracle (LOCALNO) oracle 3489325 3488836 0 21:21 pts/0 00:00:00 grep --colorauto 3485015 . oraenv [oracle~]$ s SQL*…...

JSON(详解)

目录 什么是JSON&#xff1f; 哪里会用到JSON&#xff1f; JSON的特点 JSON的优点 JSON的缺点 JSON和cJSON的关系 什么是JSON&#xff1f; JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式。它以易于阅读和编写的文本格式来存储和表示…...

1-多媒体通信概述

文章目录 媒体和多媒体媒体多媒体VarityIntergrationInteraction 多媒体通信(MMC)业务类型 MMC主要问题和关键技术主要问题关键技术 MMC发展动向重要事件趋势 标准化组织 媒体和多媒体 媒体 承载信息的载体. 感知媒体, 表示媒体, 显示媒体, 存储媒体, 传输媒体. 多媒体 Var…...

k8s集群环境搭建

k8s环境集群搭建 1.简述2.k8s概述3.软硬件配置3.1 硬件配置3.2 软件配置4 环境配置4.1使用root权限4.2 节点ip配置4.3 设置系统主机名4.4 解析主机名4.5 关闭swap4.6 设置时间同步5 安装集群所需软件5.1 安装 docker5.2 安装cri-dockerd5.3 安装 Kubernetes6 K8s集群实例部署6.…...

Makefile 基础教程:从零开始学习

在软件开发过程中&#xff0c;Makefile是一个非常重要的工具&#xff0c;它可以帮助我们自动构建程序&#xff0c;管理程序依赖关系&#xff0c;提高开发效率。本篇博客将从基础开始&#xff0c;介绍Makefile的相关知识&#xff0c;帮助大家快速掌握Makefile的使用方法 Makefil…...

绝对路径与相对路径

目录 一、绝对路径 二、相对路径 三、举例 一、绝对路径 绝对路径是指从根目录开始的完整路径&#xff0c;包括所有父目录的路径&#xff0c;直到目标文件或者目录 所在的位置。 全文件名全路径文件名绝对路经完整的路径 例如:在windows系统中&#xff0c;绝对路径通常以…...

电大搜题:开启智慧学习新时代

近年来&#xff0c;随着社会的发展和科技的迅猛进步&#xff0c;远程教育成为了广大学子继续教育的新选择。而在重庆&#xff0c;一所备受关注的远程教育学府——重庆开放大学&#xff0c;以其开放的教育理念和多元的学习方式&#xff0c;为广大学生提供了便捷而高效的学习平台…...

Android Studio 导出 jar

AS版本&#xff1a;Android Studio Giraffe | 2022.3.1 Patch 1 1、File——New Module——Android Library 2、mylibrary——main——新建功能类 3、mylibrary——build.gradle——android {}内复制以下代码——Sync Now //Copy类型 tasks.register(makeJar, Copy) { //删…...

【Leetcode】【每日一题】【中等】1465. 切割后面积最大的蛋糕

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/maximum-area-of-a-piece-of-cak…...