Источник на гриле давно я наконец нашел лучших результатов обработки пользовательских обработок OAUTH2!

Java Spring Cloud
Источник на гриле давно я наконец нашел лучших результатов обработки пользовательских обработок OAUTH2!

Адрес фактического центра электронной коммерции 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 в микросервисах для реализации унифицированной аутентификации и решений для аутентификации, наконец, завершено!

Адрес исходного кода проекта

GitHub.com/macro-positive/…