网站首页 > 技术文章 正文
一、前言
网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 网关来做,这样既提高业务灵活性又不缺安全性。
RBAC基于角色访问控制,目前使用最为广泛的权限模型。相信大家对这种权限模型已经比较了解了。此模型有三个用户、角色和权限,在传统的权限模型用户直接关联加了角色,解耦了用户和权限,使得权限系统有了更清晰的职责划分和更高的灵活度。
二、Gateway鉴权
1、依赖配置
<!--引入SpringSecurity 启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
2、添加安全拦截配置 SecurityConfig
/**
* @description 安全配置类
*/
@EnableWebFluxSecurity
public class SecurityConfig {
@Resource
private SecurityProperties securityProperties;
/**
* 认证失败处理类 Bean
*/
@Resource
private ServerAuthenticationEntryPoint serverAuthenticationEntryPoint;
/**
* 权限不够处理器 Bean
*/
@Resource
private ServerAccessDeniedHandler serverAccessDeniedHandler;
/**
* 用户权限鉴权处理
*/
@Resource
private ReactiveAuthorizationManager reactiveAuthorizationManager;
/**
* 存储认证授权的相关信息
*/
@Resource
private ServerSecurityContextRepository securityContextRepository;
/**
* 认证处理
*/
@Resource
private ReactiveAuthenticationManager reactiveAuthenticationManager;
//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable()// CSRF 禁用,因为不使用 Session
.cors().and()
// 登录认证处理
.authenticationManager(reactiveAuthenticationManager())
.securityContextRepository(securityContextRepository)
// 请求拦截处理
.authorizeExchange(exchange -> exchange
.pathMatchers(Convert.toStrArray(securityProperties.getPermitAllUrls())).permitAll()
.anyExchange().access(reactiveAuthorizationManager)
)
//自定义的 Spring Security 处理器
.exceptionHandling().authenticationEntryPoint(serverAuthenticationEntryPoint)//认证失败处理类
.accessDeniedHandler(serverAccessDeniedHandler)//权限不够处理器
;
return http.build();
}
/**
* 注册用户信息验证管理器,可按需求添加多个按顺序执行
*/
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authentication -> {
return Mono.empty();// 其他登陆方式 (比如手机号验证码登陆) 可在此设置不得抛出异常或者 Mono.error
});
// 必须放最后不然会优先使用用户名密码校验但是用户名密码不对时此 AuthenticationManager 会调用 Mono.error 造成后面的 AuthenticationManager 不生效
managers.add(reactiveAuthenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
}
2.1、实现ReactiveAuthenticationManager类
/**
* 认证处理
*/
@Component
@Primary
public class ReactiveAuthenticationManagerImpl implements ReactiveAuthenticationManager {
@Autowired
private TokenStore tokenStore;
@Resource
private PermissionFeignApi permissionFeignApi;
@Resource
private RedisTemplate redisTemplate;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
LoginUser loginUser = null;
String token = authentication.getPrincipal().toString();
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(token);
if (null == oAuth2AccessToken || null == oAuth2Authentication || oAuth2AccessToken.isExpired()) {
return Mono.error(new InvalidTokenException("token已过期或无效"));
}
Object principal = oAuth2Authentication.getPrincipal();
if (principal instanceof String) {
loginUser = JsonUtils.parseObject(principal.toString(), LoginUser.class);
}
Set<String> perms = permissionFeignApi.listRolePerms(loginUser.getTenantId(),token, loginUser.getRoles().stream().collect(Collectors.joining(","))).getCheckedData();
loginUser.setPerms(perms);
//存入redis中
redisTemplate.opsForValue().set(SecurityConstants.USER_INFO_KEY_PREFIX+token.split(",")[2],loginUser,2, TimeUnit.HOURS);
// 获取角色
Set<GrantedAuthority> authoritieSet = loginUser.getPerms().stream().map(perm -> new SimpleGrantedAuthority(perm)).collect(Collectors.toSet());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authoritieSet);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return Mono.just(authenticationToken);
}
}
2.2、实现 ReactiveAuthorizationManager类,检查授权的对象类型
/**
* 用户权限鉴权处理
*/
@Component
@Slf4j
public class ReactiveAuthorizationManagerImpl implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
//将主要权限校验放到注解 Permit 中实现
return new AuthorizationDecision(true);
}).defaultIfEmpty(new AuthorizationDecision(false));
}
@Override
public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
return check(authentication, object)
.filter(AuthorizationDecision::isGranted)
.switchIfEmpty(Mono.defer(() -> {
String body = JsonUtils.toJsonString(CommonResult.error(FORBIDDEN));
return Mono.error(new AccessDeniedException(body));
})).flatMap(d -> Mono.empty());
}
}
2.3、实现ServerSecurityContextRepository
/**
* 存储认证授权的相关信息
*/
@Component
@Slf4j
public class ServerSecurityContextRepositoryImpl implements ServerSecurityContextRepository {
@Resource
private ReactiveAuthenticationManager tokenAuthenticationManager;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
List<String> headers = request.getHeaders().get(HEADER_AUTHORIZATION);
try {
if (!CollectionUtils.isEmpty(headers)) {
String authorization = headers.get(0);
if (StringUtils.isNotEmpty(authorization)) {
String token = authorization.split(" ")[1];
if (StringUtils.isNotEmpty(token)) {
return tokenAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(token, null)).map(SecurityContextImpl::new);
}
}
}
} catch (Exception e) {
log.error("token不存在或无效,{}", e.getMessage(), e);
return Mono.error(new InvalidTokenException("token无效"));
}
return Mono.empty();
}
}
2.4、实现ServerAccessDeniedHandler 权限不够处理
/**
* 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
*/
@Slf4j
@Component
public class ServerAccessDeniedHandlerImpl implements ServerAccessDeniedHandler {
@Resource
private RedisSecurityCofig redisSecurityCofig;
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException ex) {
log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]",exchange.getRequest().getURI(), redisSecurityCofig.getUserId(), ex);
// 返回 403
return GateWayWebUtils.writeJSON(exchange, CommonResult.error(FORBIDDEN));
}
}
2.5、实现ServerAuthenticationEntryPoint 认证失败处理
/**
* 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
*/
@Slf4j
@Component
public class ServerAuthenticationEntryPointImpl implements ServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex){
log.warn("[commence][访问 URL({}) 时,没有登录]", exchange.getRequest().getURI(), ex);
// 返回 401
return GateWayWebUtils.writeJSON(exchange, CommonResult.error(UNAUTHORIZED));
}
}
3、配置 JwtAccessTokenConverter 所使用的密钥信息
@Configuration
public class JWTTokenConfig {
//签名密钥 默认 iot-cloud
@Value("${com.shx.signingKey:shxCloud}")
private String signingKey;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
}
4、网关认证过虑器 GatewayAuthFilter
/**
* @description 网关认证过虑器
*/
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
/**
* 免登录的 URL 列表 (白名单)
*/
@Resource
private SecurityProperties securityProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//请求的url
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
//添加忽略swagger文档
List<String> permitAllUrls = securityProperties.getPermitAllUrls();
for (String url : permitAllUrls) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
return chain.filter(exchange);
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst(HEADER_AUTHORIZATION);
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = null;
try {
token = tokenStr.split(" ")[1];
} catch (Exception e) {
throw new ServiceException("认证令牌无效");
}
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
@Override
public int getOrder() {
return 0;
}
}
@Data
@Configuration
@ConfigurationProperties("com.shx.security")
public class SecurityProperties {
/**
* 免登录的 URL 列表
*/
private List<String> permitAllUrls = Collections.emptyList();
}
Spring Cloud Gateway 根据作用范围划分为 GatewayFilter 和 GlobalFilter
- GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上,通过spring.cloud.default-filters配置在全局,作用在所有路由上。
- GlobalFilter : 不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器。
5、启动服务 测试
首先通过网关访问订单详情
然后再通过网关获取令牌
然后使用新获取到的令牌来访问订单详情
到此 网关中的鉴权功能开发完成。
猜你喜欢
- 2024-12-23 Kong 优雅实现微服务网关鉴权,登录场景落地实战篇
- 2024-12-23 微服务实战系列(九)-注册中心与网关高可用架构设计
- 2024-12-23 使用Kong作为微服务网关 微服务api网关
- 2024-12-23 微服务架构中API网关介绍 微服务api网关的作用
- 2024-12-23 万字长文详解微服务网关(中) 微服务网关是什么?
- 2024-12-23 微服务API聚合网关 An Aggregation API Gateway
- 2024-12-23 浅谈微服务:通信之网关 Ready 微服务架构中网关的作用
- 2024-12-23 国产微服务网关Apache APISIX 上手
- 2024-12-23 微服务架构之API网关——在微服务项目中的技术框架和用法实践
- 2024-12-23 38 微服务网关GateWay 统一鉴权 微服务网关权限控制
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)