赞
踩
上篇文章主要讲解Oauth2模块、user-service模块、feign模块,那么作为重中之重的gateway,我们将其做成资源服务器来进行开发。
资源服务器在实际开发有两种实现方式:
(1)gateway做网关转发,不做资源服务器,由各个微服务模块自己去做资源服务器;
(2)gateway做网关转发 并且 做资源服务器。
前者方案使得每一个微服务模块都需要导入oauth2相关依赖,并且做处理,过于繁琐且耦合高。
所以本文章在接下来介绍,也就是文章的重点,并且会介绍到如何解决通过gateway去认证授权,跳转到oauth2认证授权后,跳转不回或重定向不到gatway的bug。
- <dependencies>
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-oauth2-jose</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-oauth2-resource-server</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-oauth2</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-gateway</artifactId>
- </dependency>
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
- </dependency>
-
- <!--加载bootstrap 文件-->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-bootstrap</artifactId>
- </dependency>
- <!--客户端负载均衡loadbalancer-->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-loadbalancer</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </dependency>
- <dependency>
- <groupId>com.white</groupId>
- <artifactId>common</artifactId>
- <version>1.0</version>
- <scope>compile</scope>
- </dependency>
-
- </dependencies>
- server:
- port: 10000
-
- spring:
- application:
- name: gateway
- profiles:
- active: dev
- cloud:
- gateway:
- routes:
- - id: user
- uri: lb://user-service # 客户端负载均衡 loadbalancer
- predicates:
- - Path=/user/**,/admin/**
- - id: order
- uri: lb://order-service
- predicates:
- - Path=/order/**
- - id: oauth
- uri: lb://oauth-service
- predicates:
- - Path=/uaa/**
- nacos:
- discovery:
- server-addr: localhost:8848
- redis:
- host: 127.0.0.1
- port: 6379
- security:
- oauth2:
- resourceserver:
- jwt:
- #配置RSA的公钥访问地址 端口对应上篇文章的oauth2模块服务的端口
- jwk-set-uri: 'http://localhost:8101/uaa/rsa/publicKey'
- main:
- web-application-type: reactive
- @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- public class GatewayApp
- {
- public static void main( String[] args )
- {
-
- SpringApplication.run(GatewayApp.class,args);
- }
- }
- package com.white.gateway.config;
-
- import org.springframework.stereotype.Component;
-
- import java.util.ArrayList;
- import java.util.List;
-
- @Component
- public class IgnoreUrlsConfig {
- public List<String> getUrls() {
- ArrayList<String> objects = new ArrayList<>();
- objects.add("/uaa/**");
- objects.add("/user/**");
- return objects;
- }
- }
- package com.white.gateway.filter;
-
- import com.white.gateway.config.IgnoreUrlsConfig;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.server.reactive.ServerHttpRequest;
- import org.springframework.stereotype.Component;
- import org.springframework.util.AntPathMatcher;
- import org.springframework.util.PathMatcher;
- import org.springframework.web.server.ServerWebExchange;
- import org.springframework.web.server.WebFilter;
- import org.springframework.web.server.WebFilterChain;
- import reactor.core.publisher.Mono;
- import var.TokenVar;
-
- import java.net.URI;
- import java.util.List;
-
- /**
- * 白名单路径访问时需要移除JWT请求头
- */
- @Component
- public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
- @Autowired
- private IgnoreUrlsConfig ignoreUrlsConfig;
-
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
- ServerHttpRequest request = exchange.getRequest();
- URI uri = request.getURI();
- PathMatcher pathMatcher = new AntPathMatcher();
-
- //白名单路径移除JWT请求头
- List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
- for (String ignoreUrl : ignoreUrls) {
- if (pathMatcher.match(ignoreUrl, uri.getPath())) {
- request = request.mutate().header(TokenVar.TOKEN_HEAD, "").build();
- exchange = exchange.mutate().request(request).build();
-
- return chain.filter(exchange);
- }
- }
-
- return chain.filter(exchange);
- }
- }
注意:这里拦截了路径为/oauth/authorize,在其进行跳转的时候构建响应包装类,解决通过gateway去oauth认证时,oauth成功登录后跳转不回gateway网关的bug。
- package com.white.gateway.filter;
-
- import cn.hutool.core.util.StrUtil;
- import cn.hutool.json.JSONUtil;
- import cn.hutool.json.JSONObject;
- import com.alibaba.cloud.commons.lang.StringUtils;
- import com.nimbusds.jose.JWSObject;
- import org.reactivestreams.Publisher;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cloud.gateway.filter.GatewayFilterChain;
- import org.springframework.cloud.gateway.filter.GlobalFilter;
- import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
- import org.springframework.core.Ordered;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpRequest;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import reactor.core.publisher.Mono;
- import var.TokenVar;
-
- import java.text.ParseException;
- import java.util.Objects;
-
- /**
- * 将登录用户的JWT转化成用户信息的全局过滤器
- */
- @Component
- public class AuthGlobalFilter implements GlobalFilter, Ordered {
- @Autowired
- private RedisTemplate redisTemplate;
-
- @Override
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- //TODO
- String path = exchange.getRequest().getPath().value();
- System.out.println("拦截到的路径:::" + path);
- if (path.contains("/oauth/authorize") || path.contains("/auth/authorize") || path.contains("/auth/loginBySms")) {
- //构建响应包装类
- HttpResponseDecorator responseDecorator = new HttpResponseDecorator(exchange.getRequest(), exchange.getResponse(), "http://localhost:10000");
- return chain
- .filter(exchange.mutate().response(responseDecorator).build());
- }
-
-
- //认证信息从Header 或 请求参数 中获取
- ServerHttpRequest serverHttpRequest = exchange.getRequest();
- String token = serverHttpRequest.getHeaders().getFirst(TokenVar.TOKEN_HEAD);
- if (Objects.isNull(token)) {
- token = serverHttpRequest.getQueryParams().getFirst(TokenVar.TOKEN_HEAD);
- }
-
- if (StrUtil.isEmpty(token)) {
- return chain.filter(exchange);
- }
- try {
- //从token中解析用户信息并设置到Header中去
- String realToken = token.replace(TokenVar.TOKEN_PREFIX, "");
- JWSObject jwsObject = JWSObject.parse(realToken);
- String userStr = jwsObject.getPayload().toString();
-
- // 黑名单token(登出、修改密码)校验
- JSONObject jsonObject = JSONUtil.parseObj(userStr);
- String jti = jsonObject.getStr("jti");
-
- Boolean isBlack = redisTemplate.hasKey(TokenVar.TOKEN_BLACKLIST_PREFIX + jti);
- if (isBlack) {
-
- }
-
- // 存在token且不是黑名单,request写入JWT的载体信息
- ServerHttpRequest request = serverHttpRequest.mutate().header(TokenVar.USER_TOKEN_HEADER, userStr).build();
- exchange = exchange.mutate().request(request).build();
- } catch (ParseException e) {
- e.printStackTrace();
- }
-
- return chain.filter(exchange);
- }
-
- public class HttpResponseDecorator extends ServerHttpResponseDecorator {
-
- private String proxyUrl;
-
- private ServerHttpRequest request;
-
- public HttpResponseDecorator(ServerHttpRequest request, ServerHttpResponse delegate, String proxyUrl) {
- super(delegate);
- this.request = request;
- this.proxyUrl = proxyUrl;
- }
-
- @Override
- public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
- HttpStatus status = this.getStatusCode();
- if (status.equals(HttpStatus.FOUND)) {
- String domain = "";
- if (StringUtils.isBlank(proxyUrl)) {
- domain = request.getURI().getScheme() + "://" + request.getURI().getAuthority();
- } else {
- domain = proxyUrl;
- }
- String location = getHeaders().getFirst("Location");
- String replaceLocation = location.replaceAll("^((ht|f)tps?):\\/\\/(\\d{1,3}.){3}\\d{1,3}(:\\d+)?", domain);
- getHeaders().set("Location", replaceLocation);
- }
- this.getStatusCode();
- return super.writeWith(body);
- }
- }
-
- @Override
- public int getOrder() {
- return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
- }
- }
注意:这里的鉴权采用了对redis数据进行读取后,匹配当前的路径和请求方式是否与redis数据中一致,在此前提下判断当前的token是否与redis中存放的身份权限一致或包含其中,如果包含或一致才可以放行去请求资源,否则请求资源失败。
至于redis的数据从哪来,下面程序中是模拟添加了一个,实际开发中,在MySQL数据库创建一张相应的请求方式+路径,以及请求时用户必须要有的权限是什么的数据表,通过初次查询缓存到redis中,之后就可以通过redis进行数据读取了,内存速度快。
- package com.white.gateway.config;
-
- import cn.hutool.core.collection.CollectionUtil;
- import cn.hutool.core.convert.Convert;
- import cn.hutool.core.util.StrUtil;
- import jdk.nashorn.internal.runtime.GlobalConstants;
- import lombok.AllArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.http.HttpMethod;
- import org.springframework.http.server.reactive.ServerHttpRequest;
- 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.web.server.authorization.AuthorizationContext;
- import org.springframework.stereotype.Component;
- import org.springframework.util.AntPathMatcher;
- import org.springframework.util.PathMatcher;
- import reactor.core.publisher.Mono;
-
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- /**
- * @ResourceServerManager.java的作用:鉴权管理器的相关配置
- * 负责被ResourceServerConfig.java文件引用
- * @author: white文
- * @time: 2023/5/30 0:57
- */
- @Component
- @AllArgsConstructor
- @Slf4j
- public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
-
- @Autowired
- private RedisTemplate redisTemplate;
- @Override
- public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
- ServerHttpRequest request = authorizationContext.getExchange().getRequest();
- if (request.getMethod() == HttpMethod.OPTIONS) { // 预检请求放行
- return Mono.just(new AuthorizationDecision(true));
- }
- PathMatcher pathMatcher = new AntPathMatcher();
- String method = request.getMethodValue();
- String path = request.getURI().getPath();
- String restfulPath = method + ":" + path; // RESTFul接口权限设计: https://www.cnblogs.com/haoxianrui/p/14961707.html
- String token = request.getHeaders().getFirst("Authorization");
-
-
- // 如果token为空 或 不是以"bearer "为前缀 则无效并且需要鉴权
- if (!StrUtil.isNotBlank(token) || !StrUtil.startWithIgnoreCase(token, "Bearer ") ) {
- log.info("token为空 或 不是以 bearer 为前缀 则无效并且需要鉴权");
- return Mono.just(new AuthorizationDecision(false));
- }
- log.info("鉴权开始");
-
- /**
- * 鉴权开始
- *
- * 缓存取 [URL权限-角色集合] 规则数据
- * urlPermRolesRules = [{'key':'GET:/admin/*','value':['ADMIN','TEST']},...]
- */
- Map<String, Object> urlPermRolesRules = redisTemplate.opsForHash().entries("auth:resourceRolesMap");
- if (urlPermRolesRules.isEmpty()) {
- log.info("空的,我手动加一些上去");
- ArrayList<String> objects = new ArrayList<>();
- objects.add("TEST");
- objects.add("USER");
- redisTemplate.opsForHash().put("auth:resourceRolesMap","GET:/admin/*",objects);
- urlPermRolesRules = redisTemplate.opsForHash().entries("auth:resourceRolesMap");
- }
-
- // 根据请求路径获取有访问权限的角色列表
- List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
- boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
-
- for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
- String perm = permRoles.getKey();
- System.out.println("路径:"+perm+" 值:"+permRoles.getValue().toString());
- // 判断传过来的 方法:路径 是否在redis缓存中
- if (pathMatcher.match(perm, restfulPath)) {
- List<String> roles = Convert.toList(String.class, permRoles.getValue());
- // 加入授权数组中
- authorizedRoles.addAll(roles);
- if (requireCheck == false) {
- requireCheck = true;
- }
- }
- }
- // 没有设置拦截规则放行
- if (requireCheck == false) {
- return Mono.just(new AuthorizationDecision(true));
- }
-
- // 判断JWT中携带的用户角色是否有权限访问
- Mono<AuthorizationDecision> authorizationDecisionMono = mono
- .filter(Authentication::isAuthenticated)
- .flatMapIterable(Authentication::getAuthorities)
- .map(GrantedAuthority::getAuthority)
- .any(authority -> {
- String roleCode = StrUtil.removePrefix(authority,"ROLE_");// ROLE_ADMIN移除前缀ROLE_得到用户的角色编码ADMIN
- if (String.valueOf("ADMIN").equals(roleCode)) {
- return true; // 如果是超级管理员则放行
- }
- boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
- return hasAuthorized;
- })
- .map(AuthorizationDecision::new)
- .defaultIfEmpty(new AuthorizationDecision(false));
- return authorizationDecisionMono;
- }
- }
-
-
在该文件下,如果想实现 token无效或者已过期自定义响应(ServerAuthenticationEntryPoint) 和 自定义未授权响应(ServerAccessDeniedHandler) 的话,可以自定义配置返回前端的相关配置,以下代码没做实现,采用程序默认返回给前端的401,如下效果:
- package com.white.gateway.config;
-
- import cn.hutool.core.convert.Convert;
- import cn.hutool.core.util.ArrayUtil;
- import com.white.gateway.filter.IgnoreUrlsRemoveJwtFilter;
- import lombok.Setter;
- import lombok.SneakyThrows;
- import lombok.extern.slf4j.Slf4j;
- 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.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.jwt.Jwt;
- 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.web.server.SecurityWebFilterChain;
- import reactor.core.publisher.Mono;
-
- import java.security.interfaces.RSAPublicKey;
- import java.util.List;
-
- /**
- * 资源服务器配置
- */
-
- @Configuration
- @EnableWebFluxSecurity
- @Slf4j
- public class ResourceServerConfig {
-
- @Autowired
- private ResourceServerManager resourceServerManager;
-
- @Autowired
- private IgnoreUrlsConfig ignoreUrlsConfig;
-
- @Autowired
- private IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
-
- @Bean
- public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
- http
- .oauth2ResourceServer()
- .jwt()
- .jwtAuthenticationConverter(jwtAuthenticationConverter());
-
- //TODO 对白名单路径,直接移除JWT请求头
- http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
-
- http.authorizeExchange()
- //白名单配置
- .pathMatchers(Convert.toStrArray(ignoreUrlsConfig.getUrls())).permitAll()
- //鉴权管理器配置
- .anyExchange().access(resourceServerManager)
- .and().csrf().disable();
-
- return http.build();
- }
-
- /**
- * @link https://blog.csdn.net/qq_24230139/article/details/105091273
- * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
- * 需要把jwt的Claim中的authorities加入
- * 方案:重新定义权限管理器,默认转换器JwtGrantedAuthoritiesConverter
- */
- @Bean
- public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
- JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
- jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
- jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
-
- JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
- jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
- return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
- }
-
- }
- package com.white.gateway.config;
-
- import com.fasterxml.jackson.annotation.JsonAutoDetect;
- import com.fasterxml.jackson.annotation.PropertyAccessor;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.cache.annotation.CachingConfigurerSupport;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- import javax.annotation.Resource;
-
- /*
- * Redis配置
- * 解决redis在业务逻辑处理层RedisCon上不出错,缓存序列化问题
- * @author: white
- * */
- @Configuration
- @EnableCaching
- public class RedisConfig extends CachingConfigurerSupport {
- @Resource
- RedisConnectionFactory redisConnectionFactory;
- @Bean
- public RedisTemplate<String,Object> redisTemplate(){
- System.out.println("gateway 读取redis配置");
- RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- //Json序列化配置
- //1、String的序列化
- StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
- // key采用String的序列化方式
- redisTemplate.setKeySerializer(stringRedisSerializer);
- // hash的key也采用String的序列化方式
- redisTemplate.setHashKeySerializer(stringRedisSerializer);
-
- //2、json解析任意的对象(Object),变成json序列化
- Jackson2JsonRedisSerializer<Object> serializer=new Jackson2JsonRedisSerializer<Object>(Object.class);
- ObjectMapper mapper=new ObjectMapper(); //用ObjectMapper进行转义
- mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- //该方法是指定序列化输入的类型,就是将数据库里的数据按照一定类型存储到redis缓存中。
- mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- serializer.setObjectMapper(mapper);
-
- // value序列化方式采用jackson
- redisTemplate.setValueSerializer(serializer);
- // hash的value序列化方式采用jackson
- redisTemplate.setHashValueSerializer(serializer);
-
- return redisTemplate;
- }
- }
- package var;
-
- public interface TokenVar {
- public static final String APP_SECRET ="white";
- public static final String TOKEN_HEAD="Authorization"; // 认证信息Http请求头
-
- public static final String TOKEN_PREFIX = "Bearer "; // JWT令牌前缀
- /**
- * JWT存储权限前缀
- */
- String AUTHORITY_PREFIX = "ROLE_";
-
- /**
- * JWT存储权限属性
- */
- String AUTHORITY_CLAIM_NAME = "authorities";
-
- /**
- * 后台client_id
- */
- String ADMIN_CLIENT_ID = "api-admin";
-
- /**
- * 前端client_id
- */
- String PORTAL_CLIENT_ID = "api-portal";
-
- /**
- * 后台接口路径匹配
- */
- String ADMIN_URL_PATTERN = "/admin/**";
-
- /**
- * Redis缓存权限规则key
- */
- String RESOURCE_ROLES_MAP_KEY = "auth:resourceRolesMap";
-
- /**
- * 用户信息Http请求头
- */
- String USER_TOKEN_HEADER = "user";
-
- /**
- * 黑名单
- */
- String TOKEN_BLACKLIST_PREFIX = "blacklist";
- }
与第二篇文章的测试相同,只不过我们将地址的端口改成10000,意思是通过网关去请求oauth认证授权。
http://localhost:10000/uaa/oauth/authorize?client_id=123&response_type=code&scop=all&redirect_uri=http://localhost:10000
自动跳转到:http://localhost:10000/uaa/login
在前面的过滤中,我们仅仅对/user/**,/uaa/**两个路径进行白名单路径设置,而在user-service模块中,小编还设置一个/admin/**的一个路径,该路径没有被设置进白名单,并且在redis的设置中,该路径需要权限为:admin。
现在不带token来请求/admin/1,如下,结果为401:
现在我们带着token,并且token中的用户信息权限为admin
到此,gateway+oauth整合完成了,接下来会在评论区出下一章,下一章会针对oauth的推出登录如何解决以及如何整合第三方应用进行登录(如:gitee平台)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。