网易首页 > 网易号 > 正文 申请入驻

SpringCloud Gateway + Jwt + Oauth2 实现网关的鉴权操作,真是绝了!

0
分享至

一、背景

随着我们的微服务越来越多,如果每个微服务都要自己去实现一套鉴权操作,那么这么操作比较冗余,因此我们可以把鉴权操作统一放到网关去做,如果微服务自己有额外的鉴权处理,可以在自己的微服务中处理。

二、需求

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包
com.alibaba.cloudgroupId>
spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
org.springframework.cloudgroupId>
spring-cloud-starter-gatewayartifactId>
dependency>
org.springframework.bootgroupId>
spring-boot-starter-oauth2-resource-serverartifactId>
dependency>
org.springframework.bootgroupId>
spring-boot-starter-securityartifactId>
dependency>
org.springframework.cloudgroupId>
spring-cloud-starter-loadbalancerartifactId>
dependency>
2、自定义授权管理器

自定义授权管理器,判断用户是否有权限访问

此处我们简单判断

  • 放行所有的 OPTION 请求。

  • 判断某个请求(url)用户是否有权限访问。

  • 所有不存在的请求(url)直接无权限访问。

  • 另外公众 号Java精选,回复java面试,获取Springcloud面试资料,支持在线刷题。

package com.huan.study.gateway.config;

import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.Objects;

/**
* 自定义授权管理器,判断用户是否有权限访问
*/
@Component
@Slf4j
public class CustomReactiveAuthorizationManager implements ReactiveAuthorizationManager {

/**
* 此处保存的是资源对应的权限,可以从数据库中获取
*/
private static final Map AUTH_MAP = Maps.newConcurrentMap();

@PostConstruct
public void initAuthMap() {
AUTH_MAP.put("/user/findAllUsers", "user.userInfo");
AUTH_MAP.put("/user/addUser", "ROLE_ADMIN");
}

@Override
public Mono check(Mono authentication, AuthorizationContext authorizationContext) {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();

// 带通配符的可以使用这个进行匹配
PathMatcher pathMatcher = new AntPathMatcher();
String authorities = AUTH_MAP.get(path);
log.info("访问路径:[{}],所需要的权限是:[{}]", path, authorities);

// option 请求,全部放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}

// 不在权限范围内的url,全部拒绝
if (!StringUtils.hasText(authorities)) {
return Mono.just(new AuthorizationDecision(false));
}

return authentication
.filter(Authentication::isAuthenticated)
.filter(a -> a instanceof JwtAuthenticationToken)
.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(new AuthorizationDecision(false));
}
}
3、token认证失败、或超时的处理package com.huan.study.gateway.config;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
* 认证失败异常处理
*/
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@Override
public Mono commence(ServerWebExchange exchange, AuthenticationException ex) {

return Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String body = "{\"code\":401,\"msg\":\"token不合法或过期\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer))
.doOnError(error -> DataBufferUtils.release(buffer));
});
}
}
4、用户没有权限的处理package com.huan.study.gateway.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
* 无权限访问异常
*/
@Slf4j
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {

@Override
public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) {

ServerHttpRequest request = exchange.getRequest();

return exchange.getPrincipal()
.doOnNext(principal -> log.info("用户:[{}]没有访问:[{}]的权限.", principal.getName(), request.getURI()))
.flatMap(principal -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
String body = "{\"code\":403,\"msg\":\"您无权限访问\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer))
.doOnError(error -> DataBufferUtils.release(buffer));
});
}
}
5、将token信息传递到下游服务器中package com.huan.study.gateway.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

/**
* 将token信息传递到下游服务中 公众 号Java精选,有惊喜
*
* @author huan.fu 2021/8/25 - 下午2:49
*/
public class TokenTransferFilter implements WebFilter {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

static {
OBJECT_MAPPER.registerModule(new Jdk8Module());
OBJECT_MAPPER.registerModule(new JavaTimeModule());
}

@Override
public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.cast(JwtAuthenticationToken.class)
.flatMap(authentication -> {
ServerHttpRequest request = exchange.getRequest();
request = request.mutate()
.header("tokenInfo", toJson(authentication.getPrincipal()))
.build();

ServerWebExchange newExchange = exchange.mutate().request(request).build();

return chain.filter(newExchange);
});
}

public String toJson(Object obj) {
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return null;
}
}
}
6、网关层面的配置package com.huan.study.gateway.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
* 资源服务器配置
*/
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

@Autowired
private CustomReactiveAuthorizationManager customReactiveAuthorizationManager;

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.jwtDecoder(jwtDecoder())
.and()
// 认证成功后没有权限操作
.accessDeniedHandler(new CustomServerAccessDeniedHandler())
// 还没有认证时发生认证异常,比如token过期,token不合法
.authenticationEntryPoint(new CustomServerAuthenticationEntryPoint())
// 将一个字符串token转换成一个认证对象
.bearerTokenConverter(new ServerBearerTokenAuthenticationConverter())
.and()
.authorizeExchange()
// 所有以 /auth/** 开头的请求全部放行
.pathMatchers("/auth/**", "/favicon.ico").permitAll()
// 所有的请求都交由此处进行权限判断处理
.anyExchange()
.access(customReactiveAuthorizationManager)
.and()
.exceptionHandling()
.accessDeniedHandler(new CustomServerAccessDeniedHandler())
.authenticationEntryPoint(new CustomServerAuthenticationEntryPoint())
.and()
.csrf()
.disable()
.addFilterAfter(new TokenTransferFilter(), SecurityWebFiltersOrder.AUTHENTICATION);

return http.build();
}

/**
* 从jwt令牌中获取认证对象
*/
public Converter> jwtAuthenticationConverter() {

// 从jwt 中获取该令牌可以访问的权限
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 取消权限的前缀,默认会加上SCOPE_
authoritiesConverter.setAuthorityPrefix("");
// 从那个字段中获取权限
authoritiesConverter.setAuthoritiesClaimName("scope");

JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
// 获取 principal name
jwtAuthenticationConverter.setPrincipalClaimName("sub");
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);

return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}

/**
* 解码jwt
*/
public ReactiveJwtDecoder jwtDecoder() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Resource resource = new FileSystemResource("/Users/huan/code/study/idea/spring-cloud-alibaba-parent/gateway-oauth2/new-authoriza-server-public-key.pem");
String publicKeyStr = String.join("", Files.readAllLines(resource.getFile().toPath()));
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);

return NimbusReactiveJwtDecoder.withPublicKey(rsaPublicKey)
.signatureAlgorithm(SignatureAlgorithm.RS256)
.build();
}
}
7、网关yaml配置文件spring:
application:
name: gateway-auth
cloud:
nacos:
discovery:
server-addr: localhost:8847
gateway:
routes:
- id: user-provider
uri: lb://user-provider
predicates:
- Path=/user/**
filters:
- RewritePath=/user(?/?.*), $\{segment}
compatibility-verifier:
# 取消SpringCloud SpringCloudAlibaba SpringBoot 等的版本检查
enabled: false
server:
port: 9203
debug: 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精选,回复“加群”,即可入群!

(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

------ 特别推荐 ------

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
99年禹作敏吞药自杀后,葬入大邱庄祖坟,坟墓前冷冷清清无人祭拜

99年禹作敏吞药自杀后,葬入大邱庄祖坟,坟墓前冷冷清清无人祭拜

我是斌哥哥
2024-06-20 17:27:40
第一次性生活有多痛?进不去怎么办

第一次性生活有多痛?进不去怎么办

喜马拉雅主播暮霭
2024-06-12 09:53:49
浑身都是“寄生虫”的3种鱼,卖鱼老板自己从来不吃,白送也别要

浑身都是“寄生虫”的3种鱼,卖鱼老板自己从来不吃,白送也别要

吃货的分享
2024-06-19 19:50:34
男生要坚持多久,女生才会觉得爽?

男生要坚持多久,女生才会觉得爽?

喜马拉雅主播暮霭
2024-06-19 11:58:36
黑龙江省拜泉县人大常委会四级调研员赫秀军主动投案接受纪律审查和监察调查

黑龙江省拜泉县人大常委会四级调研员赫秀军主动投案接受纪律审查和监察调查

鲁中晨报
2024-06-20 10:51:03
史上最乱中国男篮?大牌球星报到就离开,核心主力根本不回归

史上最乱中国男篮?大牌球星报到就离开,核心主力根本不回归

德译洋洋
2024-06-20 12:22:42
乔治:去年莫雷对哈登顶薪的食言会影响76人的名声

乔治:去年莫雷对哈登顶薪的食言会影响76人的名声

懂球帝
2024-06-20 15:47:11
姜萍越来越危险了!

姜萍越来越危险了!

财经要参
2024-06-20 08:30:33
震惊!旁边的同事,猝死了

震惊!旁边的同事,猝死了

蚂蚁大喇叭
2024-06-19 09:44:52
广东两名市委原常委、政法委原书记被处理

广东两名市委原常委、政法委原书记被处理

新京报
2024-06-19 19:52:07
痛心!又一老总跳楼身亡,年仅55岁,知情人曝轻生原因,太可惜了

痛心!又一老总跳楼身亡,年仅55岁,知情人曝轻生原因,太可惜了

娱乐小可爱蛙
2024-06-11 20:34:17
新援首秀,申花3外援组首发11人,戴伟浚拿弱旅找自信 门将换替补

新援首秀,申花3外援组首发11人,戴伟浚拿弱旅找自信 门将换替补

替补席看球
2024-06-20 17:08:20
复旦法学院毕业典礼:学生打老师,原因曝出,知情人透露更多细节

复旦法学院毕业典礼:学生打老师,原因曝出,知情人透露更多细节

小淇言说
2024-06-19 23:06:10
一个敢问,一个敢答,公司有计划收购贵州茅台吗?回答:没有!

一个敢问,一个敢答,公司有计划收购贵州茅台吗?回答:没有!

财经市界
2024-06-20 08:44:04
说透了美国做着和俄核战、印太全面战争准备,印太针对超过对苏联

说透了美国做着和俄核战、印太全面战争准备,印太针对超过对苏联

邵旭峰域
2024-06-19 15:30:02
质疑姜萍作弊的猎巫闹剧,该结束了!

质疑姜萍作弊的猎巫闹剧,该结束了!

独角鲸工作坊
2024-06-20 18:49:45
后续!继央视为俞莉打抱不平后,党媒也来力挺,网友:说的太好了

后续!继央视为俞莉打抱不平后,党媒也来力挺,网友:说的太好了

户外小阿隋
2024-06-20 15:26:05
上海某账户炒股3月赚17万,警方调查发现,户主竟是58岁的农民工

上海某账户炒股3月赚17万,警方调查发现,户主竟是58岁的农民工

史二了
2024-06-20 16:34:56
小舒梅切尔谈欧洲杯用球:它很滑,不是为门将设计的,而是为前锋

小舒梅切尔谈欧洲杯用球:它很滑,不是为门将设计的,而是为前锋

直播吧
2024-06-19 16:40:09
导弹从天而降!船只起火一名菲船员失踪,菲方:将采取必要措施

导弹从天而降!船只起火一名菲船员失踪,菲方:将采取必要措施

兵说
2024-06-19 17:20:34
2024-06-20 19:14:44
Java精选
Java精选
一场永远也演不完的戏
1551文章数 3855关注度
往期回顾 全部

科技要闻

小米SU7流量泼天,富贵却被蔚来接住了

头条要闻

女大学生称按摩时遭男技师扒内裤 警方初步判断是擦边

头条要闻

女大学生称按摩时遭男技师扒内裤 警方初步判断是擦边

体育要闻

绿军的真老大,开始备战下赛季了

娱乐要闻

叶舒华参加柯震东生日聚会,五毒俱全

财经要闻

深圳一网红学位房14万/平跌到4万/平

汽车要闻

售价11.79-14.39万元 新一代哈弗H6正式上市

态度原创

手机
艺术
亲子
公开课
军事航空

手机要闻

小米vivo荣耀三大旗舰手机年底发布 我已经闻到火药味了

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

亲子要闻

宝宝满眼都是妈妈,母子俩长得太像了,连笑容都一样

公开课

近视只是视力差?小心并发症

军事要闻

乌媒:乌军遭受一系列惨痛失败 乌军总司令或将被解职

无障碍浏览 进入关怀版