Spring Gateway + Oauth2 + Jwt网关统一鉴权

news2024/11/19 3:29:06

之前文章里说过,分布式系统的鉴权有两种方式,一是在网关进行统一的鉴权操作,二是在各个微服务里单独鉴权。

第二种方式比较常见,代码网上也是很多。今天主要是说第一种方式。

1.网关鉴权的流程

重要前提:需要收集各个接口的uri路径和所需权限列表的对应关系,并存入缓存。

2.收集uri路径和对应权限

服务启动的时候,执行缓存数据的初始化操作:扫描服务内的所有controller接口方法,利用反射,获取方法的完整uri路径,方法上指定注解中的权限值,再存入Redis缓存。

服务启动时做一些操作,方法有很多,可以继承CommandLineRunner或者其他方式。不熟悉的可以去查一下有关资料。

因为后续可能会有很多微服务,因此将该缓存数据的初始化的操作放在common模块中,微服务依赖该模块完成。

1.1.初始化方法

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import com.eden4cloud.common.core.contant.CacheConstants;
import com.eden4cloud.common.security.anno.Perms;
import com.eden4cloud.common.security.constant.MethodTypeConstant;
import com.eden4cloud.common.security.utils.RequestUriUtils;
import com.eden4cloud.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Param:
 * @Return:
 * @Date: 2022/12/3 15:31
 * @Author: Yan
 * @Description: 接口权限初始化采集,获取数据库中的权限标识,没有权限标识的其它接口使用**表示
 */
@Slf4j
public class ApiPermsInit implements ApplicationContextAware {

    /**
     * 接口路径及权限列表
     * 比如:/user/list<br>
     * 不支持@PathVariable格式的URI
     */
    public static List<Map<String, String>> oauthUrls = new ArrayList<>();
    Map<String, String> uriAuthMap = new HashMap<>();

    /**
     * Url参数需要解密的配置
     * 比如:/user/list?name=加密内容<br>
     * 格式:Key API路径  Value 需要解密的字段
     * 示列:/user/list  [name,age]
     */
    public static Map<String, List<String>> requestDecryptParamMap = new HashMap<>();

    private String applicationPath;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.applicationPath = ctx.getEnvironment().getProperty("spring.application.name");
        Map<String, Object> beanMap = ctx.getBeansWithAnnotation(Controller.class);
        initData(beanMap);
        if (CollectionUtil.isNotEmpty(uriAuthMap)) {
            redisTemplate.boundHashOps(CacheConstants.OAUTH_URLS).putAll(uriAuthMap);
        }
    }

    /**
     * 初始化,获取所有接口的加解密配置状态并保存
     *
     * @param beanMap
     */
    private void initData(Map<String, Object> beanMap) {
        if (beanMap != null) {
            beanMap.values().parallelStream().map(Object::getClass).forEach(clz -> {
                for (Method method : clz.getDeclaredMethods()) {
                    String uriKey = RequestUriUtils.getApiUri(clz, method, applicationPath);
                    //收集带有Perms注解的api接口的uri路径
                    Perms perms = AnnotationUtils.findAnnotation(method, Perms.class);
                    if (StringUtils.isNotEmpty(uriKey) && perms != null && ArrayUtil.isNotEmpty(perms.value())) {
                        //解析权限标识
                        String authValue = StringUtil.join(perms.value(), StrPool.COMMA);
                        uriAuthMap.put(uriKey, authValue);
                    } else if (uriKey.startsWith(MethodTypeConstant.GET)) {
                        /* 屏蔽没有请求方式的api接口 */
                        //没有权限标识的,直接使用**表示
                        uriAuthMap.put(uriKey, "**");
                    } else if (uriKey.startsWith(MethodTypeConstant.POST)) {
                        //没有权限标识的,直接使用**表示
                        uriAuthMap.put(uriKey, "**");
                    } else if (uriKey.startsWith(MethodTypeConstant.PUT)) {
                        //没有权限标识的,直接使用**表示
                        uriAuthMap.put(uriKey, "**");
                    } else if (uriKey.startsWith(MethodTypeConstant.DELETE)) {
                        //没有权限标识的,直接使用**表示
                        uriAuthMap.put(uriKey, "**");
                    } else {
                        //不在上述情况中的,一般为框架提供的api接口
                        log.info(uriKey);
                    }
                }
            });
        }
    }
}

1.2.对应的工具类

import com.eden4cloud.common.security.constant.MethodTypeConstant;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Method;

public class RequestUriUtils {

    private static final String SEPARATOR = "/";

    /**
     * 获取接口的uri路径
     *
     * @param clz
     * @param method
     * @param applicationPath
     * @return
     */
    public static String getApiUri(Class<?> clz, Method method, String applicationPath) {
        String methodType = "";
        StringBuilder uri = new StringBuilder();

        // 处理类路径
        RequestMapping reqMapping = AnnotationUtils.findAnnotation(clz, RequestMapping.class);
        if (reqMapping != null && reqMapping.value().length > 0) {
            uri.append(formatUri(reqMapping.value()[0]));
        }

        //处理方法上的路径
        GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);
        PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);
        RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
        PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);
        DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);

        if (getMapping != null && getMapping.value().length > 0) {
            methodType = MethodTypeConstant.GET;
            uri.append(formatUri(getMapping.value()[0]));
        } else if (postMapping != null && postMapping.value().length > 0) {
            methodType = MethodTypeConstant.POST;
            uri.append(formatUri(postMapping.value()[0]));
        } else if (putMapping != null && putMapping.value().length > 0) {
            methodType = MethodTypeConstant.PUT;
            uri.append(formatUri(putMapping.value()[0]));
        } else if (deleteMapping != null && deleteMapping.value().length > 0) {
            methodType = MethodTypeConstant.DELETE;
            uri.append(formatUri(deleteMapping.value()[0]));
        } else if (requestMapping != null && requestMapping.value().length > 0) {
            RequestMethod requestMethod = RequestMethod.GET;
            if (requestMapping.method().length > 0) {
                requestMethod = requestMapping.method()[0];
            }
            methodType = requestMethod.name().toLowerCase() + ":";
            uri.append(formatUri(requestMapping.value()[0]));
        }

        // 框架自带的接口,返回null后,直接忽略处理
        if (uri.indexOf("${") > 0) {
            return "";
        }

        // 针对Rest请求,路径上的请求参数进行处理,以**代替
        int idx = uri.indexOf("{");
        if (idx > 0) {
            uri = new StringBuilder(uri.substring(0, idx)).append("**");
        }

        return methodType + SEPARATOR + applicationPath + uri;
    }

    private static String formatUri(String uri) {
        if (uri.startsWith(SEPARATOR)) {
            return uri;
        }
        return SEPARATOR + uri;
    }
}

收集结果:

 说明:

  1. 要求一个请求的完整路径格式为:请求方式:/服务名/类路径/方法路径;
  2. 请求方式必须要有,防止路径处理后,会出现重复,加上请求方式可以极大避免;
  3. 服务名是为了做网关路由使用,在配置网关的路由规则时,断言的路由规则即为/服务名;代码里获取服务路径的方式是:ctx.getEnvironment().getProperty("spring.application.name");如果觉得不安全可以在yml文件中自定义一个路径名,改一下此处的获取值即可。只需要记得一定要和网关的路由断言规则匹配就行!!!!
  4. 类路径必须有。
  5. 方法路径必须有。方法上的第一个路径必须是固定路径,而不能是请求参数,另外不同方法上的第一个固定路径避免设置成相同的;也是为了防止最终出现请求方式相同、路径也相同的情况。
  6. 对Rest风格,且方法路径上带有路径参数的路径必须做特殊处理,即将路径参数替换成**。举例如下:原完整路径为/user-Service/user/queryUser/{username}/{age},方法上的路径为/queryUser/{username}/{age}不论有多少个路径参数,从第一个路径参数开始,全部替换掉,处理为/queryUser/**,最终存入缓存所使用的完整路径为:GET:/user-Service/user/queryUser/**。否则当请求到达网关,你想要根据路径去缓存中匹配对应的路径时,你会发现没办法处理,因为从请求的uri路径上你是看不出来哪是固定路径,哪是路径参数的。例如:/user-Service/user/queryUser/zhangsan/18,这个例子你虽然你看都知道哪个是路径参数,但毕竟是框架,万一路径上有很多的/../../..,还怎么猜?有些规则该定死,还是要定死的。
  7. 真实请求到达网关后,我们对真实请求也做了一些处理,即:只保留/服务名/类路径/方法路径的第一个,后续的路径均使用一个/**替换掉。举例:真实请求为/user-Service/user/queryUser/zhangsan/18,我们处理后为/user-Service/user/queryUser/**。

经过上述的规定和路径处理后,在下面的代码中进行匹配操作:

  •  methodValue + uri + CacheConstants.FUZZY_PATH即为/user-Service/user/queryUser/**
  • k.toString()即为/user-Service/user/queryUser/**;
  • 判断结果为true

2.网关整合Security Oauth2

Gateway网关是基于webFlux实现的,所以和一般微服务整合方式不太一样。

2.1.认证管理器

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @Since: 2023/4/13
 * @Author: Yan
 * @Description Jwt认证管理器,对token的真实性、有效性进行校验
 */
@Component
@Slf4j
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {

    @Autowired
    private 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 -> {
                    //解析令牌
                    OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
                    if (oAuth2AccessToken == null) {
                        return Mono.error(new InvalidBearerTokenException("无效的token"));
                    } else if (oAuth2AccessToken.isExpired()) {
                        return Mono.error(new InvalidBearerTokenException("token已过期"));
                    }
                    OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(accessToken);
                    if (oAuth2Authentication == null) {
                        return Mono.error(new InvalidBearerTokenException("无效的token"));
                    } else {
                        return Mono.just(oAuth2Authentication);
                    }
                }))
                .cast(Authentication.class);
    }
}

2.2..鉴权管理器

import cn.hutool.core.text.AntPathMatcher;
import cn.hutool.core.text.StrPool;
import com.eden4cloud.common.core.contant.CacheConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Since: 2023/4/13
 * @Author: Yan
 * @Description Jwt鉴权管理器:从Redis中获取所请求的Url所需要的权限,和用户token中所携带的权限进行比对
 */
@Slf4j
@Component
public class JwtAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    private AntPathMatcher matcher = new AntPathMatcher();

    /**
     * *****当前匹配方法要求一个API接口方法上必须要有路径*****
     *
     * @param mono
     * @param authorizationContext
     * @return
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        // 处理当前请求的uri路径,最终格式为:/服务路径/类路径/方法上的路径 /eden-system/user/add
        StringBuilder builder = new StringBuilder();
        String[] split = authorizationContext.getExchange().getRequest().getURI().getPath().split(StrPool.SLASH);
        String uri = builder.append(StrPool.SLASH).append(split[1])//服务路径
                .append(StrPool.SLASH).append(split[2])//类路径
                .append(StrPool.SLASH).append(split[3])//方法路径
                .toString();
        // 请求方式拼接处理 ,格式为 GET:
        String methodValue = authorizationContext.getExchange().getRequest().getMethodValue() + StrPool.COLON;
        // 获取所有路径的权限列表
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(CacheConstants.OAUTH_PERMS);
        List<String> authorities = new ArrayList<>();
        AtomicBoolean authFlag = new AtomicBoolean(false);
        entries.forEach((k, v) -> {
            // 根据请求uri路径,获取到匹配的缓存权限数据
            if (k.equals(methodValue + uri)
                    || matcher.match(methodValue + uri + CacheConstants.FUZZY_PATH, k.toString())) {
                if (CacheConstants.ANONYMOUS.equals(v.toString())) {
                    // 权限为**,表示允许匿名访问
                    authFlag.set(true);
                } else {
                    // 收集当前路径所需的权限列表
                    authorities.addAll(Arrays.asList((v.toString()).split(StrPool.COMMA)));
                }
            }
        });
        // Collection<? extends GrantedAuthority> authorities1 = mono.block().getAuthorities();


        List<String> finalAuthorities = authorities;
        return mono
                //判断是否认证成功
                .filter(Authentication::isAuthenticated)
                //获取认证后的全部权限列表
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                //如果包含在url要求的权限内,则返回true
                .any(auth -> authFlag.get() || finalAuthorities.contains(auth))
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false))
                ;
    }

}

2.3.security安全配置

import com.eden4cloud.gateway.component.JwtAuthorizationManager;
import com.eden4cloud.gateway.handler.RequestAccessDeniedHandler;
import com.eden4cloud.gateway.handler.RequestAuthenticationEntrypoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.web.cors.reactive.CorsWebFilter;

/**
 * @Since: 2023/4/2
 * @Author: Yan
 * @Description security安全配置
 */
@Configuration
@EnableWebFluxSecurity
public class EdenGatewayWebSecurityConfig {

    @Autowired
    private JwtAuthorizationManager jwtAuthorizationManager;

    @Autowired
    private ReactiveAuthenticationManager authenticationManager;

    @Autowired
    private RequestAuthenticationEntrypoint requestAuthenticationEntrypoint;

    @Autowired
    private RequestAccessDeniedHandler requestAccessDeniedHandler;

    @Autowired
    private CorsWebFilter corsWebFilter;

    // @Autowired
    // private GlobalAuthenticationFilter authenticationFilter;

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

        http
                .csrf().disable()
                .authorizeExchange()
                //对oauth的端点进行放行
                .pathMatchers("/eden-oauth/oauth/**").permitAll()
                //其他请求必须鉴权,使用鉴权管理器
                .anyExchange().access(jwtAuthorizationManager)
                .and()
                //鉴权异常处理
                .exceptionHandling()
                .authenticationEntryPoint(requestAuthenticationEntrypoint)
                .accessDeniedHandler(requestAccessDeniedHandler)
                .and()
                //跨域过滤器
                .addFilterAt(corsWebFilter, SecurityWebFiltersOrder.CORS)
                //token认证过滤器
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                // .addFilterAfter(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
        ;
        return http.build();
    }
}

2.4.将网关作为资源服务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

/**
 * @Author: Yan
 * @Since: 2023/2/4
 * @Description: 资源服务器解析鉴权配置类
 */
@Configuration
public class EdenGatewayResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    //公钥
    private static final String RESOURCE_ID = "eden-gateway";

    /**
     * Http安全配置,对每个到达系统的http请求链接进行校验
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**").permitAll();
    }

    /**
     * 资源服务的安全配置
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true);
    }
}

2.5.其他配置

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * @CreateTime: 2023-01-2023/1/11 15:09
 * @Author: Yan
 * @Description  注册网关过滤器示例
 */
@Configuration
@AutoConfigureAfter(GatewayAutoConfiguration.class)
public class GatewayRoutesConfiguration {

    /**
     * 跨域配置
     *
     * @return
     */
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

    // @Bean(name = "ipKeyResolver")
    // public KeyResolver userIpKeyResolver() {
    //     return new IpKeyResolver();
    // }
    //
    // @Bean
    // public RouteLocator routeLocator(RouteLocatorBuilder builder) {
    //     StripPrefixGatewayFilterFactory filterFactory = new StripPrefixGatewayFilterFactory();
    //     StripPrefixGatewayFilterFactory.Config partsConfig = filterFactory.newConfig();
    //     partsConfig.setParts(1);
    //
    //     MyGatewayFilterFactory factory = new MyGatewayFilterFactory();
    //     MyGatewayFilterFactory.PathsConfig pathsConfig = factory.newConfig();
    //     pathsConfig.setPaths(Arrays.asList("/AAA", "/BBB"));
    //
    //     return builder.routes()
    //             .route(r -> r.path("/life/**")
    //                     .uri("lb://eden-life")
    //                     .filters(factory.apply(pathsConfig), filterFactory.apply(partsConfig))
    //                     .id("eden-life"))
    //             .build();
    // }
}

2.6.自定义鉴权过滤器工厂

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.eden4cloud.common.security.exception.InvalidTokenException;
import com.eden4cloud.common.security.utils.JwtUtils;
import com.eden4cloud.common.util.sign.Base64;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

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

/**
 * @CreateTime: 2023-01-2023/1/11 9:34
 * @Author: Yan
 * @Description 自定义鉴权过滤器工厂:设置访问白名单;重新封装鉴权认证通过的请求头;
 */
//1. 编写实现类继承AbstractGatewayFilterFactory抽象类
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.IgnoreUrlsConfig> {//4. 指定泛型,静态的内部实体类

    @Autowired
    private TokenStore tokenStore;

    //5. 重写无参构造方法,指定内部实体类接收参数
    public AuthGatewayFilterFactory() {
        super(IgnoreUrlsConfig.class);
    }

    @Override
    public GatewayFilter apply(IgnoreUrlsConfig config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            //直接放行部分请求路径,如登录、退出等  需要排除的路径弄成可yaml配置的
            boolean flag = config.ignoreUrls.contains(request.getURI().getPath())
                    || config.ignoreUrls.stream().filter(i -> i.endsWith("/**"))
                    .anyMatch(i -> exchange.getRequest().getURI().getPath().startsWith(i.replace("**", "")));
            if (flag) {
                return chain.filter(exchange);
            }
            //重新封装新的请求中数据
            ServerWebExchange webExchange = rebuildRequestHeaders(exchange, request);
            if (webExchange == null) {
                return Mono.error(new InvalidTokenException());
            }
            return chain.filter(webExchange);
        };
    }

    /**
     * 重新封装新的请求数据
     *
     * @param exchange
     * @param request
     * @return
     */
    private ServerWebExchange rebuildRequestHeaders(ServerWebExchange exchange, ServerHttpRequest request) {
        //获取请求头中的令牌
        String token = JwtUtils.getToken(request);
        if (StrUtil.isBlank(token)) {
            return null;
        }

        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
        Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();
        List<String> authorities = (List<String>) additionalInformation.get("authorities");
        //获取用户名
        String username = additionalInformation.get("user_name").toString();
        //获取用户权限
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username", username);
        jsonObject.put("authorities", authorities);
        //将解析后的token加密后重新放入请求头,方便后续微服务解析获取用户信息
        String base64 = Base64.encode(jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8));
        request = exchange.getRequest().mutate().header("token", base64).build();
        exchange.mutate().request(request);
        return exchange;
    }

    /**
     * 6. 重写shortcutFieldOrder()指定接收参数的字段顺序
     * Returns hints about the number of args and the order for shortcut parsing.
     *
     * @return the list of hints
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("ignoreUrls");
    }

    /**
     * 7. 重写shortcutType()指定接收参数的字段类型
     */
    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }

    /**
     * 3. 定义匿名内部实体类,定义接收参数的字段
     */
    @Data
    public static class IgnoreUrlsConfig {
        //传递多个参数
        private List<String> ignoreUrls;
    }
}

2.7.网关路由规则

#路由配置
spring:
  cloud:
    gateway:
      routes:
        - id: eden-life
          uri: lb://eden-life
          predicates:
            - Path=/eden-life/**
          filters:
            - StripPrefix=1
            - name: Auth
              args:
                ignoreUrls:
                  - /auth-server/login
                  - /oauth/**
                  - /life/**

2.6和2.7主要是展示配置了动态的请求白名单功能。基于nacos的配置中心功能,可以实现动态刷新,白名单设置实时生效。

2.8.异常处理

import cn.hutool.json.JSONUtil;
import com.eden4cloud.common.core.entity.R;
import com.eden4cloud.common.security.exception.SecurityExceptionEnum;
import org.apache.http.HttpHeaders;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @Since: 2023/4/17
 * @Author: Yan
 * @Description TODO
 */
@Component
public class RequestAccessDeniedHandler implements ServerAccessDeniedHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        DataBuffer buffer = response.bufferFactory()
                .wrap(JSONUtil.toJsonStr(R.error(SecurityExceptionEnum.NO_PERMISSION.getMsg())).getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}
import cn.hutool.json.JSONUtil;
import com.eden4cloud.common.core.entity.R;
import com.eden4cloud.common.security.exception.SecurityExceptionEnum;
import org.apache.http.HttpHeaders;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @Since: 2023/4/17
 * @Author: Yan
 * @Description TODO
 */
@Component
public class RequestAuthenticationEntrypoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        DataBuffer buffer = response.bufferFactory()
                .wrap(JSONUtil.toJsonStr(R.error(SecurityExceptionEnum.INVALID_TOKEN.getMsg())).getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

循环代码模型构建方法

循环结构是源代码程序的重要结构&#xff0c;然而即使是简单的循环程序&#xff0c;也很容易出错&#xff0c;循环中的很多错误往往需要执行多次或者在某些特定的情况下才能被发现&#xff0c;检测这些错误的代价很高&#xff0c;所以需要重点开展对软件循环代码的安全性分析研…

简单聊下HBase

大家好&#xff0c;我是易安&#xff01; Google发表了三篇论文&#xff0c;即GFS、MapReduce和BigTable&#xff0c;被誉为“三驾马车”&#xff0c;开启了大数据时代。今天我们来聊一下BigTable对应的NoSQL系统HBase&#xff0c;看看它是如何处理海量数据的。 在计算机数据存…

Mybatis 全局配置文件 mybatis-config.xml

1、全局配置文件的用处 mybatis通过配置文件可以配置数据源、事务管理器、运行时行为、处理别名、类型处理、插件等信息。在mybatis应用初始化时&#xff0c;程序会解析全局配置文件&#xff0c;使用配置的信息实例化Configuration组件&#xff0c;完成基本配置的初始化。在my…

图论 Union-Find 并查集算法

union-find API&#xff1a; class UF { public:/* 将 p 和 q 连接 */void union(int p, int q);/* 判断 p 和 q 是否连通 */bool connected(int p, int q);/* 返回图中有多少个连通分量 */int count(); };连通性概念 触点&#xff1a;每个单独的不与任何点相连的点叫做触点 连…

绿色智慧档案馆构想之智慧档案馆环境综合管控一体化平台

【智慧档案馆整体效果图】 智慧档案库房一体化平台通过智慧档案管理&#xff0c;实现智慧档案感知协同处置功能&#xff1b;实现对档案实体的智能化识别、定位、跟踪监控&#xff1b;实现对档案至智能密集架、空气恒湿净化一体设备、安防设备&#xff0c;门禁设备等智能化巡检与…

camunda流程引擎receive task节点用途

Camunda的Receive Task用于在流程中等待外部系统或服务发送消息。当接收到消息后&#xff0c;Receive Task将流程继续执行。Receive Task通常用于与Send Task配合使用&#xff0c;以便流程可以在发送和接收消息之间进行交互。 Receive Task可以用于以下场景&#xff1a; 1、等…

DAB-DETR代码学习记录之模型解析

DAB-DETR是吸收了Deformable-DETR&#xff0c;Conditional-DETR&#xff0c;Anchor-DETR等基础上完善而来的。其主要贡献为将query初始化为x,y,w,h思维坐标形式。 这篇博文主要从代码角度来分析DAB-DETR所完成的工作。 DAB-DETR主要是对Decoder模型进行改进。博主也主要是对Dec…

【C++】6. 内联函数

文章目录 前言一、宏函数二、内联函数三、内联函数的易错点 前言 当我们调用函数时&#xff0c;是有很多消耗的。其中最大的销毁就是为函数开辟空间 - 函数栈帧。 如果我们有一个函数&#xff0c;很短&#xff0c;而且要调用很多次&#xff0c;比如Swap()。它所造成消耗就比较…

机器学习笔记Python笔记:HMM(隐马尔科夫模型)

1 引子&#xff1a;猜天气小游戏 一对异地恋的情侣&#xff0c;女朋友想根据男友的心情猜测男友所在城市的天气 1.1 天气和心情一定一一对应 晴天——>高兴雨天——>烦躁 可以根据心情唯一确定天气 1.2 天气和心情没有一一对应 晴天——>80%高兴&#xff0c;20%烦…

有关实现深拷贝的四种方法

深拷贝与浅拷贝: 在开始之前我们需要先了解一下什么是浅拷贝和深拷贝&#xff0c;其实深拷贝和浅拷贝都是针对的引用类型&#xff0c;JS中的变量类型分为值类型&#xff08;基本类型&#xff09;和引用类型&#xff1b;对值类型进行复制操作会对值进行一份拷贝&#xff0c;而对…

Logstash学习

一、Logstash基础 1、什么是Logstash logstash是一个数据抽取工具&#xff0c;将数据从一个地方转移到另一个地方。下载地址&#xff1a;https://www.elastic.co/cn/downloads/logstash logstash之所以功能强大和流行&#xff0c;还与其丰富的过滤器插件是分不开的&#xff…

CDGP认证|ChatGPT的出现,对数据治理行业冲击如何?

ChatGPT的出现对数据治理有很多好处&#xff0c;其中最明显的是提供了更高效、更准确和更自动化的数据处理和分析服务,可以帮助企业和组织更好地管理和利用数据资源&#xff0c;提高数据质量和决策效率。此外&#xff0c;ChatGPT还能够发现隐藏在大量数据中的信息和趋势&#x…

OJ练习第82题——填充书架

填充书架 力扣链接&#xff1a;1105. 填充书架 题目描述 给定一个数组 books &#xff0c;其中 books[i] [thicknessi, heighti] 表示第 i 本书的厚度和高度。你也会得到一个整数 shelfWidth 。 按顺序 将这些书摆放到总宽度为 shelfWidth 的书架上。 先选几本书放在书架…

Nexus 组件发布失败、npm 登录失败 解决过程

目录 参考发布文章进行打包 提示发布成功&#xff0c;但在 Nexus 里没发现组件 测试 yarn 发布 测试 npm 发布&#xff08;解决登录失败&#xff09; Nexus 设置 Sonatype Nexus Repository Manager 相关权限 参考发布文章进行打包 整体发布&#xff1a;根目录运行 yarn r…

直播软件app开发:如何保证音视频质量?

随着社交媒体的发展&#xff0c;视频直播已成为越来越流行的社交方式。直播软件app开发也因此成为了一个热门话题。在开发直播软件app时&#xff0c;保证音视频质量是至关重要的。本文将介绍如何确保你的直播软件app在音视频质量方面表现出色。 确定音视频质量标准 首先&…

第七章 建造者模式

文章目录 前言一、传统方式解决盖房子需求完整代码抽象房子类 AbstractHouse实现子类 普通房子实现子类 高楼大厦客户端盖房子 二、引入建造者模式建造者模式的四个角色&#xff1a; 产品、抽象建造者、具体建造者、指挥者完整代码House类 (产品角色)抽象父类&#xff08;抽象建…

CV 领域的 ChatGPT?MetaAI 推出“最强”大视觉模型 SAM

出品人&#xff1a;Towhee 技术团队 随着 ChatGPT 引起一波又一波的“GPT热潮”&#xff0c;自然语言领域又一次成为了人工智能的讨论焦点。大家不由得思考&#xff0c;计算机视觉领域里是否会出现这样一个堪称划时代的模型&#xff1f;在这种万众瞩目的时候&#xff0c;一直处…

Python3《机器学习实战》学习笔记(七):支持向量机原理篇之手撕线性SVM

文章目录 一、SVM介绍二、线性SVM2.1 数学建模2.1.1决策面方程2.1.2"分类间隔"方程2.1.3约束条件2.1.4线性SVM优化问题基本描述2.1.5求解准备(讲讲凸函数)2.1.6拉格朗日函数2.1.7KKT条件2.1.8对偶问题求解2.1.9最后求解 2.2 SMO算法 三、代码实战3.1准备数据 一、SVM…

M_Map工具箱简介及地理图形绘制

M_Map工具箱简介及地理图形绘制 1 M_Map简介1.1 具体代码说明 2 地理图形绘制案例2.1 M_Map给定案例2.1.1 M_Map Logo2.1.2 Lambert Conformal Conic projection of North American Topography2.1.3 Stereographic projection of North Polar regions2.1.4 Colourmaps 2.2 案例…

vue封装公共组件库并发布到npm库详细教程

vue组件封装的原理&#xff1a;利用vue框架提供的api: Vue.use( plugin )&#xff0c;我们需要把封装好组件的项目打包成vue库&#xff0c;并提供install方法&#xff0c;然后发布到npm中。Vue.use( plugin )的时候会自动执行插件中的install方法。 一、组件库代码目录 目录…