From bcd544b309b69834e4410b3b407266a8847f5b52 Mon Sep 17 00:00:00 2001 From: jumuning <840256574@qq.com> Date: Sun, 29 May 2022 16:16:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=AF=86=E7=A0=81=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=8A=A0sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/auth.sql | 81 +++++ .../AuthorizationServerConfiguration.java | 82 +++-- .../auth/config/WebSecurityConfiguration.java | 11 - .../pig/auth/endpoint/LoginEndpoint.java | 330 +++++++++--------- .../pig/auth/support/OAuth2EndpointUtils.java | 50 +++ .../auth/support/OAuth2ErrorCodesExpand.java | 33 ++ ...eOwnerPasswordAuthenticationConverter.java | 78 +++++ ...ceOwnerPasswordAuthenticationProvider.java | 279 +++++++++++++++ ...ourceOwnerPasswordAuthenticationToken.java | 80 +++++ .../pig/auth/support/ScopeException.java | 28 ++ pig-common/pig-common-security/pom.xml | 6 +- .../PigResourceServerAutoConfiguration.java | 75 ++++ .../PigTokenStoreAutoConfiguration.java | 13 +- 13 files changed, 941 insertions(+), 205 deletions(-) create mode 100644 db/auth.sql create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2EndpointUtils.java create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ErrorCodesExpand.java create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationConverter.java create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationProvider.java create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationToken.java create mode 100644 pig-auth/src/main/java/com/pig4cloud/pig/auth/support/ScopeException.java diff --git a/db/auth.sql b/db/auth.sql new file mode 100644 index 00000000..aea28210 --- /dev/null +++ b/db/auth.sql @@ -0,0 +1,81 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for oauth2_authorization +-- ---------------------------- +DROP TABLE IF EXISTS `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, + `attributes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, + `state` varchar(500) DEFAULT NULL, + `authorization_code_value` blob, + `authorization_code_issued_at` timestamp NULL DEFAULT NULL, + `authorization_code_expires_at` timestamp NULL DEFAULT NULL, + `authorization_code_metadata` varchar(2000) DEFAULT NULL, + `access_token_value` blob, + `access_token_issued_at` timestamp NULL DEFAULT NULL, + `access_token_expires_at` timestamp NULL DEFAULT NULL, + `access_token_metadata` varchar(2000) DEFAULT NULL, + `access_token_type` varchar(100) DEFAULT NULL, + `access_token_scopes` varchar(1000) DEFAULT NULL, + `oidc_id_token_value` blob, + `oidc_id_token_issued_at` timestamp NULL DEFAULT NULL, + `oidc_id_token_expires_at` timestamp NULL DEFAULT NULL, + `oidc_id_token_metadata` varchar(2000) DEFAULT NULL, + `refresh_token_value` blob, + `refresh_token_issued_at` timestamp NULL DEFAULT NULL, + `refresh_token_expires_at` timestamp NULL DEFAULT NULL, + `refresh_token_metadata` varchar(2000) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for oauth2_authorization_consent +-- ---------------------------- +DROP TABLE IF EXISTS `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`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Records of oauth2_authorization_consent +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for oauth2_registered_client +-- ---------------------------- +DROP TABLE IF EXISTS `oauth2_registered_client`; +CREATE TABLE `oauth2_registered_client` ( + `id` varchar(100) NOT NULL, + `client_id` varchar(100) NOT NULL, + `client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `client_secret` varchar(200) DEFAULT NULL, + `client_secret_expires_at` timestamp NULL 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`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Records of oauth2_registered_client +-- ---------------------------- +BEGIN; +INSERT INTO `oauth2_registered_client` VALUES ('jumuning', 'jumuning', '2021-11-24 10:39:41', '{bcrypt}$2a$10$aNZ7R/TpKdRBrPT/gl7Avur0mj.1MAwbz47RT1Lm0sNZm51K4WFvC', NULL, 'jumuning', 'client_secret_post,client_secret_basic', 'refresh_token,client_credentials,password,authorization_code', 'https://www.baidu.com', 'message.read,role.admin', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.client.require-proof-key\":false,\"settings.client.require-authorization-consent\":false}', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.token.reuse-refresh-tokens\":true,\"settings.token.id-token-signature-algorithm\":[\"org.springframework.security.oauth2.jose.jws.SignatureAlgorithm\",\"RS256\"],\"settings.token.access-token-time-to-live\":[\"java.time.Duration\",3600.000000000],\"settings.token.refresh-token-time-to-live\":[\"java.time.Duration\",3600.000000000]}'); +INSERT INTO `oauth2_registered_client` VALUES ('pig', 'pig', '2021-11-24 16:35:24', '{bcrypt}$2a$10$oKyVIM.bR8Bjt5PCMZzRJedqEfaQkUhfLkbxpNfM8xPS/JnjtVFZ2', NULL, 'pig', 'client_secret_post,client_secret_basic', 'refresh_token,client_credentials,password,authorization_code', 'https://pig4cloud.com', 'message.read,message.write', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.client.require-proof-key\":false,\"settings.client.require-authorization-consent\":false}', '{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.token.reuse-refresh-tokens\":true,\"settings.token.id-token-signature-algorithm\":[\"org.springframework.security.oauth2.jose.jws.SignatureAlgorithm\",\"RS256\"],\"settings.token.access-token-time-to-live\":[\"java.time.Duration\",10800.000000000],\"settings.token.refresh-token-time-to-live\":[\"java.time.Duration\",10800.000000000]}'); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java index 0a304a9f..72f2e188 100755 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java @@ -17,25 +17,37 @@ package com.pig4cloud.pig.auth.config; import com.nimbusds.jose.jwk.source.JWKSource; +import com.pig4cloud.pig.auth.support.OAuth2ResourceOwnerPasswordAuthenticationConverter; +import com.pig4cloud.pig.auth.support.OAuth2ResourceOwnerPasswordAuthenticationProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2TokenFormat; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.token.*; +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.util.matcher.RequestMatcher; + +import java.util.Arrays; /** * @author lengleng @@ -44,32 +56,43 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration(proxyBeanMethods = false) public class AuthorizationServerConfiguration { + private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/login"; + + + /** + * 定义 Spring Security 的拦截器链,比如我们的 授权url、获取token的url 需要由那个过滤器来处理,此处配置这个。 1.开放oauth2 + * 相关地址 2.增加密码模式的扩展 方法 addCustomOAuth2ResourceOwnerPasswordAuthenticationProvider + */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { - OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); - return http.formLogin(Customizer.withDefaults()).build(); + http.apply(authorizationServerConfigurer.tokenEndpoint( + (tokenEndpoint) -> tokenEndpoint.accessTokenRequestConverter(new DelegatingAuthenticationConverter( + Arrays.asList(new OAuth2AuthorizationCodeAuthenticationConverter(), + new OAuth2RefreshTokenAuthenticationConverter(), + new OAuth2ClientCredentialsAuthenticationConverter(), + new OAuth2ResourceOwnerPasswordAuthenticationConverter()))))); + authorizationServerConfigurer.authorizationEndpoint( + authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)); + + RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); + + http.requestMatcher(endpointsMatcher) + .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) + .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)).apply(authorizationServerConfigurer); + + SecurityFilterChain securityFilterChain = http.formLogin(Customizer.withDefaults()).build(); + + // Custom configuration for Resource Owner Password grant type. Current + // implementation has no support for Resource Owner + // Password grant type + addCustomOAuth2PasswordAuthenticationProvider(http); + + return securityFilterChain; } - // @formatter:off - @Bean - public RegisteredClientRepository registeredClientRepository() { - RegisteredClient client = RegisteredClient.withId("pig") - .clientId("pig") - .clientSecret("{noop}pig") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantTypes(authorizationGrantTypes -> { - authorizationGrantTypes.add(AuthorizationGrantType.AUTHORIZATION_CODE); - authorizationGrantTypes.add(AuthorizationGrantType.REFRESH_TOKEN); - }) - .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED).build()) - .redirectUri("https://pig4cloud.com") - .build(); - return new InMemoryRegisteredClientRepository(client); - } - - // @formatter:on @Bean public OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) { JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource); @@ -84,4 +107,23 @@ public class AuthorizationServerConfiguration { return ProviderSettings.builder().build(); } + + /** + * 扩展密码模式 + */ + @SuppressWarnings("all") + private void addCustomOAuth2PasswordAuthenticationProvider(HttpSecurity http) { + + AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); + OAuth2TokenGenerator oAuth2TokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class); + OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class); + + OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = + new OAuth2ResourceOwnerPasswordAuthenticationProvider(authenticationManager,authorizationService,oAuth2TokenGenerator); + + // This will add new authentication provider in the list of existing authentication providers. + http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider); + + } + } diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/WebSecurityConfiguration.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/WebSecurityConfiguration.java index 91ff8bed..1ebeb3fb 100755 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/WebSecurityConfiguration.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/WebSecurityConfiguration.java @@ -46,15 +46,4 @@ public class WebSecurityConfiguration { return http.build(); } - - // @formatter:off - @Bean - UserDetailsService users() { - UserDetails user = User.builder() - .username("admin") - .password("{noop}123456") - .roles("USER") - .build(); - return new InMemoryUserDetailsManager(user); - } } diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/LoginEndpoint.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/LoginEndpoint.java index abb101af..157e9815 100755 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/LoginEndpoint.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/LoginEndpoint.java @@ -1,165 +1,165 @@ -package com.pig4cloud.pig.auth.endpoint; - -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import com.pig4cloud.pig.common.core.constant.SecurityConstants; -import com.pig4cloud.pig.common.core.util.R; -import com.pig4cloud.pig.common.security.service.PigUserDetailsService; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.springframework.core.Ordered; -import org.springframework.http.HttpHeaders; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.*; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; -import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; -import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; -import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; -import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; -import org.springframework.util.CollectionUtils; -import org.springframework.web.bind.annotation.*; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.temporal.ChronoUnit; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * 登录端点 - * - * @author lengleng - * @date 2022/5/27 - */ -@RestController -@RequiredArgsConstructor -@RequestMapping("/oauth") -public class LoginEndpoint { - - private final OAuth2AuthorizationService tokenService; - - private final RegisteredClientRepository registeredClientRepository; - - private final OAuth2TokenGenerator tokenGenerator; - - private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); - - private final OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); - - private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); - - @SneakyThrows - @PostMapping("/token") - public void login(HttpServletRequest request, HttpServletResponse response, String username, String password) { - - // 获取请求header 中的basic 信息 - UsernamePasswordAuthenticationToken clientAuthentication = authenticationConverter.convert(request); - RegisteredClient client = registeredClientRepository.findByClientId(clientAuthentication.getName()); - - // 根据用户名查询用户 - Map userDetailsServiceMap = SpringUtil - .getBeansOfType(PigUserDetailsService.class); - Optional optional = userDetailsServiceMap.values().stream() - .filter(service -> service.support(client.getClientId(), AuthorizationGrantType.PASSWORD.getValue())) - .max(Comparator.comparingInt(Ordered::getOrder)); - UserDetails userDetails = optional.get().loadUserByUsername(username); - - // 生成accessToken - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, - null); - DefaultOAuth2TokenContext.Builder builder = DefaultOAuth2TokenContext.builder().registeredClient(client) - .principal(authenticationToken).tokenType(OAuth2TokenType.ACCESS_TOKEN) - .authorizationGrantType(AuthorizationGrantType.PASSWORD); - OAuth2Token generatedAccessToken = this.tokenGenerator.generate(builder.build()); - OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, - generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), - generatedAccessToken.getExpiresAt(), builder.build().getAuthorizedScopes()); - - OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(client) - .principalName(authenticationToken.getName()).authorizationGrantType(AuthorizationGrantType.PASSWORD); - if (generatedAccessToken instanceof ClaimAccessor) { - authorizationBuilder.token(accessToken, - (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, - ((ClaimAccessor) generatedAccessToken).getClaims())); - } - else { - authorizationBuilder.accessToken(accessToken); - } - - // 创建刷新令牌 - OAuth2Token generatedRefreshToken = this.refreshTokenGenerator - .generate(builder.tokenType(OAuth2TokenType.REFRESH_TOKEN) - .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).build()); - authorizationBuilder.refreshToken((OAuth2RefreshToken) generatedRefreshToken); - OAuth2Authorization authorization = authorizationBuilder.build(); - - // 保存认证信息 - tokenService.save(authorization); - - // 对外输出 - Map additionalParameters = new HashMap<>(); - additionalParameters.put("license", "pig"); - additionalParameters.put(SecurityConstants.DETAILS_USER, userDetails); - OAuth2AccessTokenAuthenticationToken oAuth2AccessTokenAuthenticationToken = new OAuth2AccessTokenAuthenticationToken( - client, authenticationToken, accessToken, (OAuth2RefreshToken) generatedRefreshToken, - additionalParameters); - sendAccessTokenResponse(response, oAuth2AccessTokenAuthenticationToken); - - } - - @GetMapping("/info") - public OAuth2Authorization info(String token) { - return tokenService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); - } - - private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication) - throws IOException { - - OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication; - - OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken(); - OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken(); - Map additionalParameters = accessTokenAuthentication.getAdditionalParameters(); - - OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) - .tokenType(accessToken.getTokenType()).scopes(accessToken.getScopes()); - 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(); - ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse); - } - - @DeleteMapping("/logout") - public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { - if (StrUtil.isBlank(authHeader)) { - return R.ok(); - } - - String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim(); - OAuth2Authorization oAuth2Authorization = tokenService.findByToken(tokenValue, OAuth2TokenType.ACCESS_TOKEN); - tokenService.remove(oAuth2Authorization); - return R.ok(); - } - -} +//package com.pig4cloud.pig.auth.endpoint; +// +//import cn.hutool.core.util.StrUtil; +//import cn.hutool.extra.spring.SpringUtil; +//import com.pig4cloud.pig.common.core.constant.SecurityConstants; +//import com.pig4cloud.pig.common.core.util.R; +//import com.pig4cloud.pig.common.security.service.PigUserDetailsService; +//import lombok.RequiredArgsConstructor; +//import lombok.SneakyThrows; +//import org.springframework.core.Ordered; +//import org.springframework.http.HttpHeaders; +//import org.springframework.http.converter.HttpMessageConverter; +//import org.springframework.http.server.ServletServerHttpResponse; +//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +//import org.springframework.security.core.Authentication; +//import org.springframework.security.core.userdetails.UserDetails; +//import org.springframework.security.oauth2.core.*; +//import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +//import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +//import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +//import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +//import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +//import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +//import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +//import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +//import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator; +//import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +//import org.springframework.security.web.authentication.www.BasicAuthenticationConverter; +//import org.springframework.util.CollectionUtils; +//import org.springframework.web.bind.annotation.*; +// +//import javax.servlet.http.HttpServletRequest; +//import javax.servlet.http.HttpServletResponse; +//import java.io.IOException; +//import java.time.temporal.ChronoUnit; +//import java.util.Comparator; +//import java.util.HashMap; +//import java.util.Map; +//import java.util.Optional; +// +///** +// * 登录端点 +// * +// * @author lengleng +// * @date 2022/5/27 +// */ +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/oauth") +//public class LoginEndpoint { +// +// private final OAuth2AuthorizationService tokenService; +// +// private final RegisteredClientRepository registeredClientRepository; +// +// private final OAuth2TokenGenerator tokenGenerator; +// +// private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); +// +// private final OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); +// +// private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter(); +// +// @SneakyThrows +// @PostMapping("/token") +// public void login(HttpServletRequest request, HttpServletResponse response, String username, String password) { +// +// // 获取请求header 中的basic 信息 +// UsernamePasswordAuthenticationToken clientAuthentication = authenticationConverter.convert(request); +// RegisteredClient client = registeredClientRepository.findByClientId(clientAuthentication.getName()); +// +// // 根据用户名查询用户 +// Map userDetailsServiceMap = SpringUtil +// .getBeansOfType(PigUserDetailsService.class); +// Optional optional = userDetailsServiceMap.values().stream() +// .filter(service -> service.support(client.getClientId(), AuthorizationGrantType.PASSWORD.getValue())) +// .max(Comparator.comparingInt(Ordered::getOrder)); +// UserDetails userDetails = optional.get().loadUserByUsername(username); +// +// // 生成accessToken +// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, +// null); +// DefaultOAuth2TokenContext.Builder builder = DefaultOAuth2TokenContext.builder().registeredClient(client) +// .principal(authenticationToken).tokenType(OAuth2TokenType.ACCESS_TOKEN) +// .authorizationGrantType(AuthorizationGrantType.PASSWORD); +// OAuth2Token generatedAccessToken = this.tokenGenerator.generate(builder.build()); +// OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, +// generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), +// generatedAccessToken.getExpiresAt(), builder.build().getAuthorizedScopes()); +// +// OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(client) +// .principalName(authenticationToken.getName()).authorizationGrantType(AuthorizationGrantType.PASSWORD); +// if (generatedAccessToken instanceof ClaimAccessor) { +// authorizationBuilder.token(accessToken, +// (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, +// ((ClaimAccessor) generatedAccessToken).getClaims())); +// } +// else { +// authorizationBuilder.accessToken(accessToken); +// } +// +// // 创建刷新令牌 +// OAuth2Token generatedRefreshToken = this.refreshTokenGenerator +// .generate(builder.tokenType(OAuth2TokenType.REFRESH_TOKEN) +// .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).build()); +// authorizationBuilder.refreshToken((OAuth2RefreshToken) generatedRefreshToken); +// OAuth2Authorization authorization = authorizationBuilder.build(); +// +// // 保存认证信息 +// tokenService.save(authorization); +// +// // 对外输出 +// Map additionalParameters = new HashMap<>(); +// additionalParameters.put("license", "pig"); +// additionalParameters.put(SecurityConstants.DETAILS_USER, userDetails); +// OAuth2AccessTokenAuthenticationToken oAuth2AccessTokenAuthenticationToken = new OAuth2AccessTokenAuthenticationToken( +// client, authenticationToken, accessToken, (OAuth2RefreshToken) generatedRefreshToken, +// additionalParameters); +// sendAccessTokenResponse(response, oAuth2AccessTokenAuthenticationToken); +// +// } +// +// @GetMapping("/info") +// public OAuth2Authorization info(String token) { +// return tokenService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); +// } +// +// private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication) +// throws IOException { +// +// OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication; +// +// OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken(); +// OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken(); +// Map additionalParameters = accessTokenAuthentication.getAdditionalParameters(); +// +// OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) +// .tokenType(accessToken.getTokenType()).scopes(accessToken.getScopes()); +// 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(); +// ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); +// this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse); +// } +// +// @DeleteMapping("/logout") +// public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) { +// if (StrUtil.isBlank(authHeader)) { +// return R.ok(); +// } +// +// String tokenValue = authHeader.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), StrUtil.EMPTY).trim(); +// OAuth2Authorization oAuth2Authorization = tokenService.findByToken(tokenValue, OAuth2TokenType.ACCESS_TOKEN); +// tokenService.remove(oAuth2Authorization); +// return R.ok(); +// } +// +//} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2EndpointUtils.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2EndpointUtils.java new file mode 100644 index 00000000..e3bbeeb1 --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2EndpointUtils.java @@ -0,0 +1,50 @@ +package com.pig4cloud.pig.auth.support; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author jumuning + * @description OAuth2 端点工具 + */ +public class OAuth2EndpointUtils { + + static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private OAuth2EndpointUtils() { + } + + public static MultiValueMap getParameters(HttpServletRequest request) { + Map parameterMap = request.getParameterMap(); + MultiValueMap parameters = new LinkedMultiValueMap<>(parameterMap.size()); + parameterMap.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + parameters.add(key, value); + } + } + }); + return parameters; + } + + public static boolean matchesPkceTokenRequest(HttpServletRequest request) { + return AuthorizationGrantType.AUTHORIZATION_CODE.getValue() + .equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) + && request.getParameter(OAuth2ParameterNames.CODE) != null + && request.getParameter(PkceParameterNames.CODE_VERIFIER) != null; + } + + 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); + } + +} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ErrorCodesExpand.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ErrorCodesExpand.java new file mode 100644 index 00000000..1f87283c --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ErrorCodesExpand.java @@ -0,0 +1,33 @@ +package com.pig4cloud.pig.auth.support; + +/** + * @author jumuning + * @description OAuth2 异常信息 + */ +public interface OAuth2ErrorCodesExpand { + + /** 用户名未找到 */ + String USERNAME_NOT_FOUND = "username_not_found"; + + /** 错误凭证 */ + String BAD_CREDENTIALS = "bad_credentials"; + + /** 用户被锁 */ + String USER_LOCKED = "user_locked"; + + /** 用户禁用 */ + String USER_DISABLE = "user_disable"; + + /** 用户过期 */ + String USER_EXPIRED = "user_expired"; + + /** 证书过期 */ + String CREDENTIALS_EXPIRED = "credentials_expired"; + + /** scope 为空异常 */ + String SCOPE_IS_EMPTY = "scope_is_empty"; + + /** 未知的登录异常 */ + String UN_KNOW_LOGIN_ERROR = "un_know_login_error"; + +} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationConverter.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationConverter.java new file mode 100644 index 00000000..8222550e --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationConverter.java @@ -0,0 +1,78 @@ +package com.pig4cloud.pig.auth.support; + +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 javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author jumuning + * @date 2021/9/14 OAuth2 资源所有者密码认证转换器 + */ +public class OAuth2ResourceOwnerPasswordAuthenticationConverter implements AuthenticationConverter { + + @Override + public Authentication convert(HttpServletRequest request) { + + // grant_type (REQUIRED) + String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); + if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) { + return null; + } + + MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); + + // scope (OPTIONAL) + 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 requestedScopes = null; + if (StringUtils.hasText(scope)) { + requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); + } + + // username (REQUIRED) + String username = parameters.getFirst(OAuth2ParameterNames.USERNAME); + if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + // password (REQUIRED) + String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD); + if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); + if (clientPrincipal == null) { + OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT, + OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI); + } + + Map 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 OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal, + requestedScopes, additionalParameters); + + } + +} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationProvider.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationProvider.java new file mode 100644 index 00000000..f514672d --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationProvider.java @@ -0,0 +1,279 @@ +package com.pig4cloud.pig.auth.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.security.authentication.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +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.context.ProviderContextHolder; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.security.Principal; +import java.time.Instant; +import java.util.*; +import java.util.function.Supplier; + +/** + * @author jumuning + * @description 处理用户名密码授权 + */ +public class OAuth2ResourceOwnerPasswordAuthenticationProvider implements AuthenticationProvider { + + private static final Logger LOGGER = LogManager.getLogger(OAuth2ResourceOwnerPasswordAuthenticationProvider.class); + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + + private final OAuth2AuthorizationService authorizationService; + + private final OAuth2TokenGenerator tokenGenerator; + + private final AuthenticationManager authenticationManager; + + private final MessageSourceAccessor messages = new MessageSourceAccessor(new SpringSecurityMessageSource(), + Locale.CHINA); + + @Deprecated + private Supplier refreshTokenGenerator; + + /** + * Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the + * provided parameters. + * @param authorizationService the authorization service + * @param tokenGenerator the token generator + * @since 0.2.3 + */ + public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager, + OAuth2AuthorizationService authorizationService, + OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authenticationManager = authenticationManager; + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Deprecated + public void setRefreshTokenGenerator(Supplier refreshTokenGenerator) { + Assert.notNull(refreshTokenGenerator, "refreshTokenGenerator cannot be null"); + this.refreshTokenGenerator = refreshTokenGenerator; + } + + /** + * Performs authentication with the same contract as + * {@link AuthenticationManager#authenticate(Authentication)} + * . + * @param authentication the authentication request object. + * @return a fully authenticated object including credentials. May return + * null if the AuthenticationProvider is unable to support + * authentication of the passed Authentication object. In such a case, + * the next AuthenticationProvider that supports the presented + * Authentication class will be tried. + * @throws AuthenticationException if authentication fails. + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + OAuth2ResourceOwnerPasswordAuthenticationToken resouceOwnerPasswordAuthentication = (OAuth2ResourceOwnerPasswordAuthenticationToken) authentication; + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient( + resouceOwnerPasswordAuthentication); + + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + assert registeredClient != null; + if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); + } + + Set authorizedScopes = registeredClient.getScopes(); + // Default to configured scopes + if (!CollectionUtils.isEmpty(resouceOwnerPasswordAuthentication.getScopes())) { + for (String requestedScope : resouceOwnerPasswordAuthentication.getScopes()) { + if (!registeredClient.getScopes().contains(requestedScope)) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); + } + } + authorizedScopes = new LinkedHashSet<>(resouceOwnerPasswordAuthentication.getScopes()); + } + else { + throw new ScopeException(OAuth2ErrorCodesExpand.SCOPE_IS_EMPTY); + } + + Map additionalParameters = resouceOwnerPasswordAuthentication.getAdditionalParameters(); + String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); + String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); + + try { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + username, password); + LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken); + + Authentication usernamePasswordAuthentication = authenticationManager + .authenticate(usernamePasswordAuthenticationToken); + + // @formatter:off + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(usernamePasswordAuthentication) + .providerContext(ProviderContextHolder.getProviderContext()) + .authorizedScopes(authorizedScopes) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .authorizationGrant(resouceOwnerPasswordAuthentication); + // @formatter:on + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization + .withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName()) + .authorizationGrantType(AuthorizationGrantType.PASSWORD) + .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes); + + // ----- 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()); + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder + .token(accessToken, + (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, + ((ClaimAccessor) generatedAccessToken).getClaims())) + .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes) + .attribute(Principal.class.getName(), usernamePasswordAuthentication); + } + 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)) { + + if (this.refreshTokenGenerator != null) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive()); + refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt); + } + else { + 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); + + LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken"); + + // ----- 扩展 暴露信息 ----- + Map objectMap = new HashMap<>(); + objectMap.put(OAuth2ParameterNames.USERNAME, username); + + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, + refreshToken, objectMap); + + } + catch (Exception ex) { + LOGGER.error("problem in authenticate", ex); + throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex); + } + + } + + @Override + public boolean supports(Class authentication) { + boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication); + LOGGER.debug("supports authentication=" + authentication + " returning " + supports); + return supports; + } + + /** + * 登录异常转换为oauth2异常 + * @param authentication 身份验证 + * @param authenticationException 身份验证异常 + * @return {@link OAuth2AuthenticationException} + */ + private OAuth2AuthenticationException oAuth2AuthenticationException(Authentication authentication, + AuthenticationException authenticationException) { + if (authenticationException instanceof UsernameNotFoundException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USERNAME_NOT_FOUND, + this.messages.getMessage("JdbcDaoImpl.notFound", new Object[] { authentication.getName() }, + "Username {0} not found"), + "")); + } + if (authenticationException instanceof BadCredentialsException) { + return new OAuth2AuthenticationException( + new OAuth2Error(OAuth2ErrorCodesExpand.BAD_CREDENTIALS, this.messages.getMessage( + "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), "")); + } + if (authenticationException instanceof LockedException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_LOCKED, this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"), "")); + } + if (authenticationException instanceof DisabledException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_DISABLE, + this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"), + "")); + } + if (authenticationException instanceof AccountExpiredException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_EXPIRED, this.messages + .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"), "")); + } + if (authenticationException instanceof CredentialsExpiredException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.CREDENTIALS_EXPIRED, + this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", + "User credentials have expired"), + "")); + } + if (authenticationException instanceof ScopeException) { + return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE, + this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "invalid_scope"), "")); + } + return new OAuth2AuthenticationException(OAuth2ErrorCodesExpand.UN_KNOW_LOGIN_ERROR); + } + + 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); + } + +} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationToken.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationToken.java new file mode 100644 index 00000000..ba03ef6a --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/OAuth2ResourceOwnerPasswordAuthenticationToken.java @@ -0,0 +1,80 @@ +package com.pig4cloud.pig.auth.support; + +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import java.util.*; + +/** + * @author jumuning + * @description 密码授权token信息 + */ +public class OAuth2ResourceOwnerPasswordAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = -6067207202119450764L; + + private final AuthorizationGrantType authorizationGrantType; + + private final Authentication clientPrincipal; + + private final Set scopes; + + private final Map additionalParameters; + + /** + * Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided + * parameters. + * @param clientPrincipal the authenticated client principal + */ + + public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType, + Authentication clientPrincipal, @Nullable Set scopes, + @Nullable Map additionalParameters) { + super(Collections.emptyList()); + Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null"); + Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); + this.authorizationGrantType = authorizationGrantType; + this.clientPrincipal = clientPrincipal; + this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet()); + this.additionalParameters = Collections.unmodifiableMap( + additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap()); + } + + /** + * Returns the authorization grant type. + * @return the authorization grant type + */ + public AuthorizationGrantType getGrantType() { + return this.authorizationGrantType; + } + + @Override + public Object getPrincipal() { + return this.clientPrincipal; + } + + @Override + public Object getCredentials() { + return ""; + } + + /** + * Returns the requested scope(s). + * @return the requested scope(s), or an empty {@code Set} if not available + */ + public Set getScopes() { + return this.scopes; + } + + /** + * Returns the additional parameters. + * @return the additional parameters + */ + public Map getAdditionalParameters() { + return this.additionalParameters; + } + +} diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/ScopeException.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/ScopeException.java new file mode 100644 index 00000000..019692d9 --- /dev/null +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/ScopeException.java @@ -0,0 +1,28 @@ +package com.pig4cloud.pig.auth.support; + +import org.springframework.security.core.AuthenticationException; + +/** + * @author jumuning + * @description ScopeException 异常信息 + */ +public class ScopeException extends AuthenticationException { + + /** + * Constructs a ScopeException with the specified message. + * @param msg the detail message. + */ + public ScopeException(String msg) { + super(msg); + } + + /** + * Constructs a {@code ScopeException} with the specified message and root cause. + * @param msg the detail message. + * @param cause root cause + */ + public ScopeException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/pig-common/pig-common-security/pom.xml b/pig-common/pig-common-security/pom.xml index f2d7ce03..3baa59b0 100755 --- a/pig-common/pig-common-security/pom.xml +++ b/pig-common/pig-common-security/pom.xml @@ -64,5 +64,9 @@ org.springframework spring-webmvc - + + org.springframework.boot + spring-boot-starter-jdbc + + diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java index ecf30c63..e696416a 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigResourceServerAutoConfiguration.java @@ -16,21 +16,88 @@ package com.pig4cloud.pig.common.security.component; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pig.admin.api.feign.RemoteUserService; +import com.pig4cloud.pig.common.security.service.PigUser; +import com.pig4cloud.pig.common.security.service.PigUserDetailsServiceImpl; +import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import java.util.LinkedHashSet; +import java.util.List; + /** * @author lengleng * @date 2020-06-23 */ @EnableConfigurationProperties(PermitAllUrlProperties.class) +@RequiredArgsConstructor public class PigResourceServerAutoConfiguration { + + private final RemoteUserService remoteUserService; + + private final CacheManager cacheManager; + + /** + * 配置授权服务器连接数据库,增加自定义序列化数据 + */ + @Bean + public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, + registeredClientRepository); + JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper( + registeredClientRepository); + + ObjectMapper objectMapper = new ObjectMapper(); + ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + objectMapper.registerModules(securityModules); + objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + + // You will need to write the Mixin for your class so Jackson can marshall it. + objectMapper.addMixIn(LinkedHashSet.class, LinkedHashSet.class); + objectMapper.addMixIn(PigUser.class, PigUser.class); + + rowMapper.setObjectMapper(objectMapper); + service.setAuthorizationRowMapper(rowMapper); + + return service; + } + + + @Bean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + /** + * 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息, 此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限 + */ + @Bean + public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); + } + + @Bean("pms") public PermissionService permissionService() { return new PermissionService(); @@ -51,10 +118,18 @@ public class PigResourceServerAutoConfiguration { return new CustomOpaqueTokenIntrospector(authorizationService); } + @Bean + public UserDetailsService userDetailsService(){ + return new PigUserDetailsServiceImpl(remoteUserService ,cacheManager); + } + // @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/auth/oauth2/jwks").build(); return jwtDecoder; } + + + } diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java index 4b320e3c..c26064b5 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/component/PigTokenStoreAutoConfiguration.java @@ -4,12 +4,8 @@ import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; -import com.pig4cloud.pig.common.security.service.PigRedisOAuth2AuthorizationService; import lombok.SneakyThrows; import org.springframework.context.annotation.Bean; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; - import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; @@ -22,10 +18,11 @@ import java.util.UUID; */ public class PigTokenStoreAutoConfiguration { - @Bean - public OAuth2AuthorizationService authorizationService(RedisTemplate redisTemplate) { - return new PigRedisOAuth2AuthorizationService(redisTemplate); - } + //todo 暂时屏蔽redis 权限配置,存在不兼容的问题 +// @Bean +// public OAuth2AuthorizationService authorizationService(RedisTemplate redisTemplate) { +// return new PigRedisOAuth2AuthorizationService(redisTemplate); +// } @Bean @SneakyThrows