|
一、背景
随着我们的微服务越来越多,如果每个微服务都要自己去实现一套鉴权操作,那么这么操作比较冗余,因此我们可以把鉴权操作统一放到网关去做,如果微服务自己有额外的鉴权处理,可以在自己的微服务中处理。
二、需求
1、在网关层完成url层面的鉴权操作。
- 所有的OPTION请求都放行。
- 所有不存在请求,直接都拒绝访问。
- user-provider服务的findAllUsers需要user.userInfo权限才可以访问。
2、将解析后的jwt token当做请求头传递到下游服务中。3、整合Spring Security Oauth2 Resource Server。
三、前置条件
1、搭建一个可用的认证服务器
https://juejin.cn/post/6985411823144615972
2、知道Spring Security Oauth2 Resource Server资源服务器如何使用
https://juejin.cn/post/6985893815500406791
四、项目结构

五、网关层代码的编写
1、引入jar包
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>2、自定义授权管理器
自定义授权管理器,判断用户是否有权限访问
此处我们简单判断
- 放行所有的 OPTION 请求。
- 判断某个请求(url)用户是否有权限访问。
- 所有不存在的请求(url)直接无权限访问。
- 另外公众 号Java精选,回复java面试,获取Springcloud面试资料,支持在线刷题。
packagecom.huan.study.gateway.config;importcom.google.common.collect.Maps;importlombok.extern.slf4j.Slf4j;importorg.springframework.http.HttpMethod;importorg.springframework.http.server.reactive.ServerHttpRequest;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.authorization.AuthorizationDecision;importorg.springframework.security.authorization.ReactiveAuthorizationManager;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;importorg.springframework.security.web.server.authorization.AuthorizationContext;importorg.springframework.stereotype.Component;importorg.springframework.util.AntPathMatcher;importorg.springframework.util.PathMatcher;importorg.springframework.util.StringUtils;importorg.springframework.web.server.ServerWebExchange;importreactor.core.publisher.Mono;importjavax.annotation.PostConstruct;importjava.util.Map;importjava.util.Objects;/***自定义授权管理器,判断用户是否有权限访问*/@Component@Slf4jpublicclassCustomReactiveAuthorizationManagerimplementsReactiveAuthorizationManager<AuthorizationContext>{/***此处保存的是资源对应的权限,可以从数据库中获取*/privatestaticfinalMap<String,String>AUTH_MAP=Maps.newConcurrentMap();@PostConstructpublicvoidinitAuthMap(){AUTH_MAP.put(&#34;/user/findAllUsers&#34;,&#34;user.userInfo&#34;);AUTH_MAP.put(&#34;/user/addUser&#34;,&#34;ROLE_ADMIN&#34;);}@OverridepublicMono<AuthorizationDecision>check(Mono<Authentication>authentication,AuthorizationContextauthorizationContext){ServerWebExchangeexchange=authorizationContext.getExchange();ServerHttpRequestrequest=exchange.getRequest();Stringpath=request.getURI().getPath();//带通配符的可以使用这个进行匹配PathMatcherpathMatcher=newAntPathMatcher();Stringauthorities=AUTH_MAP.get(path);log.info(&#34;访问路径:[{}],所需要的权限是:[{}]&#34;,path,authorities);//option请求,全部放行if(request.getMethod()==HttpMethod.OPTIONS){returnMono.just(newAuthorizationDecision(true));}//不在权限范围内的url,全部拒绝if(!StringUtils.hasText(authorities)){returnMono.just(newAuthorizationDecision(false));}returnauthentication.filter(Authentication::isAuthenticated).filter(a->ainstanceofJwtAuthenticationToken).cast(JwtAuthenticationToken.class).doOnNext(token->{System.out.println(token.getToken().getHeaders());System.out.println(token.getTokenAttributes());}).flatMapIterable(AbstractAuthenticationToken::getAuthorities).map(GrantedAuthority::getAuthority).any(authority->Objects.equals(authority,authorities)).map(AuthorizationDecision::new).defaultIfEmpty(newAuthorizationDecision(false));}}3、token认证失败、或超时的处理
packagecom.huan.study.gateway.config;importorg.springframework.core.io.buffer.DataBuffer;importorg.springframework.core.io.buffer.DataBufferUtils;importorg.springframework.http.HttpStatus;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.server.ServerAuthenticationEntryPoint;importorg.springframework.web.server.ServerWebExchange;importreactor.core.publisher.Mono;importjava.nio.charset.StandardCharsets;/***认证失败异常处理*/publicclassCustomServerAuthenticationEntryPointimplementsServerAuthenticationEntryPoint{@OverridepublicMono<Void>commence(ServerWebExchangeexchange,AuthenticationExceptionex){returnMono.defer(()->Mono.just(exchange.getResponse())).flatMap(response->{response.setStatusCode(HttpStatus.UNAUTHORIZED);Stringbody=&#34;{\&#34;code\&#34;:401,\&#34;msg\&#34;:\&#34;token不合法或过期\&#34;}&#34;;DataBufferbuffer=response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));returnresponse.writeWith(Mono.just(buffer)).doOnError(error->DataBufferUtils.release(buffer));});}}4、用户没有权限的处理
packagecom.huan.study.gateway.config;importlombok.extern.slf4j.Slf4j;importorg.springframework.context.annotation.Bean;importorg.springframework.core.io.buffer.DataBuffer;importorg.springframework.core.io.buffer.DataBufferUtils;importorg.springframework.http.HttpStatus;importorg.springframework.http.server.reactive.ServerHttpRequest;importorg.springframework.http.server.reactive.ServerHttpResponse;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.web.server.authorization.ServerAccessDeniedHandler;importorg.springframework.web.server.ServerWebExchange;importreactor.core.publisher.Mono;importjava.nio.charset.StandardCharsets;/***无权限访问异常*/@Slf4jpublicclassCustomServerAccessDeniedHandlerimplementsServerAccessDeniedHandler{@OverridepublicMono<Void>handle(ServerWebExchangeexchange,AccessDeniedExceptiondenied){ServerHttpRequestrequest=exchange.getRequest();returnexchange.getPrincipal().doOnNext(principal->log.info(&#34;用户:[{}]没有访问:[{}]的权限.&#34;,principal.getName(),request.getURI())).flatMap(principal->{ServerHttpResponseresponse=exchange.getResponse();response.setStatusCode(HttpStatus.FORBIDDEN);Stringbody=&#34;{\&#34;code\&#34;:403,\&#34;msg\&#34;:\&#34;您无权限访问\&#34;}&#34;;DataBufferbuffer=response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));returnresponse.writeWith(Mono.just(buffer)).doOnError(error->DataBufferUtils.release(buffer));});}}5、将token信息传递到下游服务器中
packagecom.huan.study.gateway.config;importcom.fasterxml.jackson.core.JsonProcessingException;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.datatype.jdk8.Jdk8Module;importcom.fasterxml.jackson.datatype.jsr310.JavaTimeModule;importorg.springframework.http.server.reactive.ServerHttpRequest;importorg.springframework.security.core.context.ReactiveSecurityContextHolder;importorg.springframework.security.core.context.SecurityContext;importorg.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;importorg.springframework.web.server.ServerWebExchange;importorg.springframework.web.server.WebFilter;importorg.springframework.web.server.WebFilterChain;importreactor.core.publisher.Mono;/***将token信息传递到下游服务中 公众 号Java精选,有惊喜**@authorhuan.fu2021/8/25-下午2:49*/publicclassTokenTransferFilterimplementsWebFilter{privatestaticfinalObjectMapperOBJECT_MAPPER=newObjectMapper();static{OBJECT_MAPPER.registerModule(newJdk8Module());OBJECT_MAPPER.registerModule(newJavaTimeModule());}@OverridepublicMono<Void>filter(ServerWebExchangeexchange,WebFilterChainchain){returnReactiveSecurityContextHolder.getContext().map(SecurityContext::getAuthentication).cast(JwtAuthenticationToken.class).flatMap(authentication->{ServerHttpRequestrequest=exchange.getRequest();request=request.mutate().header(&#34;tokenInfo&#34;,toJson(authentication.getPrincipal())).build();ServerWebExchangenewExchange=exchange.mutate().request(request).build();returnchain.filter(newExchange);});}publicStringtoJson(Objectobj){try{returnOBJECT_MAPPER.writeValueAsString(obj);}catch(JsonProcessingExceptione){returnnull;}}}6、网关层面的配置
packagecom.huan.study.gateway.config;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.convert.converter.Converter;importorg.springframework.core.io.FileSystemResource;importorg.springframework.core.io.Resource;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;importorg.springframework.security.config.web.server.SecurityWebFiltersOrder;importorg.springframework.security.config.web.server.ServerHttpSecurity;importorg.springframework.security.oauth2.jose.jws.SignatureAlgorithm;importorg.springframework.security.oauth2.jwt.Jwt;importorg.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;importorg.springframework.security.oauth2.jwt.ReactiveJwtDecoder;importorg.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;importorg.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;importorg.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;importorg.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;importorg.springframework.security.web.server.SecurityWebFilterChain;importreactor.core.publisher.Mono;importjava.io.IOException;importjava.nio.file.Files;importjava.security.KeyFactory;importjava.security.NoSuchAlgorithmException;importjava.security.interfaces.RSAPublicKey;importjava.security.spec.InvalidKeySpecException;importjava.security.spec.X509EncodedKeySpec;importjava.util.Base64;/***资源服务器配置*/@Configuration@EnableWebFluxSecuritypublicclassResourceServerConfig{@AutowiredprivateCustomReactiveAuthorizationManagercustomReactiveAuthorizationManager;@BeanpublicSecurityWebFilterChainsecurityWebFilterChain(ServerHttpSecurityhttp)throwsNoSuchAlgorithmException,IOException,InvalidKeySpecException{http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter()).jwtDecoder(jwtDecoder()).and()//认证成功后没有权限操作.accessDeniedHandler(newCustomServerAccessDeniedHandler())//还没有认证时发生认证异常,比如token过期,token不合法.authenticationEntryPoint(newCustomServerAuthenticationEntryPoint())//将一个字符串token转换成一个认证对象.bearerTokenConverter(newServerBearerTokenAuthenticationConverter()).and().authorizeExchange()//所有以/auth/**开头的请求全部放行.pathMatchers(&#34;/auth/**&#34;,&#34;/favicon.ico&#34;).permitAll()//所有的请求都交由此处进行权限判断处理.anyExchange().access(customReactiveAuthorizationManager).and().exceptionHandling().accessDeniedHandler(newCustomServerAccessDeniedHandler()).authenticationEntryPoint(newCustomServerAuthenticationEntryPoint()).and().csrf().disable().addFilterAfter(newTokenTransferFilter(),SecurityWebFiltersOrder.AUTHENTICATION);returnhttp.build();}/***从jwt令牌中获取认证对象*/publicConverter<Jwt,?extendsMono<?extendsAbstractAuthenticationToken>>jwtAuthenticationConverter(){//从jwt中获取该令牌可以访问的权限JwtGrantedAuthoritiesConverterauthoritiesConverter=newJwtGrantedAuthoritiesConverter();//取消权限的前缀,默认会加上SCOPE_authoritiesConverter.setAuthorityPrefix(&#34;&#34;);//从那个字段中获取权限authoritiesConverter.setAuthoritiesClaimName(&#34;scope&#34;);JwtAuthenticationConverterjwtAuthenticationConverter=newJwtAuthenticationConverter();//获取principalnamejwtAuthenticationConverter.setPrincipalClaimName(&#34;sub&#34;);jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);returnnewReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);}/***解码jwt*/publicReactiveJwtDecoderjwtDecoder()throwsIOException,NoSuchAlgorithmException,InvalidKeySpecException{Resourceresource=newFileSystemResource(&#34;/Users/huan/code/study/idea/spring-cloud-alibaba-parent/gateway-oauth2/new-authoriza-server-public-key.pem&#34;);StringpublicKeyStr=String.join(&#34;&#34;,Files.readAllLines(resource.getFile().toPath()));byte[]publicKeyBytes=Base64.getDecoder().decode(publicKeyStr);X509EncodedKeySpeckeySpec=newX509EncodedKeySpec(publicKeyBytes);KeyFactorykeyFactory=KeyFactory.getInstance(&#34;RSA&#34;);RSAPublicKeyrsaPublicKey=(RSAPublicKey)keyFactory.generatePublic(keySpec);returnNimbusReactiveJwtDecoder.withPublicKey(rsaPublicKey).signatureAlgorithm(SignatureAlgorithm.RS256).build();}}7、网关yaml配置文件
spring:application:name:gateway-authcloud:nacos:discovery:server-addr:localhost:8847gateway:routes:-id:user-provideruri:lb://user-providerpredicates:-Path=/user/**filters:-RewritePath=/user(?<segment>/?.*),$\{segment}compatibility-verifier:#取消SpringCloudSpringCloudAlibabaSpringBoot等的版本检查enabled:falseserver:port:9203debug:true六、演示
1、客户端gateway 在认证服务器拥有的权限为user.userInfo

2、user-provider服务提供了一个apifindAllUsers,它会返回 系统中存在的用户(假的数据) 和 解码后的token信息。面试宝典:https://www.yoodb.com
3、在网关层面,findAllUsers 需要的权限为user.userInfo,正好gateway这个客户端有这个权限,所以可以访问。
七、代码路径
https://gitee.com/huan1993/spring-cloud-alibaba-parent/tree/master/gateway-oauth2
作者:huan1993
https://juejin.cn/post/7000353332824899614
公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!Java精选面试题(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!------ 特别推荐 ------特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注。点击“阅读原文”,了解更多精彩内容!文章有帮助的话,点在看,转发吧! |
|