概念部分
在网关集成Oauth2.0后,我们的流程架构如上。主要逻辑如下:
1、客户端应用通过api网关请求认证服务器获取access_token http://localhost:8090/auth-service/oauth/token
2、认证服务器返回access_token
{ "access_token": "f938d0c1-9633-460d-acdd-f0693a6b5f4c", "token_type": "bearer", "refresh_token": "4baea735-3c0d-4dfd-b826-91c6772a0962", "expires_in": 43199, "scope": "web"}
3、客户端携带access_token通过API网关访问后端服务
4、API网关收到access_token后通过 AuthenticationWebFilter
对access_token认证
5、API网关转发后端请求,后端服务请求Oauth2认证服务器获取当前用户
在前面文章中我们搭建好了单独的Oauth2认证授权服务,基本功能框架都实现了,这次主要是来实现第四条,SpringCloud 整合 Oauth2 后如何进行access_token过滤校验。
代码示例
引入组件
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
主要引入跟oauth2相关的jar包,这里还需要引入数据库相关的jar包,因为我们的token是存在数据库中,要想在网关层校验token的有效性必须先从数据库取出token。
bootstrap.yml 配置修改
spring: application: name: cloud-gatewaydatasource: type: com.zaxxer.hikari.HikariDataSourceurl: jdbc:mysql://xx.0.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=falseusername: xxxxxpassword: xxxxxxxdriver-class-name: com.mysql.jdbc.Driver
主要配置oauth2的数据库连接地址
自定义认证接口管理类
在webFlux环境下通过实现 ReactiveAuthenticationManager
接口 自定义认证接口管理,由于我们的token是存在jdbc中所以命名上就叫ReactiveJdbcAuthenticationManager
publicclassReactiveJdbcAuthenticationManagerimplementsReactiveAuthenticationManager { privateTokenStoretokenStore; publicJdbcAuthenticationManager(TokenStoretokenStore){ this.tokenStore=tokenStore; } publicMono<Authentication>authenticate(Authenticationauthentication) { returnMono.justOrEmpty(authentication) .filter(a->ainstanceofBearerTokenAuthenticationToken) .cast(BearerTokenAuthenticationToken.class) .map(BearerTokenAuthenticationToken::getToken) .flatMap((accessToken->{ log.info("accessToken is :{}",accessToken); OAuth2AccessTokenoAuth2AccessToken=this.tokenStore.readAccessToken(accessToken); //根据access_token从数据库获取不到OAuth2AccessTokenif(oAuth2AccessToken==null){ returnMono.error(newInvalidTokenException("invalid access token,please check")); }elseif(oAuth2AccessToken.isExpired()){ returnMono.error(newInvalidTokenException("access token has expired,please reacquire token")); } OAuth2AuthenticationoAuth2Authentication=this.tokenStore.readAuthentication(accessToken); if(oAuth2Authentication==null){ returnMono.error(newInvalidTokenException("Access Token 无效!")); }else { returnMono.just(oAuth2Authentication); } })).cast(Authentication.class); } }
网关层的安全配置
publicclassSecurityConfig { privatestaticfinalStringMAX_AGE="18000L"; privateDataSourcedataSource; privateAccessManageraccessManager; /*** 跨域配置*/publicWebFiltercorsFilter() { return (ServerWebExchangectx, WebFilterChainchain) -> { ServerHttpRequestrequest=ctx.getRequest(); if (CorsUtils.isCorsRequest(request)) { HttpHeadersrequestHeaders=request.getHeaders(); ServerHttpResponseresponse=ctx.getResponse(); HttpMethodrequestMethod=requestHeaders.getAccessControlRequestMethod(); HttpHeadersheaders=response.getHeaders(); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders()); if (requestMethod!=null) { headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name()); } headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE); if (request.getMethod() ==HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); returnMono.empty(); } } returnchain.filter(ctx); }; } SecurityWebFilterChainwebFluxSecurityFilterChain(ServerHttpSecurityhttp) throwsException{ //token管理器ReactiveAuthenticationManagertokenAuthenticationManager=newReactiveJdbcAuthenticationManager(newJdbcTokenStore(dataSource)); //认证过滤器AuthenticationWebFilterauthenticationWebFilter=newAuthenticationWebFilter(tokenAuthenticationManager); authenticationWebFilter.setServerAuthenticationConverter(newServerBearerTokenAuthenticationConverter()); http .httpBasic().disable() .csrf().disable() .authorizeExchange() .pathMatchers(HttpMethod.OPTIONS).permitAll() .anyExchange().access(accessManager) .and() // 跨域过滤器 .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS) //oauth2认证过滤器 .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION); returnhttp.build(); } }
这个类是SpringCloug Gateway 与 Oauth2整合的关键,通过构建认证过滤器 AuthenticationWebFilter
完成Oauth2.0的token校验。
AuthenticationWebFilter
通过我们自定义的 ReactiveJdbcAuthenticationManager
完成token校验。
我们在这里还加入了CORS
过滤器,以及权限管理器 AccessManager
权限管理器
publicclassAccessManagerimplementsReactiveAuthorizationManager<AuthorizationContext> { privateSet<String>permitAll=newConcurrentHashSet<>(); privatestaticfinalAntPathMatcherantPathMatcher=newAntPathMatcher(); publicAccessManager (){ permitAll.add("/"); permitAll.add("/error"); permitAll.add("/favicon.ico"); permitAll.add("/**/v2/api-docs/**"); permitAll.add("/**/swagger-resources/**"); permitAll.add("/webjars/**"); permitAll.add("/doc.html"); permitAll.add("/swagger-ui.html"); permitAll.add("/**/oauth/**"); permitAll.add("/**/current/get"); } /*** 实现权限验证判断*/publicMono<AuthorizationDecision>check(Mono<Authentication>authenticationMono, AuthorizationContextauthorizationContext) { ServerWebExchangeexchange=authorizationContext.getExchange(); //请求资源StringrequestPath=exchange.getRequest().getURI().getPath(); // 是否直接放行if (permitAll(requestPath)) { returnMono.just(newAuthorizationDecision(true)); } returnauthenticationMono.map(auth-> { returnnewAuthorizationDecision(checkAuthorities(exchange, auth, requestPath)); }).defaultIfEmpty(newAuthorizationDecision(false)); } /*** 校验是否属于静态资源* @param requestPath 请求路径* @return*/privatebooleanpermitAll(StringrequestPath) { returnpermitAll.stream() .filter(r->antPathMatcher.match(r, requestPath)).findFirst().isPresent(); } //权限校验privatebooleancheckAuthorities(ServerWebExchangeexchange, Authenticationauth, StringrequestPath) { if(authinstanceofOAuth2Authentication){ OAuth2Authenticationathentication= (OAuth2Authentication) auth; StringclientId=athentication.getOAuth2Request().getClientId(); log.info("clientId is {}",clientId); } Objectprincipal=auth.getPrincipal(); log.info("用户信息:{}",principal.toString()); returntrue; } }
主要是过滤掉静态资源,将来一些接口权限校验也可以放在这里。
测试
- 通过网关调用auth-service获取 access_token
- 在Header上添加认证访问后端服务
- 网关过滤器进行token校验
- 权限管理器校验
- 去认证服务器校验当前用户
- 返回正常结果
- 故意写错access_token,返回错误响应
- 请求头上去掉access_token,直接返回
401 Unauthorized
总结
通过以上几步我们将SpringCloud Gateway整合好了Oauth2.0,这样我们整个项目也基本完成了,后面几期再来对项目进行优化,欢迎持续关注。