提交 14b8bd8d 编写于 作者: 郝先瑞

feat: 微信小程序授权认证扩展(临时提交勿clone)

上级 9a991973
package com.youlai.auth.authentication.wechat;
package com.youlai.auth.authentication.miniapp;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.authentication.password.ResourceOwnerPasswordAuthenticationToken;
import com.youlai.auth.util.OAuth2EndpointUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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;
/**
......@@ -26,70 +18,50 @@ import java.util.stream.Collectors;
*
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter
*/
public class WechatMiniProgramAuthenticationConverter implements AuthenticationConverter {
public class WxMiniAppAuthenticationConverter implements AuthenticationConverter {
public static final String ACCESS_TOKEN_REQUEST_ERROR_URI ="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html";
String CODE = "code";
String IV = "iv";
String ENCRYPTED_DATA = "encryptedData";
public static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html";
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!WeChatMiniProgramAuthenticationToken.WECHAT_MINI_PROGRAM.getValue().equals(grantType)) {
if (!WxMiniAppAuthenticationToken.WX_MINI_APP.getValue().equals(grantType)) {
return null;
}
MultiValueMap<String, String> 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,
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(WechatMiniProgramParameterNames.CODE);
String code = parameters.getFirst(WxMiniAppParameterNames.CODE);
if (StrUtil.isBlank(code)) {
throwError(
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
WechatMiniProgramParameterNames.CODE,
WxMiniAppParameterNames.CODE,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// encryptedData (REQUIRED)
String encryptedData = parameters.getFirst(WechatMiniProgramParameterNames.ENCRYPTED_DATA);
String encryptedData = parameters.getFirst(WxMiniAppParameterNames.ENCRYPTED_DATA);
if (StrUtil.isBlank(encryptedData)) {
throwError(
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
WechatMiniProgramParameterNames.ENCRYPTED_DATA,
WxMiniAppParameterNames.ENCRYPTED_DATA,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// iv (REQUIRED)
String iv = parameters.getFirst(WechatMiniProgramParameterNames.IV);
String iv = parameters.getFirst(WxMiniAppParameterNames.IV);
if (StrUtil.isBlank(iv)) {
throwError(
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
WechatMiniProgramParameterNames.IV,
WxMiniAppParameterNames.IV,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
throwError(
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ErrorCodes.INVALID_CLIENT,
ACCESS_TOKEN_REQUEST_ERROR_URI);
......@@ -101,32 +73,20 @@ public class WechatMiniProgramAuthenticationConverter implements AuthenticationC
.filter(e ->
!e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
&& !e.getKey().equals(OAuth2ParameterNames.SCOPE)
&& !e.getKey().equals(WxMiniAppParameterNames.CODE)
&& !e.getKey().equals(WxMiniAppParameterNames.ENCRYPTED_DATA)
&& !e.getKey().equals(WxMiniAppParameterNames.IV)
)
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
return new ResourceOwnerPasswordAuthenticationToken(
return new WxMiniAppAuthenticationToken(
clientPrincipal,
requestedScopes,
additionalParameters
additionalParameters,
code,
encryptedData,
iv
);
}
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);
}
}
package com.youlai.auth.authentication.wechat;
package com.youlai.auth.authentication.miniapp;
import cn.hutool.core.lang.Assert;
import com.youlai.auth.authentication.password.ResourceOwnerPasswordAuthenticationToken;
import com.youlai.auth.userdetails.member.MmsUserDetailsService;
import com.youlai.auth.userdetails.member.MemberUserDetailsService;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
......@@ -22,84 +24,87 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 微信认证提供者
* 微信认证 Provider
*
* @author haoxr
* @since 3.0.0
*/
@Slf4j
public class WechatMiniProgramAuthenticationProvider implements AuthenticationProvider {
public class WxMiniAppAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final MmsUserDetailsService mmsUserDetailsService;
private final MemberUserDetailsService memberUserDetailsService;
/**
* Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public WechatMiniProgramAuthenticationProvider(
public WxMiniAppAuthenticationProvider(
OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
MmsUserDetailsService mmsUserDetailsService
MemberUserDetailsService memberUserDetailsService
) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
this.mmsUserDetailsService = mmsUserDetailsService;
this.memberUserDetailsService = memberUserDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
WeChatMiniProgramAuthenticationToken authenticationToken = (WeChatMiniProgramAuthenticationToken) authentication;
authenticationToken.getIv()
WxMiniAppAuthenticationToken authenticationToken = (WxMiniAppAuthenticationToken) authentication;
// 参数
String code = authenticationToken.getCode();
String encryptedData = authenticationToken.getEncryptedData();
String iv = authenticationToken.getIv();
Map<String, Object> additionalParameters = authenticationToken.getAdditionalParameters();
// 验证客户端是否已认证
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authenticationToken);
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(authenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// 验证客户端是否支持(grant_type=password)授权模式
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
if (registeredClient == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "注册客户不能为空", null);
throw new OAuth2AuthenticationException(error);
}
// 密码验证
Map<String, Object> additionalParameters = authenticationToken.getAdditionalParameters();
String code = (String) additionalParameters.get("code");
String encryptedData = (String) additionalParameters.get("encryptedData");
String iv = (String) additionalParameters.get("iv");
UserDetails userDetails = mmsUserDetailsService.loadUserByWechatCode(code, encryptedData, iv);
UserDetails userDetails = memberUserDetailsService.loadUserByWechatCode(code, encryptedData, iv);
UsernamePasswordAuthenticationToken principal = UsernamePasswordAuthenticationToken.authenticated(userDetails, null,
userDetails.getAuthorities());
WeChatMiniProgramAuthenticationToken weChatMiniProgramAuthenticationToken =new WeChatMiniProgramAuthenticationToken()
Authentication usernamePasswordAuthentication =new WeChatMiniProgramAuthenticationToken();
List<GrantedAuthority> authorities = new ArrayList<>();
WxMiniAppAuthenticationToken wxMiniAppAuthenticationToken = new WxMiniAppAuthenticationToken(authorities,
clientPrincipal, principal, user, additionalParameters, details, appid, code, openid);
// 生成 access_token
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(userDetails.getUsername())
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizationGrantType(AuthorizationGrantType.PASSWORD);
// @formatter:on
......
package com.youlai.auth.authentication.wechat;
package com.youlai.auth.authentication.miniapp;
import lombok.Getter;
import org.springframework.security.core.Authentication;
......@@ -15,12 +15,12 @@ import java.util.Map;
* @see OAuth2AuthorizationCodeAuthenticationToken
* @since 3.0.0
*/
public class WeChatMiniProgramAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
public class WxMiniAppAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
/**
* 授权类型:微信小程序
*/
public static final AuthorizationGrantType WECHAT_MINI_PROGRAM = new AuthorizationGrantType("wechat_mini_program");
public static final AuthorizationGrantType WX_MINI_APP = new AuthorizationGrantType("wx_mini_app");
@Getter
private final String code;
......@@ -32,30 +32,22 @@ public class WeChatMiniProgramAuthenticationToken extends OAuth2AuthorizationGra
private final String iv;
/**
* @see org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames#SCOPE
*/
private final String scope;
/**
* Sub-class constructor.
*
* @param clientPrincipal the authenticated client principal
* @param additionalParameters the additional parameters
*/
protected WeChatMiniProgramAuthenticationToken(
protected WxMiniAppAuthenticationToken(
Authentication clientPrincipal,
Map<String, Object> additionalParameters,
String code,
String encryptedData,
String iv,
String scope
String iv
) {
super(WeChatMiniProgramAuthenticationToken.WECHAT_MINI_PROGRAM, clientPrincipal, additionalParameters);
super(WxMiniAppAuthenticationToken.WX_MINI_APP, clientPrincipal, additionalParameters);
this.code = code;
this.encryptedData = encryptedData;
this.iv = iv;
this.scope = scope;
}
}
package com.youlai.auth.authentication.wechat;
package com.youlai.auth.authentication.miniapp;
/**
* 微信小程序参数名称
......@@ -6,7 +6,7 @@ package com.youlai.auth.authentication.wechat;
* @author haoxr
* @since 3.0.0
*/
public interface WechatMiniProgramParameterNames {
public interface WxMiniAppParameterNames {
String CODE = "code";
......
package com.youlai.auth.authentication.mobile;
import cn.hutool.core.util.StrUtil;
import com.youlai.auth.userdetails.member.MmsUserDetailsService;
import com.youlai.auth.userdetails.member.MemberUserDetailsService;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.web.exception.BizException;
import com.youlai.mall.ums.api.MemberFeignClient;
......@@ -44,7 +44,7 @@ public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
// 比对成功删除缓存的验证码
redisTemplate.delete(codeKey);
}
UserDetails userDetails = ((MmsUserDetailsService) userDetailsService).loadUserByMobile(mobile);
UserDetails userDetails = ((MemberUserDetailsService) userDetailsService).loadUserByMobile(mobile);
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(userDetails, authentication.getCredentials(), new HashSet<>());
result.setDetails(authentication.getDetails());
return result;
......
package com.youlai.auth.authentication.refresh;
import com.youlai.auth.userdetails.member.MmsUserDetailsService;
import com.youlai.auth.util.RequestUtils;
import com.youlai.common.constant.SecurityConstants;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
import java.util.Map;
/**
* 刷新token再次认证 UserDetailsService
*
* @author <a href="mailto:xianrui0365@163.com">haoxr</a>
* @since 2021/10/2
*/
@NoArgsConstructor
public class PreAuthenticatedUserDetailsService<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {
/**
* 客户端ID和用户服务 UserDetailService 的映射
*/
private Map<String, UserDetailsService> userDetailsServiceMap;
public PreAuthenticatedUserDetailsService(Map<String, UserDetailsService> userDetailsServiceMap) {
Assert.notNull(userDetailsServiceMap, "userDetailsService cannot be null.");
this.userDetailsServiceMap = userDetailsServiceMap;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsServiceMap, "UserDetailsService must be set");
}
/**
* 重写PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 属性,可根据客户端和认证方式选择用户服务 UserDetailService 获取用户信息 UserDetail
*
* @param authentication
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
String clientId = RequestUtils.getClientId();
// 获取认证身份标识,默认是用户名:username
UserDetailsService userDetailsService = userDetailsServiceMap.get(clientId);
switch (clientId) {
case SecurityConstants.APP_CLIENT_ID -> {
// 移动端的用户体系是会员,认证方式是通过手机号 mobile 认证
MmsUserDetailsService mmsUserDetailsService = (MmsUserDetailsService) userDetailsService;
return mmsUserDetailsService.loadUserByUsername(authentication.getName());
}
case SecurityConstants.WEAPP_CLIENT_ID -> {
// 小程序的用户体系是会员,认证方式是通过微信三方标识 openid 认证
MmsUserDetailsService mmsUserDetailsService = (MmsUserDetailsService) userDetailsService;
return mmsUserDetailsService.loadUserByOpenId(authentication.getName());
}
// 管理系统的用户体系是系统用户,认证方式通过用户名 username 认证
default -> {
return userDetailsService.loadUserByUsername(authentication.getName());
}
}
}
}
......@@ -17,7 +17,7 @@ import java.util.HashSet;
* @since 2021/9/27
*/
@Data
public class MmsUserDetails implements UserDetails {
public class MemberUserDetails implements UserDetails {
private Long memberId;
private String username;
......@@ -34,7 +34,7 @@ public class MmsUserDetails implements UserDetails {
*
* @param member 小程序会员用户认证信息
*/
public MmsUserDetails(MemberAuthDTO member) {
public MemberUserDetails(MemberAuthDTO member) {
this.setMemberId(member.getMemberId());
this.setUsername(member.getUsername());
this.setEnabled(GlobalConstants.STATUS_YES.equals(member.getStatus()));
......
......@@ -24,11 +24,12 @@ import org.springframework.stereotype.Service;
/**
* 商城会员用户认证服务
*
* @author <a href="mailto:xianrui0365@163.com">haoxr</a>
* @author haoxr
* @since 3.0.0
*/
@Service("memberUserDetailsService")
@RequiredArgsConstructor
public class MmsUserDetailsService implements UserDetailsService {
public class MemberUserDetailsService implements UserDetailsService {
private final MemberFeignClient memberFeignClient;
private final WxMaService wxMaService;
......@@ -46,12 +47,12 @@ public class MmsUserDetailsService implements UserDetailsService {
* @return
*/
public UserDetails loadUserByMobile(String mobile) {
MmsUserDetails userDetails = null;
MemberUserDetails userDetails = null;
Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
if (Result.isSuccess(result)) {
MemberAuthDTO member = result.getData();
if (null != member) {
userDetails = new MmsUserDetails(member);
userDetails = new MemberUserDetails(member);
}
}
if (userDetails == null) {
......@@ -103,7 +104,7 @@ public class MmsUserDetailsService implements UserDetailsService {
throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
}
UserDetails userDetails = new MmsUserDetails(memberAuthInfo);
UserDetails userDetails = new MemberUserDetails(memberAuthInfo);
if (!userDetails.isEnabled()) {
throw new DisabledException("该账户已被禁用!");
} else if (!userDetails.isAccountNonLocked()) {
......
package com.youlai.auth.util;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
/**
* Utility methods for the OAuth 2.0 {@link AuthenticationProvider}'s.
*
* @author Joe Grandja
* @since 0.0.3
*/
public class OAuth2AuthenticationProviderUtils {
public OAuth2AuthenticationProviderUtils() {
}
public 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);
}
public static <T extends OAuth2Token> OAuth2Authorization invalidate(
OAuth2Authorization authorization, T token) {
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization)
.token(token,
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
if (OAuth2RefreshToken.class.isAssignableFrom(token.getClass())) {
authorizationBuilder.token(
authorization.getAccessToken().getToken(),
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode =
authorization.getToken(OAuth2AuthorizationCode.class);
if (authorizationCode != null && !authorizationCode.isInvalidated()) {
authorizationBuilder.token(
authorizationCode.getToken(),
(metadata) ->
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true));
}
}
// @formatter:on
return authorizationBuilder.build();
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册