Микросервисы SpringCloud Alibaba на практике 14 — SpringCloud Gateway интегрирует Oauth2.0

Java

Введение: в последней статье мы извлекли отдельную службу аутентификации.Основным содержанием этой главы является интеграция Oauth2 с Spring Cloud Gateway.

Раздел концепции

file

После того, как шлюз интегрирует Oauth2.0, наша архитектура процесса будет такой, как указано выше. Основная логика следующая:
1. Клиентское приложение запрашивает у сервера аутентификации получение access_token через API-шлюз.http://localhost:8090/auth-service/oauth/token
2. Сервер аутентификации возвращает access_token

{
  "access_token": "f938d0c1-9633-460d-acdd-f0693a6b5f4c",
  "token_type": "bearer",
  "refresh_token": "4baea735-3c0d-4dfd-b826-91c6772a0962",
  "expires_in": 43199,
  "scope": "web"
}

3. Клиент имеет access_token для доступа к серверной службе через шлюз API.

file

4. После того, как API-шлюз получит access_token, он передаетAuthenticationWebFilterАвторизоваться на access_token

5. Шлюз API перенаправляет внутренний запрос, а внутренняя служба запрашивает сервер аутентификации Oauth2 для получения текущего пользователя.

В предыдущей статье мы построили отдельный сервис аутентификации и авторизации Oauth2 и реализовали базовую функциональную структуру, на этот раз мы в основном реализуем четвертый пункт, как выполнять фильтрацию и проверку access_token после того, как SpringCloud интегрирует Oauth2.

пример кода

импортные компоненты

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

В основном вводится пакет jar, связанный с oauth2, а также необходимо представить пакет jar, связанный с базой данных, поскольку наш токен хранится в базе данных, чтобы проверить действительность токена на уровне шлюза, токен должен быть извлекается из базы данных в первую очередь.

модификация конфигурации bootstrap.yml

spring:
  application:
    name: cloud-gateway
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://xx.0.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: xxxxx
    password: xxxxxxx
    driver-class-name: com.mysql.jdbc.Driver

В основном настройте адрес подключения к базе данных oauth2

Пользовательский класс управления интерфейсом проверки подлинности

В среде webFlux путем реализацииReactiveAuthenticationManagerИнтерфейс Пользовательское управление интерфейсом аутентификации, потому что наш токен хранится в jdbc, поэтому имя называетсяReactiveJdbcAuthenticationManager

@Slf4j
public class ReactiveJdbcAuthenticationManager implements ReactiveAuthenticationManager {

    private TokenStore tokenStore;

    public JdbcAuthenticationManager(TokenStore tokenStore){
        this.tokenStore = tokenStore;
    }

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.justOrEmpty(authentication)
                .filter(a -> a instanceof BearerTokenAuthenticationToken)
                .cast(BearerTokenAuthenticationToken.class)
                .map(BearerTokenAuthenticationToken::getToken)
                .flatMap((accessToken ->{
                    log.info("accessToken is :{}",accessToken);
                    OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
                    //根据access_token从数据库获取不到OAuth2AccessToken
                    if(oAuth2AccessToken == null){
                        return Mono.error(new InvalidTokenException("invalid access token,please check"));
                    }else if(oAuth2AccessToken.isExpired()){
                        return Mono.error(new InvalidTokenException("access token has expired,please reacquire token"));
                    }

                    OAuth2Authentication oAuth2Authentication =this.tokenStore.readAuthentication(accessToken);
                    if(oAuth2Authentication == null){
                        return Mono.error(new InvalidTokenException("Access Token 无效!"));
                    }else {
                        return Mono.just(oAuth2Authentication);
                    }
                })).cast(Authentication.class);
    }
}

Конфигурация безопасности уровня шлюза

@Configuration
public class SecurityConfig {
    private static final String MAX_AGE = "18000L";
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AccessManager accessManager;

    /**
     * 跨域配置
     */
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                HttpHeaders requestHeaders = request.getHeaders();
                ServerHttpResponse response = ctx.getResponse();
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
                HttpHeaders headers = response.getHeaders();
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }

    @Bean
    SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
        //token管理器
        ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJdbcAuthenticationManager(new JdbcTokenStore(dataSource));
        //认证过滤器
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

        http
                .httpBasic().disable()
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange().access(accessManager)
                .and()
                // 跨域过滤器
                .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
                //oauth2认证过滤器
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}

Этот класс является ключом к интеграции SpringClug Gateway с Oauth2 путем создания фильтров аутентификации.AuthenticationWebFilterЗавершите проверку токена Oauth2.0.AuthenticationWebFilterчерез наш обычайReactiveJdbcAuthenticationManagerЗавершите проверку токена. Мы также добавили сюдаCORSФильтры и диспетчер разрешенийAccessManager

Менеджер прав

@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private Set<String> permitAll = new ConcurrentHashSet<>();
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();


    public AccessManager (){
        permitAll.add("/");
        permitAll.add("/error");
        permitAll.add("/favicon.ico");
        permitAll.add("/**/v2/api-docs/**");
        permitAll.add("/**/swagger-resources/**");
        permitAll.add("/webjars/**");
        permitAll.add("/doc.html");
        permitAll.add("/swagger-ui.html");
        permitAll.add("/**/oauth/**");
        permitAll.add("/**/current/get");
    }

    /**
     * 实现权限验证判断
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        //请求资源
        String requestPath = exchange.getRequest().getURI().getPath();
        // 是否直接放行
        if (permitAll(requestPath)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        return authenticationMono.map(auth -> {
            return new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath));
        }).defaultIfEmpty(new AuthorizationDecision(false));

    }

    /**
     * 校验是否属于静态资源
     * @param requestPath 请求路径
     * @return
     */
    private boolean permitAll(String requestPath) {
        return permitAll.stream()
                .filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent();
    }

    //权限校验
    private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
        if(auth instanceof OAuth2Authentication){
            OAuth2Authentication athentication = (OAuth2Authentication) auth;
            String clientId = athentication.getOAuth2Request().getClientId();
            log.info("clientId is {}",clientId);
        }

        Object principal = auth.getPrincipal();
        log.info("用户信息:{}",principal.toString());
        return true;
    }
}

Основное предназначение — отфильтровать статические ресурсы, в дальнейшем сюда же можно будет вынести некоторые проверки разрешений интерфейса.

контрольная работа

  • Вызов службы авторизации через шлюз для получения access_token

    file

  • Добавить аутентификацию для доступа к серверным службам в заголовке

    file

  • Фильтр шлюза для проверки токена

    file

  • Проверка диспетчера разрешений

    file

  • Перейдите на сервер аутентификации, чтобы проверить текущего пользователя.

    file

  • вернуть нормальный результат

    file

  • Умышленно написать неверный access_token, вернуть ответ об ошибке

    file

  • Удалите access_token из заголовка запроса и верните напрямую401 Unauthorized

    file

Суммировать

Выполнив описанные выше шаги, мы интегрировали Spring Cloud Gateway с Oauth2.0, так что весь наш проект в основном завершен, и мы оптимизируем его на следующих нескольких этапах Добро пожаловать, продолжайте обращать внимание.

file

Серия статей SpringCloud Alibaba