Springboot Security 前后端分离模式自由接口最小工作模型

news2024/9/21 23:36:17

但凡讲解Springboot Security的教程,都是根据其本身的定义,前后端整合在一起,登录采用form或者basic。我们现在的很多项目,前后端分离,form登录已经不适用了。很多程序的架构要求所有的接口都采用application/json方式,因此basic登录模式也几乎不用。我们需要纯粹使用自己的自由接口来实现注册登录,以及其他业务接口访问的身份验证和授权。这里的设计是用户身份验证与授权的模块跟业务模块的身份权限验证是分开的。不过为了紧凑,我把两部分放在一起做了一个最小工作模型。

 这是最小模型的基本结构,核心的类就config中两个。其他的都是工作模型。

一、创建Springboot web项目,添加pom

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

二、一个用户构建token签发数据的类

package com.chris.demo.domain;

/**
 * @author Chris Chan
 * Create On 2022/11/24 10:19
 * Use for:
 * Explain:
 */
public class JwtResult {
    /**
     * 访问令牌
     */
    private String accessToken;
    /**
     * 访问令牌过期时间(毫秒)
     */
    private Long accessTokenExpire;
    /**
     * 刷新令牌
     */
    private String refreshToken;
    /**
     * 刷新令牌过期时间(毫秒)
     */
    private Long refreshExpire;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public Long getAccessTokenExpire() {
        return accessTokenExpire;
    }

    public void setAccessTokenExpire(Long accessTokenExpire) {
        this.accessTokenExpire = accessTokenExpire;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public Long getRefreshExpire() {
        return refreshExpire;
    }

    public void setRefreshExpire(Long refreshExpire) {
        this.refreshExpire = refreshExpire;
    }
}

我们构架一个数据类在内存中存放用户数据,实际上存取数据库也应该有一个ORM类。

package com.chris.demo.domain;

/**
 * @author Chris Chan
 * Create On 2022/11/23 16:03
 * Use for:
 * Explain:
 */
public class UserModel {
    private String username;
    private String password;
    private String authorities;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getAuthorities() {
        return authorities;
    }

    public void setAuthorities(String authorities) {
        this.authorities = authorities;
    }
}

三、设计JavaWebToken的工具

package com.chris.demo.utils;

import com.chris.demo.domain.JwtResult;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Chris Chan
 * Create On 2022/11/24 10:18
 * Use for: JWT工具
 * Explain:
 */
public class JwtUtil {
    /**
     * 访问令牌过期时间(天)
     */
    private static int ACCESS_TOKEN_XPIRE_DAYS = 7;
    /**
     * 刷新令牌过期时间(天)
     */
    private static int REFRESH_TOKEN_EXPIRE_DAYS = 30;
    /**
     * 签名指纹 加密解密要一致
     */
    private static String SIGN_KEY = "NDHHKHJKFWHEUIFKK8384SDNJAFYQJ723HF7823F3BJ";

    /**
     * 通过用户名构建jwt
     *
     * @param username
     * @param authorties
     * @param expire
     * @return
     */
    public static String buildJwtByUsername(String username, String authorties, long expire) {
        //有效载荷
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", username);
        claims.put("authorities", authorties);

        //过期时间
        Date now = new Date();
        Date expireTime = new Date(expire);

        return Jwts.builder()
                .setId("yyds")
                .setSubject("chris_jwt")
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expireTime)
                .signWith(SignatureAlgorithm.HS512, SIGN_KEY)
                .compact();
    }

    /**
     * 从jwt中解析出用户名
     *
     * @param token
     * @return
     */
    public static String getUsernameFromJwt(String token) {
        Object obj = parseClaimsBody(token)
                .get("username");
        return String.valueOf(obj);
    }

    /**
     * 从jwt中解析出权限
     *
     * @param token
     * @return
     */
    public static String getAuthoritiesFromJwt(String token) {
        Object obj = parseClaimsBody(token)
                .get("authorities");
        return String.valueOf(obj);
    }

    private static Claims parseClaimsBody(String token) {
        return Jwts.parser()
                .setSigningKey(SIGN_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 构建登录结果JwtResult
     *
     * @param username
     * @param authorities
     * @return
     */
    public static JwtResult buildJwtResultByUsername(String username, String authorities) {
        Date now = new Date();

        JwtResult jwtResult = new JwtResult();

        long accessTokenExpire = now.getTime() + 3600000 * 24 * ACCESS_TOKEN_XPIRE_DAYS;
        long refreshTokenExpire = now.getTime() + 3600000 * 24 * REFRESH_TOKEN_EXPIRE_DAYS;

        jwtResult.setAccessToken(buildJwtByUsername(username, authorities, accessTokenExpire));
        jwtResult.setAccessTokenExpire(accessTokenExpire);
        jwtResult.setRefreshToken(buildJwtByUsername(username, authorities, refreshTokenExpire));
        jwtResult.setRefreshExpire(refreshTokenExpire);

        return jwtResult;
    }
}

工具中三个主要方法,构建jwt,解析用户名、权限列表和构建登录结果。其中解析方法适用于一般业务模块验证token使用的。可以分离出去,但是key一定要保持一致。

四、正菜之一:访问过滤器。调用一般业务接口时需要检查是否携带令牌,格式是否正确,验证之后进行授权。

package com.chris.demo.config;

import com.chris.demo.utils.JwtUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author Chris Chan
 * Create On 2022/11/23 17:07
 * Use for:
 * Explain:
 */
public class AccessFilter extends BasicAuthenticationFilter {
    private static List<String> passList;

    public static void setPassList(List<String> passList) {
        AccessFilter.passList = passList;
    }

    public AccessFilter(AuthenticationManager authenticationManager, List<String> passList) {
        super(authenticationManager);
        AccessFilter.passList = passList;
    }

    /**
     * 传入白名单和用户服务
     *
     * @param authenticationManager
     * @param passes
     */
    public AccessFilter(AuthenticationManager authenticationManager, String... passes) {
        super(authenticationManager);
        AccessFilter.passList = Arrays.stream(passes).collect(Collectors.toList());
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //白名单放行
        String requestURI = request.getRequestURI();

        if (passList.contains(requestURI)) {
            chain.doFilter(request, response);
            return;
        }
        //读取Authorization
        String authorization = request.getHeader("Authorization");
        if (null == authorization) {
            throw new RuntimeException("没有发现令牌");
        }
        //格式校验
        if (!authorization.startsWith("Bearer ")) {
            throw new RuntimeException("令牌格式错误");
        }

        //取得token
        String jwt = authorization.replace("Bearer ", "");
        //todo 尝试解析jwt,获取用户名,有异常抛出,
        String username = JwtUtil.getUsernameFromJwt(jwt);
        String authorities = JwtUtil.getAuthoritiesFromJwt(jwt);

        //构建权限token,放入上下文
        List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(authorities);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorityList);
        SecurityContextHolder.getContext().setAuthentication(token);

        chain.doFilter(request, response);
    }
}

五、正菜之二:Security配置文件

package com.chris.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Chris Chan
 * Create On 2022/11/23 16:04
 * Use for:
 * Explain:
 * @link . https://blog.csdn.net/weixin_46684099/article/details/117434577
 * @link . https://blog.csdn.net/chihaihai/article/details/104678864
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .mvcMatchers("/");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //通关白名单
        String[] passes = {
                "/api/user/reg",
                "/api/user/login"
        };
        http
                .authorizeRequests()
                .antMatchers(passes).permitAll()
                .anyRequest().authenticated()
                .and()
                .cors()
                .and()
                .csrf().disable()
                .addFilter(new AccessFilter(authenticationManager(), passes));
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

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

核心在第二个configre方法,没有配置form和basic登录,只是对某些接口放行。包含一个访问过滤器设置。过滤器设置的白名单跟此处配置的完全授权的白名单没有必然的关系。不过一般是一致的。他们作用于两个过滤器,要都能通过才行。

六、模型文件:模拟数据管理类,业务处理类,接口类等

package com.chris.demo.dao;

import com.chris.demo.domain.UserModel;
import org.springframework.stereotype.Repository;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Chris Chan
 * Create On 2022/11/24 9:57
 * Use for: 模拟数据库管理用户
 * Explain:
 */
@Repository
public class UserReporitory {
    private static ConcurrentHashMap<String, UserModel> userMap = new ConcurrentHashMap<>();

    /**
     * 保存用户
     * @param userModel
     */
    public void save(UserModel userModel) {
        userMap.put(userModel.getUsername(), userModel);
    }

    /**
     * 获取用户信息
     *
     * @param username
     * @return
     */
    public UserModel findByUsername(String username) {
        return userMap.get(username);
    }
}
package com.chris.demo.service;

import com.chris.demo.dao.UserReporitory;
import com.chris.demo.domain.JwtResult;
import com.chris.demo.domain.UserModel;
import com.chris.demo.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.security.auth.login.CredentialException;

/**
 * @author Chris Chan
 * Create On 2022/11/23 16:25
 * Use for:
 * Explain:
 */
@Service
public class UserService {

    @Autowired
    PasswordEncoder passwordEncoder;
    @Autowired
    UserReporitory userReporitory;

    /**
     * 注册
     *
     * @param username
     * @param password
     */
    public void reg(String username, String password) {
        reg(username, password, "USER");
    }

    /**
     * 注册
     *
     * @param username
     * @param password
     */
    public void reg(String username, String password, String roles) {
        UserModel userModel = new UserModel();
        userModel.setUsername(username);
        userModel.setPassword(passwordEncoder.encode(password));
        userModel.setAuthorities(roles);

        userReporitory.save(userModel);
    }

    /**
     * 登录
     *
     * @param username
     * @param password
     * @return
     */
    public JwtResult login(String username, String password) {
        //检查用户是否存在
        UserModel userModel = userReporitory.findByUsername(username);
        if (null == userModel) {
            throw new UsernameNotFoundException("用户不存在");
        }
        //匹配密码
        String passwordEnc = userModel.getPassword();
        if (!passwordEncoder.matches(password, passwordEnc)) {
            try {
                throw new CredentialException("密码错误");
            } catch (CredentialException e) {
                e.printStackTrace();
            }
            return null;
        }
        return JwtUtil.buildJwtResultByUsername(username, userModel.getAuthorities());
    }

}

这个UserService并没有去实现Securty的UserDetailService接口,因为逻辑完全是由我们自己处理的。

package com.chris.demo.web;

import com.chris.demo.domain.JwtResult;
import com.chris.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Chris Chan
 * Create On 2022/11/23 16:09
 * Use for: 用户接口
 * Explain:
 */
@RestController
@RequestMapping("api/user")
public class UserController {
    @Autowired
    UserService accountService;

    /**
     * 模拟注册
     *
     * @param username
     * @param password
     * @return
     */
    @GetMapping("reg")
    public String reg(String username, String password, String roles) {
        accountService.reg(username, password, roles);
        return "reg success";
    }

    /**
     * 模拟登录
     *
     * @param username
     * @param password
     * @return
     */
    @GetMapping("login")
    public JwtResult login(String username, String password) {
        return accountService.login(username, password);
    }


}
package com.chris.demo.web;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.security.RolesAllowed;

/**
 * @author Chris Chan
 * Create On 2022/11/23 16:41
 * Use for: 业务接口
 * Explain:
 */
@RestController
@RequestMapping("api/biz")
public class BizController {
    /**
     * 模拟业务调用
     *
     * @return
     */
    @GetMapping("test")
    public String test() {
        return "test success.";
    }

    /**
     * 权限验证
     * @return
     */
    @RolesAllowed({"ADMIN"})
    @GetMapping("test2")
    public String test2() {
        return "test2 success.";
    }
}

两个接口文件,一个设计为UAA模块,一个设计为业务模块。业务接口有一个是测试权限的。

测试结果:

1.先注册一个用户:

 给定的权限是ROLE_USER,前面的ROLE_前缀是必须要加的,这是jsr250的权限校验规范要求的。

2.登录,获得jwt

3.将accessToken填入Authorization的Bearer Token模式,调用没有权限限制的接口

 

4. 调用有权限限制的接口,报403

 

 5.另外注册一个用户权限设置为ROLE_ADMIN,登录获取accessToken,重新调用限权接口

6.前端调用接口的时候实在header中增加一个key为Authorization的数据,其值为accessToken加上bearer+一个空格的前缀,就像这样

 

完美!! 

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

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

相关文章

RabbitMQ顺序性、可靠性、重复消费、消息堆积解决方案

RabbitMQ顺序性、可靠性&#xff08;消息丢失&#xff09;、重复消费、消息堆积解决方案 顺序性 RabbitMQ使用过程中&#xff0c;有些业务场景需要我们保证顺序消费&#xff0c;例如&#xff1a;业务上产生三条消息&#xff0c;分别是对数据的增加、修改、删除操作&#xff0…

【Java八股文总结】之Linux常用指令

文章目录Linux简介一、Linux目录结构二、Linux常用指令Linux简介 一、Linux目录结构 bin&#xff08;binaries&#xff09;&#xff1a;存放二进制可执行文件。 sbin&#xff08;super user binaries&#xff09;&#xff1a;存放二进制可执行文件&#xff0c;只有root才能访…

怎么把图片转换成表格?分享三个简单方法给你

你们是否在工作的时候会遇到这样的情况&#xff1a;收到同事发来的一张表格图片&#xff0c;需要你进行汇总登记&#xff0c;通常这种时候&#xff0c;你们都会怎么做呢&#xff1f;是根据图片的内容&#xff0c;手动输入制作成一份表格吗&#xff1f;虽然这样子可以进行表格的…

c++ 旅行商问题(动态规划)

目录一、旅行商问题简介旅行商问题问题概述问题由来二、基本思路三、实现1、状态压缩2、状态转移四、代码复杂度分析一、旅行商问题简介 旅行商问题 TSP&#xff0c;即旅行商问题&#xff0c;又称TSP问题&#xff08;Traveling Salesman Problem&#xff09;&#xff0c;是数学…

网络编程基础知识

文章目录1、网络概念2、协议3、网络分层4、网络传输流程5、端口号1、网络概念 先有计算机还是先有网络呢&#xff1f; 答案是先有计算机&#xff0c;为了数据研究和沟通的需求产生的网络&#xff0c;网络的产生是为了提升效率的。 那什么是网络呢&#xff1f; 网络指的是网络协…

实现一个自定义的vue脚手架

开发背景 博客很久没有更新了&#xff0c; 今天更新一个好玩的&#xff0c;等我将vue3的东西彻底搞明白我会更新一个vue3的系列&#xff0c;到时候会更新稍微勤一点&#xff0c;在使用vuecli的时候发现他的脚手架很有意思&#xff0c;用了几年了&#xff0c;但是一直没有好好研…

HTML CSS 网页设计作业「动漫小站」

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 文章目录一、网页介绍一…

Neon intrinsics 简明教程

文章目录前言SIMD & NEONNEON intrinsicsNEON intrinsics 学习资料寄存器向量数据类型NENO intrinsics 命名方式NEON Intrinsics 查询三种处理方式&#xff1a;Long/Wide/NarrowNENO intrinsics 手册Addition 向量加法Vector add: vadd{q}_type. Vr[i]:Va[i]Vb[i]Vector lo…

Python-Flask 模型介绍和配置(6)

Flask数据模型和连接数据库一、安装二、配置数据库连接、创建模型类三、使用命令创建数据库表四、以注册为例flask是基于MTV的结构&#xff0c;其中M指的就是模型&#xff0c;即数据模型&#xff0c;在项目中对应的是数据库。flask与数据库建立联系有很多方法&#xff0c;但一般…

《安富莱嵌入式周报》第292期:树莓派单片机100M双通道示波器开源,MDK5.38发布,万用表单芯片解决方案,8通道±25V模拟前端芯片,开源贴片拾取电机板

往期周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新视频教程&#xff1a; GUI综合实战视频教程第3期&#xff1a;GUIX Studio一条龙设计主界面&#xff0c;底栏和…

【计算机毕业设计】32.学生宿舍管理系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘 要 随着计算机技术的飞速发展及其在宿舍管理方面应用的普及&#xff0c;利用计算机实现对学生宿舍管理势在必行。经过实际的需求分析&#xff0c;本系统采用Eclipse作为开发工具&#xff0c;采用功能强大的MySQL…

计算狗携手成都超算中心和重庆大学,共同助力“碳中和”

为了积极稳妥推进碳达峰碳中和&#xff0c;加快成渝双城经济圈建设。成都计算狗牵手国家超算中心和重庆大学&#xff0c;开展了关于二氧化碳电催化还原反应的路径计算工作&#xff0c;积极推动川渝两地实现产学研合作和成果落地转化&#xff0c;深入推进能源革命。 电催化还原二…

APS生产排单软件模拟排程功能

APS生产排单软件通过预先设定好相关基本资料与约束规则&#xff0c;当订单、机台、工具、材料、上下班时间等任何影响生产计划的因素变化后&#xff0c;执行“一键式排程计算”&#xff0c;系统即可生成生产详细排程。 通过选择不同的排产方案&#xff0c;可以实现不同的排程效…

3.60 怎么对OrCAD的网络标号进行统一批量修改?OrCAD中怎么设置复制位号的增加机制?

笔者电子信息专业硕士毕业&#xff0c;获得过多次电子设计大赛、大学生智能车、数学建模国奖&#xff0c;现就职于南京某半导体芯片公司&#xff0c;从事硬件研发&#xff0c;电路设计研究。对于学电子的小伙伴&#xff0c;深知入门的不易&#xff0c;特开次博客交流分享经验&a…

CANoe-vTESTstudio之Test Diagram编辑器(入门介绍)

1. 什么是Test Diagram编辑器 Test Diagram编辑器和Test Table编辑器不同 Test Table编辑器可以在编辑区域直接添加测试元素Test Case/Test Sequence/Test Fixture/Test Group,在CANoe软件的Test Unit里生成测试用例 Test Diagram编辑器以图形的方式定义实际的测试顺序、设…

springcloud16:总结配置中心+消息中心总结篇

架构图 启动分布式配置中心服务端从github中获取配置文件客户端访问服务端获取配置文件 当github中更改配置文件时&#xff0c;服务端可以立刻更改&#xff0c;但是客户端需要重启才能获取到更改的配置文件&#xff0c;如何优化&#xff1f; 即可以通过运维人员去手动刷新客户…

爬虫到底难在哪里?

爬虫本质是采集数据&#xff0c;通俗的讲就是模拟人在App或者浏览器的操作步骤自动化获取数据&#xff0c;本身没有什么难度&#xff0c;伪造HTTP 请求就好。 但是有些公司会给你设置采集障碍&#xff0c;大公司还有专门的安全团队防采集。 你看搞安全的程序员或者黑客平均技术…

【设计模式】组合模式(Composite Pattern)

组合模式属于结构型模式&#xff0c;又可以叫做部分-整体模式&#xff0c;主要解决客户程序在具有整体和部分的层次结构中&#xff0c;处理一组相似对象比处理单一对象费时费力的问题。例如&#xff0c;一个图形&#xff0c;它可以是一个简单的圆形、方形或一条线&#xff08;部…

paddleocr检测模型训练记录

标注好数据集后 分为训练集、测试集 数据集格式需要与配置文件一致&#xff0c;为了方便&#xff0c;我直接使用以下格式。 PaddleOCR主目录下&#xff0c;自己新建文件夹&#xff1a;car_plate_images/images_det train、test、里面是图片 det_label_test、det_label_train、…

Python遥感开发之GDAL读写遥感影像

Python遥感开发之GDAL读写遥感影像1 读取tif信息方法一2 读取tif信息方法二3 自己封装读取tif的方法&#xff08;推荐&#xff09;4 对读取的tif数据进行简单运算5 写出tif影像(推荐)前言&#xff1a;主要介绍了使用GDAL读写遥感影像数据的操作&#xff0c;包括读取行、列、投影…