Security 与响应式 webFlux(三) 完结撒花

本贴最后更新于 1300 天前,其中的信息可能已经时过境迁

前言

之前已经讲过 WebFlux 与 Security 的一种结合方式,又因为业务原因放弃了第一种实现的方式。

为了动态的 RBAC(Role-Based Access Control)接口级的权限管理和利用 Webflux 的吞吐和响应能力,又写另一种方式,这种方式放弃了一部分 Security 的能力,而利用自身的能力去书写,所以更加灵活,重要的是,已经把该踩的坑已经全部踩平,如有新坑,还望互相交流。

实践篇

Config 配置

之前已经写过,第一步需要配置 Scurity 的 Config,跟之前有一些区别

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private SecurityContextRepository securityContextRepository;


    //security的鉴权排除列表
    private static final String[] excludedAuthPages = {
            "/auth/login",
            "/auth/logout"
    };

    @Bean
    SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {

        return http
		.exceptionHandling()
                .authenticationEntryPoint((swe, e) -> {
                    return Mono.fromRunnable(() -> {
                        swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                    });
                }).accessDeniedHandler((swe, e) -> {
                    return Mono.fromRunnable(() -> {
                        swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                    });
                }).and()
                .csrf().disable()
                .formLogin().disable()
                .httpBasic().disable()
                .authenticationManager(authenticationManager)
                .securityContextRepository(securityContextRepository)
                .authorizeExchange()
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .pathMatchers(excludedAuthPages).permitAll()
                .anyExchange().authenticated()
                .and().build();
    }
}

从上往下开始说明区别

1.增加认证异常处理,之前有登录成功与失败的类,现在已经不需要了,直接在配置遇到异常的处理方式

.authenticationEntryPoint : 认证失败进行 HTTP 401 状态码返回

.accessDeniedHandler : 访问被拒绝进行 HTTP 403 状态码返回

.formLogin : Security 登录认证功能关闭

.httpBasic : httpBasic 功能关闭

.authenticationManage : 重写认证管理,并进行配置

.securityContextRepository : 重写 Security 上下文存储库并进行配置(这个在上章提到过)

省下的配置都已经讲过了,就不多说了,配置根据自身业务需要在进行修改,有很多功能。

securityContextRepository 类

重写方法

@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {

    @Value("${jwt.sign}")
    private String sign;


    @Override
    public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange swe) {
        ServerHttpRequest request = swe.getRequest();
        String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String authToken = authHeader.substring(7);
            User user = JwtUtil.verifyUserJwtToken(authToken, sign);
            if (authUser == null || authUser.getType() == null) {
                return Mono.empty();
            }
	//动态RBAC认证  伪代码 业务代码已删

//                    AtomicBoolean passAuthentication = new AtomicBoolean(false);
//                    user.getPermission().stream().forEach(permission ->{
//                        PathPattern pattern=new PathPatternParser().parse(permission.getPermission());
//                        if (pattern.matches(request.getPath().pathWithinApplication()) && request.getMethodValue().equals(permission.getRequestType())){
//                            passAuthentication.set(true);
//                        }
//                    });
//                    if(!passAuthentication.get()){
//                        return Mono.empty();
//                    }
  
            Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
            return this.authenticationManager.authenticate(auth).map((authentication) -> {
                return new SecurityContextImpl(authentication);
            });
        } else {
            return Mono.empty();
        }
    }

}

通过 JWT 方式解析出自己的 token 利用 token 自己去通过 redis 等方式换取自己接口权限能力,在利用请求路径中的接口路径来进行对比,如果权限认证失败就 mono.empty();

在往常的代码中,咱们使用 Secuirty 的动态 RBAC 或路径匹配上,通常用 AntPathRequestMatcher, 在 WebFlux 中,虽然也可以使用此方法,但方法匹配中需要的 HttpServletRequest 则提供不了,所以在 WebFlux 源码中,看到 WebFlux 中都是在使用 PathPattern 进行路径匹配,如果有更好的方式记得 @ 我。

AuthenticationManage 类

@Component
@Slf4j
public class AuthenticationManager implements ReactiveAuthenticationManager {

    @Value("${jwt.sign}")
    private String sign;


    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String authToken = authentication.getCredentials().toString();

        try {
            User user = JwtUtil.verifyUserJwtToken(authToken, sign);
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(user.getRole));
            return Mono.just(new UsernamePasswordAuthenticationToken(authentication, null, authorities));
        } catch (Exception e) {
            return Mono.empty();
        }
    }

}

securityContextRepository 类接收到 token 后就会进入认证类 AuthenticationManage 类

在这里,你可以把当前已登录的用户的 token 进行验证是否过期,token 续期,角色和权限的赋予等等,如果用户验证没问题 就可以 return Mono.just(new UsernamePasswordAuthenticationToken(authentication, null, authorities)); 进行认证通过给当前线程赋予相应身份,不通过就 return Mono.empty() 进行 401 返回

token 认证总结

上述就是当用户已经登录有 Token 该如何鉴权,如何通过 Security 认证的方案了,里面有些地方 需要根据自身业务去进行完善,接下如何获取 token 就比较简单了 大致讲一下 就是通过接口 自己比较认证,认证成功后根据 jwt 生成 token 返回给前端

Controller 获取 token

跟正常 MVC 一样 可以自己写 controller 层 service serviceImpl 层等,因为是 gateway 获取库的数据可以基于底层服务获取,调用 feign 或 Http 请求等方式获取用户信息,当然也可以直接连数据库进行调用

@RestController
public class AuthenticationController {

    @PostMapping("/login")
    public Mono<Result<TokenVO>> login(@RequestBody AuthVO authVO) {
       UserDtail user =userFeignClient.Detail(AuthVO);
        if (user!= null && new BCryptPasswordEncoder().matches(user.getPassword(),authVO.getPassword()) ) {  
	TokenVO tokenVO = new TokenVO();
	tokenVO.setToken(JwtUtil.createUserJwtToken(authUser,sign));
    }
    return Mono.just(Result.success(tokenVO));

}

通过查询用户数据后 根据密码查看是否正确,正确后生成 token,因原版代码包含大量业务逻辑等,所以简化了几十倍,根据业务自己写吧。

feign 的调用中的坑

因为 Webflux 的原因,feign 调用会报错,需要 httpConverts 一下

@Configuration
public class HttpMsgConverConfig {
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress());
    }

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

这样就可以调用 feign 了

总结

根据 Webflux 与 Security 的结合 网上资料比较少,并且往往不能结合现在的动态 RBAC 与分布式系统,在权限更加细粒化的需求的阶段,在以大型微服务为基础进行鉴权认证,保证速度、吞吐、响应为要求,希望能帮助有同样困惑的人。

ps:在上个月 2020 年 8 月 21 号 新的 oauth 来了!!!

Authorization Server 将替代 Spring Security OAuthSpring 社区提供 OAuth2.0 授权服务器支持。经过四个月的努力,Spring Authorization Server 项目中的 OAuth2.0 授权服务器开发库正式发布了第一个版本。

0.0.1 版本已经发布,在之前 Oauth2.0 比较难用的情况下,spring 从放弃 oauth 到社区回归 到迎接新的变化社区回归 到 0.0.1 的版本, 希望新版本的 oauth 系统 可以更加贴近现在服务变化所需要的东西。

之前的 Oauth 有很多不方便的地方,管理成本也比单纯的 Security 大,希望新版本能好用,之后放弃单纯的 Security 迎接新的 Oauth 授权,咱们之后的新 Oauth 见。

  • Security
    9 引用 • 15 回帖
  • OAuth

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 6 关注
  • Web
    115 引用 • 430 回帖 • 8 关注
  • RBAC
    4 引用 • 6 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 其实对于 webflux 的客户端、资源服务器,Spring Security 的 OAuth 是早已经有了的。参考 https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide 非常简单的配置可以完美适配,并且完美兼容 webflux。所以对于 JWT 的认证和生成,是完全可以使用这一套的。只需要注入一个 bean 即可,参考官网文档资源服务器这一节。用起来十分方便的,自定义程度也是非常高的了。包含在如下几个库中:

    • spring-security-oauth2-core
    • spring-security-oauth2-client
    • spring-security-oauth2-jose
    • spring-security-oauth2-resource-server

    参考 https://docs.spring.io/spring-security/site/docs/current/reference/html5/#modules 这几个库涵盖了 JWT、OAuth 的所有东西,直接用即可,会方便很多,提供本地验证和自省端点验证两种方式是适用于任何场景了。我做动态 RBAC 的时候是在 ReactiveAuthorizationManager 完成的,并没有再去实现一个 ServerSecurityContextRepository。个人感觉如果太多东西自己来管理,那么 spring security 的优势就大打折扣了。用了 Kotlin 的协程,用 Mono.zip 也可以达到同样的效果

    /**
     * Role-Based Access Control.
     * All requests are here to decide whether to continue.
     *
     * @author <a href="https://echocow.cn">EchoCow</a>
     * @date 2020/3/15 下午6:23
     */
    @Component
    class SecurityAuthentication(
        private val mongoOperations: ReactiveMongoOperations,
        @Value("\${spring.security.oauth2.resourceserver.public-id}")
        private val publicId: String,
        @Value("\${spring.security.oauth2.resourceserver.auth-id}")
        private val authId: String
    ) : ReactiveAuthorizationManager<AuthorizationContext> {
      private val antPathMatcher = AntPathMatcher()
    
      /**
       * Authorization Decision。
       *
       * It will check user roles from token. There are three types of permissions here:
       *
       * <ol>
       *   <li>Protected resource: Login users can access. Such as user info endpoint.Resource role is ROLE_PUBLIC(1)</li>
       *   <li>Public resource: Everyone can access, don't need login. Such as login endpoint.Resource role is ROLE_NO_LOGIN(2)</li>
       *   <li>Role resource: Users with the specified role can access. Such as resource crud endpoint.</li>
       * </ol>
       *
       * The user info come from [authentication],
       * - If user login, [Authentication.isAuthenticated] is true.
       * - If user is anonymous, [Authentication.isAuthenticated] is false.
       *
       * And you must check the url and method from [context]. It include all information with this request.
       * The more info, see [AuthorizationContext.exchange].
       *
       * Extension:
       * You can check other info with request, such as remote address.
       */
      override fun check(authentication: Mono<Authentication>, context: AuthorizationContext): Mono<AuthorizationDecision> {
        val request = context.exchange.request
        // Get all resource.
        val resource = mongoOperations
            .find(Query(
                where(Authority::isEnable).`is`(true)
                    .and(Authority::method).`is`(request.method!!.name)
            ), Authority::class.java)
            // Url matches
            .filter { antPathMatcher.match(it.url ?: "", request.uri.path) }
            .map { it.roles }
            .flatMap { fromIterable(it) }
            .distinct()
            .cache()
        return mono {
          val auth = authentication.awaitFirstOrNull()
          val roleIds = resource.collectList().awaitSingle()
          AuthorizationDecision(when {
            roleIds.contains(publicId) -> true
            roleIds.contains(authId) -> auth?.isAuthenticated ?: false
            else ->
              // Role-Based Access Control resource.
              auth?.authorities
                  ?.map { authority -> (authority as RoleGrantedAuthority).getId() }
                  ?.filter { id -> roleIds.contains(id) }
                  ?.count()
                  ?: 0 > 0
          })
        }
      }
    
    }
    
    

    fegin 的确会报错,因此我选择使用的 WebClient 配合 ReactorLoadBalancerExchangeFilterFunction 来达到服务间的调用,当然这样就需要自己写调用了,但也不难。不过也有 webflux 版本的 fegin ,但是并没有想象中的好用。 对了,gateway 我这边并没有问题。

    之前的 OAuth2 并不难用,很多企业通过它来自定义很多流程上的东西,他的自定义程度是非常非常高的,只是因为时间太久了放弃了,并且大部分不符合 RFC 6749 的规范,如果你看新版的 Spring Authorization Server ,你就会发现他的 zenhub 看板都是完全跟着 RFC 走的。这是和上一版最大的区别,上一版并没有严格参照 RFC,因此很多非 Spring 应用的客户端需要做适配。当然,新的版本依旧还不支持 webflux。

    我毕业设计就是全套 Reactive 微服务,包括 Spring Cloud 和 Webflux 和 Vert.x 可以互相交流下哈。

    1 回复
    3 操作
    lizhongyue248 在 2020-09-06 13:41:00 更新了该回帖
    lizhongyue248 在 2020-09-06 13:23:13 更新了该回帖
    lizhongyue248 在 2020-09-06 13:15:59 更新了该回帖
  • 其他回帖
  • 补一份用 spring security resource server 本地验证的来解析并认证 JWT 的配置,由于自己的项目已经在用了,不方便更改权限,不然可以分享出来一起学习

      @Bean
      fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http
          // Other Config......
          .oauth2ResourceServer { resourceServer ->
            resourceServer.jwt { jwt ->
              jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
            }
          }
          .csrf()
          .disable()
          .build()
    
      fun grantedAuthoritiesExtractor(): ReactiveJwtAuthenticationConverterAdapter {
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
        return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
      }
    
      inner class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
        override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
          val authorities = jwt.claims
              .getOrDefault("roles", emptyList<JSONObject>()) as Collection<JSONObject>
          return authorities
              .map { role -> RoleGrantedAuthority(role) }
              .toList()
        }
      }
    

    可以看得出来自定义非常方案,对于默认只解析 scope 的方式稍微修改就适配了,十多行代码即可。自省端点也是很简单,以前撸过

    @Bean
      fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http
          .authorizeExchange { exchanges -> exchanges.anyExchange().access(securityAuthentication) }
          .oauth2ResourceServer { resourceServer ->
            resourceServer.opaqueToken { opaqueToken ->
              // Custom Opaque Token Introspector.
              opaqueToken.introspector(CustomAuthoritiesOpaqueTokenIntrospector())
            }
          }
          .csrf()
          .disable()
          .build()
    
    
      /**
       * Default, spring security will set authority from scopes.
       * But the system is access control from roles.
       * We need custom Opaque Token Introspector.
       */
      inner class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector {
        private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector(
            opaqueTokenIntrospector.introspectionUri,
            opaqueTokenIntrospector.clientId,
            opaqueTokenIntrospector.clientSecret
        )
    
        override fun introspect(token: String): Mono<OAuth2AuthenticatedPrincipal> =
            delegate.introspect(token)
                .map { principal ->
                  // Build Principal
                  DefaultOAuth2AuthenticatedPrincipal(
                      principal.name, principal.attributes, extractAuthorities(principal))
                }
    
        private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection<GrantedAuthority> =
            // The authority from roles fields.
            (principal.getAttribute<List<JSONObject>>("roles") ?: emptyList())
                .map { role -> RoleGrantedAuthority(role) }
      }
    

    其他的还有很多,比如 JWT issue 验证,时间验证,有效期验证等等,官方都做好了的,直接用即可。

    现在对于 webflux,security 支持已经非常好了,不需要自定义那么多东西的。

    另外官方整合了 Nimbus 库,可以说是目前为止我用过最好用的了,JWS、JWE、JWK 等等。但是资料少,需要一定的官网文档阅读能力。

    3 操作
    lizhongyue248 在 2020-09-06 13:42:15 更新了该回帖
    lizhongyue248 在 2020-09-06 13:41:48 更新了该回帖
    lizhongyue248 在 2020-09-06 13:39:01 更新了该回帖
  • 哈哈哈不需要配置多个认证服务,择优用之,很多时候没有必要走 oauth 那一套的。

    我开发微信小程序的时候也是前后端分离然后使用 jwe。自己写了一个过滤器进行 token 生成和获取,然后仅仅用 Spring Security OAuth Resource Server ,整个认证、授权的流程就完成了。加起来五六十十行代码(大多都是配置)。也就用到了他的 jose 和 resource server 两个模块而已。但是省掉了至少三个类,还不用自己去管理 Spring 的东西。

    互相学习哈 ~ 也可以看看我的关于 security 的文章,额,我指的是最新的一篇。其余的都过时了。

  • hong1yuan
    作者

    我们没有直接用 oauth2.0 是因为还得多配置个认证服务,考虑到微服务和高可用和一定的踩坑成本、并且暂时没有 SSO 单点登陆的需求,所以就没有采用。

    在 Spring Authorization Server 出现后在考虑版本稳定后有 SSO 相关的需求后踩踩坑。

    看了你的代码我感觉我可以在优化一下我的代码,有些地方处理的不太优雅,虽然完成了业务.....

    主要是网上这种结合资源太少,我看官网的文档也没找到很多有用的东西,可能是找的方向不太对,也可能是那堆英文快看头晕了

    还是得感谢你的建议,提升技术全靠道友~

    1 回复
  • 查看全部回帖