mirror of
https://gitee.com/youlaitech/youlai-mall.git
synced 2024-12-22 12:48:59 +08:00
refactor: 验证码重构
This commit is contained in:
parent
6620b5434d
commit
fe80aa3b76
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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())
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.youlai.gateway.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.5.1
|
||||
*/
|
||||
public enum CaptchaTypeEnum {
|
||||
|
||||
/**
|
||||
* 圆圈干扰验证码
|
||||
*/
|
||||
CIRCLE,
|
||||
/**
|
||||
* GIF验证码
|
||||
*/
|
||||
GIF,
|
||||
/**
|
||||
* 干扰线验证码
|
||||
*/
|
||||
LINE,
|
||||
/**
|
||||
* 扭曲干扰验证码
|
||||
*/
|
||||
SHEAR
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user