之前文章里說過,分布式系統(tǒng)的鑒權(quán)有兩種方式,一是在網(wǎng)關(guān)進(jìn)行統(tǒng)一的鑒權(quán)操作,二是在各個微服務(wù)里單獨(dú)鑒權(quán)。
第二種方式比較常見,代碼網(wǎng)上也是很多。今天主要是說第一種方式。
1.網(wǎng)關(guān)鑒權(quán)的流程
重要前提:需要收集各個接口的uri路徑和所需權(quán)限列表的對應(yīng)關(guān)系,并存入緩存。
2.收集uri路徑和對應(yīng)權(quán)限
服務(wù)啟動的時候,執(zhí)行緩存數(shù)據(jù)的初始化操作:掃描服務(wù)內(nèi)的所有controller接口方法,利用反射,獲取方法的完整uri路徑,方法上指定注解中的權(quán)限值,再存入Redis緩存。
服務(wù)啟動時做一些操作,方法有很多,可以繼承CommandLineRunner或者其他方式。不熟悉的可以去查一下有關(guān)資料。
因?yàn)楹罄m(xù)可能會有很多微服務(wù),因此將該緩存數(shù)據(jù)的初始化的操作放在common模塊中,微服務(wù)依賴該模塊完成。
1.1.初始化方法
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import com.eden4cloud.common.core.contant.CacheConstants;
import com.eden4cloud.common.security.anno.Perms;
import com.eden4cloud.common.security.constant.MethodTypeConstant;
import com.eden4cloud.common.security.utils.RequestUriUtils;
import com.eden4cloud.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Param:
* @Return:
* @Date: 2022/12/3 15:31
* @Author: Yan
* @Description: 接口權(quán)限初始化采集,獲取數(shù)據(jù)庫中的權(quán)限標(biāo)識,沒有權(quán)限標(biāo)識的其它接口使用**表示
*/
@Slf4j
public class ApiPermsInit implements ApplicationContextAware {
/**
* 接口路徑及權(quán)限列表
* 比如:/user/list<br>
* 不支持@PathVariable格式的URI
*/
public static List<Map<String, String>> oauthUrls = new ArrayList<>();
Map<String, String> uriAuthMap = new HashMap<>();
/**
* Url參數(shù)需要解密的配置
* 比如:/user/list?name=加密內(nèi)容<br>
* 格式:Key API路徑 Value 需要解密的字段
* 示列:/user/list [name,age]
*/
public static Map<String, List<String>> requestDecryptParamMap = new HashMap<>();
private String applicationPath;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.applicationPath = ctx.getEnvironment().getProperty("spring.application.name");
Map<String, Object> beanMap = ctx.getBeansWithAnnotation(Controller.class);
initData(beanMap);
if (CollectionUtil.isNotEmpty(uriAuthMap)) {
redisTemplate.boundHashOps(CacheConstants.OAUTH_URLS).putAll(uriAuthMap);
}
}
/**
* 初始化,獲取所有接口的加解密配置狀態(tài)并保存
*
* @param beanMap
*/
private void initData(Map<String, Object> beanMap) {
if (beanMap != null) {
beanMap.values().parallelStream().map(Object::getClass).forEach(clz -> {
for (Method method : clz.getDeclaredMethods()) {
String uriKey = RequestUriUtils.getApiUri(clz, method, applicationPath);
//收集帶有Perms注解的api接口的uri路徑
Perms perms = AnnotationUtils.findAnnotation(method, Perms.class);
if (StringUtils.isNotEmpty(uriKey) && perms != null && ArrayUtil.isNotEmpty(perms.value())) {
//解析權(quán)限標(biāo)識
String authValue = StringUtil.join(perms.value(), StrPool.COMMA);
uriAuthMap.put(uriKey, authValue);
} else if (uriKey.startsWith(MethodTypeConstant.GET)) {
/* 屏蔽沒有請求方式的api接口 */
//沒有權(quán)限標(biāo)識的,直接使用**表示
uriAuthMap.put(uriKey, "**");
} else if (uriKey.startsWith(MethodTypeConstant.POST)) {
//沒有權(quán)限標(biāo)識的,直接使用**表示
uriAuthMap.put(uriKey, "**");
} else if (uriKey.startsWith(MethodTypeConstant.PUT)) {
//沒有權(quán)限標(biāo)識的,直接使用**表示
uriAuthMap.put(uriKey, "**");
} else if (uriKey.startsWith(MethodTypeConstant.DELETE)) {
//沒有權(quán)限標(biāo)識的,直接使用**表示
uriAuthMap.put(uriKey, "**");
} else {
//不在上述情況中的,一般為框架提供的api接口
log.info(uriKey);
}
}
});
}
}
}
1.2.對應(yīng)的工具類
import com.eden4cloud.common.security.constant.MethodTypeConstant;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
public class RequestUriUtils {
private static final String SEPARATOR = "/";
/**
* 獲取接口的uri路徑
*
* @param clz
* @param method
* @param applicationPath
* @return
*/
public static String getApiUri(Class<?> clz, Method method, String applicationPath) {
String methodType = "";
StringBuilder uri = new StringBuilder();
// 處理類路徑
RequestMapping reqMapping = AnnotationUtils.findAnnotation(clz, RequestMapping.class);
if (reqMapping != null && reqMapping.value().length > 0) {
uri.append(formatUri(reqMapping.value()[0]));
}
//處理方法上的路徑
GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);
PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);
RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);
DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
if (getMapping != null && getMapping.value().length > 0) {
methodType = MethodTypeConstant.GET;
uri.append(formatUri(getMapping.value()[0]));
} else if (postMapping != null && postMapping.value().length > 0) {
methodType = MethodTypeConstant.POST;
uri.append(formatUri(postMapping.value()[0]));
} else if (putMapping != null && putMapping.value().length > 0) {
methodType = MethodTypeConstant.PUT;
uri.append(formatUri(putMapping.value()[0]));
} else if (deleteMapping != null && deleteMapping.value().length > 0) {
methodType = MethodTypeConstant.DELETE;
uri.append(formatUri(deleteMapping.value()[0]));
} else if (requestMapping != null && requestMapping.value().length > 0) {
RequestMethod requestMethod = RequestMethod.GET;
if (requestMapping.method().length > 0) {
requestMethod = requestMapping.method()[0];
}
methodType = requestMethod.name().toLowerCase() + ":";
uri.append(formatUri(requestMapping.value()[0]));
}
// 框架自帶的接口,返回null后,直接忽略處理
if (uri.indexOf("${") > 0) {
return "";
}
// 針對Rest請求,路徑上的請求參數(shù)進(jìn)行處理,以**代替
int idx = uri.indexOf("{");
if (idx > 0) {
uri = new StringBuilder(uri.substring(0, idx)).append("**");
}
return methodType + SEPARATOR + applicationPath + uri;
}
private static String formatUri(String uri) {
if (uri.startsWith(SEPARATOR)) {
return uri;
}
return SEPARATOR + uri;
}
}
收集結(jié)果:
?說明:
- 要求一個請求的完整路徑格式為:請求方式:/服務(wù)名/類路徑/方法路徑;
- 請求方式必須要有,防止路徑處理后,會出現(xiàn)重復(fù),加上請求方式可以極大避免;
- 服務(wù)名是為了做網(wǎng)關(guān)路由使用,在配置網(wǎng)關(guān)的路由規(guī)則時,斷言的路由規(guī)則即為/服務(wù)名;代碼里獲取服務(wù)路徑的方式是:ctx.getEnvironment().getProperty("spring.application.name");如果覺得不安全可以在yml文件中自定義一個路徑名,改一下此處的獲取值即可。只需要記得一定要和網(wǎng)關(guān)的路由斷言規(guī)則匹配就行?。。。?/li>
- 類路徑必須有。
- 方法路徑必須有。方法上的第一個路徑必須是固定路徑,而不能是請求參數(shù),另外不同方法上的第一個固定路徑避免設(shè)置成相同的;也是為了防止最終出現(xiàn)請求方式相同、路徑也相同的情況。
- 對Rest風(fēng)格,且方法路徑上帶有路徑參數(shù)的路徑必須做特殊處理,即將路徑參數(shù)替換成**。舉例如下:原完整路徑為/user-Service/user/queryUser/{username}/{age},方法上的路徑為/queryUser/{username}/{age},不論有多少個路徑參數(shù),從第一個路徑參數(shù)開始,全部替換掉,處理為/queryUser/**,最終存入緩存所使用的完整路徑為:GET:/user-Service/user/queryUser/**。否則當(dāng)請求到達(dá)網(wǎng)關(guān),你想要根據(jù)路徑去緩存中匹配對應(yīng)的路徑時,你會發(fā)現(xiàn)沒辦法處理,因?yàn)閺恼埱蟮膗ri路徑上你是看不出來哪是固定路徑,哪是路徑參數(shù)的。例如:/user-Service/user/queryUser/zhangsan/18,這個例子你雖然你看都知道哪個是路徑參數(shù),但畢竟是框架,萬一路徑上有很多的/../../..,還怎么猜?有些規(guī)則該定死,還是要定死的。
- 真實(shí)請求到達(dá)網(wǎng)關(guān)后,我們對真實(shí)請求也做了一些處理,即:只保留/服務(wù)名/類路徑/方法路徑的第一個,后續(xù)的路徑均使用一個/**替換掉。舉例:真實(shí)請求為/user-Service/user/queryUser/zhangsan/18,我們處理后為/user-Service/user/queryUser/**。
經(jīng)過上述的規(guī)定和路徑處理后,在下面的代碼中進(jìn)行匹配操作:
- ?methodValue + uri + CacheConstants.FUZZY_PATH即為/user-Service/user/queryUser/**;
- k.toString()即為/user-Service/user/queryUser/**;
- 判斷結(jié)果為true。
2.網(wǎng)關(guān)整合Security Oauth2
Gateway網(wǎng)關(guān)是基于webFlux實(shí)現(xiàn)的,所以和一般微服務(wù)整合方式不太一樣。
2.1.認(rèn)證管理器
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* @Since: 2023/4/13
* @Author: Yan
* @Description Jwt認(rèn)證管理器,對token的真實(shí)性、有效性進(jìn)行校驗(yàn)
*/
@Component
@Slf4j
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.justOrEmpty(authentication)
.filter(a -> a instanceof BearerTokenAuthenticationToken)
.cast(BearerTokenAuthenticationToken.class)
.map(BearerTokenAuthenticationToken::getToken)
.flatMap((accessToken -> {
//解析令牌
OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
if (oAuth2AccessToken == null) {
return Mono.error(new InvalidBearerTokenException("無效的token"));
} else if (oAuth2AccessToken.isExpired()) {
return Mono.error(new InvalidBearerTokenException("token已過期"));
}
OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(accessToken);
if (oAuth2Authentication == null) {
return Mono.error(new InvalidBearerTokenException("無效的token"));
} else {
return Mono.just(oAuth2Authentication);
}
}))
.cast(Authentication.class);
}
}
2.2..鑒權(quán)管理器
import cn.hutool.core.text.AntPathMatcher;
import cn.hutool.core.text.StrPool;
import com.eden4cloud.common.core.contant.CacheConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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 reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Since: 2023/4/13
* @Author: Yan
* @Description Jwt鑒權(quán)管理器:從Redis中獲取所請求的Url所需要的權(quán)限,和用戶token中所攜帶的權(quán)限進(jìn)行比對
*/
@Slf4j
@Component
public class JwtAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private AntPathMatcher matcher = new AntPathMatcher();
/**
* *****當(dāng)前匹配方法要求一個API接口方法上必須要有路徑*****
*
* @param mono
* @param authorizationContext
* @return
*/
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
// 處理當(dāng)前請求的uri路徑,最終格式為:/服務(wù)路徑/類路徑/方法上的路徑 /eden-system/user/add
StringBuilder builder = new StringBuilder();
String[] split = authorizationContext.getExchange().getRequest().getURI().getPath().split(StrPool.SLASH);
String uri = builder.append(StrPool.SLASH).append(split[1])//服務(wù)路徑
.append(StrPool.SLASH).append(split[2])//類路徑
.append(StrPool.SLASH).append(split[3])//方法路徑
.toString();
// 請求方式拼接處理 ,格式為 GET:
String methodValue = authorizationContext.getExchange().getRequest().getMethodValue() + StrPool.COLON;
// 獲取所有路徑的權(quán)限列表
Map<Object, Object> entries = redisTemplate.opsForHash().entries(CacheConstants.OAUTH_PERMS);
List<String> authorities = new ArrayList<>();
AtomicBoolean authFlag = new AtomicBoolean(false);
entries.forEach((k, v) -> {
// 根據(jù)請求uri路徑,獲取到匹配的緩存權(quán)限數(shù)據(jù)
if (k.equals(methodValue + uri)
|| matcher.match(methodValue + uri + CacheConstants.FUZZY_PATH, k.toString())) {
if (CacheConstants.ANONYMOUS.equals(v.toString())) {
// 權(quán)限為**,表示允許匿名訪問
authFlag.set(true);
} else {
// 收集當(dāng)前路徑所需的權(quán)限列表
authorities.addAll(Arrays.asList((v.toString()).split(StrPool.COMMA)));
}
}
});
// Collection<? extends GrantedAuthority> authorities1 = mono.block().getAuthorities();
List<String> finalAuthorities = authorities;
return mono
//判斷是否認(rèn)證成功
.filter(Authentication::isAuthenticated)
//獲取認(rèn)證后的全部權(quán)限列表
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
//如果包含在url要求的權(quán)限內(nèi),則返回true
.any(auth -> authFlag.get() || finalAuthorities.contains(auth))
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false))
;
}
}
2.3.security安全配置
import com.eden4cloud.gateway.component.JwtAuthorizationManager;
import com.eden4cloud.gateway.handler.RequestAccessDeniedHandler;
import com.eden4cloud.gateway.handler.RequestAuthenticationEntrypoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
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.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.web.cors.reactive.CorsWebFilter;
/**
* @Since: 2023/4/2
* @Author: Yan
* @Description security安全配置
*/
@Configuration
@EnableWebFluxSecurity
public class EdenGatewayWebSecurityConfig {
@Autowired
private JwtAuthorizationManager jwtAuthorizationManager;
@Autowired
private ReactiveAuthenticationManager authenticationManager;
@Autowired
private RequestAuthenticationEntrypoint requestAuthenticationEntrypoint;
@Autowired
private RequestAccessDeniedHandler requestAccessDeniedHandler;
@Autowired
private CorsWebFilter corsWebFilter;
// @Autowired
// private GlobalAuthenticationFilter authenticationFilter;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
http
.csrf().disable()
.authorizeExchange()
//對oauth的端點(diǎn)進(jìn)行放行
.pathMatchers("/eden-oauth/oauth/**").permitAll()
//其他請求必須鑒權(quán),使用鑒權(quán)管理器
.anyExchange().access(jwtAuthorizationManager)
.and()
//鑒權(quán)異常處理
.exceptionHandling()
.authenticationEntryPoint(requestAuthenticationEntrypoint)
.accessDeniedHandler(requestAccessDeniedHandler)
.and()
//跨域過濾器
.addFilterAt(corsWebFilter, SecurityWebFiltersOrder.CORS)
//token認(rèn)證過濾器
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
// .addFilterAfter(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
;
return http.build();
}
}
2.4.將網(wǎng)關(guān)作為資源服務(wù)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
/**
* @Author: Yan
* @Since: 2023/2/4
* @Description: 資源服務(wù)器解析鑒權(quán)配置類
*/
@Configuration
public class EdenGatewayResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
//公鑰
private static final String RESOURCE_ID = "eden-gateway";
/**
* Http安全配置,對每個到達(dá)系統(tǒng)的http請求鏈接進(jìn)行校驗(yàn)
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll();
}
/**
* 資源服務(wù)的安全配置
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}
}
2.5.其他配置
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* @CreateTime: 2023-01-2023/1/11 15:09
* @Author: Yan
* @Description 注冊網(wǎng)關(guān)過濾器示例
*/
@Configuration
@AutoConfigureAfter(GatewayAutoConfiguration.class)
public class GatewayRoutesConfiguration {
/**
* 跨域配置
*
* @return
*/
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
// @Bean(name = "ipKeyResolver")
// public KeyResolver userIpKeyResolver() {
// return new IpKeyResolver();
// }
//
// @Bean
// public RouteLocator routeLocator(RouteLocatorBuilder builder) {
// StripPrefixGatewayFilterFactory filterFactory = new StripPrefixGatewayFilterFactory();
// StripPrefixGatewayFilterFactory.Config partsConfig = filterFactory.newConfig();
// partsConfig.setParts(1);
//
// MyGatewayFilterFactory factory = new MyGatewayFilterFactory();
// MyGatewayFilterFactory.PathsConfig pathsConfig = factory.newConfig();
// pathsConfig.setPaths(Arrays.asList("/AAA", "/BBB"));
//
// return builder.routes()
// .route(r -> r.path("/life/**")
// .uri("lb://eden-life")
// .filters(factory.apply(pathsConfig), filterFactory.apply(partsConfig))
// .id("eden-life"))
// .build();
// }
}
2.6.自定義鑒權(quán)過濾器工廠
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.eden4cloud.common.security.exception.InvalidTokenException;
import com.eden4cloud.common.security.utils.JwtUtils;
import com.eden4cloud.common.util.sign.Base64;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @CreateTime: 2023-01-2023/1/11 9:34
* @Author: Yan
* @Description 自定義鑒權(quán)過濾器工廠:設(shè)置訪問白名單;重新封裝鑒權(quán)認(rèn)證通過的請求頭;
*/
//1. 編寫實(shí)現(xiàn)類繼承AbstractGatewayFilterFactory抽象類
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.IgnoreUrlsConfig> {//4. 指定泛型,靜態(tài)的內(nèi)部實(shí)體類
@Autowired
private TokenStore tokenStore;
//5. 重寫無參構(gòu)造方法,指定內(nèi)部實(shí)體類接收參數(shù)
public AuthGatewayFilterFactory() {
super(IgnoreUrlsConfig.class);
}
@Override
public GatewayFilter apply(IgnoreUrlsConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
//直接放行部分請求路徑,如登錄、退出等 需要排除的路徑弄成可yaml配置的
boolean flag = config.ignoreUrls.contains(request.getURI().getPath())
|| config.ignoreUrls.stream().filter(i -> i.endsWith("/**"))
.anyMatch(i -> exchange.getRequest().getURI().getPath().startsWith(i.replace("**", "")));
if (flag) {
return chain.filter(exchange);
}
//重新封裝新的請求中數(shù)據(jù)
ServerWebExchange webExchange = rebuildRequestHeaders(exchange, request);
if (webExchange == null) {
return Mono.error(new InvalidTokenException());
}
return chain.filter(webExchange);
};
}
/**
* 重新封裝新的請求數(shù)據(jù)
*
* @param exchange
* @param request
* @return
*/
private ServerWebExchange rebuildRequestHeaders(ServerWebExchange exchange, ServerHttpRequest request) {
//獲取請求頭中的令牌
String token = JwtUtils.getToken(request);
if (StrUtil.isBlank(token)) {
return null;
}
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();
List<String> authorities = (List<String>) additionalInformation.get("authorities");
//獲取用戶名
String username = additionalInformation.get("user_name").toString();
//獲取用戶權(quán)限
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", username);
jsonObject.put("authorities", authorities);
//將解析后的token加密后重新放入請求頭,方便后續(xù)微服務(wù)解析獲取用戶信息
String base64 = Base64.encode(jsonObject.toJSONString().getBytes(StandardCharsets.UTF_8));
request = exchange.getRequest().mutate().header("token", base64).build();
exchange.mutate().request(request);
return exchange;
}
/**
* 6. 重寫shortcutFieldOrder()指定接收參數(shù)的字段順序
* Returns hints about the number of args and the order for shortcut parsing.
*
* @return the list of hints
*/
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("ignoreUrls");
}
/**
* 7. 重寫shortcutType()指定接收參數(shù)的字段類型
*/
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
/**
* 3. 定義匿名內(nèi)部實(shí)體類,定義接收參數(shù)的字段
*/
@Data
public static class IgnoreUrlsConfig {
//傳遞多個參數(shù)
private List<String> ignoreUrls;
}
}
2.7.網(wǎng)關(guān)路由規(guī)則
#路由配置
spring:
cloud:
gateway:
routes:
- id: eden-life
uri: lb://eden-life
predicates:
- Path=/eden-life/**
filters:
- StripPrefix=1
- name: Auth
args:
ignoreUrls:
- /auth-server/login
- /oauth/**
- /life/**
2.6和2.7主要是展示配置了動態(tài)的請求白名單功能?;趎acos的配置中心功能,可以實(shí)現(xiàn)動態(tài)刷新,白名單設(shè)置實(shí)時生效。文章來源:http://www.zghlxwxcb.cn/news/detail-770505.html
2.8.異常處理文章來源地址http://www.zghlxwxcb.cn/news/detail-770505.html
import cn.hutool.json.JSONUtil;
import com.eden4cloud.common.core.entity.R;
import com.eden4cloud.common.security.exception.SecurityExceptionEnum;
import org.apache.http.HttpHeaders;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @Since: 2023/4/17
* @Author: Yan
* @Description TODO
*/
@Component
public class RequestAccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
DataBuffer buffer = response.bufferFactory()
.wrap(JSONUtil.toJsonStr(R.error(SecurityExceptionEnum.NO_PERMISSION.getMsg())).getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
import cn.hutool.json.JSONUtil;
import com.eden4cloud.common.core.entity.R;
import com.eden4cloud.common.security.exception.SecurityExceptionEnum;
import org.apache.http.HttpHeaders;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @Since: 2023/4/17
* @Author: Yan
* @Description TODO
*/
@Component
public class RequestAuthenticationEntrypoint implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
DataBuffer buffer = response.bufferFactory()
.wrap(JSONUtil.toJsonStr(R.error(SecurityExceptionEnum.INVALID_TOKEN.getMsg())).getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
到了這里,關(guān)于Spring Gateway + Oauth2 + Jwt網(wǎng)關(guān)統(tǒng)一鑒權(quán)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!