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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 本文来分享一下,最近我在自己的项目中实现的认证服务,目前比较简单,就是可以提供一个公共的服务,专门来处理登录请求,然后我还在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

相关文章
|
6月前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
387 69
利用Spring Cloud Gateway Predicate优化微服务路由策略
|
4月前
|
前端开发 Java Nacos
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
421 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
|
7月前
|
负载均衡 Java 应用服务中间件
Gateway服务网关
Gateway服务网关
229 1
Gateway服务网关
|
6月前
|
JavaScript Java Kotlin
深入 Spring Cloud Gateway 过滤器
Spring Cloud Gateway 是新一代微服务网关框架,支持多种过滤器实现。本文详解了 `GlobalFilter`、`GatewayFilter` 和 `AbstractGatewayFilterFactory` 三种过滤器的实现方式及其应用场景,帮助开发者高效利用这些工具进行网关开发。
751 1
|
7月前
|
负载均衡 Java API
项目中用的网关Gateway及SpringCloud
Spring Cloud Gateway 是一个功能强大、灵活易用的API网关解决方案。通过配置路由、过滤器、熔断器和限流等功能,可以有效地管理和保护微服务。本文详细介绍了Spring Cloud Gateway的基本概念、配置方法和实际应用,希望能帮助开发者更好地理解和使用这一工具。通过合理使用Spring Cloud Gateway,可以显著提升微服务架构的健壮性和可维护性。
264 0
|
7月前
|
NoSQL Java Redis
springCloud中将redis共用到common模块
通过将Redis配置和操作服务提取到Common模块,可以在Spring Cloud微服务架构中实现高效的代码复用和统一管理。这种设计不仅简化了各个服务的配置和依赖管理,还提高了代码的可维护性和可读性。希望本文对你在Spring Cloud项目中集成和使用Redis有所帮助。
275 0
|
9月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
432 5
|
9月前
|
Java 开发者 Spring
Spring Cloud Gateway 中,过滤器的分类有哪些?
Spring Cloud Gateway 中,过滤器的分类有哪些?
243 3
|
8月前
|
负载均衡 Java API
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
161 0
|
9月前
|
监控 负载均衡 安全
微服务(五)-服务网关zuul(一)
微服务(五)-服务网关zuul(一)