优化。 优化SAS 客户端认证过程中的异常输出

This commit is contained in:
lbw 2022-06-18 23:56:13 +08:00
parent 825403ee3a
commit 812a6016ed
8 changed files with 349 additions and 25 deletions

View File

@ -69,8 +69,10 @@ public class AuthorizationServerConfiguration {
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(new PigAuthenticationSuccessEventHandler()) // 登录成功处理器 .accessTokenResponseHandler(new PigAuthenticationSuccessEventHandler()) // 登录成功处理器
.errorResponseHandler(new PigAuthenticationFailureEventHandler());// 登录失败处理器 .errorResponseHandler(new PigAuthenticationFailureEventHandler());// 登录失败处理器
}).authorizationEndpoint( // 授权码端点个性化confirm页面 }).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
authorizationEndpoint -> authorizationEndpoint.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI))); oAuth2ClientAuthenticationConfigurer.errorResponseHandler(new PigAuthenticationFailureEventHandler()))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
DefaultSecurityFilterChain securityFilterChain = http.requestMatcher(endpointsMatcher) DefaultSecurityFilterChain securityFilterChain = http.requestMatcher(endpointsMatcher)

View File

@ -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.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pig.admin.api.vo.TokenVo; 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.CacheConstants;
import com.pig4cloud.pig.common.core.constant.CommonConstants; import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants; 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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken; 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.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 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.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.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; 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.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
@ -75,7 +77,7 @@ public class PigTokenEndpoint {
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); private final AuthenticationFailureHandler authenticationFailureHandler = new PigAuthenticationFailureEventHandler();
private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationService authorizationService;
@ -135,21 +137,20 @@ public class PigTokenEndpoint {
*/ */
@SneakyThrows @SneakyThrows
@GetMapping("/check_token") @GetMapping("/check_token")
public void checkToken(String token, HttpServletResponse response) { public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
if (StrUtil.isBlank(token)) { if (StrUtil.isBlank(token)) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
this.errorHttpResponseConverter.write(new OAuth2Error(OAuth2ErrorCodesExpand.TOKEN_MISSING), null, this.authenticationFailureHandler.onAuthenticationFailure(request, response,
httpResponse); new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
} }
OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); OAuth2Authorization authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
// 如果令牌不存在 返回401 // 如果令牌不存在 返回401
if (authorization == null) { if (authorization == null || authorization.getAccessToken() == null) {
httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); this.authenticationFailureHandler.onAuthenticationFailure(request, response,
this.errorHttpResponseConverter.write(new OAuth2Error(OAuth2ErrorCodesExpand.TOKEN_MISSING), null, new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
httpResponse);
} }
Map<String, Object> claims = authorization.getAccessToken().getClaims(); Map<String, Object> claims = authorization.getAccessToken().getClaims();

View File

@ -29,8 +29,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.AuthenticationException; 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.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@ -73,15 +71,13 @@ public class PigAuthenticationFailureEventHandler implements AuthenticationFailu
logVo.setUpdateBy(username); logVo.setUpdateBy(username);
SpringContextHolder.publishEvent(new SysLogEvent(logVo)); SpringContextHolder.publishEvent(new SysLogEvent(logVo));
// 写出错误信息 // 写出错误信息
sendErrorResponse(request, response, exception); sendErrorResponse(response, exception);
} }
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, private void sendErrorResponse(HttpServletResponse response, AuthenticationException exception) throws IOException {
AuthenticationException exception) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
this.errorHttpResponseConverter.write(R.failed(error.getDescription()), MediaType.APPLICATION_JSON, this.errorHttpResponseConverter.write(R.failed(exception.getLocalizedMessage()), MediaType.APPLICATION_JSON,
httpResponse); httpResponse);
} }

View File

@ -76,7 +76,7 @@ public class R<T> implements Serializable {
return restResult(data, CommonConstants.FAIL, msg); return restResult(data, CommonConstants.FAIL, msg);
} }
private static <T> R<T> restResult(T data, int code, String msg) { public static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>(); R<T> apiResult = new R<>();
apiResult.setCode(code); apiResult.setCode(code);
apiResult.setData(data); apiResult.setData(data);

View File

@ -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<T>} 的访问操作,例子 <pre>
* R<Integer> 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<String> s = RetOps.of(result)
* .assertDataNotNull(r -> new IllegalStateException("nani??"))
* .map(i -> Integer.toHexString(i))
* .peek();
* </pre>
*
* @author CJ (power4j@outlook.com)
* @date 2022/5/12
* @since 4.4
*/
public class RetOps<T> {
/** 状态码为成功 */
public static final Predicate<R<?>> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getCode();
/** 数据有值 */
public static final Predicate<R<?>> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData());
/** 数据有值,并且包含元素 */
public static final Predicate<R<?>> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData());
/** 状态码为成功并且有值 */
public static final Predicate<R<?>> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA);
private final R<T> original;
// ~ 初始化
// ===================================================================================================
RetOps(R<T> original) {
this.original = original;
}
public static <T> RetOps<T> of(R<T> original) {
return new RetOps<>(Objects.requireNonNull(original));
}
// ~ 杂项方法
// ===================================================================================================
/**
* 观察原始值
* @return R
*/
public R<T> peek() {
return original;
}
/**
* 读取{@code code}的值
* @return 返回code的值
*/
public int getCode() {
return original.getCode();
}
/**
* 读取{@code data}的值
* @return 返回 Optional 包装的data
*/
public Optional<T> getData() {
return Optional.of(original.getData());
}
/**
* 有条件地读取{@code data}的值
* @param predicate 断言函数
* @return 返回 Optional 包装的data,如果断言失败返回empty
*/
public Optional<T> getDataIf(Predicate<? super R<?>> predicate) {
return predicate.test(original) ? getData() : Optional.empty();
}
/**
* 读取{@code msg}的值
* @return 返回Optional包装的 msg
*/
public Optional<String> 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 <Ex> 异常类型
* @return 返回实例以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertCode(int expect, Function<? super R<T>, ? extends Ex> func)
throws Ex {
if (codeNotEquals(expect)) {
throw func.apply(original);
}
return this;
}
/**
* 断言成功
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertSuccess(Function<? super R<T>, ? extends Ex> func) throws Ex {
return assertCode(CommonConstants.SUCCESS, func);
}
/**
* 断言业务数据有值
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertDataNotNull(Function<? super R<T>, ? extends Ex> func) throws Ex {
if (Objects.isNull(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 断言业务数据有值,并且包含元素
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertDataNotEmpty(Function<? super R<T>, ? extends Ex> func) throws Ex {
if (ObjectUtil.isNotEmpty(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 对业务数据(data)转换
* @param mapper 业务数据转换函数
* @param <U> 数据类型
* @return 返回新实例以便于继续进行链式操作
*/
public <U> RetOps<U> map(Function<? super T, ? extends U> mapper) {
R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
/**
* 对业务数据(data)转换
* @param predicate 断言函数
* @param mapper 业务数据转换函数
* @param <U> 数据类型
* @return 返回新实例以便于继续进行链式操作
* @see RetOps#CODE_SUCCESS
* @see RetOps#HAS_DATA
* @see RetOps#HAS_ELEMENT
* @see RetOps#DATA_AVAILABLE
*/
public <U> RetOps<U> mapIf(Predicate<? super R<T>> predicate, Function<? super T, ? extends U> mapper) {
R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
// ~ 数据消费
// ===================================================================================================
/**
* 消费数据,注意此方法保证数据可用
* @param consumer 消费函数
*/
public void useData(Consumer<? super T> consumer) {
consumer.accept(original.getData());
}
/**
* 条件消费(错误代码匹配某个值)
* @param consumer 消费函数
* @param codes 错误代码集合,匹配任意一个则调用消费函数
*/
public void useDataOnCode(Consumer<? super T> consumer, int... codes) {
useDataIf(o -> Arrays.stream(codes).filter(c -> original.getCode() == c).findFirst().isPresent(), consumer);
}
/**
* 条件消费(错误代码表示成功)
* @param consumer 消费函数
*/
public void useDataIfSuccess(Consumer<? super T> 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<? super R<T>> predicate, Consumer<? super T> consumer) {
if (predicate.test(original)) {
consumer.accept(original.getData());
}
}
}

View File

@ -5,7 +5,8 @@ import com.pig4cloud.pig.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService; import com.pig4cloud.pig.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pig.common.core.constant.CacheConstants; import com.pig4cloud.pig.common.core.constant.CacheConstants;
import com.pig4cloud.pig.common.core.constant.SecurityConstants; 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.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
@ -82,9 +83,10 @@ public class PigRemoteRegisteredClientRepository implements RegisteredClientRepo
@SneakyThrows @SneakyThrows
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null") @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
public RegisteredClient findByClientId(String clientId) { public RegisteredClient findByClientId(String clientId) {
R<SysOauthClientDetails> detailsR = clientDetailsService.getClientDetailsById(clientId,
SecurityConstants.FROM_IN); SysOauthClientDetails clientDetails = RetOps
SysOauthClientDetails clientDetails = detailsR.getData(); .of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN))
.assertDataNotNull(result -> new OAuthClientException("clientId 不合法")).getData().get();
RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId()) RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId())
.clientId(clientDetails.getClientId()) .clientId(clientDetails.getClientId())

View File

@ -35,4 +35,9 @@ public interface OAuth2ErrorCodesExpand {
/** 未知的登录异常 */ /** 未知的登录异常 */
String UN_KNOW_LOGIN_ERROR = "un_know_login_error"; String UN_KNOW_LOGIN_ERROR = "un_know_login_error";
/**
* 不合法的Token
*/
String INVALID_BEARER_TOKEN = "invalid_bearer_token";
} }

View File

@ -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 <code>ScopeException</code> 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);
}
}