springcloud + nacos微服務(四)--網關服務token認證

微服務要求實現身份認證,  不可能在每個服務中做認證 

既然所有的服務都走網關, 那麼把認證放在網關中, 將用戶信息放在請求中, 後面的服務可以從中獲取用戶信息, 以便進行數據權限操作

具體看代碼:

https://gitee.com/gongwei_3/spring-cloud-gateway-token

思路:

1, 過濾所有的請求, 如果是login, 那麼從請求中獲取username, 然後放行, 驗證微服務驗證用戶名和密碼後, 攔截響應體, 如果驗證通過, 那麼通過username生成token, 修改響應體, 把token放到響應體中, 這裏要求驗證接口是post,並且字段是username和password, 響應體也要統一

2, 如果是白名單,直接放行,  如果是其他接口, 那麼獲取token, 並驗證(包括刷新token), 獲取token的用戶名, 將用戶名放在請求頭中, 刷新後的token放在響應體中

 

實現

經過查spring文檔 https://www.springcloud.cc/

參考: https://blog.51cto.com/thinklili/2329184?cid=725051

發現了一個預言類,ReadBodyPredicateFactory ,發現裏面緩存了request body的信息,於是在自定義router中配置了ReadBodyPredicateFactory,然後在filter中通過cachedRequestBodyObject緩存字段獲取request body信息,這種解決,一不會帶來重複讀取問題,二不會帶來requestbody取不全問題。三在低版本的Spring Cloud Finchley.SR2也可以運行。

 

ReadRequestBodyRouterConfig配置路由表,配置了ReadBodyPredicateFactory
package com.gw.nacosgatewaydemo1.config;

import com.gw.nacosgatewaydemo1.filter.AuthFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

/**
 * 從登錄請求中獲取請求body的登錄數據
 */
@EnableAutoConfiguration
@Configuration
public class ReadRequestBodyRouterConfig {

    private static final Logger log = LoggerFactory.getLogger(ReadRequestBodyRouterConfig.class);

    @Autowired
    private AuthFilter authFilter;

    private static final String SERVICE = AuthFilter.LOGIN_URL_PATTERN;
    private static final String URI = "lb://nacos-demo1";

    @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder) {

        /*
        route2 是post請求,Content-Type是application/x-www-form-urlencoded,readbody爲String.class
        route3 是post請求,Content-Type是application/json,readbody爲Object.class
         */
        RouteLocatorBuilder.Builder routes = builder.routes();
        RouteLocatorBuilder.Builder serviceProvider = routes
                .route("route2",
                        r -> r
                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                                .and()
                                .method(HttpMethod.POST)
                                .and()
                                .readBody(String.class, readBody -> {
                                    log.info("request method POST, Content-Type is application/x-www-form-urlencoded, body  is:{}", readBody);
                                    return true;
                                })
                                .and()
                                .path(SERVICE)
                                .filters(f -> {
                                    f.filter(authFilter);
                                    return f;
                                })
                                .uri(URI))
                .route("route3",
                        r -> r
                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                                .and()
                                .method(HttpMethod.POST)
                                .and()
                                .readBody(Object.class, readBody -> {
                                    log.info("request method POST, Content-Type is application/json, body  is:{}", readBody);
                                    return true;
                                })
                                .and()
                                .path(SERVICE)
                                .filters(f -> {
                                    f.filter(authFilter);
                                    return f;
                                })
                                .uri(URI));
        RouteLocator routeLocator = serviceProvider.build();
        log.info("custom RouteLocator is loading ... {}", routeLocator);
        return routeLocator;
    }
}

 

全局認證過濾器,

package com.gw.nacosgatewaydemo1.filter;

import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.gw.nacosgatewaydemo1.util.JwtUtil;
import com.gw.nacosgatewaydemo1.util.SerializeUtil;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * 身份認證
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered, GatewayFilter {

    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    /**
     * 緩存的請求body
     */
    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    public static final String LOGIN_USERNAME_KEY = "username";

    public static final String LOGIN_URL_PATTERN = ".*/login";

    private static final Set<String> ignore = new HashSet<>();

    static {
        ignore.add(".*/swagger-resources.*");
        ignore.add(".*/configuration.*");
        ignore.add(".*/v2/api-docs.*");
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        String url = exchange.getRequest().getURI().getPath();
        log.debug(url);

        //登錄
        if (isLogin(url)) {
            return doLogin(exchange, chain);
        }

        //跳過不需要驗證的路徑
        if (isIgnore(url)) {
            return chain.filter(exchange);
        }

        //需要驗證token
        Map claims = JwtUtil.validToken(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
        if (claims == null) {
            //token驗證不通過
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        } else {
            //刷新token
            String username = (String) claims.get(LOGIN_USERNAME_KEY);
            String token = (String) claims.get(HttpHeaders.AUTHORIZATION);
            //將登錄用戶的信息存在請求頭中,提供給後面其他服務
            ServerHttpRequest exchangeRequest = exchange.getRequest().mutate().header(LOGIN_USERNAME_KEY, username).build();
            ServerWebExchange build = exchange.mutate().request(exchangeRequest).build();

            exchange.getResponse().getHeaders().add(HttpHeaders.AUTHORIZATION, token);

            return chain.filter(build);
        }
    }

    private Mono<Void> doLogin(ServerWebExchange exchange, GatewayFilterChain chain) {

        Object loginData;

        ServerHttpRequest exchangeRequest = exchange.getRequest();
        String method = exchangeRequest.getMethodValue();
        if ("POST".equals(method)) {
            //從login請求中獲取用戶信息,要求login接口是post,入參是body,字段名字段名是username和password, 返回統一的json數據,
            loginData = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
            //暫時不支持get請求的登錄
//        } else if ("GET".equals(method)) {
//            loginData = exchangeRequest.getQueryParams();
        } else {
            exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
            return exchange.getResponse().setComplete();
        }

        //從登錄請求頭獲取用戶名
        Map map = SerializeUtil.toObject(loginData.toString(), Map.class);
        String username = (String) map.get(LOGIN_USERNAME_KEY);

        //根據用戶名生成token
        String token = JwtUtil.genToken(username);

        //去用戶服務驗證用戶名和密碼正確後,修改返回體,並將token寫進返回體中
        ServerHttpResponse originalResponse = exchange.getResponse();
        DataBufferFactory bufferFactory = originalResponse.bufferFactory();
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (HttpStatus.OK.equals(getStatusCode()) && body instanceof Flux) {
                    // 獲取ContentType,判斷是否返回JSON格式數據
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
                    if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) {
                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {//解決返回體分段傳輸
                            List<String> list = Lists.newArrayList();
                            dataBuffers.forEach(dataBuffer -> {
                                try {
                                    byte[] content = new byte[dataBuffer.readableByteCount()];
                                    dataBuffer.read(content);
                                    DataBufferUtils.release(dataBuffer);

                                    list.add(new String(content, StandardCharsets.UTF_8));
                                } catch (Exception e) {
                                    log.info("動態加載API加密規則失敗,失敗原因:{}", Throwables.getStackTraceAsString(e));
                                }
                            });
                            Joiner joiner = Joiner.on("");
                            String responseData = joiner.join(list);

                            Map map = SerializeUtil.toObject(responseData, Map.class);
                            //todo 這裏只是判斷了http的狀態, 還要根據用戶驗證接口去判斷是否登錄成功, 具體和登陸接口保持一致
                            //可能密碼不對, 但是返回的是200
                            map.put("data", token);
                            String res = SerializeUtil.toJson(map);

                            originalResponse.getHeaders().setContentLength(res.length());
                            return bufferFactory.wrap(res.getBytes(StandardCharsets.UTF_8));
                        }));
                    }
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }


    @Override
    public int getOrder() {
        return -100;
    }

    private boolean isIgnore(String url) {
        for (String urlPatter : ignore) {
            return Pattern.matches(urlPatter, url);
        }
        return false;
    }

    private boolean isLogin(String url) {
        return Pattern.matches(LOGIN_URL_PATTERN, url);
    }


}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章