基于微服务网关gateway的token认证机制、限流熔断

引入maven依赖

         
            org.projectlombok
            lombok
        
        
            io.jsonwebtoken
            jjwt
            0.9.0
            compile
        

JwtTokenFilter.java

实现拦截器、判断url地址信息以及权限信息



/**
 * @Author Lxq
 * @Date 2020/4/28 16:58
 * @Version 1.0
 * 描述: JwtToken 过滤器
 */
@Component
//读取 yml 文件下的 com.gpdi.jwt
@ConfigurationProperties("com.gpdi.jwt")
@Setter
@Getter
@Slf4j
public class JwtTokenFilter implements GlobalFilter, Ordered {

    private String[] skipAuthUrls;


    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        //跳过不需要验证的路径
        if (null != skipAuthUrls && Arrays.asList(skipAuthUrls).contains(url)) {
            return chain.filter(exchange);
        }
        //获取token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        ServerHttpResponse resp = exchange.getResponse();
        if (StringUtils.isBlank(token)) {
            // 没有token
            return authErro(resp, "请登陆!");
        } else {
            //有token
            try {
                JwtUtil.checkToken(token);
                return chain.filter(exchange);
            } catch (ExpiredJwtException e) {
                log.error(e.getMessage(), e);
                if (e.getMessage().contains("Allowed clock skew")) {
                    return authErro(resp, "认证过期");
                } else {
                    return authErro(resp, "认证失败");
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                return authErro(resp, "认证失败");
            }
        }
    }

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

    /**
     * 认证错误返回的消息
     *
     * @param resp
     * @param mess
     * @return
     */
    private Mono authErro(ServerHttpResponse resp, String mess) {
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        ReturnData returnData = new ReturnData<>(cn.hutool.http.HttpStatus.HTTP_UNAUTHORIZED, mess, mess);

        String returnStr = "";
        if (returnData != null){
            returnStr = JSONObject.toJSONString(returnData);
        }
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));

    }
}
JwtUtil.java
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * @description:
 * @author: Lxq
 * @date: 2020/5/20 8:34
 * 描述: jwt 工具类
 */
@Slf4j
public class JwtUtil {

    /**
     * 密钥
     */
    private static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c";


    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.decodeBase64(KEY);
        SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * @param id        是JWT的唯一标识
     * @param object    jwt签发对象
     * @param subject   代表这个JWT的主体
     * @param ttlMillis 设置过期时间
     * @return
     * @throws Exception
     */
    public static String createJWT(Integer id, Object object, String subject, long ttlMillis) {

        // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map claims = new HashMap<>();
        JSONObject jsonObject = (JSONObject) object;
        claims.put("user", jsonObject);
        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
        // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        SecretKey key = generalKey();
        // 下面就是在为payload添加各种标准声明和私有声明了
        // 这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(String.valueOf(id))
                // iat: jwt的签发时间
                .setIssuedAt(now)
                // userName:jwt签发人
                .setIssuer(jsonObject.getString("username"))
                // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志
                .setSubject(subject)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, key);

        // 设置过期时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) {
        //签名秘钥,和生成的签名的秘钥一模一样
        SecretKey key = generalKey();
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(jwt).getBody();
        return claims;
    }

    /**
     * 检查token
     *
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        Claims claims = JwtUtil.parseJWT(jwtToken);
        // 角色id|权限id|权限列表
        String subject = claims.getSubject();
        Object pSubject = JSONObject.parse(subject);
        Object obj = claims.get("user");
        /*
            获取token的过期时间,和当前时间作比较,如果小于当前时间,则token过期
         */
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date expiration = claims.getExpiration();
        log.info("======== token的过期时间:" + df.format(expiration));
        return true;
    }

}

 UserDTO.java

import lombok.*;

/**
 * @Author Lxq
 * @Date 2020/4/28 16:54
 * @Version 1.0
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {

    private String userName;

    private String password;
}

ReturnData.java

/**
 * @Author Lxq
 * @Date 2020/4/28 16:53
 * @Version 1.0
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ReturnData {

    private int status;

    private String mass;

    private T data;

    public ReturnData(int status, String mass) {
        this.status = status;
        this.mass = mass;
    }
}

application.yml

com:
  gpdi:
    jwt:
      #跳过认证的路由
      skip-auth-urls:
      - /baidu
      - /test
      # 单位小时
      effective-time: 2
JsonSentinelGatewayBlockExceptionHandler.java
自定义限流熔断的处理器
import cn.hutool.http.HttpStatus;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.function.Supplier;
import com.alibaba.fastjson.JSONObject;
import com.gpdi.wireless.dto.ReturnData;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @Author Lxq
 * @Date 2020/4/29 12:55
 * @Version 1.0
 * 自定义限流hanlder
 */
public class JsonSentinelGatewayBlockExceptionHandler implements WebExceptionHandler {

    private List viewResolvers;
    private List> messageWriters;
    private final Supplier contextSupplier = () -> {
        return new ServerResponse.Context() {
            @Override
            public List> messageWriters() {
                return JsonSentinelGatewayBlockExceptionHandler.this.messageWriters;
            }

            @Override
            public List viewResolvers() {
                return JsonSentinelGatewayBlockExceptionHandler.this.viewResolvers;
            }
        };
    };

    public JsonSentinelGatewayBlockExceptionHandler(List viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolvers;
        this.messageWriters = serverCodecConfigurer.getWriters();
    }


    @Override
    public Mono handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        } else {
            return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> {
                return this.writeResponse(response, exchange);
            });
        }
    }

    private Mono handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }

    /**
     * 重写返回数据
     *
     * @param response
     * @param exchange
     * @return
     */
    private Mono writeResponse(ServerResponse response, ServerWebExchange exchange) {
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        ReturnData stringReturnData = new ReturnData<>(HttpStatus.HTTP_FORBIDDEN, "当前访问人数过多,请稍后再试", "当前访问人数过多,请稍后再试");
        byte[] datas = JSONObject.toJSONString(stringReturnData).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }
}
GatewayConfiguration.java
将自定义的限流熔断处理器加入到配置中
package com.gpdi.wireless.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @Author Lxq
 * @Date 2020/4/29 12:40
 * @Version 1.0
 */
@Configuration
public class GatewayConfiguration {

    private final List viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    /**
     * 配置SentinelGatewayBlockExceptionHandler,限流后异常处理
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // 默认的限流的handler
        // return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
        // 自定义的限流handler
        return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }


    /**
     * 配置SentinelGatewayFilter
     *
     * @return
     */
    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    // 提前加载限流规则
//    @PostConstruct
//    public void doInit() {
//        initGatewayRules();
//    }

    /**
     * 配置限流规则
     */
    private void initGatewayRules() {
        Set rules = new HashSet<>();
        rules.add(new GatewayFlowRule("payment_route")
                .setCount(1) // 限流阈值
                .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
        );
        // 添加分组 限流
//        rules.add(new GatewayFlowRule("customized_api"));
        GatewayRuleManager.loadRules(rules);
    }


    /**
     * 自定义分组限流
     */
    private void initCustomizedApis() {
        Set definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("customized_api")
                .setPredicateItems(new HashSet() {{
                    // test完全匹配
                    add(new ApiPathPredicateItem().setPattern("/test"));
                    // payment/开头的
                    add(new ApiPathPredicateItem().setPattern("/payment/**")
                            .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX));
                }});
        definitions.add(api1);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }


}

 

你可能感兴趣的:(spring,cloud,alibaba)