SpringCloud微服务中如何实现多端认证?

开发 架构
本文提出了一种灵活、可扩展的方案,以满足 DailyMart 在业务发展过程中的多端认证需求。通过使用请求路径区分不同端的请求来源,并在 SpringCloud Gateway 网关中实现相应的过滤器进行认证,方案具有灵活性、可扩展性和可维护性。

概述

DailyMart是一个ToC的在线购物商城,目前仅支持通过浏览器访问。在商城中的所有操作都需要用户先登录。为了实现这一需求,我们可以采用以下技术方案:

  1. 用户通过SpringCloud Gateway访问CustomerService进行登录认证。认证成功后,服务器返回一个JWT(JSON Web Token)。在接下来的操作中,用户需要在请求头中携带此Token。
  2. 在网关服务中,我们创建了一个名为ApiAuthenticatorFilter的过滤器。该过滤器用于验证请求头中是否包含Token,并检查Token的有效性。如果请求头中没有携带Token,或者Token失效,则不允许访问后端接口。

详细交互流程如下图1所示:

图1:PC认证流程图1:PC认证流程

多端认证需求

这种架构在初期可以满足业务的发展需求。然而,随着业务的扩展,我们需要考虑到现在大部分用户使用手机进行购物的情况。因此,DailyMart也需要支持手机端访问。但与浏览器不同,手机端的认证机制可能会有所不同。

例如,浏览器端的Token有效期通常设定为1小时,而手机端的Token有效期通常设置为7天或更长。此外,浏览器端的Token采用JWT这种去中心化的认证机制,而手机端的Token采用中心化的认证机制,需要调用手机端服务进行登录认证。

同时,为了扩展业务,其他一些第三方应用可能也需要调用DailyMart的后端服务来获取数据,对于第三方的应用一般采用appId + appSecret的方式进行认证,同时需要对接口参数进行签名防止出现篡改和重放。(此方案在前文中有详细说明,可以通过链接跳转访问查看。)

现在的问题是,如何在原有架构的基础上满足这三种不同形式的认证需求呢?

图片图片

解决方案

要解决这个问题,最关键在于如何判断请求的来源,是来自浏览器端的请求、手机端的请求还是第三方的请求?

我们可以通过请求路径进行区分,对于不同端的请求使用不同的路径进行标识,可以做如下约定:

  • 手机端请求,需要在请求路径上带有/ph/
  • 浏览器请求,需要在请求路径上带有/pd/
  • 第三方请求,需要在路径请求上带有/pt/
  • ...

最终规定接口的完整请求路径为:/服务名/api/来源标识/接口路径/,如:http://localhost:9090/customer-service/api/pd/customer/info

这样在SpringCloud Gateway网关先获取请求的路径,再根据请求的路径判断请求来源,最后根据请求来源实现不同的认证方案。

解决这个问题的关键在于如何判断请求的来源,即是来自浏览器端、手机端还是第三方应用?

我们可以通过请求路径进行区分,对于不同端的请求使用不同的路径进行标识。例如:

  • 手机端请求,在请求路径上带有 /ph/
  • 浏览器端请求,在请求路径上带有 /pd/
  • 第三方请求,在请求路径上带有 /pt/
  • ...

最终,我们规定接口的完整请求路径为:/服务名/api/来源标识/接口路径/,例如:http://localhost:9090/customer-service/api/pd/customer/info

这样,在SpringCloud Gateway网关中,我们需要创建一个过滤器,首先获取请求的路径,然后根据请求的路径判断请求来源,最后根据请求来源实现不同的认证方案。

代码实现

有了解决方案,我们就很容易完成代码实现了。

为了满足多端认证的需求,在网关服务中我们可以抽取一个公共的认证接口ApiAuthenticator,具体的认证逻辑由具体实现类实现。

图片图片

在上面的类图中,ProtectedApiAuthenticator用于实现第三方的认证逻辑,DefaultApiAuthenticator用于实现浏览器端的认证逻辑。

在网关过滤器ApiAuthenticatorFilter中,我们首先根据请求路径获取请求来源,然后根据请求来源找到对应的实现类。

@Component
@Slf4j
public class ApiAuthenticatorFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI uri = exchange.getRequest().getURI();
        String rawPath = uri.getRawPath();
        // 静态接口直接过滤
        if (handleExcludeUrl(rawPath)) {
            return chain.filter(exchange);
        }
      
        // 获取认证逻辑
        ApiAuthenticator apiAuthenticator = getApiAuthenticator(rawPath);
        AuthenticatorResult authenticatorResult = apiAuthenticator.auth(exchange);
        
        if (!authenticatorResult.isResult()) {
            return Mono.error(new HttpServerErrorException(
                    HttpStatus.METHOD_NOT_ALLOWED, authenticatorResult.getMessage()));
        }
        
        return chain.filter(exchange);
        
    }
    
    
    
    /**
     * 确定认证策略
     * @param rawPath 请求路径
     */
    private ApiAuthenticator getApiAuthenticator(String rawPath) {
        String[] parts = rawPath.split("/");
        if (parts.length >= 4) {
            String parameter = parts[3];
            return switch (parameter) {
                case PROTECT_PATH -> new ProtectedApiAuthenticator();
                case PRIVATE_PATH -> new PrivateApiAuthenticator();
                case PUBLIC_PATH -> new PublicApiAuthenticator();
                case DEFAULT_PATH -> new DefaultApiAuthenticator();
                default -> throw new IllegalStateException("Unexpected value: " + parameter);
            };
        }
        return new DefaultApiAuthenticator();
    }
 
}

以下是浏览器端的认证逻辑,它会验证JWT token的有效性。如果token失效,则直接返回错误提示给用户,引导其重新登录。

@Component
@Slf4j
public class DefaultApiAuthenticator implements ApiAuthenticator {
    
    @Override
    public AuthenticatorResult auth(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders httpHeaders = request.getHeaders();
        
        // 获取JWT请求头 Authorization
        String token = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
        
        if (Objects.nonNull(token)) {
            try {
                String subjectFromJWT = JwtUtil.getSubjectFromJWT(token);
                log.info("用户请求token: {} , 身份Subject:{}", token, subjectFromJWT);
                //重新设置请求头
                mutateNewHeader(exchange, subjectFromJWT);

                return new AuthenticatorResult(true, "认证通过");
            } catch (ParseException | JOSEException e) {
                log.error("token解析失败");
                return new AuthenticatorResult(false, "Token错误,请重新登录!");
            }
            
        }
        
        return new AuthenticatorResult(false, "Token为空,请重新登录!");
    }
}

小结

本文提出了一种灵活、可扩展的方案,以满足 DailyMart 在业务发展过程中的多端认证需求。通过使用请求路径区分不同端的请求来源,并在 SpringCloud Gateway 网关中实现相应的过滤器进行认证,方案具有灵活性、可扩展性和可维护性。

责任编辑:武晓燕 来源: JAVA日知录
相关推荐

2024-09-04 11:16:44

端口Spring配置类

2024-03-06 08:36:36

2024-06-04 10:05:48

微服务网关日志

2022-03-03 08:51:09

微服务认证中心授权模式

2024-07-31 09:09:20

2023-03-01 08:57:32

2020-12-17 08:10:19

身份验证授权微服务

2022-04-14 08:51:49

微服务Redisson分布式锁

2021-06-09 09:42:50

SpringCloud微服务灰度发布

2021-03-09 09:33:42

网关授权微服务

2022-04-09 14:45:02

微服务常见概念Spring

2023-02-07 07:43:27

微服务应用框架

2020-11-15 23:48:57

服务网格微服务网络网络技术

2024-03-27 09:01:53

微服务FeignToken

2021-12-29 08:30:48

微服务架构开发

2024-11-06 16:27:12

2022-09-06 10:29:27

无服务器Serverless

2024-07-10 10:51:39

SpringEureka数据中心

2022-07-11 14:53:37

微服务容器IT

2020-05-26 11:59:30

日志链路微服务架构
点赞
收藏

51CTO技术栈公众号