Адрес фактического центра электронной коммерции SpringBoot (35k+star):GitHub.com/macro-positive/…
Резюме
существует«Идеальное решение для разрешений микросервисов, Spring Cloud Gateway + Oauth2 реализует унифицированную аутентификацию и аутентификацию! 》В этой статье мы рассказали об использовании Oauth2 в микросервисах, но не настроили результаты обработки Oauth2 по умолчанию. Иногда мы действительно надеемся, что аутентификация и авторизация в Oauth2 могут вернуть результат в указанном нами формате, например, результат аутентификации при входе в систему, результат сбоя аутентификации шлюза и так далее. В этой статье будет подробно представлена схема кастомной обработки результатов в Oauth2, надеюсь она будет всем полезна!
какая проблема
Настройка результатов обработки OAUTH2 в основном для унифицирования формата возвращенной информации от интерфейса, начиная с следующих аспектов.
- Настраиваемый результат проверки подлинности при входе в систему OAUTH2 и результат отказа;
- Срок действия токена JWT истек, или подпись неверна, и аутентификация шлюза не возвращает результат;
- При доступе к интерфейсу белого списка с просроченным или неправильно подписанным токеном JWT шлюзу не удается пройти прямую аутентификацию.
Настроить результаты аутентификации при входе
Аутентификация прошла успешно и результат возвращен
- Давайте сначала посмотрим на результат возврата по умолчанию и получим доступ к интерфейсу аутентификации входа Oauth2:http://localhost:9201/auth/oauth/token
- То, что мы использовали раньше, — это унифицированный общий возвращаемый результат.
CommonResult, результат Oauth2 явно противоречив, и его необходимо унифицировать Общий формат возвращаемого результата следующий:
/**
* 通用返回对象
* Created by macro on 2019/4/19.
*/
public class CommonResult<T> {
private long code;
private String message;
private T data;
}
- Фактически, мы можем настроить интерфейс аутентификации при входе в систему Oauth2, если найдем ключевой класс, который
org.springframework.security.oauth2.provider.endpoint.TokenEndpoint, который определяет хорошо знакомый нам интерфейс аутентификации при входе. Нам нужно только переписать интерфейс аутентификации при входе, напрямую вызвать логику реализации по умолчанию, а затем обработать результат, возвращаемый по умолчанию. Ниже приведена логика реализации по умолчанию;
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
}
- Мы инкапсулируем необходимую информацию JWT в объект, а затем помещаем ее в наш общий возвращаемый результат.
dataатрибут;
/**
* Oauth2获取Token返回信息封装
* Created by macro on 2020/7/17.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class Oauth2TokenDto {
/**
* 访问令牌
*/
private String token;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 访问令牌头前缀
*/
private String tokenHead;
/**
* 有效时间(秒)
*/
private int expiresIn;
}
- Создавать
AuthController, настроить интерфейс аутентификации входа Oauth2 по умолчанию;
/**
* 自定义Oauth2获取令牌接口
* Created by macro on 2020/7/17.
*/
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private TokenEndpoint tokenEndpoint;
/**
* Oauth2登录认证
*/
@RequestMapping(value = "/token", method = RequestMethod.POST)
public CommonResult<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
.token(oAuth2AccessToken.getValue())
.refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
.expiresIn(oAuth2AccessToken.getExpiresIn())
.tokenHead("Bearer ").build();
return CommonResult.success(oauth2TokenDto);
}
}
- Вызов интерфейса аутентификации входа снова, мы можем обнаружить, что возвращенный результат стал форматом, который соответствует нашему результату возврата!
Ошибка аутентификации возвращает результат
- Результаты успешной аутентификации унифицированы, и мы должны унифицировать результаты неудачной аутентификации Давайте сначала посмотрим на результаты исходной неудачной аутентификации;
- Если мы более внимательно посмотрим на реализацию аутентификации входа по умолчанию, мы обнаружим, что многие операции, которые не прошли аутентификацию, будут запущены напрямую.
OAuth2Exceptionисключение, дляControllerВыброшенное исключение, мы можем использовать@ControllerAdviceаннотация для глобальной обработки;
/**
* 全局处理Oauth2抛出的异常
* Created by macro on 2020/7/17.
*/
@ControllerAdvice
public class Oauth2ExceptionHandler {
@ResponseBody
@ExceptionHandler(value = OAuth2Exception.class)
public CommonResult handleOauth2(OAuth2Exception e) {
return CommonResult.failed(e.getMessage());
}
}
- Когда мы вводим неверный пароль и снова вызываем интерфейс аутентификации при входе, мы обнаруживаем, что результат неудачной аутентификации также унифицирован.
Результат неудачной аутентификации пользовательского шлюза
- Когда мы используем просроченный или неправильно подписанный токен JWT для доступа к интерфейсу, требующему разрешений, код состояния будет возвращен напрямую.
401;
- Этот возвращаемый результат не соответствует нашему общему формату результатов. Фактически, мы хотим вернуть код состояния как
200, а затем вернуть следующую информацию о формате;
{
"code": 401,
"data": "Jwt expired at 2020-07-10T08:38:40Z",
"message": "暂未登录或token已经过期"
}
- Вот очень простая модификация, просто добавьте строку кода, чтобы изменить конфигурацию безопасности шлюза.
ResourceServerConfig, установите сервер ресурсовServerAuthenticationEntryPointТы сможешь;
/**
* 资源服务器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果(新添加的)
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
}
- После того, как добавление будет завершено, снова получите доступ к интерфейсу, требующему разрешения, и результат, который мы хотим, будет возвращен.
Совместимый интерфейс белого списка
- На самом деле, всегда была проблема с интерфейсом белого списка.При доступе с просроченным или неправильно подписанным токеном JWT результат истечения срока действия токена будет возвращен напрямую.Мы можем посетить интерфейс аутентификации входа и попробовать его;
- Очевидно, что это интерфейс белого списка, но доступ к переносимому токену запрещен, что, очевидно, немного неразумно. Как это решить, давайте сначала посмотрим, как получить доступ без токена;
- Фактически, нам нужно только добавить фильтр перед фильтром проверки подлинности по умолчанию Oauth2.Если это интерфейс белого списка, мы можем напрямую удалить заголовок проверки подлинности.Во-первых, определите наш фильтр;
/**
* 白名单路径访问时需要移除JWT请求头
* Created by macro on 2020/7/24.
*/
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
//白名单路径移除JWT请求头
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
request = exchange.getRequest().mutate().header("Authorization", "").build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);
}
}
return chain.filter(exchange);
}
}
- Затем настройте этот фильтр перед фильтром проверки подлинности по умолчанию и настройте его в ResourceServerConfig;
/**
* 资源服务器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径,直接移除JWT请求头(新添加的)
http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
.anyExchange().access(authorizationManager)//鉴权管理器配置
.and().exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
.and().csrf().disable();
return http.build();
}
}
- Зайдите еще раз с просроченным заголовком запроса и обнаружите, что к нему можно получить доступ в обычном режиме.
Суммировать
На данный момент использование Oauth2 в микросервисах для реализации унифицированной аутентификации и решений для аутентификации, наконец, завершено!