diff --git a/base-framework-security-starter/pom.xml b/base-framework-security-starter/pom.xml index d302395..58f1804 100644 --- a/base-framework-security-starter/pom.xml +++ b/base-framework-security-starter/pom.xml @@ -88,5 +88,9 @@ spring-boot-starter-data-redis true + + org.springframework.boot + spring-boot-starter-oauth2-client + \ No newline at end of file diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/AuthenticationProviderConfiguration.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/AuthenticationProviderConfiguration.java new file mode 100644 index 0000000..e4a99a4 --- /dev/null +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/AuthenticationProviderConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fuhouyu.framework.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + *

+ * oidc配置 + *

+ * + * @author fuhouyu + * @since 2024/11/4 22:06 + */ +@Configuration(proxyBeanMethods = false) +public class AuthenticationProviderConfiguration { + + /** + * dao层实现 + * + * @param passwordEncoder 密码管理器 + * @param userDetailsService 用户详情接口 + * @return dao默认实现 + */ + @Bean + public AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService, + PasswordEncoder passwordEncoder) { + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(passwordEncoder); + daoAuthenticationProvider.setUserDetailsService(userDetailsService); + return daoAuthenticationProvider; + } + +// /** +// * oidc provider +// * +// * @param userDetailsService 用户详情service +// * @param clientRegistrationRepository 客户端仓库信息 +// * @return oidcProvider +// */ +// @Bean +// public AuthenticationProvider oidcAuthenticationProvider(UserDetailsService userDetailsService, +// ClientRegistrationRepository clientRegistrationRepository) { +// return new OidcAuthenticationProvider(new DefaultOAuth2UserService(), userDetailsService, clientRegistrationRepository); +// } + + +} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/OpenPlatformConfiguration.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/OpenPlatformConfiguration.java deleted file mode 100644 index bc50d83..0000000 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/OpenPlatformConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fuhouyu.framework.security; - -import com.fuhouyu.framework.security.properties.OpenPlatformAuthProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - *

- * 开放平台自动装配类 - *

- * - * @author fuhouyu - * @since 2024/8/15 16:23 - */ -@Configuration -@EnableConfigurationProperties(OpenPlatformAuthProperties.class) -public class OpenPlatformConfiguration { - - -} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/SecurityAutoConfiguration.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/SecurityAutoConfiguration.java index 3d4711b..17f6efe 100644 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/SecurityAutoConfiguration.java +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/SecurityAutoConfiguration.java @@ -23,6 +23,7 @@ import com.fuhouyu.framework.security.token.TokenStoreCache; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -30,8 +31,6 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.ProviderManager; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.List; @@ -44,9 +43,12 @@ * @author fuhouyu * @since 2024/8/15 16:22 */ -@Configuration -@Import({OpenPlatformConfiguration.class}) -@AutoConfigureAfter(CacheAutoConfiguration.class) +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ + CacheAutoConfiguration.class, + OAuth2ClientAutoConfiguration.class +}) +@Import({AuthenticationProviderConfiguration.class}) public class SecurityAutoConfiguration { /** @@ -66,17 +68,11 @@ public TokenStore tokenStore(CacheService cacheService) { * 认证管理器配置这里可以进行除其他登录模式的扩展,需要实现{@link AuthenticationProvider} * * @param authenticationProviders 认证提供者集合 - * @param userDetailsService 用户接口详情 - * @param passwordEncoder 密码认证管理器 * @return 认证管理器 */ - @Bean("authenticationManager") + @Bean @Primary - public AuthenticationManager authenticationManager( - List authenticationProviders, - UserDetailsService userDetailsService, - PasswordEncoder passwordEncoder) { - authenticationProviders.add(daoAuthenticationProvider(userDetailsService, passwordEncoder)); + public AuthenticationManager authenticationManager(List authenticationProviders) { return new ProviderManager(authenticationProviders); } @@ -91,19 +87,5 @@ public PasswordEncoder passwordEncoder() { return PasswordEncoderFactory.createDelegatingPasswordEncoder("sm3"); } - /** - * dao层实现 - * - * @param passwordEncoder 密码管理器 - * @param userDetailsService 用户详情接口 - * @return dao默认实现 - */ - private AuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService, - PasswordEncoder passwordEncoder) { - DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(passwordEncoder); - daoAuthenticationProvider.setUserDetailsService(userDetailsService); - return daoAuthenticationProvider; - } - } diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/GrantTypeAuthenticationTokenEnum.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/GrantTypeAuthenticationTokenEnum.java index fb04278..c2dae49 100644 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/GrantTypeAuthenticationTokenEnum.java +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/GrantTypeAuthenticationTokenEnum.java @@ -16,8 +16,7 @@ package com.fuhouyu.framework.security.core; import com.fuhouyu.framework.common.utils.JacksonUtil; -import com.fuhouyu.framework.security.core.authentication.refreshtoken.RefreshAuthenticationProvider; -import com.fuhouyu.framework.security.core.authentication.wechat.WechatAppletsPlatformProvider; +import com.fuhouyu.framework.security.core.provider.refreshtoken.RefreshAuthenticationProvider; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AbstractAuthenticationToken; @@ -62,16 +61,7 @@ public Class getAuthenticationTokenCl return (Class) RefreshAuthenticationProvider.RefreshAuthenticationToken.class; } }, - - /** - * 微信小程序 - */ - WECHAT_APPLETS("WECHAT_APPLETS") { - @Override - public Class getAuthenticationTokenClass() { - return (Class) WechatAppletsPlatformProvider.WechatAppletsAuthenticationToken.class; - } - }; + ; private final String grantType; diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsPlatformProvider.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsPlatformProvider.java deleted file mode 100644 index f0d334c..0000000 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsPlatformProvider.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fuhouyu.framework.security.core.authentication.wechat; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fuhouyu.framework.common.utils.JacksonUtil; -import com.fuhouyu.framework.common.utils.LoggerUtil; -import com.fuhouyu.framework.security.core.AbstractAuthenticationProvider; -import com.fuhouyu.framework.security.properties.OpenPlatformAuthProperties; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.web.client.RestTemplate; - -import java.util.List; -import java.util.Objects; -import java.util.function.Function; - -/** - *

- * 微信小程序提供者 - *

- * - * @author fuhouyu - * @since 2024/8/15 12:32 - */ -@Slf4j -public class WechatAppletsPlatformProvider extends AbstractAuthenticationProvider { - - private final RestTemplate restTemplate; - - private final UserDetailsService userDetailsService; - - private final OpenPlatformAuthProperties.AuthDetail authDetail; - - - /** - * 构造函数 - * - * @param restTemplate restTemplate - * @param userDetailsService 用户详情接口 - * @param authDetail 第三方平台认证相关的信息 - */ - public WechatAppletsPlatformProvider(RestTemplate restTemplate, - UserDetailsService userDetailsService, - OpenPlatformAuthProperties.AuthDetail authDetail) { - super(null); - this.restTemplate = restTemplate; - this.userDetailsService = userDetailsService; - this.authDetail = authDetail; - } - - /** - * 构造函数 - * - * @param restTemplate restTemplate - * @param userDetailsService 用户接口详情 - * @param authDetail 认证详情 - * @param registerFunction 注册函数 - */ - public WechatAppletsPlatformProvider(RestTemplate restTemplate, - UserDetailsService userDetailsService, - OpenPlatformAuthProperties.AuthDetail authDetail, - Function registerFunction) { - super(registerFunction); - this.restTemplate = restTemplate; - this.userDetailsService = userDetailsService; - this.authDetail = authDetail; - } - - - @Override - public WechatAppletsUserInfo loadPlatformUser(Authentication authentication) { - String requestUrl = this.getRequestUrl((String) authentication.getPrincipal()); - ResponseEntity wechatAppletsUserInfoEntity = restTemplate.getForEntity(requestUrl, String.class); - if (!Objects.equals(wechatAppletsUserInfoEntity.getStatusCode().value(), 200)) { - LoggerUtil.error(log, "微信小程序请求url失败,jsCode:{}, 请求参数:{}, 返回的状态码:{}", - authentication.getPrincipal(), requestUrl.replaceAll(authDetail.getClientSecret(), "*"), - wechatAppletsUserInfoEntity.getStatusCode().value()); - throw new IllegalArgumentException("微信小程序登录不正常,请检索配置参数是否正确"); - } - WechatAppletsUserInfo wechatAppletsUserInfo = - JacksonUtil.readValue(wechatAppletsUserInfoEntity.getBody(), WechatAppletsUserInfo.class); - if (Objects.nonNull(wechatAppletsUserInfo.getErrCode()) && !Objects.equals(wechatAppletsUserInfo.getErrCode(), 0)) { - throw new IllegalArgumentException(String.format("微信小程序登录失败:%s", wechatAppletsUserInfo.getErrMsg())); - } - return wechatAppletsUserInfo; - } - - @Override - public UserDetails loadUserDetails(WechatAppletsUserInfo wechatAppletsUserInfo) throws UsernameNotFoundException { - return this.userDetailsService.loadUserByUsername(wechatAppletsUserInfo.getOpenId()); - } - - @Override - public boolean supports(Class authentication) { - return WechatAppletsAuthenticationToken.class.isAssignableFrom(authentication); - } - - /** - * 获取app拼接后的url - * - * @param jsCode 登录时获取的 code,可通过wx.login获取 - * @return app参数 - */ - private String getRequestUrl(String jsCode) { - return String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", - authDetail.getLoginUrl(), - authDetail.getClientId(), - authDetail.getClientSecret(), - jsCode); - } - - /** - * 使用它才支持微信小程序认证 - */ - @EqualsAndHashCode(callSuper = true) - public static class WechatAppletsAuthenticationToken extends AbstractAuthenticationToken { - - /** - * 微信一次性认证码 - */ - private final String jsCode; - - - /** - * 构造函数 - * - * @param jsCode 登录时获取的 code,可通过wx.login获取 - */ - @JsonCreator - public WechatAppletsAuthenticationToken( - @JsonProperty("jsCode") String jsCode) { - super(List.of()); - this.jsCode = jsCode; - } - - @Override - public Object getCredentials() { - return null; - } - - @Override - public Object getPrincipal() { - return this.jsCode; - } - } -} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsUserInfo.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsUserInfo.java deleted file mode 100644 index d16972f..0000000 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/wechat/WechatAppletsUserInfo.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fuhouyu.framework.security.core.authentication.wechat; - -import com.fasterxml.jackson.annotation.JsonAlias; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -/** - *

- * 微信用户详情 - *

- * - * @author fuhouyu - * @since 2024/8/15 12:33 - */ -@ToString -@Getter -@Setter -public class WechatAppletsUserInfo { - - /** - * 会话密钥 - */ - @JsonAlias("session_key") - private String sessionKey; - - /** - * 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回 - */ - @JsonAlias("unionid") - private String unionId; - - /** - * 错误信息 - */ - @JsonAlias("errmsg") - private String errMsg; - - /** - * 用户唯一标识 - */ - @JsonAlias("openid") - private String openId; - - /** - * 错误码 - */ - @JsonAlias("errcode") - private Integer errCode; -} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationProvider.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationProvider.java new file mode 100644 index 0000000..048b92f --- /dev/null +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationProvider.java @@ -0,0 +1,290 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fuhouyu.framework.security.core.provider.oidc; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +/** + *

+ * oidc 认证token + *

+ * + * @author fuhouyu + * @since 2024/11/4 20:08 + */ +@Slf4j +public class OidcAuthenticationProvider implements AuthenticationProvider { + + private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response"; + + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; + + private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce"; + private static final MediaType APPLICATION_FORM_URLENCODED_UTF8 = new MediaType( + MediaType.APPLICATION_FORM_URLENCODED, StandardCharsets.UTF_8); + private final GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities); + private final UserDetailsService userDetailsService; + private final OAuth2UserService userService; + private final ClientRegistrationRepository clientRegistrationRepository; + private final JwtDecoderFactory jwtDecoderFactory = new OidcIdTokenDecoderFactory(); + private final RestOperations restOperations; + + public OidcAuthenticationProvider(OAuth2UserService userService, + UserDetailsService userDetailsService, + ClientRegistrationRepository clientRegistrationRepository) { + this.userService = userService; + this.userDetailsService = userDetailsService; + this.clientRegistrationRepository = clientRegistrationRepository; + RestTemplate restTemplate = new RestTemplate( + Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + this.restOperations = restTemplate; + } + + static String createHash(String nonce) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OidcAuthenticationToken oidcAuthenticationToken = (OidcAuthenticationToken) authentication; + ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(oidcAuthenticationToken.getClientId()); + if (Objects.isNull(clientRegistration)) { + throw new IllegalArgumentException("invalid client id " + oidcAuthenticationToken.getClientId()); + } + RequestEntity requestEntity = this.createRequestEntity(clientRegistration); + OAuth2AccessTokenResponse accessTokenResponse = this.getResponse(requestEntity); + Map additionalParameters = this.getAdditionalParameters(accessTokenResponse, clientRegistration); + OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse); + validateNonce(oidcAuthenticationToken.getNonce(), idToken); + OAuth2User oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration, + accessTokenResponse.getAccessToken(), idToken, additionalParameters)); + UserDetails userDetails = this.userDetailsService.loadUserByUsername(oidcUser.getName()); + if (Objects.isNull(userDetails)) { + throw new IllegalArgumentException("系统中的用户不存在"); + } + Collection mappedAuthorities = this.authoritiesMapper + .mapAuthorities(oidcUser.getAuthorities()); + OidcAuthenticationToken result = new OidcAuthenticationToken( + oidcAuthenticationToken.getCode(), + oidcAuthenticationToken.getState(), + clientRegistration.getClientId(), + oidcUser, mappedAuthorities, accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken()); + result.setDetails(userDetails); + return result; + } + + /** + * 获取扩展信息 + * + * @param accessTokenResponse token 响应 + * @param clientRegistration 客户端注册信息 + * @return parameters· + */ + private Map getAdditionalParameters(OAuth2AccessTokenResponse accessTokenResponse, ClientRegistration clientRegistration) { + Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) { + OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, + "Missing (required) ID Token in Token Response for Client Registration: " + + clientRegistration.getRegistrationId(), + null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + } + return additionalParameters; + } + + /** + * 创建oidcToken + * + * @param clientRegistration 客户端注册信息 + * @param accessTokenResponse token响应 + * @return oidcToken + */ + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, + OAuth2AccessTokenResponse accessTokenResponse) { + JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); + Jwt jwt = getJwt(accessTokenResponse, jwtDecoder); + return new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), + jwt.getClaims()); + } + + /** + * 获取jwtToken + * + * @param accessTokenResponse token响应 + * @param jwtDecoder jwt解析 + * @return jwt + */ + private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) { + try { + Map parameters = accessTokenResponse.getAdditionalParameters(); + return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN)); + } catch (JwtException ex) { + OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex); + } + } + + private void validateNonce(String requestNonce, OidcIdToken idToken) { + if (requestNonce == null) { + return; + } + String nonceHash = getNonceHash(requestNonce); + String nonceHashClaim = idToken.getNonce(); + if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + } + + private String getNonceHash(String requestNonce) { + try { + return createHash(requestNonce); + } catch (NoSuchAlgorithmException ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE); + throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + } + } + + /** + * 获取token + * + * @param request 请求实体 + * @return token + */ + private OAuth2AccessTokenResponse getResponse(RequestEntity request) { + try { + ResponseEntity tokenResponse = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class); + Assert.notNull(tokenResponse, + "The authorization server responded to this Authorization Code grant request with an empty body; as such, it cannot be materialized into an OAuth2AccessTokenResponse instance. Please check the HTTP response code in your server logs for more details."); + return tokenResponse.getBody(); + } catch (RestClientException ex) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, + "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + + ex.getMessage(), + null); + throw new OAuth2AuthorizationException(oauth2Error, ex); + } + } + + /** + * 创建请求实体 + * + * @param clientRegistration 客户端注册信息 + * @return 请求实体 + */ + private RequestEntity createRequestEntity(ClientRegistration clientRegistration) { + URI uri = UriComponentsBuilder + .fromUriString(clientRegistration.getProviderDetails().getTokenUri()) + .build() + .toUri(); + return new RequestEntity<>(this.createParameters(clientRegistration), + this.createHttpHeaders(clientRegistration), HttpMethod.POST, uri); + } + + /** + * 获取请求头 + * + * @param clientRegistration 客户端信息 + * @return http请求头 + */ + private HttpHeaders createHttpHeaders(ClientRegistration clientRegistration) { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(APPLICATION_JSON)); + headers.setContentType(APPLICATION_FORM_URLENCODED_UTF8); + if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) { + String clientId = URLEncoder.encode(clientRegistration.getClientId(), StandardCharsets.UTF_8); + String clientSecret = URLEncoder.encode(clientRegistration.getClientSecret(), StandardCharsets.UTF_8); + headers.setBasicAuth(clientId, clientSecret); + } + return headers; + } + + /** + * 设置参数 + * + * @param clientRegistration 客户端注册信息 + * @return 参数对象 + */ + private MultiValueMap createParameters(ClientRegistration clientRegistration) { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC + .equals(clientRegistration.getClientAuthenticationMethod())) { + parameters.set(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + } + if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) { + parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret()); + } + + String redirectUri = clientRegistration.getRedirectUri(); + if (redirectUri != null) { + parameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri); + } + return parameters; + } + + @Override + public boolean supports(Class authentication) { + return OidcAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationToken.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationToken.java new file mode 100644 index 0000000..ab4a513 --- /dev/null +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/oidc/OidcAuthenticationToken.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fuhouyu.framework.security.core.provider.oidc; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; + +/** + *

+ * oidcToken + *

+ * + * @author fuhouyu + * @since 2024/11/4 20:27 + */ +@Getter +@Setter +@ToString +public class OidcAuthenticationToken extends AbstractAuthenticationToken { + + private final String code; + + private final String state; + + private final String clientId; + + private final transient OAuth2User principal; + + private final OAuth2AccessToken accessToken; + + private final OAuth2RefreshToken refreshToken; + + private String nonce; + + public OidcAuthenticationToken(String code, String state, + String clientId, + OAuth2User principal, + Collection authorities, + OAuth2AccessToken accessToken, + @Nullable OAuth2RefreshToken refreshToken) { + super(authorities); + this.code = code; + this.state = state; + this.clientId = clientId; + this.principal = principal; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + + public OidcAuthenticationToken(String code, String state, String clientId) { + this(code, state, clientId, null, Collections.emptyList(), null, null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public OAuth2User getPrincipal() { + return this.principal; + } +} diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/refreshtoken/RefreshAuthenticationProvider.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/refreshtoken/RefreshAuthenticationProvider.java similarity index 97% rename from base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/refreshtoken/RefreshAuthenticationProvider.java rename to base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/refreshtoken/RefreshAuthenticationProvider.java index 4ef8292..bcb01c5 100644 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/authentication/refreshtoken/RefreshAuthenticationProvider.java +++ b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/core/provider/refreshtoken/RefreshAuthenticationProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.fuhouyu.framework.security.core.authentication.refreshtoken; +package com.fuhouyu.framework.security.core.provider.refreshtoken; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/properties/OpenPlatformAuthProperties.java b/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/properties/OpenPlatformAuthProperties.java deleted file mode 100644 index 929fb87..0000000 --- a/base-framework-security-starter/src/main/java/com/fuhouyu/framework/security/properties/OpenPlatformAuthProperties.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fuhouyu.framework.security.properties; - -import com.fuhouyu.framework.common.constants.ConfigPropertiesConstant; -import com.fuhouyu.framework.security.core.GrantTypeAuthenticationTokenEnum; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.Map; - -/** - *

- * 开放平台配置项 - *

- * - * @author fuhouyu - * @since 2024/8/15 12:39 - */ -@ConfigurationProperties(prefix = OpenPlatformAuthProperties.PREFIX) -@ToString -@Getter -@Setter -public class OpenPlatformAuthProperties { - - /** - * 配置文件前缀 - */ - public static final String PREFIX = ConfigPropertiesConstant.PROPERTIES_PREFIX + "open-platform"; - - /** - * 客户端相关配置 - */ - private Map auth; - - /** - * 授权的详情 - */ - @ToString - @Getter - @Setter - public static class AuthDetail { - - /** - * 客户端id - */ - private String clientId; - - /** - * 客户端密钥 - */ - private String clientSecret; - - /** - * 登录的url,一般为换取token - */ - private String loginUrl; - - /** - * 用户详情的url - */ - private String userInfoUrl; - - } -} diff --git a/base-framework-security-starter/src/main/java/module-info.java b/base-framework-security-starter/src/main/java/module-info.java index 0d11b7b..124212d 100644 --- a/base-framework-security-starter/src/main/java/module-info.java +++ b/base-framework-security-starter/src/main/java/module-info.java @@ -29,14 +29,15 @@ requires spring.security.oauth2.core; requires spring.data.redis; requires spring.web; + requires spring.security.oauth2.client; + requires spring.security.oauth2.jose; opens com.fuhouyu.framework.security to spring.core; opens com.fuhouyu.framework.security.entity to com.esotericsoftware.kryo.kryo5; - exports com.fuhouyu.framework.security.core.authentication.refreshtoken to com.fasterxml.jackson.databind; + exports com.fuhouyu.framework.security.core.provider.refreshtoken to com.fasterxml.jackson.databind; exports com.fuhouyu.framework.security; exports com.fuhouyu.framework.security.core; exports com.fuhouyu.framework.security.entity; - exports com.fuhouyu.framework.security.properties; exports com.fuhouyu.framework.security.serializer; exports com.fuhouyu.framework.security.token; } \ No newline at end of file diff --git a/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/BaseComponent.java b/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/BaseComponent.java index 68f5556..87e6512 100644 --- a/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/BaseComponent.java +++ b/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/BaseComponent.java @@ -15,7 +15,7 @@ */ package com.fuhouyu.framework.security; -import com.fuhouyu.framework.security.core.authentication.refreshtoken.RefreshAuthenticationProvider; +import com.fuhouyu.framework.security.core.provider.refreshtoken.RefreshAuthenticationProvider; import com.fuhouyu.framework.security.token.TokenStore; import org.springframework.boot.test.context.TestComponent; import org.springframework.context.annotation.Bean; diff --git a/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/OidcProviderTest.java b/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/OidcProviderTest.java new file mode 100644 index 0000000..0569f01 --- /dev/null +++ b/base-framework-security-starter/src/test/java/com/fuhouyu/framework/security/OidcProviderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fuhouyu.framework.security; + +import com.fuhouyu.framework.cache.CacheAutoConfiguration; +import com.fuhouyu.framework.cache.CaffeineCacheConfiguration; +import com.fuhouyu.framework.security.core.provider.oidc.OidcAuthenticationToken; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.test.context.TestPropertySource; + +/** + *

+ * 测试类 + *

+ * + * @author fuhouyu + * @since 2024/11/4 22:12 + */ +@SpringBootTest(classes = { + OAuth2ClientAutoConfiguration.class, + CacheAutoConfiguration.class, + CaffeineCacheConfiguration.class, + SecurityAutoConfiguration.class, + BaseComponent.class +}) +@SpringBootApplication +@Disabled +@TestPropertySource(locations = {"classpath:application.yaml"}) +class OidcProviderTest { + + private static final String CODE = "code"; + + private static final String STATE = "state"; + + @Autowired + private AuthenticationManager authenticationManager; + + @Test + void testOidc() { + OidcAuthenticationToken oidcAuthenticationToken = new OidcAuthenticationToken(CODE, STATE, "gitlab"); + authenticationManager.authenticate(oidcAuthenticationToken); + } +} diff --git a/base-framework-security-starter/src/test/resources/application.yaml b/base-framework-security-starter/src/test/resources/application.yaml new file mode 100644 index 0000000..d700ca3 --- /dev/null +++ b/base-framework-security-starter/src/test/resources/application.yaml @@ -0,0 +1,25 @@ +base: + framework: + cache: + service: + cache-service-type: caffeine +spring: + security: + oauth2: + client: + registration: + gitlab: + provider: gitlab + client-id: b8cfade18e2d4a264c768076e0fdb0492bcb35802a85e1ff6c73ebe16043a268 + client-secret: gloas-1f0346b92b61a5d04f3a98df8abbe79ad380c500817011a9f06b3a47f6e0980c + authorization-grant-type: authorization_code + redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client + scope: openid,profile + client-name: gitlab + provider: + gitlab: + authorization-uri: https://gitlab.com/oauth/authorize + token-uri: https://gitlab.com/oauth/token + user-info-uri: https://gitlab.com/oauth/userinfo + jwk-set-uri: https://gitlab.com/oauth/discovery/keys + user-name-attribute: email \ No newline at end of file