diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java index 79e7c290..77074ab6 100755 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/config/AuthorizationServerConfiguration.java @@ -69,8 +69,10 @@ public class AuthorizationServerConfiguration { tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter .accessTokenResponseHandler(new PigAuthenticationSuccessEventHandler()) // 登录成功处理器 .errorResponseHandler(new PigAuthenticationFailureEventHandler());// 登录失败处理器 - }).authorizationEndpoint( // 授权码端点个性化confirm页面 - authorizationEndpoint -> authorizationEndpoint.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI))); + }).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证 + oAuth2ClientAuthenticationConfigurer.errorResponseHandler(new PigAuthenticationFailureEventHandler()))// 处理客户端认证异常 + .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面 + .consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI))); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); DefaultSecurityFilterChain securityFilterChain = http.requestMatcher(endpointsMatcher) diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java index 7be42f7a..3c790de9 100644 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/endpoint/PigTokenEndpoint.java @@ -22,6 +22,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails; import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; import com.pig4cloud.pig.admin.api.vo.TokenVo; +import com.pig4cloud.pig.auth.support.handler.PigAuthenticationFailureEventHandler; import com.pig4cloud.pig.common.core.constant.CacheConstants; import com.pig4cloud.pig.common.core.constant.CommonConstants; import com.pig4cloud.pig.common.core.constant.SecurityConstants; @@ -44,18 +45,19 @@ import org.springframework.security.authentication.event.LogoutSuccessEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; -import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.security.Principal; import java.util.List; @@ -75,7 +77,7 @@ public class PigTokenEndpoint { private final HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); - private final HttpMessageConverter errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); + private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler(); private final OAuth2AuthorizationService authorizationService; @@ -135,21 +137,20 @@ public class PigTokenEndpoint { */ @SneakyThrows @GetMapping("/check_token") - public void checkToken(String token, HttpServletResponse response) { + public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) { ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); if (StrUtil.isBlank(token)) { httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); - this.errorHttpResponseConverter.write(new OAuth2Error(OAuth2ErrorCodesExpand.TOKEN_MISSING), null, - httpResponse); + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING)); } OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); // 如果令牌不存在 返回401 - if (authorization == null) { - httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); - this.errorHttpResponseConverter.write(new OAuth2Error(OAuth2ErrorCodesExpand.TOKEN_MISSING), null, - httpResponse); + if (authorization == null || authorization.getAccessToken() == null) { + this.authenticationFailureHandler.onAuthenticationFailure(request, response, + new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN)); } Map claims = authorization.getAccessToken().getClaims(); diff --git a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationFailureEventHandler.java b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationFailureEventHandler.java index 65e10833..57efd6b9 100644 --- a/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationFailureEventHandler.java +++ b/pig-auth/src/main/java/com/pig4cloud/pig/auth/support/handler/PigAuthenticationFailureEventHandler.java @@ -29,8 +29,6 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -73,15 +71,13 @@ public class PigAuthenticationFailureEventHandler implements AuthenticationFailu logVo.setUpdateBy(username); SpringContextHolder.publishEvent(new SysLogEvent(logVo)); // 写出错误信息 - sendErrorResponse(request, response, exception); + sendErrorResponse(response, exception); } - private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException { - OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); + private void sendErrorResponse(HttpServletResponse response, AuthenticationException exception) throws IOException { ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); - this.errorHttpResponseConverter.write(R.failed(error.getDescription()), MediaType.APPLICATION_JSON, + httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); + this.errorHttpResponseConverter.write(R.failed(exception.getLocalizedMessage()), MediaType.APPLICATION_JSON, httpResponse); } diff --git a/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/R.java b/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/R.java index 9e6425bd..7b79afa2 100755 --- a/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/R.java +++ b/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/R.java @@ -76,7 +76,7 @@ public class R implements Serializable { return restResult(data, CommonConstants.FAIL, msg); } - private static R restResult(T data, int code, String msg) { + public static R restResult(T data, int code, String msg) { R apiResult = new R<>(); apiResult.setCode(code); apiResult.setData(data); diff --git a/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/RetOps.java b/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/RetOps.java new file mode 100644 index 00000000..684d1a39 --- /dev/null +++ b/pig-common/pig-common-core/src/main/java/com/pig4cloud/pig/common/core/util/RetOps.java @@ -0,0 +1,289 @@ +/* + * + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + * + */ + +package com.pig4cloud.pig.common.core.util; + +import cn.hutool.core.util.ObjectUtil; +import com.pig4cloud.pig.common.core.constant.CommonConstants; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * 简化{@code R} 的访问操作,例子
+ * R result = R.ok(0);
+ * // 使用场景1: 链式操作: 断言然后消费
+ * RetOps.of(result)
+ * 		.assertCode(-1,r -> new RuntimeException("error "+r.getCode()))
+ * 		.assertDataNotEmpty(r -> new IllegalStateException("oops!"))
+ * 		.useData(System.out::println);
+ *
+ * // 使用场景2: 读取原始值(data),这里返回的是Optional
+ * RetOps.of(result).getData().orElse(null);
+ *
+ * // 使用场景3: 类型转换
+ * R s = RetOps.of(result)
+ *        .assertDataNotNull(r -> new IllegalStateException("nani??"))
+ *        .map(i -> Integer.toHexString(i))
+ *        .peek();
+ * 
+ * + * @author CJ (power4j@outlook.com) + * @date 2022/5/12 + * @since 4.4 + */ +public class RetOps { + + /** 状态码为成功 */ + public static final Predicate> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getCode(); + + /** 数据有值 */ + public static final Predicate> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData()); + + /** 数据有值,并且包含元素 */ + public static final Predicate> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData()); + + /** 状态码为成功并且有值 */ + public static final Predicate> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA); + + private final R original; + + // ~ 初始化 + // =================================================================================================== + + RetOps(R original) { + this.original = original; + } + + public static RetOps of(R original) { + return new RetOps<>(Objects.requireNonNull(original)); + } + + // ~ 杂项方法 + // =================================================================================================== + + /** + * 观察原始值 + * @return R + */ + public R peek() { + return original; + } + + /** + * 读取{@code code}的值 + * @return 返回code的值 + */ + public int getCode() { + return original.getCode(); + } + + /** + * 读取{@code data}的值 + * @return 返回 Optional 包装的data + */ + public Optional getData() { + return Optional.of(original.getData()); + } + + /** + * 有条件地读取{@code data}的值 + * @param predicate 断言函数 + * @return 返回 Optional 包装的data,如果断言失败返回empty + */ + public Optional getDataIf(Predicate> predicate) { + return predicate.test(original) ? getData() : Optional.empty(); + } + + /** + * 读取{@code msg}的值 + * @return 返回Optional包装的 msg + */ + public Optional getMsg() { + return Optional.of(original.getMsg()); + } + + /** + * 对{@code code}的值进行相等性测试 + * @param value 基准值 + * @return 返回ture表示相等 + */ + public boolean codeEquals(int value) { + return original.getCode() == value; + } + + /** + * 对{@code code}的值进行相等性测试 + * @param value 基准值 + * @return 返回ture表示不相等 + */ + public boolean codeNotEquals(int value) { + return !codeEquals(value); + } + + /** + * 是否成功 + * @return 返回ture表示成功 + * @see CommonConstants#SUCCESS + */ + public boolean isSuccess() { + return codeEquals(CommonConstants.SUCCESS); + } + + /** + * 是否失败 + * @return 返回ture表示失败 + */ + public boolean notSuccess() { + return !isSuccess(); + } + + // ~ 链式操作 + // =================================================================================================== + + /** + * 断言{@code code}的值 + * @param expect 预期的值 + * @param func 用户函数,负责创建异常对象 + * @param 异常类型 + * @return 返回实例,以便于继续进行链式操作 + * @throws Ex 断言失败时抛出 + */ + public RetOps assertCode(int expect, Function, ? extends Ex> func) + throws Ex { + if (codeNotEquals(expect)) { + throw func.apply(original); + } + return this; + } + + /** + * 断言成功 + * @param func 用户函数,负责创建异常对象 + * @param 异常类型 + * @return 返回实例,以便于继续进行链式操作 + * @throws Ex 断言失败时抛出 + */ + public RetOps assertSuccess(Function, ? extends Ex> func) throws Ex { + return assertCode(CommonConstants.SUCCESS, func); + } + + /** + * 断言业务数据有值 + * @param func 用户函数,负责创建异常对象 + * @param 异常类型 + * @return 返回实例,以便于继续进行链式操作 + * @throws Ex 断言失败时抛出 + */ + public RetOps assertDataNotNull(Function, ? extends Ex> func) throws Ex { + if (Objects.isNull(original.getData())) { + throw func.apply(original); + } + return this; + } + + /** + * 断言业务数据有值,并且包含元素 + * @param func 用户函数,负责创建异常对象 + * @param 异常类型 + * @return 返回实例,以便于继续进行链式操作 + * @throws Ex 断言失败时抛出 + */ + public RetOps assertDataNotEmpty(Function, ? extends Ex> func) throws Ex { + if (ObjectUtil.isNotEmpty(original.getData())) { + throw func.apply(original); + } + return this; + } + + /** + * 对业务数据(data)转换 + * @param mapper 业务数据转换函数 + * @param 数据类型 + * @return 返回新实例,以便于继续进行链式操作 + */ + public RetOps map(Function mapper) { + R result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg()); + return of(result); + } + + /** + * 对业务数据(data)转换 + * @param predicate 断言函数 + * @param mapper 业务数据转换函数 + * @param 数据类型 + * @return 返回新实例,以便于继续进行链式操作 + * @see RetOps#CODE_SUCCESS + * @see RetOps#HAS_DATA + * @see RetOps#HAS_ELEMENT + * @see RetOps#DATA_AVAILABLE + */ + public RetOps mapIf(Predicate> predicate, Function mapper) { + R result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg()); + return of(result); + } + + // ~ 数据消费 + // =================================================================================================== + + /** + * 消费数据,注意此方法保证数据可用 + * @param consumer 消费函数 + */ + public void useData(Consumer consumer) { + consumer.accept(original.getData()); + } + + /** + * 条件消费(错误代码匹配某个值) + * @param consumer 消费函数 + * @param codes 错误代码集合,匹配任意一个则调用消费函数 + */ + public void useDataOnCode(Consumer consumer, int... codes) { + useDataIf(o -> Arrays.stream(codes).filter(c -> original.getCode() == c).findFirst().isPresent(), consumer); + } + + /** + * 条件消费(错误代码表示成功) + * @param consumer 消费函数 + */ + public void useDataIfSuccess(Consumer consumer) { + useDataIf(CODE_SUCCESS, consumer); + } + + /** + * 条件消费 + * @param predicate 断言函数 + * @param consumer 消费函数,断言函数返回{@code true}时被调用 + * @see RetOps#CODE_SUCCESS + * @see RetOps#HAS_DATA + * @see RetOps#HAS_ELEMENT + * @see RetOps#DATA_AVAILABLE + */ + public void useDataIf(Predicate> predicate, Consumer consumer) { + if (predicate.test(original)) { + consumer.accept(original.getData()); + } + } + +} diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigRemoteRegisteredClientRepository.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigRemoteRegisteredClientRepository.java index 4fe9e3a9..10d1e9b6 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigRemoteRegisteredClientRepository.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/service/PigRemoteRegisteredClientRepository.java @@ -5,7 +5,8 @@ import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails; import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; import com.pig4cloud.pig.common.core.constant.CacheConstants; import com.pig4cloud.pig.common.core.constant.SecurityConstants; -import com.pig4cloud.pig.common.core.util.R; +import com.pig4cloud.pig.common.core.util.RetOps; +import com.pig4cloud.pig.common.security.util.OAuthClientException; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.cache.annotation.Cacheable; @@ -82,9 +83,10 @@ public class PigRemoteRegisteredClientRepository implements RegisteredClientRepo @SneakyThrows @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null") public RegisteredClient findByClientId(String clientId) { - R detailsR = clientDetailsService.getClientDetailsById(clientId, - SecurityConstants.FROM_IN); - SysOauthClientDetails clientDetails = detailsR.getData(); + + SysOauthClientDetails clientDetails = RetOps + .of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)) + .assertDataNotNull(result -> new OAuthClientException("clientId 不合法")).getData().get(); RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId()) .clientId(clientDetails.getClientId()) diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuth2ErrorCodesExpand.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuth2ErrorCodesExpand.java index 69882d18..02065137 100644 --- a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuth2ErrorCodesExpand.java +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuth2ErrorCodesExpand.java @@ -35,4 +35,9 @@ public interface OAuth2ErrorCodesExpand { /** 未知的登录异常 */ String UN_KNOW_LOGIN_ERROR = "un_know_login_error"; + /** + * 不合法的Token + */ + String INVALID_BEARER_TOKEN = "invalid_bearer_token"; + } diff --git a/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuthClientException.java b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuthClientException.java new file mode 100644 index 00000000..ab7c8ddd --- /dev/null +++ b/pig-common/pig-common-security/src/main/java/com/pig4cloud/pig/common/security/util/OAuthClientException.java @@ -0,0 +1,29 @@ +package com.pig4cloud.pig.common.security.util; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; + +/** + * @author lengleng + * @description OAuthClientException 异常信息 + */ +public class OAuthClientException extends OAuth2AuthenticationException { + + /** + * Constructs a ScopeException with the specified message. + * @param msg the detail message. + */ + public OAuthClientException(String msg) { + super(new OAuth2Error(msg), msg); + } + + /** + * Constructs a {@code ScopeException} with the specified message and root cause. + * @param msg the detail message. + * @param cause root cause + */ + public OAuthClientException(String msg, Throwable cause) { + super(new OAuth2Error(msg), cause); + } + +}