refactor: 验证码重构

This commit is contained in:
haoxr 2024-01-30 13:46:42 +08:00
parent 6620b5434d
commit fe80aa3b76
9 changed files with 318 additions and 41 deletions

View File

@ -76,21 +76,21 @@ public class CaptchaAuthenticationConverter implements AuthenticationConverter {
}
// 验证码(必需)
String verifyCode = parameters.getFirst(CaptchaParameterNames.VERIFY_CODE);
String verifyCode = parameters.getFirst(CaptchaParameterNames.CODE);
if (StrUtil.isBlank(verifyCode)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
CaptchaParameterNames.VERIFY_CODE,
CaptchaParameterNames.CODE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}
// 验证码缓存Key(必需)
String verifyCodeKey = parameters.getFirst(CaptchaParameterNames.VERIFY_CODE_KEY);
if (StrUtil.isBlank(verifyCodeKey)) {
String verifyKey = parameters.getFirst(CaptchaParameterNames.KEY);
if (StrUtil.isBlank(verifyKey)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
CaptchaParameterNames.VERIFY_CODE_KEY,
CaptchaParameterNames.KEY,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI
);
}

View File

@ -2,8 +2,6 @@ package com.youlai.auth.authentication.captcha;
import cn.hutool.captcha.generator.MathGenerator;
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;
@ -15,6 +13,10 @@ 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.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
@ -25,10 +27,14 @@ import org.springframework.security.oauth2.server.authorization.context.Authoriz
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.security.Principal;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 验证码模式身份验证提供者
@ -43,6 +49,7 @@ import java.util.Map;
public class CaptchaAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@ -84,10 +91,10 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
// 验证码校验
Map<String, Object> additionalParameters = captchaAuthenticationToken.getAdditionalParameters();
String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE);
String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE_KEY);
String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.CODE);
String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.KEY);
String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_KEY_PREFIX + verifyCodeKey);
String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
// 验证码比对
MathGenerator mathGenerator = new MathGenerator();
@ -109,6 +116,20 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
throw new OAuth2AuthenticationException(e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
}
// 验证申请访问范围(Scope)
Set<String> authorizedScopes = registeredClient.getScopes();
Set<String> requestedScopes = captchaAuthenticationToken.getScopes();
if (!CollectionUtils.isEmpty(requestedScopes)) {
Set<String> unauthorizedScopes = requestedScopes.stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
authorizedScopes = new LinkedHashSet<>(requestedScopes);
}
// 访问令牌(Access Token) 构造器
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
@ -131,10 +152,6 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
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)
@ -164,10 +181,42 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider {
authorizationBuilder.refreshToken(refreshToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// ----- ID token -----
OidcIdToken idToken;
if (requestedScopes.contains(OidcScopes.OPENID)) {
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token
.build();
// @formatter:on
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
if (log.isTraceEnabled()) {
log.trace("Generated id token");
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
// 持久化令牌发放记录到数据库
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
additionalParameters = Collections.EMPTY_MAP;
additionalParameters = (idToken != null)
? Collections.singletonMap(OidcParameterNames.ID_TOKEN, idToken.getTokenValue())
: Collections.emptyMap();
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}

View File

@ -9,15 +9,19 @@ package com.youlai.auth.authentication.captcha;
* @since 3.0.0
*/
public final class CaptchaParameterNames {
/**
* 验证码
*/
public static final String VERIFY_CODE = "verifyCode";
/**
* 验证码缓存Key
* 验证码缓存Key (唯一标识) 用于从Redis获取验证码Code
*/
public static final String VERIFY_CODE_KEY = "verifyCodeKey";
public static final String KEY = "captchaKey";
/**
* 验证码 Code
*/
public static final String CODE = "captchaCode";
private CaptchaParameterNames() {

View File

@ -35,8 +35,8 @@ public class CaptchaAuthenticationTests {
.param(OAuth2ParameterNames.GRANT_TYPE, "captcha")
.param(OAuth2ParameterNames.USERNAME, "admin")
.param(OAuth2ParameterNames.PASSWORD, "123456")
.param(CaptchaParameterNames.VERIFY_CODE, "123456")
.param(CaptchaParameterNames.VERIFY_CODE_KEY, "123456")
.param(CaptchaParameterNames.CODE, "123456")
.param(CaptchaParameterNames.KEY, "123456")
.headers(headers))
.andDo(print())
.andExpect(status().isOk())

View File

@ -0,0 +1,53 @@
package com.youlai.gateway.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
/**
* 验证码自动装配配置
*
* @author haoxr
* @since 2023/11/24
*/
@Configuration
@RequiredArgsConstructor
public class CaptchaConfig {
private final CaptchaProperties captchaProperties;
/**
* 验证码文字生成器
*
* @return CodeGenerator
*/
@Bean
public CodeGenerator codeGenerator() {
String codeType = captchaProperties.getCode().getType();
int codeLength = captchaProperties.getCode().getLength();
if ("math".equalsIgnoreCase(codeType)) {
return new MathGenerator(codeLength);
} else if ("random".equalsIgnoreCase(codeType)) {
return new RandomGenerator(codeLength);
} else {
throw new IllegalArgumentException("Invalid captcha generator type: " + codeType);
}
}
/**
* 验证码字体
*/
@Bean
public Font captchaFont() {
String fontName = captchaProperties.getFont().getName();
int fontSize = captchaProperties.getFont().getSize();
int fontWight = captchaProperties.getFont().getWeight();
return new Font(fontName, fontWight, fontSize);
}
}

View File

@ -0,0 +1,92 @@
package com.youlai.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 验证码配置
*
* @author haoxr
* @since 2023/11/24
*/
@Component
@ConfigurationProperties(prefix = "captcha")
@Data
public class CaptchaProperties {
/**
* 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
*/
private String type;
/**
* 验证码图片宽度
*/
private int width;
/**
* 验证码图片高度
*/
private int height;
/**
* 干扰线数量
*/
private int interfereCount;
/**
* 文本透明度
*/
private Float textAlpha;
/**
* 验证码过期时间单位
*/
private Long expireSeconds;
/**
* 验证码字符配置
*/
private CodeProperties code;
/**
* 验证码字体
*/
private FontProperties font;
/**
* 验证码字符配置
*/
@Data
public static class CodeProperties {
/**
* 验证码字符类型 math-算术|random-随机字符串
*/
private String type;
/**
* 验证码字符长度type=算术时表示运算位数(1:个位数 2:十位数)type=随机字符时表示字符个数
*/
private int length;
}
/**
* 验证码字体配置
*/
@Data
public static class FontProperties {
/**
* 字体名称
*/
private String name;
/**
* 字体样式 0-普通|1-粗体|2-斜体
*/
private int weight;
/**
* 字体大小
*/
private int size;
}
}

View File

@ -0,0 +1,27 @@
package com.youlai.gateway.enums;
/**
* EasyCaptcha 验证码类型枚举
*
* @author haoxr
* @since 2.5.1
*/
public enum CaptchaTypeEnum {
/**
* 圆圈干扰验证码
*/
CIRCLE,
/**
* GIF验证码
*/
GIF,
/**
* 干扰线验证码
*/
LINE,
/**
* 扭曲干扰验证码
*/
SHEAR
}

View File

@ -1,12 +1,14 @@
package com.youlai.gateway.handler;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.GifCaptcha;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import com.youlai.common.constant.SecurityConstants;
import com.youlai.common.result.Result;
import com.youlai.gateway.config.CaptchaProperties;
import com.youlai.gateway.enums.CaptchaTypeEnum;
import com.youlai.gateway.model.CaptchaResult;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@ -16,8 +18,7 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.awt.*;
import java.util.concurrent.TimeUnit;
/**
@ -31,25 +32,48 @@ import java.util.concurrent.TimeUnit;
public class CaptchaHandler implements HandlerFunction<ServerResponse> {
private final StringRedisTemplate redisTemplate;
private final CodeGenerator codeGenerator;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
@Override
public Mono<ServerResponse> handle(ServerRequest request) {
MathGenerator mathGenerator = new MathGenerator(1);
CircleCaptcha circleCaptcha = new CircleCaptcha(150, 25, 4, 3);
circleCaptcha.setGenerator(mathGenerator);
String captchaCode = circleCaptcha.getCode(); // 验证码
String captchaBase64 = circleCaptcha.getImageBase64Data(); // 验证码图片Base64
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();
// 验证码文本缓存至Redis用于登录校验
String verifyCodeKey = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(SecurityConstants.VERIFY_CODE_CACHE_KEY_PREFIX + verifyCodeKey, captchaCode,
120, TimeUnit.SECONDS);
String captchaId = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaId, captchaCode,
captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
Map<String, String> result = new HashMap<>(2);
result.put("verifyCodeKey", verifyCodeKey);
result.put("captchaImgBase64", captchaBase64);
CaptchaResult captchaResult = CaptchaResult.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
return ServerResponse.ok().body(BodyInserters.fromValue(Result.success(result)));
return ServerResponse.ok().body(BodyInserters.fromValue(Result.success(captchaResult)));
}
}

View File

@ -0,0 +1,28 @@
package com.youlai.gateway.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 验证码响应对象
*
* @author haoxr
* @since 2023/03/24
*/
@Schema(description ="验证码响应对象")
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CaptchaResult {
@Schema(description = "验证码唯一标识(用于从Redis获取验证码Code)")
private String captchaId;
@Schema(description = "验证码图片Base64字符串")
private String captchaBase64;
}