国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁 > news >正文

網(wǎng)站建設(shè)gzdlzggseo優(yōu)化技巧有哪些

網(wǎng)站建設(shè)gzdlzgg,seo優(yōu)化技巧有哪些,網(wǎng)站建設(shè)鏈接,小程序直播系統(tǒng)開發(fā)目錄 前言無圖無真相創(chuàng)建數(shù)據(jù)庫授權(quán)服務(wù)器maven 依賴application.yml授權(quán)服務(wù)器配置AuthorizationServierConfigDefaultSecutiryConfig 密碼模式擴(kuò)展PasswordAuthenticationTokenPasswordAuthenticationConverterPasswordAuthenticationProvider JWT 自定義字段自定義認(rèn)證響應(yīng)認(rèn)…

目錄

    • 前言
    • 無圖無真相
    • 創(chuàng)建數(shù)據(jù)庫
    • 授權(quán)服務(wù)器
      • maven 依賴
      • application.yml
      • 授權(quán)服務(wù)器配置
        • AuthorizationServierConfig
        • DefaultSecutiryConfig
      • 密碼模式擴(kuò)展
        • PasswordAuthenticationToken
        • PasswordAuthenticationConverter
        • PasswordAuthenticationProvider
      • JWT 自定義字段
      • 自定義認(rèn)證響應(yīng)
        • 認(rèn)證成功響應(yīng)
        • 認(rèn)證失敗響應(yīng)
        • 配置自定義處理器
      • 密碼模式測試
        • 單元測試
        • Postman 測試
    • 資源服務(wù)器
      • maven 依賴
      • application.yml
      • 資源服務(wù)器配置
    • 認(rèn)證流程測試
      • 登錄認(rèn)證授權(quán)
      • 獲取用戶信息
    • 結(jié)語
    • 源碼
    • 參考文檔

前言

Spring Security OAuth2 的最終版本是2.5.2,并于2022年6月5日正式宣布停止維護(hù)。Spring 官方為此推出了新的替代產(chǎn)品,即 Spring Authorization Server。然而,出于安全考慮,Spring Authorization Server 不再支持密碼模式,因?yàn)槊艽a模式要求客戶端直接處理用戶的密碼。但對于受信任的第一方系統(tǒng)(自有APP和管理系統(tǒng)等),許多情況下需要使用密碼模式。在這種情況下,需要在 Spring Authorization Server 的基礎(chǔ)上擴(kuò)展密碼模式的支持。本文基于開源微服務(wù)商城項(xiàng)目 youlai-mall、Spring Boot 3 和 Spring Authorization Server 1.1 版本,演示了如何擴(kuò)展密碼模式,以及如何將其應(yīng)用于 Spring Cloud 微服務(wù)實(shí)戰(zhàn)。

無圖無真相

通過 Spring Cloud Gateway 訪問認(rèn)證中心認(rèn)證成功獲取到訪問令牌。完整源碼:youlai-mall

創(chuàng)建數(shù)據(jù)庫

Spring Authorization Server 官方提供的授權(quán)服務(wù)器示例 demo-authorizationserver 初始化數(shù)據(jù)庫所使用的3個(gè)SQL腳本路徑如下:

根據(jù)路徑找到3張表的SQL腳本

  • 令牌發(fā)放記錄表: oauth2-authorization-schema.sql
  • 授權(quán)記錄表: oauth2-authorization-consent-schema.sql
  • 客戶端信息表: oauth2-registered-client-schema.sql

整合后的完整數(shù)據(jù)庫 SQL 腳本如下:

-- ----------------------------
-- 1. 創(chuàng)建數(shù)據(jù)庫
-- ----------------------------
CREATE DATABASE IF NOT EXISTS oauth2_server DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;-- ----------------------------
-- 2. 創(chuàng)建表
-- ----------------------------
use oauth2_server;SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 2.1 oauth2_authorization 令牌發(fā)放記錄表
-- ----------------------------
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 授權(quán)記錄表
-- ----------------------------
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)
);

授權(quán)服務(wù)器

youlai-auth 模塊作為認(rèn)證授權(quán)服務(wù)器

maven 依賴

在 youlai-auth 模塊的 pom.xml 添加授權(quán)服務(wù)器依賴

<!-- Spring Authorization Server 授權(quán)服務(wù)器依賴 -->
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.1.1</version>
</dependency>

application.yml

認(rèn)證中心配置 oauth2_server 數(shù)據(jù)庫連接信息

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

授權(quán)服務(wù)器配置

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

AuthorizationServierConfig

參考: Spring Authorization Server 官方示例 demo-authorizationserver 下的 AuthorizationServerConfig.java 進(jìn)行授權(quán)服務(wù)器配置

?

package com.youlai.auth.config;/*** 授權(quán)服務(wù)器配置** @author haoxr* @since 3.0.0*/
@Configuration
@RequiredArgsConstructor
@Slf4j
public class AuthorizationServerConfig {private final OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;/*** 授權(quán)服務(wù)器端點(diǎn)配置*/@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(// 自定義授權(quán)模式轉(zhuǎn)換器(Converter)List.of(new PasswordAuthenticationConverter()))).authenticationProviders(authenticationProviders ->// <2>authenticationProviders.addAll(// 自定義授權(quán)模式提供者(Provider)List.of(new PasswordAuthenticationProvider(authenticationManager, authorizationService, tokenGenerator)))).accessTokenResponseHandler(new MyAuthenticationSuccessHandler()) // 自定義成功響應(yīng).errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定義失敗響應(yīng));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();}/*** 初始化創(chuàng)建商城管理客戶端** @param registeredClientRepository*/private void initMallAdminClient(JdbcRegisteredClientRepository registeredClientRepository) {String clientId = "mall-admin";String clientSecret = "123456";String clientName = "商城管理客戶端";/*如果使用明文,客戶端認(rèn)證時(shí)會自動(dòng)升級加密方式,換句話說直接修改客戶端密碼,所以直接使用 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) // 驗(yàn)證碼模式.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);}/*** 初始化創(chuàng)建商城APP客戶端** @param registeredClientRepository*/private void initMallAppClient(JdbcRegisteredClientRepository registeredClientRepository) {String clientId = "mall-app";String clientSecret = "123456";String clientName = "商城APP客戶端";// 如果使用明文,在客戶端認(rèn)證的時(shí)候會自動(dòng)升級加密方式,直接使用 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) // 短信驗(yàn)證碼模式.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 進(jìn)行安全配置
package com.youlai.auth.config;/*** 授權(quán)服務(wù)器安全配置** @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) ->// 不走過濾器鏈(場景:靜態(tài)資源js、css、html)web.ignoring().requestMatchers("/webjars/**","/doc.html","/swagger-resources/**","/v3/api-docs/**","/swagger-ui/**");}
}

密碼模式擴(kuò)展

PasswordAuthenticationToken
package com.youlai.auth.authentication.password;/*** 密碼授權(quán)模式身份驗(yàn)證令牌(包含用戶名和密碼等)** @author haoxr* @since 3.0.0*/
public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {public static final AuthorizationGrantType PASSWORD = new AuthorizationGrantType("password");/*** 令牌申請?jiān)L問范圍*/private final Set<String> scopes;/*** 密碼模式身份驗(yàn)證令牌** @param clientPrincipal      客戶端信息* @param scopes               令牌申請?jiān)L問范圍* @param additionalParameters 自定義額外參數(shù)(用戶名和密碼)*/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;/*** 密碼模式參數(shù)解析器* <p>* 解析請求參數(shù)中的用戶名和密碼,并構(gòu)建相應(yīng)的身份驗(yàn)證(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) {// 授權(quán)類型 (必需)String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {return null;}// 客戶端信息Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();// 參數(shù)提取驗(yàn)證MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// 令牌申請?jiān)L問范圍驗(yàn)證 (可選)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, " ")));}// 用戶名驗(yàn)證(必需)String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);if (StrUtil.isBlank(username)) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.USERNAME,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 密碼驗(yàn)證(必需)String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);if (StrUtil.isBlank(password)) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,OAuth2ParameterNames.PASSWORD,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 附加參數(shù)(保存用戶名/密碼傳遞給 PasswordAuthenticationProvider 用于身份認(rèn)證)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;/*** 密碼模式身份驗(yàn)證提供者* <p>* 處理基于用戶名和密碼的身份驗(yàn)證** @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();// 驗(yàn)證客戶端是否支持授權(quán)類型(grant_type=password)if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);}// 生成用戶名密碼身份驗(yàn)證令牌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);// 用戶名密碼身份驗(yàn)證,成功后返回帶有權(quán)限的認(rèn)證信息Authentication usernamePasswordAuthentication;try {usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);} catch (Exception e) {// 需要將其他類型的異常轉(zhuǎn)換為 OAuth2AuthenticationException 才能被自定義異常捕獲處理,邏輯源碼 OAuth2TokenEndpointFilter#doFilterInternalthrow new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());}// 驗(yàn)證申請?jiān)L問范圍(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) 構(gòu)造器DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(usernamePasswordAuthentication) // 身份驗(yàn)證成功的認(rèn)證信息(用戶名、權(quán)限等信息).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(authorizedScopes).authorizationGrantType(AuthorizationGrantType.PASSWORD) // 授權(quán)方式.authorizationGrant(resourceOwnerPasswordAuthentication) // 授權(quán)具體對象;// 生成訪問令牌(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());// 權(quán)限數(shù)據(jù)(perms)比較多通過反射移除,不隨令牌一起持久化至數(shù)據(jù)庫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();// 持久化令牌發(fā)放記錄到數(shù)據(jù)庫this.authorizationService.save(authorization);additionalParameters = Collections.emptyMap();return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);}/*** 判斷傳入的 authentication 類型是否與當(dāng)前認(rèn)證提供者(AuthenticationProvider)相匹配--模板方法* <p>* ProviderManager#authenticate 遍歷 providers 找到支持對應(yīng)認(rèn)證請求的 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) { // 系統(tǒng)用戶添加自定義字段Long userId = userDetails.getUserId();claims.claim("user_id", userId);  // 添加系統(tǒng)用戶ID// 角色集合存JWTvar authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities()).stream().collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));claims.claim(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY, authorities);// 權(quán)限集合存Redis(數(shù)據(jù)多)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}});}};}}

自定義認(rèn)證響應(yīng)

🤔 如何自定義 OAuth2 認(rèn)證成功或失敗的響應(yīng)數(shù)據(jù)結(jié)構(gòu)符合當(dāng)前系統(tǒng)統(tǒng)一的規(guī)范?

下圖左側(cè)部份是 OAuth2 原生返回(?? ),大多數(shù)情況下,我們希望返回帶有業(yè)務(wù)碼的數(shù)據(jù)(??),以方便前端進(jìn)行處理。

OAuth2 處理認(rèn)證成功或失敗源碼坐標(biāo) OAuth2TokenEndpointFilter#doFilterInternal ,如下圖:

根據(jù)源碼閱讀,發(fā)現(xiàn)只要重寫? AuthenticationSuccessHandler 和? AuthenticationFailureHandler 的邏輯,就能夠自定義認(rèn)證成功和認(rèn)證失敗時(shí)的響應(yīng)數(shù)據(jù)格式。

認(rèn)證成功響應(yīng)
package com.youlai.auth.handler;/*** 認(rèn)證成功處理器** @author haoxr* @since 3.0.0*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {/*** MappingJackson2HttpMessageConverter 是 Spring 框架提供的一個(gè) HTTP 消息轉(zhuǎn)換器,用于將 HTTP 請求和響應(yīng)的 JSON 數(shù)據(jù)與 Java 對象之間進(jìn)行轉(zhuǎn)換*/private final HttpMessageConverter<Object> accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();private Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();/*** 自定義認(rèn)證成功響應(yīng)數(shù)據(jù)結(jié)構(gòu)** @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);}
}
認(rèn)證失敗響應(yīng)
package com.youlai.auth.handler;/*** 認(rèn)證失敗處理器** @author haoxr* @since 2023/7/6*/
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {/*** MappingJackson2HttpMessageConverter 是 Spring 框架提供的一個(gè) HTTP 消息轉(zhuǎn)換器,用于將 HTTP 請求和響應(yīng)的 JSON 數(shù)據(jù)與 Java 對象之間進(jìn)行轉(zhuǎn)換*/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()) // 自定義成功響應(yīng).errorResponseHandler(new MyAuthenticationFailureHandler()) // 自定義失敗響應(yīng));}

密碼模式測試

單元測試

啟動(dòng) youlai-system 模塊,需要從其獲取系統(tǒng)用戶信息(用戶名、密碼)進(jìn)行認(rèn)證

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());}
}

單元測試通過,打印響應(yīng)數(shù)據(jù)可以看到返回的 access_token 和 refresh_token

Postman 測試
  • 請求參數(shù)

  • 認(rèn)證參數(shù)

    Authorization Type 選擇 Basic Auth , 填寫客戶端ID(mall-admin)和密鑰(123456),

資源服務(wù)器

youlai-system 系統(tǒng)管理模塊也作為資源服務(wù)器

maven 依賴

<!-- Spring Authorization Server 授權(quán)服務(wù)器依賴 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

application.yml

通過 Feign 請求 youlai-system 服務(wù)以獲取系統(tǒng)用戶認(rèn)證信息(用戶名和密碼),在用戶尚未登錄的情況下,需要將此請求的路徑配置到白名單中以避免攔截。

security:# 允許無需認(rèn)證的路徑列表whitelist-paths:# 獲取系統(tǒng)用戶的認(rèn)證信息用于賬號密碼判讀- /api/v1/users/{username}/authInfo

資源服務(wù)器配置

配置 ResourceServerConfig 位于資源服務(wù)器公共模塊 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;/*** 資源服務(wù)器配置** @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;}
}

認(rèn)證流程測試

分別啟動(dòng) youlai-mall 的 youai-auth (認(rèn)證中心)、youlai-system(系統(tǒng)管理模塊)、youali-gateway(網(wǎng)關(guān))

登錄認(rèn)證授權(quán)

  • 請求參數(shù)

  • 認(rèn)證參數(shù)

    Authorization Type 選擇 Basic Auth , 填寫客戶端ID(mall-admin)和密鑰(123456),

  • 成功響應(yīng)

    認(rèn)證成功,獲取到訪問令牌(access_token )

獲取用戶信息

使用已獲得的訪問令牌 (access_token) 向資源服務(wù)器發(fā)送請求以獲取登錄用戶信息

在這里插入圖片描述

成功地獲取登錄用戶信息的響應(yīng),而不是出現(xiàn)未授權(quán)的401錯(cuò)誤。

結(jié)語

關(guān)于 Spring Authorization Server 1.1 版本的密碼模式擴(kuò)展和在 Spring Cloud 中使用新的授權(quán)方式,可以說與 Spring Security OAuth2 的代碼相似度極高。如果您已經(jīng)熟悉 Spring Security OAuth2,那么學(xué)習(xí) Spring Authorization Server 將變得輕而易舉。后續(xù)文章會更新其他常見授權(quán)模式的擴(kuò)展,敬請期待~

源碼

本文完整源碼: youlai-mall

參考文檔

  • Spring Security 棄用 授權(quán)服務(wù)器和資源服務(wù)器

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

    Spring Security OAuth 2.0 更新路線圖

http://aloenet.com.cn/news/36465.html

相關(guān)文章:

  • 網(wǎng)站建設(shè)外包公司網(wǎng)站搜索工具
  • 小米路由做網(wǎng)站服務(wù)器代運(yùn)營靠譜嗎
  • 簡單的網(wǎng)站開發(fā)購物鏈接
  • wordpress webvr網(wǎng)絡(luò)優(yōu)化器
  • 建高鐵站賺錢嗎百度資源共享鏈接分享組
  • 烏市seo網(wǎng)絡(luò)營銷流程網(wǎng)頁怎么優(yōu)化
  • 那個(gè)視頻網(wǎng)站做公開課比較好如何查詢百度搜索關(guān)鍵詞排名
  • 如何做閑置物品自己的網(wǎng)站外鏈工廠
  • 風(fēng)險(xiǎn)網(wǎng)站怎么解決方案軟文廣告經(jīng)典案例800字
  • 網(wǎng)站建設(shè)規(guī)劃ppt模板網(wǎng)站網(wǎng)絡(luò)推廣企業(yè)
  • 無錫市網(wǎng)站建設(shè)百度公司全稱叫什么
  • 大慶市住房與城鄉(xiāng)建設(shè)局網(wǎng)站品牌宣傳文案范文
  • 小白學(xué)做網(wǎng)站買什么書哪里能搜索引擎優(yōu)化
  • 利用html做博客網(wǎng)站做百度推廣多少錢
  • 網(wǎng)站建設(shè)方案書 備案百度排名查詢
  • 網(wǎng)站建設(shè)要考慮哪些企業(yè)如何進(jìn)行網(wǎng)絡(luò)營銷
  • 做圖網(wǎng)站有哪些東西嗎建網(wǎng)站用什么軟件
  • 用java做視頻網(wǎng)站app投放渠道有哪些
  • django做的網(wǎng)站舉例資源網(wǎng)站優(yōu)化排名軟件
  • 跨越速運(yùn)在黑龍江黑河網(wǎng)點(diǎn)網(wǎng)絡(luò)優(yōu)化工程師主要做什么
  • 望牛墩做網(wǎng)站每日軍事新聞
  • 廣西中國建設(shè)銀行網(wǎng)站首頁百度網(wǎng)站收錄提交
  • 做網(wǎng)站指導(dǎo)seo流量工具
  • 菲律賓 做菠菜網(wǎng)站視頻外鏈在線生成
  • jquery 素材的網(wǎng)站seo基本概念
  • 長沙專門做網(wǎng)站建設(shè)的公司2024年1月新冠高峰期
  • 企業(yè)網(wǎng)站建立流程百度站長平臺鏈接
  • 2345電視劇網(wǎng)站免費(fèi)鄭州seo服務(wù)公司
  • sae wordpress 圖片北京網(wǎng)站優(yōu)化站優(yōu)化
  • 學(xué)做蛋糕網(wǎng)站如何讓自己網(wǎng)站排名提高