SprinCloud-Gateway 路由管理、网关Token校验、创建线程对象保存User信息

本文详细介绍了如何在Spring Cloud Gateway中创建一个Maven模块,并配置Eureka客户端。接着,展示了如何设置路由规则、跨域配置以及使用@EnableEurekaClient启动类注解。此外,还深入讲解了自定义过滤器,用于Token验证和用户信息注入,实现了全局过滤器以及过滤器级别的配置。最后,展示了如何在其他微服务中使用过滤器和拦截器进行身份识别和共享。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基础使用
创建一个maven模块
在pom中加入

 <!--引入gateway 网关-->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-gateway</artifactId>
 </dependency>
 <!-- eureka-client -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
 </dependency>

创建启动类额外添加注解@EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient
public class GatewayServer {
    public static void main(String[] args) {
        SpringApplication.run(GatewayServer.class,args);
    }
}

修改配置文件

server:
  port: 80

eureka:
  instance:
    hostname: localhost # 主机名
    lease-renewal-interval-in-seconds: 10 #间隔多久向eureka从新发送心跳
    lease-expiration-duration-in-seconds: 60 #如果90秒eureka没有收到客户端的心跳就让注册中心剔除该服务
  client:
    service-url:
      # 这里务必记得配置这个,gateway本身也是要注册到eureka的,否则会报错:所有程序都无法正确路由[其实还是能找到,这就很诡异]
      defaultZone: http://localhost:8090/eureka # eureka服务端地址通信
spring:
  application:
    name: gateway #应用名称
  cloud:
    # 网关配置
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
            - GET
            - POST
            - PUT
            - DELETE
      # 路由配置:转发规则
      routes: #集合。
        # id: 唯一标识。默认是一个UUID
        # uri: 转发路径
        # predicates: 条件,用于请求网关路径的匹配规则
        - id: gateway-consumer
          #uri: http://localhost:8089/ #这里是静态的写法直接填写匹配成功后转发的目的地址
          uri: lb://eureka-consumer/ #这里则是动态的写法,填写需要转发到的应用名称[由此可以进行负载均衡,因为同名应用可以有好几个]
          predicates:
          # 这里要注意匹配规则,比如想匹配/user前缀的域名需要使用/user/**,而不是/user或者/user/不同于nginx的配置方法
            - Path=/consumer/**,/asster/** #可以匹配多个,以','分隔


Gateway过滤器的配置与使用
例1:
在网关完成token的认证并将个人标识放入请求头进行转发
先补齐netty缺失的一个bean

@Configuration
public class Config {

    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

创建Filter类实现GlobalFilter, Ordered接口,

import com.alibaba.fastjson.JSON;
import com.pria.common.ad_enum.CodeEnum;
import com.pria.common.ad_enum.MsgEnum;
import com.pria.common.pojo.vo.HttpResult;
import com.pria.gateway.feign.SSOAuthControllerFeign;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Configuration
public class MyFilter implements GlobalFilter, Ordered {
    private final SSOAuthControllerFeign ssoAuthControllerFeign;
    public MyFilter(SSOAuthControllerFeign ssoAuthControllerFeign) {
        this.ssoAuthControllerFeign = ssoAuthControllerFeign;
    }
    private final String[] whiteList=
            {"/auth/login",
            "/auth/sendLoginCode",
            "/auth/refreshToken"
            };
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("全局过滤器执行");
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();
        // 白名单检测
        List<String> uris = Arrays.asList(whiteList);
        String uri = request.getURI().getPath();
        if (uris.contains(uri))
            return chain.filter(exchange);
        //未通过白名单的进行token校验
        List<String> tokens = request.getHeaders().get("Authorization");
        if (tokens == null || tokens.size() == 0)
            //如果不在百名单并且不含token,调用方法获取与一个默认对象写入响应体返回
            return getVoidMon(response, false, CodeEnum.TOKEN_NOT_EXIST, MsgEnum.TOKEN_NOT_EXIST);
        // sso中的服务我们使用httpResult进行响应,data是用户信息暂时包含id和language
        HttpResult httpResult = ssoAuthControllerFeign.checkToken(tokens.get(0));
        if (httpResult.getFlag()){
            // 执行放行,Token校验通过,将用户信息转成Json放入请求头
            request=exchange.getRequest().mutate().header("user", JSON.toJSONString(httpResult.getData())).build(
            return chain.filter(exchange.mutate().request(request).build());
        }
        // 拦截
        return getVoidMon(response, false,httpResult.getCode(),httpResult.getMsg());
    }
    // 配置过滤器级别,也就是顺序级别,数字越小越优先,我们也可以使用注解@Order(1)@WebFilter进行排序
    // 启动类需要添加@ServletCompontScan注解
    @Override
    public int getOrder() {
        return 0;
    }
    /**
     * 获取一个空的事务
     *
     * @param response
     * @return
     */
    public Mono<Void> getVoidMon(ServerHttpResponse response, Boolean flag, Integer code, String msg) {
        // 配置响应头
        response.getHeaders().add("Content-type", "application/json;charset=UTF-8");
        // 配置响应体 TODO 这里记得改
        HttpResult httpResult = new HttpResult(flag, code, msg);
        // 开始蜜汁操作,看不懂了
        DataBuffer dbf = response.bufferFactory().wrap(
                JSON.toJSONString(httpResult).getBytes());
        return response.writeWith(Flux.just(dbf));
    }
    /**
     * 获取一个空的事务
     *
     * @param response
     * @return
     */
    public Mono<Void> getVoidMon(ServerHttpResponse response, Boolean flag, CodeEnum codeEnum, MsgEnum msgEnum) {
        response.getHeaders().add("Content-type", "application/json;charset=UTF-8");
        HttpResult httpResult = new HttpResult(flag, codeEnum, msgEnum);
        DataBuffer dbf = response.bufferFactory().wrap(
                JSON.toJSONString(httpResult).getBytes());
        return response.writeWith(Flux.just(dbf));
    }
}

我们需要另外准备两个类,

一个为sso负责接收网关的调用,

/**
 * 解析token
 *
 * @param token
 * @return
 */
@Override
public HttpResult checkToken(String token) throws ExpiredJwtException, TokenFailureException {
    try {
        User user = jwtUtils.parseToken(token);
        return new HttpResult(true, CodeEnum.SUCCESS, MsgEnum.SUCCESS, user);
    } catch (ExpiredJwtException e) {
        e.printStackTrace();
        // 过期
        return new HttpResult(false, CodeEnum.INVALID_TOKEN, MsgEnum.TOKEN_EXPIRED);
    } catch (Exception e) {
        e.printStackTrace();
        // 版本失效
        return new HttpResult(false, CodeEnum.INVALID_TOKEN, MsgEnum.INVALID_TOKEN);
    }// 解析出错
}

另一个为jjwt工具类,进行token的校验以及数据的封装;

import com.pria.common.ad_enum.RedisKeyEnum;
import com.pria.common.exception.TokenFailureException;
import com.pria.common.pojo.po.User;
import com.pria.common.pojo.vo.MyToken;
import com.pria.sso.config.SaltParamConfig;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;

/**
 * token工具,token的成以及校验
 */
@Service
@Slf4j
public class JwtUtils {

    private final SaltParamConfig saltParamConfig;
    private final RedisTemplate<String, String> redisTemplate;

    public JwtUtils(SaltParamConfig saltParamConfig, RedisTemplate<String, String> redisTemplate) {
        this.saltParamConfig = saltParamConfig;
        this.redisTemplate = redisTemplate;
    }

    protected String tokenVersionPrefix = RedisKeyEnum.ADEVAR_TOKEN_VERSION.getKey();

    /**
     * 通过用户对象生成MyToken
     *
     * @param user
     * @return
     */
    public MyToken getToken(User user, boolean isNew) {
        String tokenVersion = UUID.randomUUID().toString().substring(0,8);
        String token = this.getAccessToken(user,tokenVersion);
        String refreshToken = this.getRefreshToken(user,tokenVersion);
        MyToken myToken = new MyToken();
        myToken.setToken(token);
        myToken.setRefreshToken(refreshToken);
        myToken.setExpired(saltParamConfig.getExpiredTime());
        myToken.setIsNew(isNew);
        return myToken;
    }


    /**
     * 传入用户生成token
     *
     * @param user
     * @return
     */
    protected String getAccessToken(User user,String tokenVersion) {
        // 载荷
        HashMap<String, Object> claimsMap = new HashMap<>();
        claimsMap.put("language", user.getLanguage());
        claimsMap.put("id", user.getId());
        claimsMap.put("tokenVersion", tokenVersion);
        String strId = user.getId().toString();
        JwtBuilder jwtBuilder = Jwts.builder()
                .setIssuedAt(new Date())
                .setClaims(claimsMap)
                //设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
                .signWith(SignatureAlgorithm.HS256, saltParamConfig.getSalt())
                .setExpiration(new Date(saltParamConfig.getExpiredTime()));
        String tokenVersionKey = tokenVersionPrefix + strId;
        // 版本有效期和token有效期保持一致
        redisTemplate.opsForValue().set(tokenVersionKey,
                tokenVersion, Duration.ofMillis(saltParamConfig.getTokenTimeOut()));
        return jwtBuilder.compact();
    }

    /**
     * 传入用户生成刷新token
     *
     * @param user
     * @return
     */
    protected String getRefreshToken(User user,String tokenVersion) {
        // 载荷
        HashMap<String, Object> claimsMap = new HashMap<>();
        claimsMap.put("language", user.getLanguage());
        claimsMap.put("id", user.getId());
        claimsMap.put("tokenVersion", tokenVersion);
        String strId = user.getId().toString();
        JwtBuilder jwtBuilder = Jwts.builder()
                .setIssuedAt(new Date())
                .setClaims(claimsMap)
                //设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
                .signWith(SignatureAlgorithm.HS256, saltParamConfig.getRefreshSalt())
                .setExpiration(new Date(saltParamConfig.getRefreshExpiredTime()));
        String tokenVersionKey = tokenVersionPrefix + strId;
        // 版本有效期和token有效期保持一致
        redisTemplate.opsForValue().set(tokenVersionKey,
                tokenVersion, Duration.ofMillis(saltParamConfig.getRefreshTokenTimeOut()));
        return jwtBuilder.compact();
    }


    /**
     * 解析token
     *
     * @return
     */
    public User parseToken(String token) throws TokenFailureException {
        Jws<Claims> claimsJws = Jwts.parser()
                //设置盐
                .setSigningKey(saltParamConfig.getSalt())
                .parseClaimsJws(token);
        //可以通过claimsJws拿到body以及heard
        Claims body = claimsJws.getBody();
        Long id = Long.parseLong(body.get("id").toString());
        String tokenVersion = (String) body.get("tokenVersion");
        String language = (String) body.get("language");

        String redisTokenVersion = redisTemplate.opsForValue().get(tokenVersionPrefix + id);
        if (StringUtils.isBlank(tokenVersion) || !tokenVersion.equals(redisTokenVersion)) {
            throw new TokenFailureException();
        }
        User user = new User();
        user.setId(id);
        user.setLanguage(language);
        return user;
    }

    /**
     * 刷新token
     *
     * @return
     */
    public MyToken refreshToken(String token) throws TokenFailureException{
        Jws<Claims> claimsJws = Jwts.parser()
                //设置盐
                .setSigningKey(saltParamConfig.getRefreshSalt())
                .parseClaimsJws(token);
        //可以通过claimsJws拿到body以及head
        Claims body = claimsJws.getBody();
        Long id = Long.parseLong(body.get("id").toString());
        String language = (String) body.get("language");
        String refreshTokenVersion = (String) body.get("tokenVersion");
        String redisRefreshTokenVersion = redisTemplate.opsForValue().get(tokenVersionPrefix + id);
        if (StringUtils.isBlank(redisRefreshTokenVersion) || !refreshTokenVersion.equals(redisRefreshTokenVersion)){
            throw new TokenFailureException("刷新令牌过期");
        }
        return this.getToken(new User(id, language), false);
    }
    //public static void main(String[] args) {
    //    JwtBuilder jwtBuilder = Jwts.builder()
    //            //设置id【唯一标识,也就是"jti":556】
    //            .setId("556")
    //            //设置签发给谁【"sub":556】
    //            .setSubject("YiJia")
    //            //设置签发时间【"iat":xxxx】
    //            .setIssuedAt(new Date())
    //            //设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
    //            .signWith(SignatureAlgorithm.HS256, "JKK&K2jhsld%ajbm4$lkz3")
    //            .setExpiration(new Date(System.currentTimeMillis()+1000));
    //    String token = jwtBuilder.compact();
    //    System.out.println("token:" + token);
    //
    //    Jws<Claims> claimsJws = Jwts.parser()
    //            .setSigningKey("JKsdasaK&K2jhsldasd%ajbmasd4$lkz3")
    //            .parseClaimsJws(token);
    //    Claims body = claimsJws.getBody();
    //    System.out.println(body.getId());
    //    System.out.println(body.getSubject());
    //}
}

然后其他微服务中我们写一个过滤器来从请求头中获取用户标识id和language并放入Threadlocal完成了身份的识别并共享
首先准备一个工具类,获取Threadlocal,以及提供user的写入和删除功能

public class WmThreadLocalUtils {

    private final  static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    /**
     * 设置当前线程中的用户
     * @param user
     */
    public static void setUser(User user){
        userThreadLocal.set(user);
    }

    /**
     * 获取线程中的用户
     * @return
     */
    public static User getUser( ){
        return userThreadLocal.get();
    }

    /**
     * 清除线程threadLocal信息
     */
    public static void removeUser(){
        userThreadLocal.remove();
    }
}

创建过滤器从请求头获取用户标识id放入线程对象[Spring也可以使用拦截器完成]

创建过滤器继承GenericFilterBean

@Order(1)
@WebFilter(filterName = "wmTokenFilter",urlPatterns = "/*")
@Log4j2
public class WmTokenFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //得到header中的信息
        String userId = request.getHeader("userId");
        if(userId != null){
            WmUser wmUser = new WmUser();
            wmUser.setId(Integer.valueOf(userId));
            WmThreadLocalUtils.setUser(wmUser);
        }
        filterChain.doFilter(request,response);
    }
}

这里同步演示一下使用拦截器释放线程中的线程对象
首先创建一个拦截器类,继承HandlerInterceptorAdapter并重写两个方法
preHandle:在请求前做些什么(可以完成如上过滤器的内容)同时还能根据返回的boolean决定是否放行
afterCompletion:在请求响应结束响应前做些什么,可以在这里清除线程对象中的数据

/**
 * 拦截器
 */
@Component
public class UserThreadLocalInterceptor extends HandlerInterceptorAdapter {

    public UserThreadLocalInterceptor() {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }
}

然后我们新建一个配置类,来配置拦截器执行顺序,从上向下执行

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器加载配置类
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserThreadLocalInterceptor userThreadLocalInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //考虑拦截器的顺序
        //registry.addInterceptor(this.userTokenInterceptor).addPathPatterns("/**");
        //token优先级高于redis,因为redis响应才需要
        //注册自定义拦截器,添加拦截路径和排除拦截路径
        registry.addInterceptor(userThreadLocalInterceptor).addPathPatterns("/**").excludePathPatterns("/doc.html").excludePathPatterns("/webjars/**");
    }
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //配置拦截器访问静态资源
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

如此这般我们就可以在请求中直接使用UerThreadLocal.get()获取User对象了;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值