前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringCloud怎么搭建GateWay网关&统一登录模块

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

作者头像
每周聚焦
发布2024-07-23 14:29:49
510
发布2024-07-23 14:29:49

项目介绍

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

  • 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去做检索模块等等。

项目目录如下:

image.png
image.png

项目的架构大概就是这样,本文想分享的登录模块,主要涉及到了两个服务: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,贴一下代码吧

代码语言:javascript
复制
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方法来完成登录参数获取与认证操作,我们要做的就是重写这个方法中的逻辑。

image.png
image.png

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

代码语言:javascript
复制
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。这是两个接口,实现这两个接口,并重写对应方法就可以实现自定义回调逻辑。

登录成功处理器

代码语言:javascript
复制
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。

代码语言:javascript
复制
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();
    }
}

登录失败处理器

代码语言:javascript
复制
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的配置方法,标注了过时,但是还能用。因为对这种方式比较熟悉,我暂时就还是用这种配置方法,后面再修改吧

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

代码语言:javascript
复制
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放到请求头中。

判断登录接口

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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中,登录拦截我采用了一个过滤器来完成。

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

代码语言:javascript
复制
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);
    }
}

跨域配置

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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;

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 项目介绍
  • blog-auth
    • 项目依赖
      • 登录参数接收
        • 登录成功&失败处理器
          • 配置类
            • 判断登录接口
            • blog-gateway
              • 路径重写
                • 登录拦截
                  • 跨域配置
                  • 前端代码
                  相关产品与服务
                  消息队列 TDMQ
                  消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档