需求设求
众所周知,一切架构都必须按需求来设计,万能构架基本上是不存在的,即使是像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;
}
}