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

这篇具有很好参考价值的文章主要介绍了Spring Gateway + Oauth2 + Jwt网关统一鉴权。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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;
    }
}

收集结果:

oauth2.1鉴权,java,分布式,开发语言

 说明:

  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/**。

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

oauth2.1鉴权,java,分布式,开发语言

  •  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.异常处理文章来源地址https://www.toymoban.com/news/detail-770505.html

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));
    }
}

到了这里,关于Spring Gateway + Oauth2 + Jwt网关统一鉴权的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring Gateway+Security+OAuth2+RBAC 实现SSO统一认证平台

    背景:新项目准备用SSO来整合之前多个项目的登录和权限,同时引入网关来做后续的服务限流之类的操作,所以搭建了下面这个系统雏形。 : Spring Gateway, Spring Security, JWT, OAuth2, Nacos, Redis, Danymic datasource, Javax, thymeleaf 如果对上面这些技术感兴趣,可以继续往下阅读 如

    2024年02月13日
    浏览(38)
  • 微服务鉴权中心之网关配置SpringSecurity+oauth2

    微服务鉴权中心流程如下: 1. 网关配置oauth2之 TokenStore存储方式,此处用 RedisTokenStore 2.网关配置security 3.网关拦截token

    2024年02月14日
    浏览(29)
  • 权限认证SpringCloud GateWay、SpringSecurity、OAuth2.0、JWT一网打尽!

    1.它是如何工作的? ​ 客户端向 Spring Cloud Gateway 发出请求。如果Gateway处理程序映射确定一个请求与路由相匹配,它将被发送到Gateway Web处理程序。这个处理程序通过一个特定于该请求的过滤器链来运行该请求。过滤器被虚线分割的原因是,过滤器可以在代理请求发送之前和

    2024年04月08日
    浏览(35)
  • spring-security -oauth2 整合 JWT

    在这个基础上,进行整合。 spring security oauth2学习 -- 快速入门_本郡主是喵的博客-CSDN博客 先把  reids,common-pools  等依赖删掉。 删掉redis的下相关配置 1.1 导入依赖 1.2 核心代码 创建 jwtTokenConfig.java 在 AuthenticationServer.java 里面新增这些。  运行,启动!  复制这个token去官网解析

    2024年02月09日
    浏览(42)
  • 【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践

            在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(OAuth2 授权码模式),这块接入工作是由我来负责,我们的项目是微服务架构,经过网上各种查阅资料发现网关作为

    2024年02月04日
    浏览(50)
  • 基于SpringCloud + Oauth2.0 + ShiroRedis + JWT + Gateway + Nacos + Nginx + Vue实现的SaaS数字商城系统

    SaaS的英文全称是Software as a Service,意思是软件即服务 ,是云计算的其中一种服务模式 SaaS是一种通过Internet提供集中托管应用程序的方式,企业用户一般通过客户端或网页来使用,无需购买、安装或维护任何软件及硬件,因此 SaaS应用程序又被称为\\\"基于Web的软件\\\" 或 \\\"托管软件

    2024年01月20日
    浏览(37)
  • JWT和OAuth2.0

    JWT和OAuth2.0没有可比性,是两个完全不同的东西。 JWT是一种认证协议,提供了一种用于发布接入令牌(Access Token),并对发布的签名接入令牌进行验证的方法。SSO私钥加密token。应用端公钥解密token, OAuth2.0是一种授权框架,提供了一套详细的授权机制(指导)。用户或应用可以通

    2023年04月16日
    浏览(33)
  • SpringSecurity+Oauth2+JWT

    快速入门 1. 创建基础项目 file == new == project == Spring Initializr ==next == web(Spring Web)、Security(Spring Security) ==一直下一步 2. 编写代码进行测试 创建controller static下创建login.html、main.html 3. 启动项目进行测试 访问http://localhost:8080/login.html 会进入SpringSecurity框架自带的登入页面 用户默认

    2024年02月12日
    浏览(27)
  • Spring Security Oauth2.1 最新版 1.1.0 整合 gateway 完成授权认证(拥抱 springboot 3.1)

    目录 背景 demo地址 版本 Spring Boot 3.1 Spring Authorization Server 1.1.0 基础 spring security OAuth2 模块构成 授权方式 认证方式 集成过程 官方demo 代码集成 依赖 授权服务AuthorizationServerConfig配置 重要组件 测试 查看授权服务配置 访问授权服务 授权 回调 获取 access_token 获取用户信息 个性

    2024年02月08日
    浏览(47)
  • Spring Security Oauth2.1 最新版 1.1.0 整合 (基于 springboot 3.1.0)gateway 完成授权认证

    目录 背景 demo地址 版本 Spring Boot 3.1 Spring Authorization Server 1.1.0 基础 spring security OAuth2 模块构成 授权方式 认证方式 集成过程 官方demo 代码集成 依赖 授权服务AuthorizationServerConfig配置 重要组件 测试 查看授权服务配置 访问授权服务 授权 回调 获取 access_token 获取用户信息 个性

    2024年02月11日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包