基于Session的认证与授权实践

news2024/11/26 15:24:37

Spring Security系列文章

  • 认证与授权之Cookie、Session、Token、JWT
  • 基于Session的认证与授权实践

基于Session的认证方式

基于 session 的认证方式如下图:

基于 session 的认证方式

基于 Session 的认证机制由 Servlet 规范定制,Servlet 容器已实现,用户通过 HttpSession 的操作方法即可实现,如下是 HttpSession 相关的操作API。

HttpSession核心方法

创建工程

本项目使用 maven 搭建,使用 SpringMVC、Servlet3.0 实现。

创建maven工程

1、导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>spring-security-study</artifactId>
    <groupId>com.msdn.security</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>springmvc-session</artifactId>

  <packaging>war</packaging>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>


  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.3.23</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.20</version>
    </dependency>
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>5.8.5</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Servlet Context配置

本案例采用 Servlet3.0 无 web.xml 方式,在 config 包下定义 WebConfig.java,它对应于 DispatcherServlet 配置。

@Configuration//就相当于springmvc.xml文件
@EnableWebMvc
@ComponentScan(basePackages = "com.msdn.security"
        ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    //视图解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

}

加载Spring容器

在 init 包下定义 Spring 容器初始化类 SpringApplicationInitializer,此类实现 WebApplicationInitializer 接口,Spring 容器启动时加载WebApplicationInitializer 接口的所有实现类。

public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

SpringApplicationInitializer 相当于 web.xml,使用了servlet3.0开发则不需要再定义 web.xml,WebConfig.class 对应以下配置的 spring-mvc.xml,web.xml的内容参考:

<web‐app>
    <listener>
        <listener‐class>org.springframework.web.context.ContextLoaderListener</listener‐class>
    </listener>
    <context‐param>
        <param‐name>contextConfigLocation</param‐name>
        <param‐value>/WEB‐INF/application‐context.xml</param‐value>
    </context‐param>
  
    <servlet>
        <servlet‐name>springmvc</servlet‐name>
        <servlet‐class>org.springframework.web.servlet.DispatcherServlet</servlet‐class>
        <init‐param>
            <param‐name>contextConfigLocation</param‐name>
            <param‐value>/WEB‐INF/spring‐mvc.xml</param‐value>
        </init‐param>
        <load‐on‐startup>1</load‐on‐startup>
    </servlet>
    <servlet‐mapping>
        <servlet‐name>springmvc</servlet‐name>
        <url‐pattern>/</url‐pattern>
    </servlet‐mapping>
 
</web‐app>

实现认证功能

认证页面

在 webapp/WEB-INF/view 下定义认证页面 login.jsp,本案例只是测试认证流程,页面没有添加css样式,页面实现可填入用户名,密码,触发登录将提交表单信息至/login,内容如下:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<p style="color: red">${msg }</p>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>
    密&nbsp;&nbsp;&nbsp;码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

在 WebConfig 中新增如下配置,将/直接导向 login.jsp 页面:

@Override
public void addViewControllers(ViewControllerRegistry registry) {
  registry.addViewController("/").setViewName("login");
}

启动项目,配置 tomcat

SprignMVC项目配置tomcat

SprignMVC项目配置tomcat

认证接口

用户进入认证页面,输入账号和密码,点击登录,请求/login 进行身份认证。

1、定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:

public interface AuthenticationService {

  /**
   * 用户认证
   *
   * @param userRequest
   * @return
   */
  User authentication(UserRequest userRequest);
}

2、表单请求参数封装为实体类

@Data
public class UserRequest {

  private String username;
  private String password;
}

3、认证成功后返回的用户详细信息

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

  private Long id;
  private String username;
  private String password;
  private String fullname;
  private String mobile;
}

4、认证服务具体实现类

@Service
public class AuthenticationServiceImpl implements AuthenticationService {

  private static Map<String, User> userMap = new HashMap<>();

  static {
    userMap.put("zhangsan", new UserDTO(1010L, "zhangsan", "123", "张三", "133443"));
    userMap.put("lisi", new UserDTO(1011L, "lisi", "456", "李四", "144553"));
  }

  @Override
  public User authentication(UserRequest userRequest) {
    User user = getUserByName(userRequest.getUsername());
    if (Objects.isNull(user)) {
      throw new RuntimeException("查询不到该用户");
    }

    if (!Objects.equals(user.getPassword(), userRequest.getPassword())) {
      throw new RuntimeException("账号或密码错误");
    }

    return user;
  }

  /**
     * 模仿从表中根据用户名查询用户信息
     *
     * @param username
     * @return
     */
  public User getUserByName(String username) {
    return userMap.get(username);
  }
}

5、controller 对 login 请求做处理

@RestController
public class LoginController {

  @Autowired
  private AuthenticationService authenticationService;

  @PostMapping(value = "/login")
  public String login(UserRequest request, Model model) {
    if (Objects.isNull(request) || isBlank(request.getUsername()) ||
        isBlank(request.getPassword())) {
      model.addAttribute("msg", "账号或密码为空");
      return "login";
    }
    try {
      User user = authenticationService.authentication(request);

      return "redirect:hello";
    } catch (Exception e) {
      model.addAttribute("msg", e.getMessage());
    }
    return "login";
  }

}

6、测试,重新启动 tomcat

输入正确的用户名和密码,则提示登录成功,如果账号或密码不输入,则会提示报错信息;如果账号或密码校验不通过,会提示具体报错。

实现会话功能

会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。

认证的目的是对系统资源的保护,每次对资源的访问,系统必须得知道是谁在访问资源,才能对该请求进行合法性拦截。因此,在认证成功后,一般会把认证成功的用户信息放入 Session中,在后续的请求中,系统能够从 Session 中获取到当前用户,用这样的方式来实现会话机制。

在上一节我们详细介绍了 Cookie 和 Session,我们此处创建的项目启动后就作为临时服务器,存储 session 信息,而客户端通常是将 sessionId 存放在 cookie 中的,所以我们还需要设置 cookie 返回给客户端。

1、cookie 操作工具类

public class CookieUtil {


  public static Cookie addUserCookie(String cookieValue) {
    return addCookie("user_session_id", cookieValue);
  }

  public static Cookie addCookie(String cookieName, String cookieValue) {
    Cookie cookie = new Cookie(cookieName, cookieValue);
    cookie.setMaxAge(3600);
    cookie.setPath("/");
    return cookie;
  }

  public static String getUserCookie(HttpServletRequest request) {
    return getCookie(request, "user_session_id");
  }

  public static String getCookie(HttpServletRequest request, String cookieName) {
    Cookie[] cookies = request.getCookies();
    String cookieValue = "";
    for (Cookie cookie : cookies) {
      if (cookieName.equals(cookie.getName())) {
        cookieValue = cookie.getValue();
      }
    }
    return cookieValue;
  }

}

2、修改 controller 中 login 方法,当认证成功后,将用户信息放入当前会话,并将 sessionId 放入 cookie 中。并增加用户登出方法,登出时将 session 置为失效。

@PostMapping(value = "/login")
public String login(UserRequest request, HttpSession session, Model model,
                    HttpServletResponse response) {
  if (Objects.isNull(request) || isBlank(request.getUsername()) ||
      isBlank(request.getPassword())) {
    model.addAttribute("msg", "账号或密码为空");
    return "login";
  }
  try {
    User user = authenticationService.authentication(request);
    String userSessionId = RandomUtil.getRandom().nextInt(10000) + "_user";
    session.setAttribute(userSessionId, user);
    Cookie cookie = CookieUtil.addUserCookie(userSessionId);
    response.addCookie(cookie);

    return "redirect:hello";
  } catch (Exception e) {
    model.addAttribute("msg", e.getMessage());
  }
  return "login";
}


@RequestMapping(value = "logout")
public String logout(HttpSession session) {
  session.invalidate();
  return "login";
}

3、在 controller 中增加资源访问测试接口,判断 session 中是否有用户

  @RequestMapping(value = "/r/r1")
  public String r1(HttpServletRequest request, Model model) {
    String userSessionId = CookieUtil.getUserCookie(request);
    HttpSession session = request.getSession();
    User user = (User) session.getAttribute(userSessionId);
    String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
    model.addAttribute("text", fullName + " 访问资源1");

    return "resource";
  }

4、重启 tomcat,未登录情况下直接访问测试资源 r/r1,具体路径为:http://localhost:8080/r/r1

实现授权功能

现在我们已经完成了用户身份凭证的校验以及登录的状态保持,并且我们也知道了如何获取当前登录用户(从Session中获取)的信息,接下来,用户访问系统需要经过授权,即需要完成如下功能:

  • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
  • 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。

1、增加权限数据

实际工作中,用户和角色关联,然后角色又和权限表关联,在本次测试阶段,为了方便操作,我们直接在 User 里增加权限属性。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

  private Long id;
  private String username;
  private String password;
  private String fullname;
  private String mobile;

  // 用户权限
  private Set<String> authorities;
}

2、并在 AuthenticationServiceImpl 认证服务具体实现类中给用户初始化权限,实际应用中肯定不会这样,会从数据库中获取用户信息。

  private static Map<String, User> userMap = new HashMap<>();

  static {
    Set<String> authoritie1 = new HashSet<>();
    authoritie1.add("p1");
    Set<String> authoritie2 = new HashSet<>();
    authoritie2.add("p2");
    userMap.put("zhangsan", new User(1010L, "zhangsan", "123", "张三", "133443", authoritie1));
    userMap.put("lisi", new User(1011L, "lisi", "456", "李四", "144553", authoritie2));
  }

3、增加测试资源

在 controller 文件中增加对资源 r1、r2 的访问

@RequestMapping(value = "/r/r1")
public String r1(HttpServletRequest request, Model model) {
  String userSessionId = CookieUtil.getUserCookie(request);
  HttpSession session = request.getSession();
  User user = (User) session.getAttribute(userSessionId);
  String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
  model.addAttribute("text", fullName + " 访问资源1");

  return "resource";
}

@RequestMapping(value = "/r/r2")
public String r2(HttpServletRequest request, Model model) {
  String userSessionId = CookieUtil.getUserCookie(request);
  HttpSession session = request.getSession();
  User user = (User) session.getAttribute(userSessionId);
  String fullName = Objects.nonNull(user) ? user.getFullname() : "匿名";
  model.addAttribute("text", fullName + " 访问资源2");

  return "resource";
}

4、实现授权拦截器

在 interceptor 包下定义 SimpleAuthenticationInterceptor 拦截器,实现授权拦截:

  1. 校验用户是否登录
  2. 校验用户是否拥有操作权限
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    String userSessionId = CookieUtil.getUserCookie(request);
    Object attribute = request.getSession().getAttribute(userSessionId);
    if (Objects.isNull(attribute)) {
      writeContent(response, "请先登录");
    }
    User user = (User) attribute;
    String requestURI = request.getRequestURI();
    if (user.getAuthorities().contains("p1") && requestURI.contains("r1")) {
      return true;
    }
    if (user.getAuthorities().contains("p2") && requestURI.contains("r2")) {
      return true;
    }
    if (requestURI.contains("resource")) {
      return true;
    }

    writeContent(response, "权限不足,无法访问");
    return false;
  }

  private void writeContent(HttpServletResponse response, String msg) throws IOException {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter writer = response.getWriter();
    writer.print(msg);
    writer.close();
    response.resetBuffer();
  }
}

在 WebConfig 中配置拦截器,匹配 /r/**的资源为受保护的系统资源,访问该资源的请求进入 SimpleAuthenticationInterceptor 拦截器。

@Autowired
private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
}

4、重启 tomcat,张三登录后,分别访问 r1 和 r2 资源,查看页面返回信息。

项目演示

1、登录

如果账号或密码为空,点击登录按钮,则会提示“账号或密码为空”。

登录时账号或密码为空

如果账号或密码错误,点击登录按钮,页面展示如下:

登录时账号或密码错误

如果账号和密码都正确,点击登录按钮,页面展示如下:

登录成功页面展示

登录成功后,我们在浏览器上查看 cookie 中存储的 sessionId。

浏览器查看sessionId

2、资源访问

张三可以访问 r1 资源,但无权访问 r2 资源。

有权访问资源r1

无权访问资源r1

李四可以访问 r2 资源,但无权访问 r1 资源。

小结

基于 session 的认证和授权方式比较简单,认证过程清晰明了,但是在大型项目中修改麻烦,不易扩展。所以实际生产中我们往往会考虑使用第三方安全框架(如 Spring Security,shiro等安全框架)来实现认证授权功能。

本文主要还是对上一篇文章中提到的知识点进行实操,方便大家直观理解,关于登录认证还有其他操作,比如记住密码等,这里就不过多介绍了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/68237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Matplotlib入门[05]——注释与标签

Matplotlib入门[05]——注释与标签 参考&#xff1a; https://ailearning.apachecn.org/ Matplotlib官网 plt.legend参数 使用Jupyter进行练习 注释 使用文本框进行注释 import numpy.random import matplotlib.pyplot as pltfig plt.figure(1, figsize(5,5)) # plt.clf…

ag-Grid Enterprise v28.2.1 企业版注册版

世界上最好的 JavaScript 网格 ag-Grid Enterprise v28.2.1 功能丰富 ag-Grid Enterprise v28.2.1 的性能、功能集和质量在 JavaScript 数据网格中前所未见。AG Grid 中的许多功能都是 AG Grid 独有的&#xff0c;并且只是将 AG Grid 归为一类&#xff0c;而不会影响质量或性能…

(详解错误情况,及解决方法)Vue 数据更新了但页面没有更新的情况

点个关注&#xff0c;赞一下栓Q 背景 在vue项目中&#xff0c;有些我们会遇到修改完数据&#xff0c;但是视图却没有更新的情况。具体的场景不一样&#xff0c;解决问题的方法也不一样。在网上看了很多文章&#xff0c;在此总结汇总一下。针对&#xff0c;数据更新视图没有更…

你知道哪些常用快捷键?电脑快捷键大全,打工人必备!

所谓的电脑快捷键&#xff0c;就是利用电脑键盘上的一个或几个按键组合完成一个功能命令&#xff0c;从而提高电脑的操作速度&#xff0c;带给我们更便捷的操作方式。电脑常用的快捷键是什么&#xff1f;以下是一些常用电脑快捷键的使用和功能的简要介绍。希望电脑快捷键大全能…

演讲实录 | OpenMLDB 整合自动特征工程

本文整理自 OpenMLDB 社区开发者、伊利诺伊大学 徐鹏程 在 OpenMLDB Meetup No.7 中的分享——《OpenMLDB 整合自动特征工程》。 大家好&#xff0c;我是来自伊利诺伊大学的硕士在读学生&#xff0c;也是 OpenMLDB 开源社区的贡献者——徐鹏程。我参与开发的项目&#xff0c;也…

RabbitMQ——RabbitMQ的六种工作模式详解

RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用来通过普通协议在完全不同的应用之间共享数据&#xff0c;RabbitMQ是使用Erlang(高并发语言)语言来编写的&#xff0c;并且RabbitMQ是基于AMQP协议的 AMQP协议 Advanced Message Queuing Protocol&#xff08;高级消…

JVM之垃圾回收器一

如何判断对象是否存活 引用计数器 给对象中添加一个引用计数器&#xff0c;每当有一个地方引用它时&#xff0c;计数器值就加1&#xff1b;当引用失效时&#xff0c;计数器值就减1&#xff1b;任何时刻计数器都为0的对象就是不可能再被使用的。 Java语言中没有选用引用计数算法…

对比分析小游戏引擎孰优孰劣

随着微信生态中&#xff0c;小程序应用指数级的增长&#xff0c;许多休闲游戏变成为了众多游戏厂商流量变现的新手段。以近期很火的“羊了个羊”为例&#xff0c;它便是我们常常所说的小游戏。 游戏和小游戏的区别 要盘点小游戏开发引擎之前&#xff0c;我们得先来了解下游戏…

leetcode 638. 大礼包-思路整理

题目 在 LeetCode 商店中&#xff0c; 有n件在售的物品。每件物品都有对应的价格。然而&#xff0c;也有一些大礼包&#xff0c;每个大礼包以优惠的价格捆绑销售一组物品。 给你一个整数数组price表示物品价格&#xff0c;其中price[i]是第i件物品的价格。另有一个整数数组nee…

扩散模型(Diffusion)最新综述+GitHub论文汇总-A Survey On Generative Diffusion

扩散模型(Diffusion Model)最新综述GitHub论文汇总-A Survey On Generative Diffusion 本综述来自香港中文大学Pheng-Ann Heng、西湖大学李子青实验室和浙江大学陈广勇团队&#xff0c;对现有的扩散生成模型进行了全面的回顾。本文首先提出了diffusion model改进算法的细化分类…

Thread类及常见方法

文章目录一、Thread常见构造方法二、Thread常见属性三、Thread常见方法start()获取当前线程休眠当前线程中断线程join()一、Thread常见构造方法 Thread类是JVM用来管理线程的一个类&#xff0c;每个线程都有唯一一个Thread对象与之对应&#xff0c;JVM会将这些对象组织起来&am…

世界杯征文活动 | 神奇!一段JavaScript代码生成会动的足球

世界杯征文活动 | 神奇&#xff01;一段JavaScript代码生成会动的足球 文章目录前言一、效果展示二、代码解析1.把足球图片转换为base64格式2.根据base64格式的字符串&#xff0c;创建img标签图片对象3.创建存放图片img的div标签对象4.使div旋转起来总结前言 花有重开日&#…

python中的import详解

0. 什么是导入&#xff1f; 导入从本质上讲&#xff0c;就是载入另一个文件&#xff0c;并能够读取那个文件的内容 0.1 模块和属性 模块往往就是变量名的封装&#xff0c;被认作是命名空间属性就是绑定在特定对象上的变量名 0.2 from和import 通过import得到了具有属性的模…

697226-52-1, 细胞穿膜肽TAT-amide

TAT-amide 是一种细胞穿透肽。细胞穿透肽 (CPPs) 是能够进入不同细胞的短氨基酸序列&#xff0c;用于细胞的物质细胞递送。TAT-amide is a cell penetrating peptide. Cell-penetrating peptides (CPPs) are short amino acid sequences able to enter different cells[1][2]. …

软件测试工程师应该学Python还是学Java?

前言 对于一个软件测试工程师来说&#xff0c;选哪一门语言来入手编程一直是件非常纠结的事情&#xff0c;当然立志做一辈子功能测试的人除外。 当你学完软件测试基本理论&#xff0c;掌握业务测试流程&#xff0c;功能测试可以搞定&#xff0c;数据库和linux玩的也很溜时&am…

Day822.Happens-Before 规则 -Java 并发编程实战

Happens-Before 规则 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于Happens-Before 规则的内容。 可见性、原子性、有序性这三者在编程领域属于共性问题&#xff0c;所有的编程语言都会遇到&#xff0c;Java 在诞生之初就支持多线程&#xff0c;自然也有针对这三者…

Blender 雕刻

文章目录简介.基本操作.进入雕刻.雕刻工作区.动态拓扑.动态拓扑属性.重构网格.物体数据属性重构网格.雕刻自带的重构网格.简介. 1 雕刻的本质是是用笔刷对顶点进行移动 2 被雕刻的物体要有足够多的顶点才行&#xff0c;可以使用动态拓扑解决顶点不够的问题 基本操作. 进入雕…

你猜,怎么用一句话证明你是项目经理?

早上好&#xff0c;我是老原。 熟悉我的老朋友都知道&#xff0c;我有两大爱好&#xff0c;一是钟情于挖招聘JD&#xff0c;二是喜欢逛知乎。 今天在知乎上看到一个很有趣的话题&#xff1a;你如何用一句话证明你是项目经理&#xff1f; 项目管理发展至今&#xff0c;有技术/…

JavaScript大作业(餐厅美食网站设计与实现)

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

【CBAM||目标识别||注意力机制||gated卷积】Convolutional Block Attention Module

这篇是2018年的paper&#xff0c;已经有很多中文资料可以学习&#xff0c;因而不做重复工作~记录一下核心要点&#xff0c;后续可阅。【学习资源】CBAM&#xff1a;卷积注意力机制模块 【学习资源】论文阅读-CBAM: Convolutional Block Attention Module CBMA:卷积注意力机制模…