package com.youlai.auth.authentication.captcha; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import com.youlai.auth.util.OAuth2AuthenticationProviderUtils; import com.youlai.common.constant.SecurityConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; 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.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.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 java.security.Principal; import java.util.Collections; import java.util.Map; /** * 验证码模式身份验证提供者 *

* 处理基于用户名和密码的身份验证 * * @author haoxr * @see org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider * @since 3.0.0 */ @Slf4j public class CaptchaAuthenticationProvider implements AuthenticationProvider { private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; private final AuthenticationManager authenticationManager; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator tokenGenerator; private final StringRedisTemplate redisTemplate; /** * 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 CaptchaAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator, StringRedisTemplate redisTemplate ) { Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); this.authenticationManager = authenticationManager; this.authorizationService = authorizationService; this.tokenGenerator = tokenGenerator; this.redisTemplate = redisTemplate; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { CaptchaAuthenticationToken captchaAuthenticationToken = (CaptchaAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils .getAuthenticatedClientElseThrowInvalidClient(captchaAuthenticationToken); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); // 验证客户端是否支持授权类型(grant_type=password) if (!registeredClient.getAuthorizationGrantTypes().contains(CaptchaAuthenticationToken.CAPTCHA)) { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT); } // 证码校验 Map additionalParameters = captchaAuthenticationToken.getAdditionalParameters(); String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE); String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE_KEY); String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_KEY_PREFIX + verifyCodeKey); if (!StrUtil.equals(verifyCode, cacheCode)) { throw new OAuth2AuthenticationException("验证码错误"); } // 生成用户名密码身份验证令牌 String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME); String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); // 用户名密码身份验证,成功后返回 带有权限的认证信息 Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); // 访问令牌(Access Token) 构造器 DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(usernamePasswordAuthentication) // 身份验证成功的认证信息(用户名、权限等信息) .authorizationServerContext(AuthorizationServerContextHolder.getContext()) .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 授权方式 .authorizationGrant(captchaAuthenticationToken) // 授权具体对象 ; // 生成访问令牌(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()); // 权限数据比较多通过反射移除不持久化至数据库 ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null); OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(usernamePasswordAuthentication.getName()) .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) .attribute(Principal.class.getName(), usernamePasswordAuthentication); 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); additionalParameters = Collections.EMPTY_MAP; return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } @Override public boolean supports(Class authentication) { return CaptchaAuthenticationToken.class.isAssignableFrom(authentication); } }