refactor: 认证中心升级(临时提交勿clone)

This commit is contained in:
郝先瑞 2023-06-06 23:00:37 +08:00
parent b019e6dda6
commit 84aa6da775
6 changed files with 309 additions and 169 deletions

View File

@ -98,7 +98,7 @@ public class ResourceOwnerPasswordAuthenticationProvider implements Authenticati
} }
// 生成 access_token // 生成 access_token
// @formatter:off // @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient) .registeredClient(registeredClient)
.principal(usernamePasswordAuthentication) .principal(usernamePasswordAuthentication)

View File

@ -18,7 +18,7 @@ public class ResourceOwnerPasswordAuthenticationToken extends OAuth2Authorizatio
* @param clientPrincipal * @param clientPrincipal
* @param additionalParameters * @param additionalParameters
*/ */
protected ResourceOwnerPasswordAuthenticationToken( public ResourceOwnerPasswordAuthenticationToken(
Authentication clientPrincipal, Authentication clientPrincipal,
@Nullable Set<String> scopes, @Nullable Set<String> scopes,
Map<String, Object> additionalParameters Map<String, Object> additionalParameters

View File

@ -0,0 +1,129 @@
package com.youlai.auth.authentication.wechat;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.authentication.password.ResourceOwnerPasswordAuthenticationToken;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
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.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 参数解析
*
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter
*/
public class WechatAuthenticationConverter implements AuthenticationConverter {
public static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!"wechat".equals(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = getParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// code (REQUIRED)
String code = parameters.getFirst("code");
if (StrUtil.isBlank(code)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"code",
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// encryptedData (REQUIRED)
String encryptedData = parameters.getFirst("encryptedData");
if (StrUtil.isBlank(encryptedData)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"encryptedData",
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// iv (REQUIRED)
String iv = parameters.getFirst("iv");
if (StrUtil.isBlank(iv)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"iv",
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ErrorCodes.INVALID_CLIENT,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Map<String, Object> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
ResourceOwnerPasswordAuthenticationToken resourceOwnerPasswordAuthenticationToken =
new ResourceOwnerPasswordAuthenticationToken(
clientPrincipal,
requestedScopes,
additionalParameters
);
return resourceOwnerPasswordAuthenticationToken;
}
public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
parameters.add(key, value);
}
});
return parameters;
}
public static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
}

View File

@ -1,77 +1,167 @@
package com.youlai.auth.authentication.wechat; package com.youlai.auth.authentication.wechat;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.hutool.core.lang.Assert;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import com.youlai.auth.authentication.password.ResourceOwnerPasswordAuthenticationToken;
import cn.binarywang.wx.miniapp.bean.WxMaUserInfo; import lombok.extern.slf4j.Slf4j;
import cn.hutool.core.bean.BeanUtil; import org.springframework.security.authentication.AuthenticationManager;
import com.youlai.auth.userdetails.member.MemberUserDetailsServiceImpl;
import com.youlai.common.result.Result;
import com.youlai.common.result.ResultCode;
import com.youlai.mall.ums.api.MemberFeignClient;
import com.youlai.mall.ums.dto.MemberAuthDTO;
import com.youlai.mall.ums.dto.MemberDTO;
import lombok.Data;
import lombok.SneakyThrows;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.*;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
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.CollectionUtils;
import java.util.HashSet; import java.security.Principal;
import java.util.*;
import java.util.stream.Collectors;
/** /**
* 微信认证提供者 * 微信认证提供者
* *
* @author <a href="mailto:xianrui0365@163.com">haoxr</a> * @author haoxr
* @since 2021/9/25 * @since 3.0.0
*/ */
@Data @Slf4j
public class WechatAuthenticationProvider implements AuthenticationProvider { public class WechatAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService; private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private WxMaService wxMaService;
private MemberFeignClient memberFeignClient;
/** private final AuthenticationManager authenticationManager;
* 微信认证 private final OAuth2AuthorizationService authorizationService;
* private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
* @param authentication
* @return
* @throws AuthenticationException
*/
@SneakyThrows
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
WechatAuthenticationToken authenticationToken = (WechatAuthenticationToken) authentication;
String code = (String) authenticationToken.getPrincipal();
WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code); /**
String openid = sessionInfo.getOpenid(); * Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
Result<MemberAuthDTO> memberAuthResult = memberFeignClient.loadUserByOpenId(openid); *
// 微信用户不存在注册成为新会员 * @param authenticationManager the authentication manager
if (memberAuthResult != null && ResultCode.USER_NOT_EXIST.getCode().equals(memberAuthResult.getCode())) { * @param authorizationService the authorization service
* @param tokenGenerator the token generator
String sessionKey = sessionInfo.getSessionKey(); * @since 0.2.3
String encryptedData = authenticationToken.getEncryptedData(); */
String iv = authenticationToken.getIv(); public WechatAuthenticationProvider(AuthenticationManager authenticationManager,
// 解密 encryptedData 获取用户信息 OAuth2AuthorizationService authorizationService,
WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, iv); OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator
) {
MemberDTO memberDTO = new MemberDTO(); Assert.notNull(authorizationService, "authorizationService cannot be null");
BeanUtil.copyProperties(userInfo, memberDTO); Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
memberDTO.setOpenid(openid); this.authenticationManager = authenticationManager;
memberFeignClient.addMember(memberDTO); this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
} }
UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByOpenId(openid);
WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>()); @Override
result.setDetails(authentication.getDetails()); public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return result;
} ResourceOwnerPasswordAuthenticationToken authenticationToken = (ResourceOwnerPasswordAuthenticationToken) authentication;
// 验证客户端是否已认证
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 验证客户端是否支持(grant_type=password)授权模式
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 密码验证
Map<String, Object> additionalParameters = authenticationToken.getAdditionalParameters();
String username = (String) additionalParameters.get("code");
String encryptedData = (String) additionalParameters.get("encryptedData");
String iv = (String) additionalParameters.get("iv");
UsernamePasswordAuthenticationToken passwordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
log.debug("got usernamePasswordAuthenticationToken=" + passwordAuthenticationToken);
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(passwordAuthenticationToken);
usernamePasswordAuthentication.setAuthenticated(true);
@Override
public boolean supports(Class<?> authentication) { // 生成 access_token
return WechatAuthenticationToken.class.isAssignableFrom(authentication); // @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(passwordAuthenticationToken);
// @formatter:on
// ----- 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());
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
// @formatter:on
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class<?> authentication) {
return ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
private static 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);
}
} }
}

View File

@ -1,64 +1,51 @@
package com.youlai.auth.authentication.wechat; package com.youlai.auth.authentication.wechat;
import jakarta.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.util.Collection; import java.util.*;
/** /**
* @author <a href="mailto:xianrui0365@163.com">haoxr</a> * 微信授权登录
* @since 2021/9/25 *
* @author haoxr
* @since 3.0.0
*/ */
public class WechatAuthenticationToken extends AbstractAuthenticationToken { public class WechatAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private static final long serialVersionUID = 550L;
private final Object principal; private final Set<String> scopes;
@Getter
private String encryptedData;
@Getter
private String iv;
/**
* 账号校验之前的token构建
*
* @param principal
*/
public WechatAuthenticationToken(Object principal, String encryptedData,String iv) {
super(null);
this.principal = principal;
this.encryptedData = encryptedData;
this.iv=iv;
setAuthenticated(false);
}
/** /**
* 账号校验成功之后的token构建 * {@link org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken}
* *
* @param principal * @param clientPrincipal
* @param authorities * @param additionalParameters
*/ */
public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { public WechatAuthenticationToken(
super(authorities); Authentication clientPrincipal,
this.principal = principal; @Nullable Set<String> scopes,
super.setAuthenticated(true); Map<String, Object> additionalParameters
) {
super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
public Set<String> getScopes() {
return this.scopes;
} }
@Override @Override
public Object getCredentials() { public Object getCredentials() {
return null; return this.getAdditionalParameters().get(OAuth2ParameterNames.PASSWORD);
} }
@Override
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
public void eraseCredentials() {
super.eraseCredentials();
}
} }

View File

@ -1,66 +0,0 @@
package com.youlai.auth.authentication.wechat;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 微信授权者
*
* @author <a href="mailto:xianrui0365@163.com">haoxr</a>
* @since 2021/9/25
*/
public class WechatTokenGranter extends AbstractTokenGranter {
/**
* 声明授权者 WechatTokenGranter 支持授权模式 wechat
* 根据接口传值 grant_type = wechat 的值匹配到此授权者
* 匹配逻辑详见下面的两个方法
*
* @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
* @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
*/
private static final String GRANT_TYPE = "wechat";
private final AuthenticationManager authenticationManager;
public WechatTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String code = parameters.get("code");
String encryptedData = parameters.get("encryptedData");
String iv = parameters.get("iv");
// 移除后续无用参数
parameters.remove("code");
parameters.remove("encryptedData");
parameters.remove("iv");
Authentication userAuth = new WechatAuthenticationToken(code, encryptedData,iv); // 未认证状态
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = this.authenticationManager.authenticate(userAuth); // 认证中
} catch (Exception e) {
throw new InvalidGrantException(e.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) { // 认证成功
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else { // 认证失败
throw new InvalidGrantException("Could not authenticate code: " + code);
}
}
}