计算机系统应用教程网站

网站首页 > 技术文章 正文

微服务网关 Gateway 进阶 - 认证鉴权

btikc 2024-12-23 08:53:02 技术文章 66 ℃ 0 评论

一、前言

网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,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、启动服务 测试

首先通过网关访问订单详情

然后再通过网关获取令牌

然后使用新获取到的令牌来访问订单详情

到此 网关中的鉴权功能开发完成。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表