Spring Security + OAuth2.0 构建微服务统一认证解决方案(四)

搭建过程可以分为以下几步

  1. 构建简单的Spring Security + OAuth2.0 认证服务
  2. 优化认证服务(使用JWT技术加强token,自定义auth接口以及返回结果)
  3. 配置gateway服务完成简单鉴权功能
  4. 优化gateway配置(添加复杂鉴权逻辑等等)

(四)优化网关鉴权,添加复杂鉴权逻辑

上篇,对简单的网关鉴权逻辑进行优化

一. 构建 AuthorizationManager 自定义鉴权逻辑

@Service
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        // 预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
 
        // 如果token以"Bearer "为前缀,到此方法里说明JWT有效即已认证,其他前缀的token则拦截
        String token = request.getHeaders().getFirst("Authorization");
        if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) {
            return Mono.just(new AuthorizationDecision(false));
        }
 
        return Mono.just(new AuthorizationDecision(true));
    }
}

在之前创建的 springSecurityFilterChain 中进行配置

@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
 
    @Autowired
    private AuthorizationManager authenticationManager;
 
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt();
 
        http.authorizeExchange()
                // 配置鉴权
                .anyExchange().access(authenticationManager)
                .and().csrf().disable();
        return http.build();
    }
}

此时请求header中Authorization字段前缀非"Bearer "时就会被直接拦截

二. 配置鉴权白名单

如获取Token这类接口,在发送请求的时候是没法携带有效token的,如果请求走网关则会被拦截。此时就需要配置白名单,跳过鉴权。

在yaml中配置一些白名单路径

secure:
  ignore:
    urls:
      - "/user-service/login"
      - "/pgcp-auth/oauth/token"

构建配置类,服务启动时读取yaml配置

@Component
@Data
@ConfigurationProperties(prefix="secure.ignore")
public class WhiteListUrlsConfig {
    private List<String> urls;
}

在之前的 AuthorizationManager 中加入白名单逻辑

@Service
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
 
    @Autowired
    private WhiteListUrlsConfig whiteListUrlsConfig;
 
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        URI uri = request.getURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
         
        // 鉴权白名单逻辑
        List<String> ignoreUrls = whiteListUrlsConfig.getUrls();
        for (String ignoreUrl : ignoreUrls) {
            if (pathMatcher.match(ignoreUrl, uri.getPath())) {
                return Mono.just(new AuthorizationDecision(true));
            }
        }
 
        // 如果token以"Bearer "为前缀,到此方法里说明JWT有效即已认证,其他前缀的token则拦截
        String token = request.getHeaders().getFirst(Auth.JWT_TOKEN_HEADER);
        if (StringUtils.isBlank(token) || !token.startsWith(Auth.JWT_TOKEN_PREFIX)) {
            return Mono.just(new AuthorizationDecision(false));
        }
 
        return Mono.just(new AuthorizationDecision(true));
    }
}

验证:通过网关路由生成token请求,可以看到在未携带token的情况下请求成功,返回了想要的结果。

三. 配置自定义gateway filter

可能在后续的请求需要用到userId这个字段,但是这个字段是被编码在JWT中的。如果把JWT带到后续的各种接口中,需要的时候进行解析,会产生大量重复代码。

干脆就在gateway这里解析,并把需要的userId字段放置入header中,后续需要自取即可

自定义filter如下完成解析的工作

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst(Auth.JWT_TOKEN_HEADER);
        if (StringUtils.isBlank(token)) {
            return chain.filter(exchange);
        }
        try {
            String accessToken = token.replace(Auth.JWT_TOKEN_PREFIX, "");
            JWSObject object = JWSObject.parse(accessToken);
            String userId = (String) object.getPayload().toJSONObject().get("userId");
 
            // 从token中解析出userId字段设置到header中
            ServerHttpRequest request = exchange.getRequest().mutate().header("userId", userId).build();
            exchange = exchange.mutate().request(request).build();
        } catch (ParseException e) {
            Asserts.fail(ResultCode.UNAUTHORIZED);
        }
        return chain.filter(exchange);
    }
 
    @Override
    public int getOrder() {
        return 0;
    }
}

验证如下,在filesystem-service中写一个简单的测试接口

@RestController
public class TestController {
    @Autowired
    private HttpServletRequest httpServletRequest;
 
    @GetMapping("/t")
    public String get(@RequestParam String str) {
        return str + " " + httpServletRequest.getHeader("userId");
    }
}

postman测试,成功!

四. 更加复杂的鉴权逻辑(Todo)

可以设置url对应的用户角色访问权限(用redis预先设置),配置鉴权逻辑进 AuthorizationManager 中。
Spring Security + OAuth2.0 构建微服务统一认证解决方案(一)
Spring Security + OAuth2.0 构建微服务统一认证解决方案(二)
Spring Security + OAuth2.0 构建微服务统一认证解决方案(三)
github 仓库

posted @ 2021-11-15 14:43  BuptWade  阅读(1782)  评论(1编辑  收藏  举报