Spring-Security前后端分离权限认证

news2024/11/23 6:27:40

前后端分离

一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误页。

但是现在前后端分离才是正道,前后端分离的话,那就需要将返回的页面换成Json格式交给前端处理了

SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展,虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流。 所以怎么让SpringSecurity变成前后端分离,可以采用Jwt来做认证

什么是jwt

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

官网: JSON Web Token Introduction - jwt.io

jwt的结构

. 分割   三部分 

Header

Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

{

"alg": "HS256",

"typ": "JWT"

}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256 (写成 HS256) ;typ属性表示这个令牌(token)的类型(type), JWT令牌统一写为JWT。

最后,将上面的JSON对象使用Base64URL算法转成字符串。

Payload(载荷)

Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。

iss (issuer):签发人

exp (expiration time):过期时间

sub (subject):主题

aud (audience):受众

nbf (Not Before):生效时间

iat (lssued At):签发时间

jti (JWT ID):编号

除了官方字段,==你还可以在这个部分定义私有字段==,下面就是一个例子。

{

"sub": "1234567890",

"name" : "John Doe",

“userid”:2

"admin": true

}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把==秘密信息==放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。

Signature

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个==密钥(secret)==。这个密钥只有==服务器才知道==,不能泄露给用户。然后,使用Header里面指定的==签名算法(默认是 HMAC SHA256)==,按照下面的公式产生签名。

HMACSHA256(

base64UrlEncode(header) + ".”"+base64UrlEncode(payload),

secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

1.项目添加hutool依赖

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>

http://t.csdnimg.cn/TA0Xx基于文章中连接数据库的实例基础上进行的前后端分离设计

2.搭建好一个vue项目

所需的导入包

3.修改配置文件 main.js

全局导入引入

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false


import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';


Vue.use(ElementUI);
import axios from 'axios'
// 后端项目的时候  http://localhost:8080
// axios设置一个默认的路径
// 创建实例时配置默认值
const instance = axios.create({
  // 访问路径的时候假的一个基础的路径
  baseURL: 'http://localhost:8080/',
  // withCredentials: true
});

请求拦截器与响应拦截器

// 请求拦截器
//
instance.interceptors.request.use( config=> {
  // config 前端  访问后端的时候  参数
  // 如果sessionStorage里面于token   携带着token  过去
  if(sessionStorage.getItem("token")){
    // token的值  放到请求头里面
    let token = sessionStorage.getItem("token");
    config.headers['token']=token;
  }
  // config.headers['Authorization']="yyl"

  return config;
}, error=> {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});



// 添加响应拦截器
instance.interceptors.response.use( response=> {
  console.log(response)
  // 状态码  500
  if(response.data.code!=200){
    alert("chucuole")
    console.log(response.data);

    router.push({path:"/login"});

    return;
  }
  return response;
}, error=> {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});

Vue.prototype.$axios = instance;
// 引入组件

挂载点

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

4.搭建一个.vue页面,并在 router 目录下的 index.js 文件配置好路由

<template>
  <div class="login-container">
    <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="login-form">
      <el-form-item label="用户名" prop="username">
        <el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="确认密码" prop="password">
        <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
      </el-form-item>


      <el-form-item>
        <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
        <el-button @click="resetForm('ruleForm')">重置</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

搭建的页面包含基本的登录表单,在新建一个页面用于成功的页面展示,如 图中跳转的main.vue

 methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          alert('submit!');
          // 请求  userlogin   userlogin
          //post i请求  json 数据  后端接受的时候  @RequestBody
          this.$axios.post("userlogin",qs.stringify(this.ruleForm)).then(r=>{
            // 获取token的值
            console.log(r.data.t);
            // 存起来
            sessionStorage.setItem("token",r.data.t)
            // 成功之后    跳转 /main
            this.$router.push("/main");

            //console.log(r.data);
          })


        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
  }
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: "about" */ '../views/login.vue')
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  },
  {
    path: '/main',
    name: 'main',
    component: () => import(/* webpackChunkName: "about" */ '../views/main.vue')
  },
]
// 针对ElementUI导航栏中重复导航报错问题
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

这里配置了导航重复导航的问题,我们在响应拦截器配置了code非200的跳转登录的情况,为了避免登录失败导致跳转登录页面,重复导航的问题

5.后端加入跨域的配置文件

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CrossConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        //corsConfiguration.setAllowCredentials(true);  // 允许 携带cookie 的信息
        corsConfiguration.addAllowedHeader("*"); // 允许所有的头
        corsConfiguration.addAllowedOrigin("*");// 允许所有的请求源
        corsConfiguration.addAllowedMethod("*");  // 所欲的方法   get post delete put
        source.registerCorsConfiguration("/**", corsConfiguration); // 所有的路径都允许跨域
        return new CorsFilter(source);
    }

}

6.统一返回数据实体

@Data
@AllArgsConstructor //
@NoArgsConstructor //
public class Result<T> {
    /**
     * code编码
     */
    private Integer code = 200;
    /**
     * 消息
     */
    private String msg = "操作成功";
    /**
     * 具体的数据
     */
    private T t;

    /**
     * 成功的静态方法
     */
    public static <T> Result  success(T t){
        return new Result<>(200,"操作成功",t);
    }

    public static <T>  Result  <T>  fail(){
        return new Result<>(500,"操作失败",null);
    }

    public static <T>  Result  <T>  forbidden(){
        return new Result<>(403,"权限不允许",null);
    }
}

7.对实现了UserDetailsService接口的service层进行了修改

@Service
public class MyUserDetailService implements UserDetailsService {
    @Resource
    private TabUserMapper userMapper;
    @Resource
    private TabUserRoleMapper userRoleMapper;
    @Resource
    private TabRoleMapper roleMapper;
    @Resource
    private TabMenuMapper menuMapper;

//    根据用户的名字 加载用户的信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        username 代表前端传递过来的名字
//        根据名字去数据库查询一下有没有这个用户的信息
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("username",username);
        TabUser tabUser = userMapper.selectOne(queryWrapper);
        if(tabUser != null) {
//            有值 查询用户对应的角色的id
            QueryWrapper queryWrapper1 = new QueryWrapper();
            queryWrapper1.eq("uid",tabUser.getId());
            List<TabUserRole> tabUserRoles = userRoleMapper.selectList(queryWrapper1);
            List<Integer> rids = tabUserRoles.stream().map(tabUserRole -> tabUserRole.getRid()).collect(Collectors.toList());
//            根据角色的id 查询rcode
            List<TabRole> tabRoles = roleMapper.selectBatchIds(rids);
//            角色的修信息 角色管理 修改角色的名字
            List<SimpleGrantedAuthority> collect = tabRoles.stream().map(tabRole -> new SimpleGrantedAuthority("ROLE_" + tabRole.getRcode())).collect(Collectors.toList());
//            根据角色的id 查询菜单的mcode
            List<TabMenu> menus = menuMapper.selectCodeByRids(rids);
            List<SimpleGrantedAuthority> resources = menus.stream().map(tabMenu -> new SimpleGrantedAuthority(tabMenu.getMcode())).collect(Collectors.toList());
//            将角色的所有信息,和资源信息合并在一起
            List<SimpleGrantedAuthority> allresource = Stream.concat(collect.stream(), resources.stream()).collect(Collectors.toList());
            return new User(username, tabUser.getPassword(), allresource);
        }
        return null;
    }
}

8.数据链路层,对前后端的认证进行判断与返回的JSON数据

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        /*解析token
        1.获取token -> 存在 -> 解析
          不存在返回 null 没有认证
        2.效验token真的还是假的 真-> 过 -> 用户的信息存放到安全框架的上下文路径里面
          假-> 返回一个Json 数据 没有认证
        * */
        String[] whitename = {"/userlogin"};
        String token = request.getHeader("token");
//        token存在
        if(StringUtils.isNotBlank(token)) {
//            存在 解析
            boolean verify = JWTUtil.verify(token, "hp".getBytes());
            if(verify) {
//                效验合格
//                获取用户的名字 和密码的信息
                JWT jwt = JWTUtil.parseToken(token);
                String username = (String) jwt.getPayload("username");
                List<String> resources = (List<String>) jwt.getPayload("resources");
//                资源的信息
                List<SimpleGrantedAuthority> collect = resources.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
//                保存用户的信息
                UsernamePasswordAuthenticationToken usertoken = new UsernamePasswordAuthenticationToken(username, null, collect);
//                存起来用户的信息
                SecurityContextHolder.getContext().setAuthentication(usertoken);
//                放行
                filterChain.doFilter(request,response);
            }else  {
                Result result = new Result(401, "没有登录", null);
                printJsonData(response,result);
            }
        }else {
//              查看是否在白名单 如果在 就放行
            String requestURL = request.getRequestURI();
            if(ArrayUtils.contains(whitename,requestURL)) {
                filterChain.doFilter(request,response);
            }else {
                Result result = new Result(401, "没有登录", null);
                printJsonData(response,result);
            }
        }
    }
    public void printJsonData(HttpServletResponse response, Result result) {
        try {
            response.setContentType("application/json;charset=utf8"); //json格式 编码是中文
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);// 使用objectMapper将result转化为json字符串
            PrintWriter writer = response.getWriter();
            writer.print(s);
            writer.flush();
            writer.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

9.对config文件进行修改(前后端分离情况)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtFilter jwtFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //    配置 登录form 表单
//    路劲前面必须加 /
        http.formLogin()
                .loginProcessingUrl("/userlogin")
                .successHandler((request, response, authentication) -> {
                    System.out.println("authentication"+authentication);
//                    资源的信息
                    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                    List<String> allresources = authorities.stream().map(s -> s.getAuthority()).collect(Collectors.toList());
                    System.out.println("allresources"+allresources);
//                    认证成功
//                    生成token
                    Map map =new HashMap<>();
                    map.put("username",authentication.getName());  // 认证成功之后 用户的名字
                    map.put("resources",allresources);
//                    资源的信息
                    设置签发时间
//                    Calendar instance = Calendar.getInstance(); //获取当前的时间
//                    Date time = instance.getTime();
                    过期的时间设置为2小时之后
//                    instance.add(Calendar.HOUR,2); //两个小时之后
//                    Date time1 = instance.getTime();
//                    map.put(JWTPayload.EXPIRES_AT,time1);
//                    map.put(JWTPayload.ISSUED_AT,time);
//                    map.put(JWTPayload.NOT_BEFORE,time);
                    String token = JWTUtil.createToken(map, "hp".getBytes());
                    System.out.println(token);
                    Result result = new Result(200,"登录成功",token);
                    printJsonData(response,result);
                }) //前后端分离的时候 认证成功 走的方法
                .failureHandler((request, response, exception) -> {
                    Result result = new Result(500, "失败", null);
                    printJsonData(response,result);
                }); //认证失败 走的方法
        http.authorizeRequests().antMatchers("/userlogin").permitAll(); //代表放行 "/userlogin"
        http.authorizeRequests().anyRequest().authenticated();
//        权限不允许的时候
        http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
            Result result = new Result(403, "权限不允许", null);
            printJsonData(response,result);
        });
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
//         csrf 方便html文件 能够通过
        http.csrf().disable();
        http.cors();   // 可以跨域

    }
    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder getPassword() {
        return new BCryptPasswordEncoder();
    }

    //    自定义用户的信息
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPassword());
    }
    public void printJsonData(HttpServletResponse response, Result result) {
        try {
            response.setContentType("application/json;charset=utf8");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            PrintWriter writer = response.getWriter();
            writer.print(s);
            writer.flush();
            writer.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.配置完成

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

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

相关文章

React状态管理方案盘点

您好&#xff0c; 如果喜欢我的文章或者想上岸大厂&#xff0c;可以关注公众号「量子前端」&#xff0c;将不定期关注推送前端好文、分享就业资料秘籍&#xff0c;也希望有机会一对一帮助你实现梦想 前言 本文不会介绍各个状态管理工具的具体使用或者如何二次封装&#xff0c…

自动驾驶系统激光雷达传感器反射率标定板

自动驾驶技术正在全球范围内快速发展和推广。在中国&#xff0c;自动驾驶技术也得到了高度重视和大力支持。中国政府已经出台了一系列政策&#xff0c;推动自动驾驶技术的发展和应用。例如&#xff0c;上海、北京等地已经开放了自动驾驶测试道路&#xff0c;并开展了自动驾驶公…

选择CRM系统主要看哪些指标?

很多企业都想选择一款好用的CRM客户管理系统&#xff0c;但是面对众多类型、品牌的CRM却犯了难。下面我们来说说&#xff0c;企业要想选到一款适合自己的、好用的CRM系统&#xff0c;主要看哪些指标&#xff1f;这里有6个步骤&#xff0c;可以帮您做到。 第1步&#xff1a;了解…

亚马逊鲲鹏系统六大优势

亚马逊鲲鹏系统六大优势凭借其独特的能力&#xff0c;完全模拟真实的人类行为。只需几个简单的步骤 就可以自由安排任务&#xff0c;让所有账户随时发挥最大的作用。 1、全自动化操作 可以全自动批量注册买家号、AI智能养号、全自动批量测评&#xff0c;模拟人类的操作行为例…

亚马逊鲲鹏系统能做什么

亚马逊鲲鹏系统是一款能绕过亚马逊智能检测&#xff0c;完全模拟人类真实行为&#xff0c;通过模拟真实的人流量来帮助你提升你的产品排名&#xff0c;让你的产品出现在搜索首页&#xff0c;从而快速提高你的销售业绩的营销工具&#xff01; 主要的功能有批量注册买家号、AI智能…

微服务概念

微服务 微服务是什么 In short, the microservice architectural style [1] is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource A…

node插件MongoDB(四)—— 库mongoose 的文档操作使用

文章目录 前言&#xff08;1&#xff09;问题&#xff1a;安装的mongoose 库版本不应该过高导致的问题&#xff08;2&#xff09;重新安装低版本 一、插入文档1. 代码2. node终端效果3. 使用mongo.exe查询数据库的内容 二、删除文档1. 删除一条2. 批量删除3. 代码 前言 &#…

新方向!文心一言X具身智能,用LLM大模型驱动智能小车

具身智能已成为近年来研究的热点领域之一。具身智能强调将智能体与实体环境相结合&#xff0c;通过智能体与环境的交互&#xff0c;来感知和理解世界&#xff0c;最终实现在真实环境中的自主决策和运动控制。 如何基于文心大模型&#xff0c;低成本入门“具身智能”&#xff0…

社区团购小程序系统源码+各种快递代收+社区便利店 带完整的搭建教程

社区团购小程序系统源码的开发背景可以追溯到近年来电商行业的快速发展&#xff0c;特别是在新冠疫情的影响下&#xff0c;线上购物在全球范围内得到了更广泛的普及和使用。社区团购作为一种新兴的电商模式&#xff0c;结合了传统团购和社交电商的优点&#xff0c;通过线上平台…

jQuery中淡入与淡出

在我们jQuery中为我们封装了很多好玩的方法&#xff0c;我为大家介绍一下淡入与淡出&#xff01; 我们需要配合事件来玩淡入淡出 淡出语法&#xff1a;fadeOut([speed,[easing],[fn]) (1)参数都可以省略 (2)speed:三种预定速度之一的字符串(“slow”“normal”or “fast”)或…

QML8、布局元素

布局元素(Layout Items) QML使用anchors(锚)对元素进行布局。anchoring(锚定)是基础元素对象的基本属性,可以被所有的可视化QML元素使用。一个anchors(锚)就像一个协议,并且比几何变化更加强大。Anchors(锚)是相对关系的表达式,你通常需要与其它元素搭配使用。 一…

如何理解CDN?说说实现原理?

面试官&#xff1a;如何理解CDN&#xff1f;说说实现原理&#xff1f; 一、是什么 CDN (全称 Content Delivery Network)&#xff0c;即内容分发网络 构建在现有网络基础之上的智能虚拟网络&#xff0c;依靠部署在各地的边缘服务器&#xff0c;通过中心平台的负载均衡、内容分…

Leo赠书活动-07期 【嵌入式虚拟化技术与应用】文末送书

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 赠书活动专栏 ✨特色专栏&#xff1a;…

Spring boot 整合grpc 运用

文章目录 GRPC基础概念&#xff1a;Protocol Buffers&#xff1a;proto 基础语法&#xff1a;调用类型&#xff1a; Spring boot 整合 grpc项目结构&#xff1a;整合代码&#xff1a;父 pomproto 模块服务端&#xff1a;客户端&#xff1a;实际调用&#xff1a; 原生集成 GRPC基…

振南技术干货集:C语言的一些“骚操作”及其深层理解(3)

注解目录 第二章《c语言的一些“操作”及其深层理解》 一、字符串的实质就是指针 &#xff08;如何将 35 转为对应的十六进制字符串”0X23”&#xff1f;&#xff09; 二 、转义符\ &#xff08;打入字符串内部的“奸细”。&#xff09; 三、字符串常量的连接 &#xff…

介绍YOLO-NAS Pose:姿势估计的技术

YOLO-NAS 姿势 YOLO-NAS Pose models是对 Pose Estimation 领域的最新贡献。今年早些时候,Deci 因其突破性的目标检测基础模型 YOLO-NAS 获得了广泛认可。在 YOLO-NAS 成功的基础上,该公司现在推出了 YOLO-NAS Pose 作为其姿势估计的对应产品。该姿势模型在延迟和准确性之间…

FinClip 产品10月报:官网新增PC终端麒麟版、UOS版下载

FinClip 的使命是使您&#xff08;业务专家和开发人员&#xff09;能够通过小程序解决关键业务流程挑战&#xff0c;并完成数字化转型的相关操作。不妨让我们看看在本月的产品与市场发布亮点&#xff0c;看看是否有助于您实现目标。 产品方面的相关动向&#x1f447;&#x1f…

拆分代码 + 动态加载 + 预加载,减少首屏资源,提升首屏性能及应用体验

github 原文地址 我们看一些针对《如何提升应用首屏加载体验》的文章&#xff0c;提到的必不可少的措施&#xff0c;便是减少首屏幕加载资源的大小&#xff0c;而减少资源大小必然会想到按需加载措施。本文提到的便是一个基于webpack 插件与 react 组件实现的一套研发高度自定…

【java】JVM-关于Object o=new Object()

请解释一下对象的创建过程?(半初始化) 加问DCL要不要加volatile问题?(指令重排) 对象在内存中的存储布局?(对象与数组的存储不同) 例如一个class对象中有三个变量&#xff0c;分别是int&#xff08;4bytes&#xff09;&#xff0c;long&#xff08;8bytes&#xff09;&#…

《QT从基础到进阶·十七》QCursor鼠标的不同位置坐标获取

一些常用鼠标图形&#xff1a; 鼠标光标相对于整个电脑屏幕的位置&#xff1a;QCursor::pos() 当前光标相对于当前窗口的位置&#xff1a;this->mapFromGlobal(QCursor::pos()) void MainWindow::mouseReleaseEvent(QMouseEvent* event) {QPoint pos event->pos(); …