当前位置: 首页 > news >正文

Spring Cloud Gateway获取认证用户信息

Spring Cloud Gateway获取认证用户信息

文章目录

  • Spring Cloud Gateway获取认证用户信息
    • 前言
    • 与Spring Security集成
      • 添加依赖
      • 配置类
    • 获取认证用户信息
      • 获取登录用户
    • 页面无限重定向登录页面解决方法
    • 总结

前言

该文章,用于记录Spring Cloud Gateway与Spring Security集成过程,以及集成过程中遇到的部分问题。

与Spring Security集成

添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

配置类

/**
 * 认证成功后处理,此处偷懒,将用户信息,使用JSON格式字符串添加请求头。
 * 后续会基于JWS生成Token。
 */
@Bean
public ServerAuthenticationSuccessHandler successHandler() {
    return (exchange, authentication) -> {
        UserDetails user = (UserDetails) authentication.getPrincipal();

        Map<String, Object> tokenInfo = new HashMap<>();
        tokenInfo.put("USER_NAME", user.getUsername());
        tokenInfo.put("AUTHORITIES", user.getAuthorities());

        ServerHttpResponse response = exchange.getExchange().getResponse();
        exchange.getExchange().getRequest().mutate().header("X-AUTHENTICATION-TOKEN", JSONObject.toJSONString(tokenInfo));

        ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(tokenInfo, HttpStatus.OK);
        return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity))));
    };
}

/**
 * 认证失败处理
 */
@Bean
public ServerAuthenticationFailureHandler failureHandler() {
    return (exchange, exception) -> {
        ServerHttpResponse response = exchange.getExchange().getResponse();

        Map<String, Object> responseBody = new HashMap<>(2);
        responseBody.put("ERROR_CODE", "000000");
        responseBody.put("ERROR_TYPE", exception.getClass().getName());
        responseBody.put("ERROR_MESSAGE", exception.getMessage());
        ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(responseBody, HttpStatus.INTERNAL_SERVER_ERROR);

        response.setStatusCode(HttpStatus.FORBIDDEN);
        return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity))));
    };
}

/** 
 * 无权限处理配置
 */
@Bean
public ServerAccessDeniedHandler accessDeniedHandler() {
    return (exchange, accessDeniedException) -> {
        ServerHttpResponse response = exchange.getResponse();

        Map<String, Object> responseBody = new HashMap<>(2);
        responseBody.put("ERROR_CODE", "000000");
        responseBody.put("ERROR_MESSAGE", "请求未授权");
        ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(responseBody, HttpStatus.FORBIDDEN);

        response.setStatusCode(HttpStatus.FORBIDDEN);
        return response.writeWith(Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(responseEntity))));
    };
}

/**
 * 类似于Spring MVC模式下,AuthenticationManager
 */
@Bean
public ReactiveAuthenticationManager authenticationManager(UserDetailsManager userDetailsManager,
                                                           PasswordEncoder passwordEncoder) {
    return authentication -> {
        final String username = authentication.getName();
        final String password = (String) authentication.getCredentials();

        return Mono.just(userDetailsManager.loadUserByUsername(username))
                .filter(user -> passwordEncoder.matches(password, user.getPassword()))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .map(user -> new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()));
    };
}

/**
 * 简易版UserDetailsManager实现类,此处仅用于模拟用户信息,真实情况,请使用数据库存储。
 */
@Bean
public UserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
    UserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(new User("que",
            passwordEncoder.encode("123456"), Arrays.asList(new SimpleGrantedAuthority("ADMIN"))));

    return manager;
}

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public ServerSecurityContextRepository contextRepository() {
    return new MemoryCacheSecurityContextRepository(5, TimeUnit.MINUTES);
//        return new WebSessionServerSecurityContextRepository();
}

/**
 * Security核心配置信息
 * 将上述配置的ServerAuthenticationSuccessHandler、ServerAuthenticationFailureHandler、ServerAccessDeniedHandler、
 * ReactiveAuthenticationManager、ServerSecurityContextRepository配置进ServerHttpSecurity。
 * 配置方式,与Spring MVC模式下的Security配置类似。
 */
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity httpSecurity,
                                                  ServerAuthenticationSuccessHandler accessHandler,
                                                  ServerAuthenticationFailureHandler failureHandler,
                                                  ServerAccessDeniedHandler accessDeniedHandler,
                                                  ReactiveAuthenticationManager authenticationManager,
                                                  ServerSecurityContextRepository securityContextRepository) {

    return httpSecurity.formLogin()
            .authenticationManager(authenticationManager)
            .authenticationSuccessHandler(accessHandler)
//                .securityContextRepository(securityContextRepository)
            .authenticationFailureHandler(failureHandler)
            .and().csrf().disable()
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
            .and()
            // 此处用于存储认证后的Authentication。
            // 默认使用WebSessionServerSecurityContextRepository。
            // 该Repository为ReactiveSecurityContextHolder获取认证信息的数据来源。细节,后续部分介绍。
            .securityContextRepository(securityContextRepository)
            // 配置自定义拦截器
            .addFilterAt(authFilter, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING)
            .authorizeExchange(exchange -> {
                exchange.pathMatchers("/login").permitAll()
                        .anyExchange().authenticated();
            })
            .build();
}

获取认证用户信息

Web模式下(Spring Cloud Gateway 使用WebFlux),可通过SecurityContextHolder.getContext获取Authentication信息。此处无法使用该方式获取Authentication。原因在于Web模式下,若使用http.formLogin进行认证的话,请求通过UsernamePasswordAuthenticationFilter过滤器后,于successfulAuthentication(AbstractAuthenticationProcessingFilter类)存储认证信息。

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

	if (logger.isDebugEnabled()) {
		logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
				+ authResult);
	}

	// 存储认证成功后的Authentication
	SecurityContextHolder.getContext().setAuthentication(authResult);

	rememberMeServices.loginSuccess(request, response, authResult);

	// Fire event
	if (this.eventPublisher != null) {
		eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
				authResult, this.getClass()));
	}

	successHandler.onAuthenticationSuccess(request, response, authResult);
}

而WebFlux,使用WebFilter完成请求过滤,不会走Web模式下的Filter,认证信息,也就不会存储进SecurityContextHolder。
同样的,针对于WebFilter,Spring Security也提供ReactiveSecurityContextHolder存储Authentication,即也是通过过滤器,设置、获取Authentication。其底层,则是使用ServerSecurityContextRepository完成。

public class ReactorContextWebFilter implements WebFilter {
	private final ServerSecurityContextRepository repository;

	public ReactorContextWebFilter(ServerSecurityContextRepository repository) {
		Assert.notNull(repository, "repository cannot be null");
		this.repository = repository;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
		return chain.filter(exchange)
			.subscriberContext(c -> c.hasKey(SecurityContext.class) ? c :
				withSecurityContext(c, exchange)
			);
	}

	private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) {
		return mainContext.putAll(this.repository.load(exchange)
			.as(ReactiveSecurityContextHolder::withSecurityContext));
	}
}

获取登录用户

完成上述操作后,即完成Security的配置。接下来,实现一个请求,用于测试Security配置。此处,通过ReactiveSecurityContextHolder.getContext()获取登录用户信息,其底层,使用ServerSecurityContextRepository.load方法,获取Authentication。

@Slf4j
@RestController
@RequestMapping("quelongjiang/gatewayController")
public class GatewayController {

    @GetMapping("info/{id}")
    public Mono<String> info(@PathVariable Integer id) throws InterruptedException {
        return ReactiveSecurityContextHolder.getContext()
                .filter(securityContext -> securityContext != null)
                .map(securityContext -> securityContext.getAuthentication())
                .map(auth -> this.getAuthUserName(auth) + ", Request Argument is " + id);
    }

    // 获取登录用户名称
    protected String getAuthUserName(Authentication auth) {
        if (!auth.isAuthenticated()) {
            return "Not Authentication";
        }
        else {
            Object principal = auth.getPrincipal();
            if (principal instanceof UserDetails) {
                return ((UserDetails) principal).getUsername();
            }
            else {
                return String.valueOf(principal);
            }
        }
    }
}

页面无限重定向登录页面解决方法

在securityFilterChain配置方法处,细心的读者会发现,有两行代码用于设置ServerSecurityContextRepository,其中第一行被注释掉。若把改行注释取消,同时将下面那行securityContextRepository(securityContextRepository)注释的话,会出现,需要认证的请求,永远会重定向到登录页面,即使已经完成认证。

该问题的原因,需通过ServerHttpSecurity看起。在ServerHttpSecurity类中,存在securityContextRepository三个方法。而当前需通过第一个方法设置,用于设置ServerHttpSecurity.securityContextRepository属性。该属性,为后续3个属性配置的默认值。
当该属性不为null时,则FormLoginSpec.securityContextRepository使用该属性,否则使用WebSessionServerSecurityContextRepository实现类,配置ReactorContextWebFilter。

在这里插入图片描述
当存在自定义ServerSecurityContextRepository实现类时,按照最初配置方式,其实配置进的是FormLoginSpec.securityContextRepository,这样会导致基于httpSecurity.formLogin,完成用户登录时,Authentication保存的是自定义的Repository,而ReactorContextWebFilter,则使用WebSessionServerSecurityContextRepository获取Authentication,导致获取不到Authentication,从而导致请求直接重定向到登录页面。

private WebFilter securityContextRepositoryWebFilter() {
	ServerSecurityContextRepository repository = this.securityContextRepository == null ?
			new WebSessionServerSecurityContextRepository() : this.securityContextRepository;
	WebFilter result = new ReactorContextWebFilter(repository);
	return new OrderedWebFilter(result, SecurityWebFiltersOrder.REACTOR_CONTEXT.getOrder());
}

总结

  • Spring Cloud Gateway(WebFlux),通过SecurityWebFilterChain配置过滤器、认证等信息。
  • 自定义ServerSecurityContextRepository时,需要配置进SecurityWebFilterChain,使其生效。
  • ServerSecurityContextRepository,需要配置进SecurityWebFilterChain.securityContextRepository属性,才能使认证、ReactorContextWebFilter过滤器,使用同一个Repository获取Authentication信息,用于避免请求重定向到登录页面的问题。

相关文章:

  • 软件项目的自动化测试
  • 华为云的云计算比阿里云的云计算认证好吗?
  • Linux教程:RocketMq介绍以及集群服务搭建(双主双从同步双写)
  • RK3399平台开发系列讲解(设备树篇)设备树资源的处理
  • 项目实战第二十二讲:使用职责链模式实现商品审核
  • 笔试强训(十三)
  • node.js基于微信小程序的外卖订餐系统 uniapp 小程序
  • u盘文件删除怎么恢复?解决方法很简单
  • 【刷题日记】笔试经典编程题目(八)
  • 阿里巴巴编程规范实战(一):编程规约之常量定义代码格式
  • 人生苦短 我用Python,零基础运行你的第一行Python代码
  • zabbix案例--zabbix监控nginx状态
  • 《Rust权威指南》读书笔记 - Chapter 1, 2
  • 框架学习——ElasticSearch分布式搜索框架
  • 羊了个羊数据结构分析与代码简单实现
  • IDEA 插件开发入门教程
  • JAVA SE 6 GC调优笔记
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • Mysql优化
  • unity如何实现一个固定宽度的orthagraphic相机
  • Vue组件定义
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 构建二叉树进行数值数组的去重及优化
  • 关于字符编码你应该知道的事情
  • 聚簇索引和非聚簇索引
  • 普通函数和构造函数的区别
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 一道面试题引发的“血案”
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • #1015 : KMP算法
  • #Spring-boot高级
  • #我与Java虚拟机的故事#连载16:打开Java世界大门的钥匙
  • $ git push -u origin master 推送到远程库出错
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • .net 验证控件和javaScript的冲突问题
  • .NET 应用启用与禁用自动生成绑定重定向 (bindingRedirect),解决不同版本 dll 的依赖问题
  • .netcore如何运行环境安装到Linux服务器
  • .NET实现之(自动更新)
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!
  • .Net组件程序设计之线程、并发管理(一)
  • /bin/rm: 参数列表过长"的解决办法
  • /dev/VolGroup00/LogVol00:unexpected inconsistency;run fsck manually
  • ::before和::after 常见的用法
  • ::什么意思
  • [.net]官方水晶报表的使用以演示下载
  • [2021]Zookeeper getAcl命令未授权访问漏洞概述与解决
  • [AI]文心一言爆火的同时,ChatGPT带来了这么多的开源项目你了解吗
  • [AutoSar]BSW_Com02 PDU详解
  • [GXYCTF2019]BabyUpload1 -- 题目分析与详解
  • [HEOI2013]ALO