Spring Security Oauth2 之 理解OAuth 2.0授权流程
1. Oauth 定义
1.1 角色
OAuth定义了四个角色:
资源所有者
一个能够授权访问受保护资源的实体。当资源所有者是一个人时,它被称为最终用户。
资源服务器
托管受保护资源的服务器能够使用访问令牌接受和响应受保护的资源请求。
客户
代表资源所有者及其授权的应用程序进行受保护的资源请求。术语客户端并不意味着任何特定的实现特征(例如,应用程序是在服务器,台式机还是其他设备上执行的)。
授权服务器
服务器在成功认证资源所有者并获得授权后向客户端发放访问令牌。
1.2 协议流程
图1中所示的抽象OAuth 2.0流程描述了四个角色之间的交互,并包含以下步骤:
(A) 客户端请求资源所有者的授权。授权请求可以直接给资源所有者(如图所示),或者优选间接地通过授权服务器作为中介。
(B) 客户端接收授权许可,这是一种代表资源所有者授权的凭证,使用本规范中定义的四种授权类型之一或使用扩展授权类型表示。授权授予类型取决于客户端用于请求授权的方法以及授权服务器支持的类型。
(C) 客户端通过向授权服务器进行认证并携带授权来请求访问令牌。
(D) 授权服务器对客户端进行身份验证并验证授权,并且如果有效则发出访问令牌。
(E) 客户端从资源服务器请求受保护的资源并通过携带访问令牌进行认证。
(F) 资源服务器验证访问令牌,并且如果有效,则为该请求提供服务。
客户从资源所有者(步骤(A)和(B)中描述)获得授权许可的首选方法是使用授权服务器作为中介
2 模式
oauth2根据使用场景不同,分成了4种模式
● 客户端模式(client credentials):标准的 Server 授权模式,非常适合 Server 端的 Web 应用。一旦资源的拥有者授权访问他们的数据之后,他们将会被重定向到 Web 应用并在 URL 的查询参数中附带一个授权码(code)。在客户端里,该 code 用于请求访问令牌(access_token)。并且该令牌交换的过程是两个服务端之前完成的,防止其他人甚至是资源拥有者本人得到该令牌。另外,在该授权模式下可以通过 refresh_token 来刷新令牌以延长访问授权时间,也是最为复杂的一种方式。
● 密码模式(resource owner password credentials) : 自己有一套用户体系,这种模式要求用户提供用户名和密码来交换访问令牌(access_token)。该模式仅用于非常值得信任的用户,例如API提供者本人所写的移动应用。虽然用户也要求提供密码,但并不需要存储在设备上。因为初始验证之后,只需将 OAuth 的令牌记录下来即可。如果用户希望取消授权,因为其真实密码并没有被记录,因此无需修改密码就可以立即取消授权。token本身也只是得到有限的授权,因此相比最传统的 username/password 授权,该模式依然更为安全。
● 授权码模式(authorization code) :该模式是所有授权模式中最简单的一种,并为运行于浏览器中的脚本应用做了优化。当用户访问该应用时,服务端会立即生成一个新的访问令牌(access_token)并通过URL的#hash段传回客户端。这时,客户端就可以利用JavaScript等将其取出然后请求API接口。该模式不需要授权码(code),当然也不会提供refresh token以获得长期访问的入口。
● 简化模式(implicit) :没有用户的概念,一种基于 APP 的密钥直接进行授权,因此 APP 的权限非常大。它适合像数据库或存储服务器这种对 API 的访问需求。
Oauth基于客户端与认证服务器验证的能力定义了两种客户端类型(以及,维护客户端认证信息的能力): 客户端模式、密码模式。
基础参数定义:
grant_type (发放令牌类型)、
client_id (客户端标识id)
username(用户帐号)
password (用户密码)
client_secret(客户端标识密钥)
refresh_token (刷新令牌)
scope(表示权限范围,可选项)
Oauth2 Client 集成
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
package com.lvyuanj.upms.oauthclient.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;@Configuration
public class UpmsOauth2LoginConfig {@EnableWebSecuritypublic static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UpmsLogoutHandler upmsLogoutHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().logout().addLogoutHandler(upmsLogoutHandler).and().oauth2Login();}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().mvcMatchers("/js/**","/css/**");}}@Beanpublic OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);}@Beanpublic OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);}@Beanpublic ClientRegistrationRepository clientRegistrationRepository() {return new InMemoryClientRegistrationRepository(this.upmsOauth2ClientRegistration());}@Bean@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.upms")public UpmsClientRegistration upmsClientRegistration(){return new UpmsClientRegistration();}@Bean@ConfigurationProperties(prefix = "spring.security.oauth2.client.provider.upms")public UpmsClientProvider upmsClientProvider(){return new UpmsClientProvider();}private ClientRegistration upmsOauth2ClientRegistration() {return ClientRegistration.withRegistrationId(this.upmsClientRegistration().getProvider()).clientId(this.upmsClientRegistration().getClientId()).clientSecret(this.upmsClientRegistration().getClientSecret()).clientAuthenticationMethod(ClientAuthenticationMethod.BASIC).authorizationGrantType(new AuthorizationGrantType(this.upmsClientRegistration().getAuthorizationGrantType())).redirectUriTemplate(this.upmsClientRegistration().getRedirectUriTemplate()).scope(this.upmsClientRegistration().getScope()).authorizationUri(this.upmsClientProvider().getAuthorizationUri()).tokenUri(this.upmsClientProvider().getTokenUri()).userInfoUri(this.upmsClientProvider().getUserInfoUri()).userNameAttributeName(this.upmsClientProvider().getUserNameAttribute()).clientName(this.upmsClientRegistration().getClientName()).build();}
}
oauth registration config
package com.lvyuanj.upms.oauthclient.config;import lombok.Data;@Data
public class UpmsClientProvider {private String authorizationUri;private String tokenUri;private String userInfoUri;private String userNameAttribute;
}package com.lvyuanj.upms.oauthclient.config;import lombok.Data;@Data
public class UpmsClientRegistration {private String provider;private String clientId;private String clientSecret;private String clientName;private String authorizationGrantType;private String redirectUriTemplate;private String scope;}
退出登陆配置
package com.lvyuanj.upms.oauthclient.config;import com.lvyuanj.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Slf4j
@Service
public class UpmsLogoutHandler implements LogoutHandler {@Value("${com.lvyuanj.upms.client.upms.logout-uri}")private String logoutUrl;@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {try {String access_token = (String) request.getSession().getAttribute("access_token");String params = "";if(StringUtils.isNotBlank(access_token)){params = "?access_token="+access_token;}response.sendRedirect(logoutUrl+params);} catch (IOException e) {e.printStackTrace();}}
}
登陆成功跳转到index
package com.lvyuanj.upms.oauthclient.controller;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Map;@Slf4j
@Controller
public class OAuth2Controller {@Autowiredprivate OAuth2AuthorizedClientService authorizedClientService;@RequestMapping("/")public String index(Model model, OAuth2AuthenticationToken authentication, HttpServletRequest request) {log.debug("authentication:"+ JSONObject.toJSONString(authentication));OAuth2AuthorizedClient authorizedClient = this.getAuthorizedClient(authentication);if(null != authorizedClient){String access_token = authorizedClient.getAccessToken().getTokenValue();log.debug("access_token: "+ access_token);request.getSession().setAttribute("access_token", access_token);model.addAttribute("userName", authentication.getName());model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());return "index";}else {return "logout";}}private OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken authentication) {return this.authorizedClientService.loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(), authentication.getName());}@RequestMapping("/userinfo")public String userinfo(Model model,OAuth2AuthenticationToken authentication) {// authentication.getAuthorizedClientRegistrationId() returns the// registrationId of the Client that was authorized during the Login flowOAuth2AuthorizedClient authorizedClient =this.authorizedClientService.loadAuthorizedClient(authentication.getAuthorizedClientRegistrationId(),authentication.getName());OAuth2AccessToken accessToken = authorizedClient.getAccessToken();System.out.println(accessToken.getTokenValue());Map userAttributes = Collections.emptyMap();String userInfoEndpointUri = authorizedClient.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();if (!StringUtils.isEmpty(userInfoEndpointUri)) {// userInfoEndpointUri is optional for OIDC ClientsuserAttributes = WebClient.builder().filter(oauth2Credentials(authorizedClient)).build().get().uri(userInfoEndpointUri).retrieve().bodyToMono(Map.class).block();}model.addAttribute("userAttributes", userAttributes);return "userinfo";}private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) {return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {ClientRequest authorizedRequest = ClientRequest.from(clientRequest).header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue()).build();return Mono.just(authorizedRequest);});}
}
Spring Security Oauth2 Client 非常重要的过滤器:OAuth2LoginAuthenticationFilter
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter#attemptAuthentication方法
public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*"; --在oauth2登陆成功之后,此过滤器拦截回调接口地址@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException {MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { // 判断回调数据是否有CODE 或者 STATE 参数OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}OAuth2AuthorizationRequest authorizationRequest =this.authorizationRequestRepository.removeAuthorizationRequest(request, response);if (authorizationRequest == null) {OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);if (clientRegistration == null) {OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,"Client Registration not found with Id: " + registrationId, null);throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());}String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery(null).build().toUriString();OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));OAuth2LoginAuthenticationToken authenticationResult =(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(),authenticationResult.getAuthorities(),authenticationResult.getClientRegistration().getRegistrationId());OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(),oauth2Authentication.getName(),authenticationResult.getAccessToken(),authenticationResult.getRefreshToken());this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);return oauth2Authentication;}
相关文章:
Spring Security Oauth2 之 理解OAuth 2.0授权流程
1. Oauth 定义 1.1 角色 OAuth定义了四个角色: 资源所有者 一个能够授权访问受保护资源的实体。当资源所有者是一个人时,它被称为最终用户。 资源服务器 托管受保护资源的服务器能够使用访问令牌接受和响应受保护的资源请求。 客户 代表资源所有…...

mysql题目4
tj11: select count(*) 员工总人数 from tb_dept a join tb_employee b on a.deptnob.deptno where a.dname 市场部...

GFS部署实验
目录 1、部署环境 编辑 2、更改节点名称 3、准备环境 4、磁盘分区,并挂载 5. 做主机映射--/etc/hosts/ 6. 复制脚本文件 7. 执行脚本完成分区 8. 安装客户端软件 1. 安装解压源包 2. 创建gfs 3. 安装 gfs 4. 开启服务 9、 添加节点到存储信任池中 1…...

最前沿・量子退火建模方法(1) : subQUBO讲解和python实现
前言 量子退火机在小规模问题上的效果得到了有效验证,但是由于物理量子比特的大规模制备以及噪声的影响,还没有办法再大规模的场景下应用。 这时候就需要我们思考,如何通过软件的方法怎么样把大的问题分解成小的问题,以便通过现在…...

如何在Linux部署MeterSphere并实现公网访问进行远程测试工作
文章目录 前言1. 安装MeterSphere2. 本地访问MeterSphere3. 安装 cpolar内网穿透软件4. 配置MeterSphere公网访问地址5. 公网远程访问MeterSphere6. 固定MeterSphere公网地址 前言 MeterSphere 是一站式开源持续测试平台, 涵盖测试跟踪、接口测试、UI 测试和性能测试等功能&am…...

postgis导入shp数据时“dbf file (.dbf) can not be opened.“
作者进行矢量数据导入数据库中出现上述报错 导致报错原因 导入的shp文件路径太深导入的shp文件名称或路径中有中文将需要导入数据的shp 文件、dbf 文件、prj 等文件放在到同一个文件夹内,且名字要一致;导入失败: 导入成功:...

StarUML笔记之从C++代码生成UML图
StarUML笔记之从C代码生成UML图 —— 2024-04-14 文章目录 StarUML笔记之从C代码生成UML图1.安装C插件2.准备好一个C代码文件放某个路径下3.点击Reverse Code选择项目文件夹4.拖动(Class)到中间画面可以形成UML5.另外一种方式:双击Type Hierarchy,然后…...
sizeof()和strlen
一、什么是sizeof() sizeof()是一个在C和C中广泛使用的操作符,用于计算数据类型或变量所占内存的字节数。它返回一个size_t类型的值,表示其操作数所占的字节数。 在使用时,sizeof()可以接收一个数据类型作为参数,也可以接收一个…...

Python学习笔记13 - 元组
什么是元组 元组的创建方式 为什么要将元组设计为不可变序列? 元组的遍历...

[leetcode]remove-duplicates-from-sorted-list-ii
. - 力扣(LeetCode) 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。 示例 1: 输入:head [1,2,3,3,4,4,5] 输出:[1,2,5]示例 2&…...
共享内存和Pytorch中的Dataloader结合
dataloader中通常使用num_workers来指定多线程来进行数据的读取。可以使用共享内存进行加速。 代码地址:https://github.com/POSTECH-CVLab/point-transformer/blob/master/util/s3dis.py 文章目录 1. 共享内存和dataloader结合1.1 在init中把所有的data存储到共享内…...

分享 WebStorm 2024 激活的方案,支持JetBrains全家桶
大家好,欢迎来到金榜探云手! WebStorm公司简介 JetBrains 是一家专注于开发工具的软件公司,总部位于捷克。他们以提供强大的集成开发环境(IDE)而闻名,如 IntelliJ IDEA、PyCharm、和 WebStorm等。这些工具…...

Android OOM问题定位、内存优化
一、OOM out of memory:简称OOM,内存溢出,申请的内存大于剩余的内存而抛出的异常。 对于Android平台,广义的OOM主要是以下几种类型 JavaNativeThread 线程数的上限默认为32768,部分华为设备的限制是500通常1000左右…...
棋盘(c++题解)
题目描述 有一个m m的棋盘,棋盘上每一个格子可能是红色、黄色或没有任何颜色的。你现在要从棋盘的最左上角走到棋盘的最右下角。 任何一个时刻,你所站在的位置必须是有颜色的(不能是无色的) ,你只能向上、下、 左、右…...

滑动窗口例题
一、209:长度最小的子数组 209:长度最小的子数组 思路:1、暴力解法:两层for循环遍历,当sum > target时计算子数组长度并与result比较,取最小的更新result。提交但是超出了时间限制。 class Solution {public int minSubArray…...

智过网:注册安全工程师注册有效期与周期解析
在职业领域,各种专业资格认证不仅是对从业者专业能力的认可,也是保障行业安全、规范发展的重要手段。其中,注册安全工程师证书在安全生产领域具有举足轻重的地位。那么,注册安全工程师的注册有效期是多久呢?又是几年一…...

腐蚀Rust 服务端搭建架设个人社区服务器Windows教程
腐蚀Rust 服务端搭建架设个人社区服务器Windows教程 大家好我是艾西,一个做服务器租用的网络架构师也是游戏热爱者。最近在steam发现rust腐蚀自建的服务器以及玩家还是非常多的,那么作为服务器供应商对这商机肯定是不会放过的哈哈哈! 艾西这…...

蓝桥杯备赛:考前注意事项
考前注意事项 1、DevCpp添加c11支持 点击 工具 - 编译选项 中添加: -stdc112、万能头文件 #include <bits/stdc.h>万能头文件的缺陷:y1 变量 在<cmath>中用过了y1变量。 #include <bits/stdc.h> using namespace std;// 错误示例 …...
111111111111
111111111111...

uniapp 卡片勾选
前言 公司的app项目使用的uniapp,项目里有一个可勾选的卡片功能,效果图如下: 找了一圈没找到什么太好的组件,于是就自己简单写了一个,记录一下。避免以后还会用到 代码 <template><view class"card-…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...

智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...

OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...