<artifactId>spring-cloud-starter-consul-discovery</artifactId> | <artifactId>spring-cloud-starter-consul-discovery</artifactId> | ||||
<version>3.1.1</version> | <version>3.1.1</version> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>com.auth0</groupId> | |||||
<artifactId>java-jwt</artifactId> | |||||
<version>3.10.0</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.alibaba</groupId> | |||||
<artifactId>fastjson</artifactId> | |||||
<version>1.2.76</version> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>org.springframework.boot</groupId> | <groupId>org.springframework.boot</groupId> | ||||
<artifactId>spring-boot-starter-actuator</artifactId> | <artifactId>spring-boot-starter-actuator</artifactId> | ||||
<scope>test</scope> | <scope>test</scope> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>org.projectlombok</groupId> | |||||
<artifactId>lombok</artifactId> | |||||
<version>1.18.22</version> | |||||
</dependency> | |||||
</dependencies> | </dependencies> | ||||
<!-- 构建环境变量 --> | <!-- 构建环境变量 --> |
package com.tuoheng.gateway.config; | |||||
import com.alibaba.fastjson.JSONObject; | |||||
import com.alibaba.fastjson.serializer.SerializerFeature; | |||||
import com.auth0.jwt.JWT; | |||||
import com.auth0.jwt.interfaces.DecodedJWT; | |||||
import com.tuoheng.gateway.model.ClientUserRoleDto; | |||||
import com.tuoheng.gateway.ustil.EncryptUtil; | |||||
import io.micrometer.core.instrument.util.StringUtils; | |||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; | |||||
import org.springframework.cloud.gateway.filter.GlobalFilter; | |||||
import org.springframework.context.annotation.Configuration; | |||||
import org.springframework.core.Ordered; | |||||
import org.springframework.core.io.buffer.DataBuffer; | |||||
import org.springframework.http.HttpStatus; | |||||
import org.springframework.http.server.reactive.ServerHttpRequest; | |||||
import org.springframework.http.server.reactive.ServerHttpResponse; | |||||
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; | |||||
import org.springframework.util.AntPathMatcher; | |||||
import org.springframework.web.server.ServerWebExchange; | |||||
import reactor.core.publisher.Mono; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.util.*; | |||||
@Configuration | |||||
public class GatewayFilterConfig implements GlobalFilter, Ordered { | |||||
private static final String USERNAME = "username"; | |||||
private static final String OUSERID = "oUserId"; | |||||
private static final String SCOPE = "scope"; | |||||
private static final String CLIENTROLELIST = "clientRoleList"; | |||||
private static final String ADMIN = "admin"; | |||||
@Override | |||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { | |||||
//todo:header里封装 Client-Id 信息 | |||||
/*String clientId = getClientId(exchange); | |||||
if(StringUtils.isEmpty(clientId)){ | |||||
return invalidClientIdMono(exchange); | |||||
} | |||||
String requestUrl = exchange.getRequest().getPath().value(); | |||||
//todo:获取当前系统、当前接口 可以访问的角色集合 start | |||||
Map<String, List<Integer>> permissionMap = getPermissionByClientId(clientId); | |||||
List<Integer> roleIds = permissionMap.get(requestUrl);*/ | |||||
//todo:获取当前系统、当前接口 可以访问的角色集合 end | |||||
String token = getToken(exchange); | |||||
String username = null; | |||||
Long oUserId = null; | |||||
List<String> authorityList = new ArrayList<>(); | |||||
List<ClientUserRoleDto> clientUserRoleDtoList = new ArrayList<>(); | |||||
if (!StringUtils.isBlank(token)) { | |||||
//token数据解析 | |||||
DecodedJWT decodedJWT = JWT.decode(token); | |||||
username = decodedJWT.getClaim(USERNAME).asString(); | |||||
oUserId = decodedJWT.getClaim(OUSERID).asLong(); | |||||
authorityList = decodedJWT.getClaim(SCOPE).asList(String.class); | |||||
clientUserRoleDtoList = decodedJWT.getClaim(CLIENTROLELIST).asList(ClientUserRoleDto.class); | |||||
} | |||||
/*if(roleIds != null){ | |||||
//说明这个url 需要一定的角色才可以访问 | |||||
//在不是admin权限的情况下进行校验 | |||||
if(!authorityList.contains(ADMIN)){ | |||||
//获取用户 client_id 对应的 roleId | |||||
ClientUserRoleDto clientUserRoleDto = clientUserRoleDtoList.stream().filter(dto -> dto.getClientId().equals(clientId)) | |||||
.findFirst().orElse(null); | |||||
if(Objects.isNull(clientUserRoleDto)){ | |||||
return forbiddenTokenMono(exchange); | |||||
} | |||||
Integer roleId = clientUserRoleDto.getRoleId(); | |||||
if(!roleIds.contains(roleId)){ | |||||
return forbiddenTokenMono(exchange); | |||||
} | |||||
} | |||||
}*/ | |||||
if (!StringUtils.isBlank(token)) { | |||||
JSONObject jsonObject = new JSONObject(); | |||||
jsonObject.put(USERNAME, username); | |||||
jsonObject.put(OUSERID, oUserId); | |||||
String base64 = EncryptUtil.encodeUTF8StringBase64(jsonObject.toJSONString()); | |||||
try { | |||||
ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("th-token", token) | |||||
.header("o-user-json", base64) | |||||
.build(); | |||||
ServerWebExchange build = exchange.mutate().request(tokenRequest).build(); | |||||
return chain.filter(build); | |||||
} catch (InvalidTokenException e) { | |||||
return invalidTokenMono(exchange); | |||||
} | |||||
} | |||||
return chain.filter(exchange); | |||||
} | |||||
/** | |||||
* 获取token | |||||
*/ | |||||
private String getToken(ServerWebExchange exchange) { | |||||
try { | |||||
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization"); | |||||
if (StringUtils.isBlank(tokenStr)) { | |||||
return null; | |||||
} | |||||
if (!tokenStr.startsWith("Bearer ")) { | |||||
return null; | |||||
} | |||||
String token = tokenStr.split("Bearer ")[1]; | |||||
if (StringUtils.isBlank(token)) { | |||||
return null; | |||||
} | |||||
return token; | |||||
} catch (Exception e) { | |||||
return null; | |||||
} | |||||
} | |||||
private String getClientId(ServerWebExchange exchange) { | |||||
try { | |||||
String clientIdStr = exchange.getRequest().getHeaders().getFirst("Client-Id"); | |||||
if (StringUtils.isBlank(clientIdStr)) { | |||||
return null; | |||||
} | |||||
return clientIdStr; | |||||
} catch (Exception e) { | |||||
return null; | |||||
} | |||||
} | |||||
/** | |||||
* 401 无效的token | |||||
*/ | |||||
private Mono<Void> invalidTokenMono(ServerWebExchange exchange) { | |||||
JSONObject json = new JSONObject(); | |||||
json.put("code", HttpStatus.UNAUTHORIZED.value()); | |||||
json.put("msg", "无效的授权Authorization信息"); | |||||
json.put("data", null); | |||||
return buildReturnMono(json, exchange, HttpStatus.UNAUTHORIZED); | |||||
} | |||||
/** | |||||
* 401 无效的clientId | |||||
*/ | |||||
private Mono<Void> invalidClientIdMono(ServerWebExchange exchange) { | |||||
JSONObject json = new JSONObject(); | |||||
json.put("code", HttpStatus.UNAUTHORIZED.value()); | |||||
json.put("msg", "无效的Client-Id信息"); | |||||
json.put("data", null); | |||||
return buildReturnMono(json, exchange, HttpStatus.UNAUTHORIZED); | |||||
} | |||||
/** | |||||
* 403 未授权的token | |||||
*/ | |||||
private Mono<Void> forbiddenTokenMono(ServerWebExchange exchange) { | |||||
JSONObject json = new JSONObject(); | |||||
json.put("code", HttpStatus.FORBIDDEN.value()); | |||||
json.put("msg", "暂无权限访问"); | |||||
json.put("data", null); | |||||
return buildReturnMono(json, exchange, HttpStatus.FORBIDDEN); | |||||
} | |||||
private Mono<Void> noTokenMono(ServerWebExchange exchange) { | |||||
JSONObject json = new JSONObject(); | |||||
json.put("code", HttpStatus.UNAUTHORIZED.value()); | |||||
json.put("msg", "未获取到请求头授权Authorization信息"); | |||||
json.put("data", null); | |||||
return buildReturnMono(json, exchange, HttpStatus.UNAUTHORIZED); | |||||
} | |||||
private Mono<Void> buildReturnMono(JSONObject json, ServerWebExchange exchange, HttpStatus httpStatus) { | |||||
ServerHttpResponse response = exchange.getResponse(); | |||||
byte[] bits = JSONObject.toJSONString(json, SerializerFeature.WriteMapNullValue).getBytes(StandardCharsets.UTF_8); | |||||
DataBuffer buffer = response.bufferFactory().wrap(bits); | |||||
response.setStatusCode(httpStatus); | |||||
//指定编码,否则在浏览器中会中文乱码 | |||||
response.getHeaders().add("Content-Type", "application/json;charset:utf-8"); | |||||
return response.writeWith(Mono.just(buffer)); | |||||
} | |||||
/** | |||||
* 根据 clientId 从业务系统获取 permission - role 数据 | |||||
* @return | |||||
*/ | |||||
private Map<String, List<Integer>> getPermissionByClientId(String clientId){ | |||||
// permissionUrl - roleIdList | |||||
Map<String, List<Integer>> map = new HashMap<>(); | |||||
if(clientId.equals("tuoheng-oidc-admin")){ | |||||
List<Integer> roleIds = new ArrayList<>(); | |||||
roleIds.add(1001); | |||||
roleIds.add(1002); | |||||
map.put("/oidc/admin/user/create", roleIds); | |||||
} | |||||
return map; | |||||
} | |||||
@Override | |||||
public int getOrder() { | |||||
return 0; | |||||
} | |||||
} |
package com.tuoheng.gateway.config; | |||||
import org.springframework.context.annotation.Bean; | |||||
import org.springframework.context.annotation.Configuration; | |||||
import org.springframework.security.oauth2.provider.token.TokenStore; | |||||
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; | |||||
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; | |||||
@Configuration | |||||
public class TokenConfig { | |||||
/** | |||||
* 秘钥串 | |||||
*/ | |||||
private static final String SIGNING_KEY = "uaa"; | |||||
@Bean | |||||
public TokenStore tokenStore() { | |||||
return new JwtTokenStore(accessTokenConverter()); | |||||
} | |||||
@Bean | |||||
public JwtAccessTokenConverter accessTokenConverter() { | |||||
JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); | |||||
converter.setSigningKey(SIGNING_KEY); | |||||
return converter; | |||||
} | |||||
} |
String[] PERMIT_PATH = permitUrlStr.split(","); | String[] PERMIT_PATH = permitUrlStr.split(","); | ||||
httpSecurity | httpSecurity | ||||
.authorizeExchange() | .authorizeExchange() | ||||
.pathMatchers("/api/system/demo/admin").hasAuthority(AuthorityConstant.SCOPE_ADMIN) | |||||
.pathMatchers("/api/system/demo/dsp").hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_DSP_MP) | |||||
.pathMatchers("/api/system/demo/hhz").hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_DSP_WEB) | |||||
.pathMatchers(OAUTH_PATH).hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_DSP_MP, AuthorityConstant.SCOPE_TUOHNEG_DSP_WEB) | |||||
//.pathMatchers("/api/system/**").hasAnyRole("ROLE_ADMIN", "ROLE_DSP") | |||||
.pathMatchers(OAUTH_PATH).hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_DSP_MP, AuthorityConstant.SCOPE_TUOHNEG_DSP_WEB) | |||||
.pathMatchers("/pilot/miniprogram/**").hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_PILOT_MP) | |||||
.pathMatchers("/pilot/admin/**").hasAnyAuthority(AuthorityConstant.SCOPE_ADMIN, AuthorityConstant.SCOPE_TUOHNEG_PILOT_ADMIN) | |||||
.pathMatchers("/oidc/admin/**").authenticated() | |||||
//.pathMatchers(PERMIT_PATH).permitAll() | //.pathMatchers(PERMIT_PATH).permitAll() | ||||
.anyExchange().permitAll() | .anyExchange().permitAll() | ||||
.and() | .and() |
*/ | */ | ||||
public static final String SCOPE_TUOHNEG_DSP_WEB = "SCOPE_tuoheng-dsp-web"; | public static final String SCOPE_TUOHNEG_DSP_WEB = "SCOPE_tuoheng-dsp-web"; | ||||
public static final String SCOPE_TUOHNEG_PILOT_ADMIN = "SCOPE_tuoheng-pilot-admin"; | |||||
public static final String SCOPE_TUOHNEG_PILOT_MP = "SCOPE_tuoheng-pilot-mp"; | |||||
public static final String SCOPE_TUOHNEG_OIDC_ADMIN = "SCOPE_tuoheng-oidc-admin"; | |||||
/** | /** | ||||
* 河湖长用户权限 | * 河湖长用户权限 | ||||
*/ | */ |
package com.tuoheng.gateway.model; | |||||
import lombok.Data; | |||||
import javax.annotation.sql.DataSourceDefinition; | |||||
/** | |||||
* @author chenjiandong | |||||
* @description: TODO | |||||
* @date 2022/10/31 9:36 | |||||
*/ | |||||
@Data | |||||
public class ClientUserRoleDto { | |||||
private String clientId; | |||||
private Integer roleId; | |||||
} |
package com.tuoheng.gateway.ustil; | |||||
import org.slf4j.Logger; | |||||
import org.slf4j.LoggerFactory; | |||||
import java.io.UnsupportedEncodingException; | |||||
import java.net.URLDecoder; | |||||
import java.net.URLEncoder; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.util.Base64; | |||||
/** | |||||
* 加密工具类 | |||||
* @Author: rosh | |||||
*/ | |||||
public final class EncryptUtil { | |||||
private EncryptUtil() { | |||||
} | |||||
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class); | |||||
public static String encodeBase64(byte[] bytes) { | |||||
return Base64.getEncoder().encodeToString(bytes); | |||||
} | |||||
public static byte[] decodeBase64(String str) { | |||||
return Base64.getDecoder().decode(str); | |||||
} | |||||
public static String encodeUTF8StringBase64(String str) { | |||||
return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8)); | |||||
} | |||||
public static String decodeUTF8StringBase64(String str) { | |||||
byte[] bytes = Base64.getDecoder().decode(str); | |||||
return new String(bytes, StandardCharsets.UTF_8); | |||||
} | |||||
public static String encodeURL(String url) { | |||||
String encoded = null; | |||||
try { | |||||
encoded = URLEncoder.encode(url, String.valueOf(StandardCharsets.UTF_8)); | |||||
} catch (UnsupportedEncodingException e) { | |||||
logger.warn("URLEncode失败", e); | |||||
} | |||||
return encoded; | |||||
} | |||||
public static String decodeURL(String url) { | |||||
String decoded = null; | |||||
try { | |||||
decoded = URLDecoder.decode(url, String.valueOf(StandardCharsets.UTF_8)); | |||||
} catch (UnsupportedEncodingException e) { | |||||
logger.warn("URLDecode失败", e); | |||||
} | |||||
return decoded; | |||||
} | |||||
} |
oauth2: | oauth2: | ||||
resource-server: | resource-server: | ||||
jwt: | jwt: | ||||
#issuer-uri: http://192.168.11.11:8090 | |||||
issuer-uri: http://oidc.dev.t-aaron.com | |||||
issuer-uri: http://192.168.11.11:8090 | |||||
#issuer-uri: http://oidc.dev.t-aaron.com | |||||
cloud: | cloud: | ||||
consul: | consul: | ||||
host: 192.168.11.13 # consul 所在服务地址 | host: 192.168.11.13 # consul 所在服务地址 | ||||
- Path=/pilot/web/** | - Path=/pilot/web/** | ||||
filters: | filters: | ||||
- StripPrefix=2 | - StripPrefix=2 | ||||
# oidc admin服务 | |||||
- id: tuoheng-oidc-admin | |||||
uri: lb://tuoheng-oidc-admin | |||||
predicates: | |||||
- Path=/oidc/admin/** | |||||
filters: | |||||
- StripPrefix=2 | |||||
# Redis数据源 | # Redis数据源 | ||||
redis: | redis: | ||||
# 缓存库默认索引0 | # 缓存库默认索引0 |
- Path=/pilot/web/** | - Path=/pilot/web/** | ||||
filters: | filters: | ||||
- StripPrefix=2 | - StripPrefix=2 | ||||
# oidc admin服务 | |||||
- id: tuoheng-oidc-admin | |||||
uri: lb://tuoheng-oidc-admin | |||||
predicates: | |||||
- Path=/oidc/admin/** | |||||
filters: | |||||
- StripPrefix=2 | |||||
# Redis数据源 | # Redis数据源 | ||||
redis: | redis: | ||||
# 缓存库默认索引0 | # 缓存库默认索引0 |
- Path=/pilot/web/** | - Path=/pilot/web/** | ||||
filters: | filters: | ||||
- StripPrefix=2 | - StripPrefix=2 | ||||
# oidc admin服务 | |||||
- id: tuoheng-oidc-admin | |||||
uri: lb://tuoheng-oidc-admin | |||||
predicates: | |||||
- Path=/oidc/admin/** | |||||
filters: | |||||
- StripPrefix=2 | |||||
# Redis数据源 | # Redis数据源 | ||||
redis: | redis: | ||||
# 缓存库默认索引0 | # 缓存库默认索引0 |
- Path=/pilot/web/** | - Path=/pilot/web/** | ||||
filters: | filters: | ||||
- StripPrefix=2 | - StripPrefix=2 | ||||
# oidc admin服务 | |||||
- id: tuoheng-oidc-admin | |||||
uri: lb://tuoheng-oidc-admin | |||||
predicates: | |||||
- Path=/oidc/admin/** | |||||
filters: | |||||
- StripPrefix=2 | |||||
# Redis数据源 | # Redis数据源 | ||||
redis: | redis: | ||||
# 缓存库默认索引0 | # 缓存库默认索引0 |