SpringCloud怎么搭建GateWay网关&统一登录模块

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 本文来分享一下,最近我在自己的项目中实现的认证服务,目前比较简单,就是可以提供一个公共的服务,专门来处理登录请求,然后我还在API网关处实现了登录拦截的效果,因为在一个博客系统中,有一些地址是可以不登录的,比方说首页;也有一些是必须登录的,比如发布文章、评论等。所以,在网关处可以支持自定义一些不需要登录的地址,一些需要登录的地址,也可以在网关处进行校验,如果未登录,可以返回JSON格式的出参,前端可以进行相关处理,比如跳转到登录页面等。

项目介绍

项目中现在有如下几个模块:

  • blog-auth:认证服务,目前只提供了基本的登录逻辑处理。
  • blog-common:公共依赖模块,一些公共的依赖、工具类、配置等都封装在这里,其它的微服务都引入这个模块
  • blog-gateway:API网关服务,是所有流量的入口。目前做了路径重写、登录校验功能。
  • blog-web:web端api服务,还未做开发,设想是将web端的所有api接口,都写到这个服务中。
  • blog-vue:博客系统的前端服务,使用Vue3开发,为了开发方便,我就将它和上面的模块放到了一个目录中,后续可能还会开发一个后台管理,也打算放在同一目录下。

技术栈方面:

  • 后端:SpringCloud、SpringBoot、Mybatis-plus、SpringSecurity等。
  • 前端:Vue3、Element Plus、Axios等。
  • 中间件:目前还没有应用到中间件,后续会引入。例如使用redis去保存登录的token、rabbitmq在登录后发送站内信或邮件、ElasticSearch去做检索模块等等。

项目目录如下:

项目的架构大概就是这样,本文想分享的登录模块,主要涉及到了两个服务:blog-auth、blog-gateway。blog-auth中引入了SpringSecurity,实现了一个基本的登录流程逻辑。blog-gateway中针对每个请求,都去判断了是否需要登录、如果是,还要判断是否已登录,如果否,会返回JSON格式的提示信息。

下面就分享一下里面的细节:

blog-auth

这个模块主要用来实现一个基本的登录流程,因为自己学习过SpringSecurity,那这里就使用了它来做登录。SpringSecurity默认的登录流程是表单登录,但我们这里是前后端分离,需要使用JSON交互,所以就做了一些相关的配置,来让SpringSecurity返回JSON数据。

SpringSecurity的登录流程都是固定的,我们只需要修改几个地方:

  1. 对于登录参数的接收:原有登录流程中是接收的表单数据,我们这里需要改造为接收JSON格式的请求入参。
  2. 对于登录后的处理:原有流程中,无论是登录成功还是失败,都是进行重定向。这里要改造为返回JSON格式的数据,登录成功的场景下,还需要返回token。
  3. 提供一个配置类,配置一些拦截规则、以及上面提到的内容等等。

下面来一一看下

项目依赖

依赖很简单,就是引入了一个SpringSecurity,贴一下代码吧

xml

代码解读

复制代码

<dependencies>
    <dependency>
        <groupId>com.xb.blog</groupId>
        <artifactId>blog-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--SpringSecurity-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

登录参数接收

SpringSecurity中,所有的功能都是由一个个的过滤器来完成的,所有的过滤器组合成一个过滤器链。登录参数接收相关的过滤器是UsernamePasswordAuthenticationFilter,我们现在要做的是重写一个过滤器,并替换掉UsernamePasswordAuthenticationFilter。

在UsernamePasswordAuthenticationFilter中,作为一个过滤器组件,首先被调用的是doFilter方法,在这个方法中,会调用attemptAuthentication方法来完成登录参数获取与认证操作,我们要做的就是重写这个方法中的逻辑。

如下代码,定义一个AuthFilter,继承自UsernamePasswordAuthenticationFilter,并重写它的attemptAuthentication方法,这个方法就是具体的登录参数的获取方法。

java

代码解读

复制代码

public class AuthFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> userInfo;
            try {
                userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                        username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

可以看到,先进行了判断,在SpringSecurity中,为了安全起见,登录请求被限制为Post请求。然后我们从请求体中获取了username和password,组合为了UsernamePasswordAuthenticationToken对象,然后调用了认证管理器的authenticate方法进行认证操作。

登录成功&失败处理器

登录成功与失败时,我们也需要返回JSON格式的数据。这里就提供两个处理器来做这件事。

SpringSecurity中的认证成功&失败处理器分别为 AuthenticationSuccessHandler、AuthenticationFailureHandler。这是两个接口,实现这两个接口,并重写对应方法就可以实现自定义回调逻辑。

登录成功处理器

java

代码解读

复制代码

@Component
public class AuthAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {

        //登录成功,生成token,并保存到响应头中
        String token = AuthUtil.createToken(((AuthUser) authentication.getPrincipal()).getUsername());
        response.setHeader("Token", token);
        response.setHeader("Access-Control-Expose-Headers", "Token");

        response.setContentType("application/json;charset=utf-8");
        Result result = new Result();
        result.setCode("0");
        result.setMessage("登录成功");
        response.getWriter().write(new ObjectMapper().writeValueAsString(result));
    }
}

这个处理器中,不只是返回了JSON格式数据。还生成了token,将它放到了请求头中,方便前端从响应头中拿到token,在后面的请求中放到请求头中。

这个操作我是经过考虑的,本想将token保存到cookie中,但是在调研过程中发现,项目中可能为了安全,将Cookie设置为HttpOnly,这样前端就没法获取cookie中的值了。所以最后采取了放在响应头中的做法,前端会保存到localStorage中,然后每次请求时,都会从localStorage中获取token放到请求头中。

这个案例中使用了一个工具类AuthUtil,是我自己封装的,里面使用了HuTool工具包的JwtUtil生成了token。

java

代码解读

复制代码

public class AuthUtil {

    private static String KEY = "KEY20240421";

    /**
     * 判断当前请求是否认证
     *
     * @param token
     * @return
     */
    public static Boolean isAuth(String token) {
        return JWTUtil.verify(token, KEY.getBytes());
    }

    /**
     * 根据username生成token
     *
     * @param username
     * @return
     */
    public static String createToken(String username) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("username", username);
        return JWTUtil.createToken(payload, KEY.getBytes());
    }

    /**
     * 解析token 获取username
     *
     * @param token
     * @return
     */
    public static String getUsernameFromToken(String token) {
        JWT jwt = JWTUtil.parseToken(token);
        return jwt.getPayload("username").toString();
    }
}

登录失败处理器

java

代码解读

复制代码

@Component
public class AuthAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Result result = new Result();
        result.setCode("1");
        result.setMessage("登录失败:"+exception.getMessage());
        response.getWriter().write(new ObjectMapper().writeValueAsString(result));
    }
}

登陆失败回调就很简单,就是返回JSON格式的数据。前端拿到后会在页面上弹窗展示。

配置类

SpringSecurity高版本弃用了继承WebSecurityConfigurerAdapter的配置方法,标注了过时,但是还能用。因为对这种方式比较熟悉,我暂时就还是用这种配置方法,后面再修改吧

代码如下,主要就是将上面定义的组件进行了配置,然后配置了一些拦截规则等等,注释比较详细,就不再赘述。

java

代码解读

复制代码

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthAuthenticationSuccessHandler authAuthenticationSuccessHandler;

    @Autowired
    private AuthAuthenticationFailureHandler authAuthenticationFailureHandler;

    /**
     * 配置过滤器链
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login/**").permitAll()
                .antMatchers("/getAuthUser/**").permitAll()
                .anyRequest().authenticated();

        http.csrf().disable();
    }

    /**
     * 提供自定义登录过滤器,定义从post请求体中获取登录请求参数
     *
     * @return
     */
    @Bean
    public AuthFilter authFilter() throws Exception {
        AuthFilter filter = new AuthFilter();
        //认证管理器
        filter.setAuthenticationManager(authenticationManagerBean());
        //认证成功处理器
        filter.setAuthenticationSuccessHandler(authAuthenticationSuccessHandler);
        //认证失败处理器
        filter.setAuthenticationFailureHandler(authAuthenticationFailureHandler);
        return filter;
    }

    /**
     * 提供认证管理器
     *
     * @return
     * @throws Exception
     */
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

上述配置完毕之后,就可以访问/login接口进行登录操作了,登录成功之后会返回JSON格式数据,并且token会被放到响应头中,前端中我将其保存到了localStorage中,然后每次请求时,都会从localStorage中获取token放到请求头中。

判断登录接口

目前还提供了一个判断当前请求是否已登录的接口,用于首页判断用户是否登录。

java

代码解读

复制代码

@RestController
public class AuthController {

    @Autowired
    private AuthUserDetailsService authUserDetailsService;

    @GetMapping("getAuthUser")
    public Result getAuthUser(HttpServletRequest request) {
        String token = request.getHeader("Token");
        if (StrUtil.isNotBlank(token)) {
            Boolean isAuth = AuthUtil.isAuth(token);
            if (isAuth) {
                String username = AuthUtil.getUsernameFromToken(token);
                UserDetails user = authUserDetailsService.loadUserByUsername(username);
                AuthUserVo authUser = new AuthUserVo();
                BeanUtils.copyProperties(user, authUser);
                return Result.success(authUser);
            }
        }
        return Result.success(null);
    }
}

blog-gateway

网关作为一切流量的入口,我主要做了两件事

  1. 路径重写
  2. 登录拦截

路径重写

我现在的设计中,后端所有的接口都是/api开头,调用不同的服务后面拼不同的后缀,例如/api/auth、/api/web等等。所以我要在网关中进行路径重写,将/api/auth/xxx 重写为/auth/xxx,这个在配置文件中配置即可。

下面的配置文件中,gateway:routes:部分是配置了路径重写。然后还进行了一个自定义配置auth:excludePaths:,这里是配置了所有不需要登录的路径。

application.yml

yml

代码解读

复制代码

# Tomcat
server:
  port: 88
# 注册中心 配置中心
spring:
  application:
    name: blog-gateway
  cloud:
    nacos:
      discovery:
        # 注册中心地址
        server-addr: 127.0.0.1:8848
#      config:
#        # 配置中心地址
#        server-addr: 127.0.0.1:8848

    gateway:
      routes:
        # web服务
        - id: web_route
          uri: lb://blog-web
          predicates:
            - Path=/api/web/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        # 认证服务
        - id: auth_route
          uri: lb://blog-auth
          predicates:
            - Path=/api/auth/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
  config:
    import: application.yml

auth:
  # 不需要登录 即可访问的地址
  excludePaths:
    - /api/auth/**

登录拦截

上面的配置文件中,配置了不需要登录就可访问的地址,在GateWay中,登录拦截我采用了一个过滤器来完成。

代码中注释齐全,就不赘述了,关于返回状态码的硬编码问题,现在还没有处理,以后会进行处理的。

java

代码解读

复制代码

/**
 * 认证过滤器:
 * 将这个过滤器配置在 NettyRoutingFilter 之前,实现在路由转发之前进行登录校验工作
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private AuthProperties authProperties;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @SneakyThrows(IOException.class)
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //判断是否需要登录
        if (isLoginRequiredForPath(request.getPath().toString())) {
            //判断用户是否已经登录
            if (!isAuth(request)) {
                Result result = new Result();
                result.setCode("99");
                result.setMessage("NO_LOGIN");
                byte[] bytes = new ObjectMapper().writeValueAsBytes(result);
                return response.writeWith(Mono.fromSupplier(() -> response.bufferFactory().wrap(bytes)));
            }
        }
        //不需要登录&已登录,放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }


    /**
     * 判断传入的地址是否需要登录
     *
     * @param path
     * @return
     */
    private boolean isLoginRequiredForPath(String path) {
        for (String pattern : authProperties.getExcludePaths()) {
            if (pathMatcher.match(pattern, path)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断该请求是否已经登录
     *
     * @param request
     * @return
     */
    private boolean isAuth(ServerHttpRequest request) {
        //获取请求头中的token
        List<String> headers = request.getHeaders().get("Token");
        String token = "";
        if (!CollUtil.isEmpty(headers)) {
            token = headers.get(0);
        }
        if (StrUtil.isBlank(token)) {
            return false;
        }

        //校验token
        return AuthUtil.isAuth(token);
    }
}

跨域配置

前后端开发中,还可能会出现跨域问题。在微服务项目中,可以在网关处统一配置跨域,提供一个配置类即可。

java

代码解读

复制代码

/**
 * 跨域配置
 */
@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.addAllowedOriginPattern("*");
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return new CorsWebFilter(source);
    }
}

前端代码

前端方面我是个小白,磕磕绊绊的实现了登录,贴一下请求拦截部分的代码吧

request.js

js

代码解读

复制代码

import axios from "axios";
import { localStorage } from "@/utils/storage";

// 创建axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_SERVICE_API,
  timeout: 50000, // 请求超时时间:50s
  headers: { "Content-Type": "application/json;charset=utf-8" },
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    //如果localStorage中有token,取出放到请求头中
    let token = localStorage.get("BLOG_TOKEN");
    if (token) {
      config.headers["Token"] = token;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    //如果响应头中有token,保存到localStorage中
    const headers = response.headers;
    let token = headers["token"];
    if (token) {
      localStorage.set("BLOG_TOKEN", token);
    }
    return response.data;
  },
  (error) => {
    console.log("请求异常:", error);
  }
);

// 导出实例
export default service;

转载来源:https://juejin.cn/post/7376915846109003817

相关文章
|
4月前
|
存储 缓存 负载均衡
Gateway 网关坑我! 被这个404 问题折腾了一年?
小富分享了一个困扰团队一年多的 SpringCloud Gateway 路由 404 问题。通过日志追踪和源码分析,发现是网关在 Nacos 配置更新后未能正确清理旧的路由权重缓存,导致负载均衡时仍使用已删除的路由数据。最终通过监听路由刷新事件并手动更新缓存,成功解决了问题。
834 125
Gateway 网关坑我! 被这个404 问题折腾了一年?
|
4月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
3月前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
408 1
|
3月前
|
机器学习/深度学习 Kubernetes API
【Azure APIM】自建网关(self-host gateway)收集请求的Header和Body内容到日志中的办法
在Azure API Management中,通过配置trace策略可完整记录API请求的Header和Body信息。在Inbound和Outbound策略中分别使用context.Request/Response.Headers和Body.As&lt;string&gt;方法捕获数据,并写入Trace日志,便于排查与审计。
136 7
|
5月前
|
安全 虚拟化
Omnissa Secure Email Gateway 2.33 - 电子邮件网关
Omnissa Secure Email Gateway 2.33 - 电子邮件网关
116 0
|
7月前
|
应用服务中间件 网络安全 数据安全/隐私保护
网关服务器配置指南:实现自动DHCP地址分配、HTTP服务和SSH无密码登录。
哇哈哈,道具都准备好了,咱们的魔术秀就要开始了。现在,你的网关服务器已经魔法满满,自动分配IP,提供网页服务,SSH登录如入无人之境。而整个世界,只会知道效果,不会知道是你在幕后操控一切。这就是真正的数字世界魔法师,随手拈来,手到擒来。
378 14
|
6月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
406 0
|
7月前
|
缓存 监控 Java
说一说 SpringCloud Gateway 堆外内存溢出排查
我是小假 期待与你的下一次相遇 ~
923 5
|
7月前
|
Java API Nacos
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
1130 69
利用Spring Cloud Gateway Predicate优化微服务路由策略