Gateway中使用SpringSecurity进行网关鉴权与权限控制

需求设求
众所周知,一切架构都必须按需求来设计,万能构架基本上是不存在的,即使是像Spring Security安全架构也只是实现了一个能用方式,并不是放之四海而皆准的,但是一个构架的良好扩展性是必须的,可以让使用者按照自己的需要进行扩展使用。所以为了说明本示例的实现,先假定这样一个需求
1,需要有一个Web网关服务进行权限统一认证
2,网关后面有一个用户管理服务,负责用户账号的管理
3,网关后面还存在其它的服务,但是这些服务需要认证成功之后才能访问
4,需要支持同一个请求可以被多个角色访问
主要技能点说明
1`修改默认登陆页面
在项目中添加完spring security依赖之后,如果不添加任何额外的配置,这时不管发送任何请求,都会跳到spring security提供的默认登陆页面。这显然不是我们想要的,那么第一步就是要显示自定义的登陆页面。
在Spring Gateway 网关项目中添加Security的配置,如下面代码所示:

@EnableWebFluxSecurity
public class WebSecurityConfig {
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        RedirectServerAuthenticationEntryPoint loginPoint = new RedirectServerAuthenticationEntryPoint("/xinyue-server-a/account/index");
        http.authorizeExchange().pathMatchers("/xinyue-server-a/easyui/**","/xinyue-server-a/js/**","/xinyue-server-a/account/index","/xinyue-server-a/account/login").permitAll()    
        .and().formLogin().loginPage("/xinyue-server-a/account/authen").authenticationEntryPoint(loginPoint)
        .and().authorizeExchange().anyExchange().authenticated()
        .and().csrf().disable();
        SecurityWebFilterChain chain = http.build();
        return chain;
    }
}

这里有一个容易出现理解错误的地址,网上有好多示例说是直接只配置loginPage("/my/login")即可,这样配置的话,需要你的登陆页面,和提交登陆信息的form的action都必须是一致的,只不过,一个是get方式请求/my/login,一个是post方式请求/my/login,但是大多数据情况下,我们的登陆页面地址,和登陆form的action地址是分离的,所以需要按我上面的方式进行配置才可以。

 http.authorizeExchange().pathMatchers("/xinyue-server-a/easyui/**","/xinyue-server-a/js/**","/xinyue-server-a/account/index","/xinyue-server-a/account/login").permitAll()   

这个配置表示这些请求都不做验证,直接放过。

.and().formLogin().loginPage("/xinyue-server-a/account/authen").authenticationEntryPoint(loginPoint)

这段配置表示需要认证的请求是/xinyue-server-a/account/authen(对手正常的Springmvc 服务来说,这个应该是登陆时form的action请求地址),如果没有认证,跳转到loginPoint设置的地址:/xinyue-server-a/account/index,即登陆页面。

 .and().authorizeExchange().anyExchange().authenticated()

这段配置表示其它请求都必须是认证(登陆成功)之后才可以访问。

Spring Cloud Gateway 认证方式
如果是微服务模式,在Spring cloud gateway网关处进行用户认证与授权有两种方式:

1,在Spring Cloud Gateway服务这里添加数据库访问,直接检测登陆信息是否正确,如果正确,再给此用户授权。
2,在网关后面专门的认证服务进行登陆信息认证,如果登陆成功,在返回的Header中添加用户认证与授权需要的信息,然后在网关处理再完成认证与授权

请求权限验证
一般来说,在管理系统中,用户拥有不同的角色,不同的角色拥有不同的权限,那么在收到请求的时候,就需要在网关验证当前用户是否拥有访问这个请求的权限,或是否是某一个角色,如果是才能进行访问,否则返回用户权限不足,拒绝访问。
现在给下面这个请求配置必须拥有Manager权限才可以访问


.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").hasRole("Manager")

如果这个时候再登陆,会发现服务器返回Access Denied,如果配置为Dev权限


.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").hasRole("Dev")

因为此用户拥有Dev权限(模拟账号),所以可以正常访问。

多个角色判断
目前Spring Security提供的模式是一个请求配置一个角色,有些复杂的系统,要求一个请求的访问权限可以被多个角色共同拥有。这就需要我们自定义一个权限的验证了。
比如添加如下配置:

.and().authorizeExchange().pathMatchers("/xinyue-server-a/account/main").access(new XinyueReactiveAuthorizationManager("Manager", "Dev"))

表示这个请求需要Manager或Dev其中一个角色就可以访问。
然后在XinyueReactiveAuthorizationManager中实现权限验证判断,详细请考参源码

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
        return authentication
                .filter(a -> a.isAuthenticated())
                .flatMapIterable( a -> a.getAuthorities())
                .map( g-> g.getAuthority())
                .any(c->{
                    //检测权限是否匹配
                    String[] roles = c.split(",");
                    for(String role:roles) {
                        if(authorities.contains(role)) {
                            return true;
                        }
                    }
                    return false;
                })
                .map( hasAuthority -> new AuthorizationDecision(hasAuthority))
                .defaultIfEmpty(new AuthorizationDecision(false));

Gateway过滤器的介绍
之前说过GateWay的组件中有Filter(过滤器)这一功能,就是web开发的三大组件(Servlet、Filter、Listener)中的Filter,但是Gateway中使用的是WebFlux,而不是Servlet,有兴趣的可以了解下。在GateWay中有很多内置的过滤器,而且我们还可以自定义一个过滤器。

Gateway内置过滤器
1·生命周期
PRE: 这种过滤器在请求被路由之前调用。
POST:这种过滤器在路由到微服务以后执行。
2·类型
GatewayFilter 和Predicate一样,用在单个路由上
GlobalFilter 用在整个网关之前
自定义过滤器
自定义一个类实现这两个类就以了,直接上代码:
我们的需求就是,只有当你的请求参数中的username=admin才给你放行。

/**
 * 自定义一个GlobalFilter类
 */
@Component //一定要让Spring管理这个bean
public class CustomFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        /*
         *这个request对象可以获取更多的内容
         *比如,如果是使用token验证的话,就可以判断它的Header中的Token值了
         *为了演示方便,我就判断了它的参数
         */
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        String username = queryParams.getFirst("username");
        if (!username.equals("admin")) {
            //不允许访问,禁止访问
            ServerHttpResponse response = exchange.getResponse();
            //这个状态码是406
            response.setStatusCode(HttpStatus.NOT_ACCEPTABLE); 
            return exchange.getResponse().setComplete();
        }
        //放行
        return chain.filter(exchange);
    }

    /**
     * 这是Ordered接口的中的方法
     * 过滤器有一个优先级的问题,这个值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return -1;
    }
}

个人spring cloud开源脚手架,快速上手开发


版权声明:本文为weixin_39654286原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。