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

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

前言

之前已经讲过 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 回帖 • 44 关注
  • Web
    118 引用 • 433 回帖 • 8 关注
  • RBAC
    4 引用 • 6 回帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • 补一份用 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 的文章,额,我指的是最新的一篇。其余的都过时了。

  • lizhongyue248 1 评论

    @Vanessa 回帖的框框打字摁 ESC 就缩下去好难受啊。输入法字母输入错了摁 ESC 清空他就缩下去,我快哭了=-=

    Vanessa
  • hong1yuan
    作者

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

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

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

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

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

    1 回复
  • 查看全部回帖