认证服务-SpringSecurity及Oauth2介绍

认证服务-SpringSecurity及Oauth2介绍

统一身份认证服务

统一身份认证服务系统:以统一身份认证服务为核心,用户登录统一身份认证服务后,即可以使用所有支持统一身份认证服务的管理应用系统。

       统一认证服务的提供方在项目实施中通常由公司平台层面提供统一平台,作为业务系统的任务是需要通过OAUTH2以调用方的方式接入平台。

OAUTH2协议说明

开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。由统一认证平台负责管理用户名和密码,统一认证作为唯一的登录入口,就是单点登录SSO了。目前主要是OAUTH2.0版本。

OAUTH2中的角色:

(1)Third-party application:第三方应用程序(client),资源的请求方。

(2)HTTP service:HTTP服务提供商,对外提供受保护资源服务。

(3)Resource Owner:资源所有者。

(4)User Agent:用户代理,通常指浏览器。

(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。

(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

OAUTH2.0流程

授权流程:

  1. 当请求某个资源时,客户端要求获取资源所有者的授权。
  2. 资源所有者同意授权。
  3. 客户端拿着资源所有者的授权向认证服务器申请访问令牌
  4. 授权服务器验证授权无误后返回访问令牌
  5. 客户端使用访问令牌向资源服务器申请访问受保护的资源
  6. 资源服务器通过授权服务器校验访问令牌通过后,返回受保护资源。

OAuth2.0的授权模式包括: 授权码模式,简化模式,密码模式和客户端模式。线面介绍一下授权码模式。

客户端的授权模式-授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

流程如下:

  1. 用户通过客户端访问受保护的资源,客户端将用户导向认证服务器。例如页面跳转
  2. 认证服务器得到用户的授权。(密码,扫码)
  3. 认证服务器在得到用户授权后返回授权码(通过回调返回)
  4. 客户端使用授权码和一个跳转URI向认证服务器申请访问令牌
  5. 认证服务器验证授权码后返回访问令牌

其中请求A中,客户端的请求方式包括:

response_type:表示授权类型,必选项,此处的值固定为"code "

client_id:表示客户端的ID,必选项

redirect_uri:表示重定向URI,可选项

scope:表示申请的权限范围,可选项

state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

C中请求包括

code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。

state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

D请求中参数包括:

grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。

code:表示上一步获得的授权码,必选项。

redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。

client_id:表示客户端ID,必选项。

E中认证服务器返回内容包括:

access_token:表示访问令牌,必选项。

token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。

expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。

refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。

scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

Spring Security原理分析

基础架构

Spring Security架构建立在Servlet 容器的Filter机制基础上,入口的建立基于Spring-web框架的应用初始化来实现。 最终实现效果是使用DelatatingFilterProxy 插入到Servlet的过滤链中, DelatatingFilterProxy代理名字为“springSecurityFilterChain”对应的Filter—ProxyFilterChain。

FilterChainProxy:是Spring-security的唯一入口。官方文档对这个设计的解释是:1. 作为一个唯一入口,方便调试和查看, 2. 作为Spring Security的中心入口,可以执行一些Spring-security 的必须操作,如清除SecurityContext避免内存泄露;加入HttpFirewall来防止各类攻击。

关注点:从一些成熟框架的实现中我们可以看到下面两个非常重要的设计原则:

  • 单一入口/职责单一原则: 无论是框架或则是功能的入口通常是一个明确的,唯一的入口,这样可以减少框架整体架构和使用的复杂度,也易于基于单一入口进行扩展和补充核心功能,将扩展点限制在入口之后。
  • 开闭原则: 对修改封闭, 对开放拒绝。 框架在通过诸如模板方法等方式实现主要框架后提供足够必要的扩展点。 过滤器模式也是提供扩展功能最常用的设计模式。

AbstractSecurityWebApplicationInitializer 通过WebApplicationInitializer接口接入Spring-web容器进行初始化,其中创建DelegatingFilterProxy的源码如下:

private void insertSpringSecurityFilterChain(ServletContext servletContext) {

           //代理指定的过滤器,其实就是FilterChainProxy

String filterName = DEFAULT_FILTER_NAME;

           DelegatingFilterProxy springSecurityFilterChain =

                     new DelegatingFilterProxy(filterName);

     ……

         registerFilter(servletContext, true, filterName, springSecurityFilterChain);

         }

理解了基于过滤器架构后,Spring-Security和核心实现就是各种类型的SecurityFilter实现了,后续针对授权和认证相关的过滤器进行重点分析。

详见官方文档:

https://docs.spring.io/spring-security/reference/servlet/architecture.html ,

认证实现

总体流程

各种认证模式的实现都是AbstractAuthenticationProcessingFilter抽象类的子类,是认证的唯一入口。

  1. AbstractAuthenticationProcessingFilter 创建基于当前用户的资质的Authentication对象,通常不同的Authentication对象对应不同的AbstractAuthenticationProcessingFilter实现。
  2. 将Authentication交由AuthenticationManager进行认证。
  3. 如果认证失败
    1. SecurityContextHolder清空。
    2. 调用RememberMeServices.loginFail,如果remember me没有配置则无操作。
    3. 调用AuthenticationFailureHandler。
  4. 如果认证成功
    1. 通知SessionAuthenticationStrategy 有新的成功登陆。
    2. 将Authentication 设置到SecurityContextHolder中。SecurityContextPersistenceFilter可以针对SecurityContext中的数据进行持久化处理。
    3. 调用RememberMeServices.loginSuccesss接口。
    4. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent事件。
    5. 调用AuthenticationSuccessHandler。

上面描述的主体框架就是AbstractAuthenticationProcessingFilter的doFilter方法的总体框架:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)

                          throws IOException, ServletException {

           ……

                  try {

                 //具体的认证过程,attemptAuthentication方法由子类实现

                          Authentication authenticationResult = attemptAuthentication(request, response);

                          if (authenticationResult == null) {

                                   // return immediately as subclass has indicated that it hasn't completed

                                   return;

                          }

                 //回话策略通知认证成功

                          this.sessionStrategy.onAuthentication(authenticationResult, request, response);

                          // Authentication success

                          if (this.continueChainBeforeSuccessfulAuthentication) {

                                   chain.doFilter(request, response);

                          }

                 //认证成功后的相关处理

                          successfulAuthentication(request, response, chain, authenticationResult);

                  }

                  catch (InternalAuthenticationServiceException failed) {

                          this.logger.error("An internal error occurred while trying to authenticate the user.", failed);

                 //认证失败后的相关处理

                          unsuccessfulAuthentication(request, response, failed);

                  }

                  catch (AuthenticationException ex) {

                          // Authentication failed

                 //认证失败后的相关处理

                          unsuccessfulAuthentication(request, response, ex);

                  }

         }

UserNamePasswordAuthenticationFilter的attemptAuthentication实现如下:

         public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)

                          throws AuthenticationException {

                  ……

                  String username = obtainUsername(request);

                  username = (username != null) ? username : "";

                  username = username.trim();

                  String password = obtainPassword(request);

                  password = (password != null) ? password : "";

          //获取用户名和密码创建UsernamePasswordAuthenticationToken类型的Authentication

          //交给AuthenticationManager进行认证

                  UsernamePasswordAuthenticationToken authRequest =

                 new UsernamePasswordAuthenticationToken(username, password);

                  setDetails(request, authRequest);

          //调用AuthenticationManager来进行认证

                  return this.getAuthenticationManager().authenticate(authRequest);

         }

AuthenticationManager架构

  1. Authentication交由AuthenticationManager进行认证,认证通过后保存再SecurityContextHolder中。
  2. AuthenticationManager通常使用实现类ProviderManager, ProviderManager负责管理各种认证实现的具体提供者, 每一个Provider针对某一特定的Authentication类型进行处理, ProviderManager 负责选择合适的AuthenticationProvider进行实际的认证。

ProviderManager的authenticate方法核心部分如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    ......

for (AuthenticationProvider provider : getProviders()) {

  //判断provider是否支持当前Authentication类型

      if (!provider.supports(toTest)) {

        continue;

      }

      ......

      try {

        //调用Provider进行实际的认证实现

        result = provider.authenticate(authentication);

        if (result != null) {

          copyDetails(authentication, result);

          break;

        }

      }

      catch (AccountStatusException | InternalAuthenticationServiceException ex) {

      ……

    }

    ......

  }

AuthenticationProvider 针对不同的Authentication,提供不同的认证实现。如:OAuth2授权码模式对应的实现类:OAuth2AuthorizationCodeAuthenticationProvider。

Spring Security OAuth2.0 授权码模式

基于上面分析的Spring-Security框架结合Oauth2.0协议的介绍,下面分析Spring-Security 中对Oauth2.0支持的实现。

授权码模式实现

授权码流程如下:

  1. 用户通过客户端访问受保护的资源,客户端将用户导向认证服务器。例如页面跳转
  2. 认证服务器得到用户的授权。(密码,扫码)
  3. 认证服务器在得到用户授权后返回授权码(通过回调返回)
  4. 客户端使用授权码和一个跳转URI向认证服务器申请访问令牌
  5. 认证服务器验证授权码后返回访问令牌

对应的Spring Security具体实现如下:

A:OAuth2AuthorizationRequestRedirectFilter  负责将用户代理跳转到授权服务器来启动授权码模式,执行请求跳转到配置的registration.{id}.reidrect-uri。

涉及的工具接口:

OAuth2AuthorizationRequestResolver:将Web请求解析为OAuth2AuthorizationRequest,默认实现为DefaultOAuth2AuthorizationRequestResolver,该解析器匹配路径为/oauth2/authorization/{registrationId}的请求,从中提取registrationId以获取对应的注册信息。(见上实例配置信息)

B: 由认证服务器完成

C:认证服务器返回授权码,OAuth2LoginAuthenticationFilter过滤器响应回调: 最终构建OAuth2AuthorizationCodeAuthenticationToken交由authenticationManager进行认证

D/E:OAuth2AuthorizationCodeAuthenticationProvider 使用授权码获取AccessToken

OAuth2AuthorizationCodeAuthenticationProvider的authentication实现源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    //校验获取Authorization Code返回的statue和请求state是否一致

    if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {

      OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);

      throw new OAuth2AuthorizationException(oauth2Error);

    }

    //获取AccessToken

    OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(

        new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),

            authorizationCodeAuthentication.getAuthorizationExchange()));

    ……

  }

OAuth2LoginAuthenticationFilter/OAuth2AuthorizationCodeGrantFilter

OAuth2LoginAuthenticationFilter 是AbstractAuthenticationProcessingFilter的实现类,处理授权码模式下的授权码返回的响应,生成OAuth2LoginAuthenticationToken委托给AuthenticationManager进行登录验证。

详细流程如下:

  1. 当终端用户已经给客户端授权后, 授权服务器将code和state参数添加到redirect_uri上,重定向到用户的终端代理上,交由OAuth2LoginAuthenticationFilter处理。
  2. OAuth2LoginAuthenticationFilter使用接收到的code生成OAuth2AuthenticationToken 并委托给AuthenticationManager进行认证。
  3. 当认证成功,将创建一个OAuth2AuthenticationToken用来代表终端用户的身份, 并通过OAuth2AuthorizedClientRepository 建立起Token和用户的关系。
  4. 最后OAuth2AuthenticationToken返回,并存储在SecurityContextRepository中完成认证过程。

OAuth2AuthorizationCodeGrantFilter 是 OncePerRequestFilter的子类,是以独立功能形式的过滤器存在的,用于获取OAuth2.0授权码,处理OAuth2.0的授权响应。

授权响应的处理如下:

  1. 当终端用户已经给客户端授权后, 授权服务器将code和state参数添加到redirect_uri上,重定向到用户的终端代理上,交由OAuth2AuthorizationCodeGrantFilter处理。
  2. OAuth2AuthorizationCodeGrantFilter使用接收到的code生成

 OAuth2AuthorizationCodeAuthenticationToken并委托给AuthenticationManager进行认证。

  1. 当认证成功,为用户创建一个授权用户,将accessToken和当前凭证保存到OAuth2AuthorizedClientRepository。

OAuth2LoginAuthenticationFilter 和 OAuth2AuthorizationCodeGrantFilter区别在前者是针对登录,是 Spirng Security 认证的一个步骤,需要保存登录用户到回话中,而OAuth2AuthorizationCodeGrantFilter是一个独立功能的过滤器, 用于帮助完成AccessToken的获取, 两者都依赖于Spring Security对Oauth2功能支持的类。如:ClientRegistration,OAuth2AuthorizationCodeAuthenticationToken, OAuth2AuthorizationCodeAuthenticationProvider, OAuth2AuthorizedClientRepository等。

AuthorizationRequestRepository

在整个认证过程中存储OAuth2AuthorizationRequest。用于关联和验证Authorization Response。

默认实现是HttpSessionOAuth2AuthorizationRequestRepository,将OAuth2AuthorizationRequest保存于HttpSession中。

OAuth2AccessTokenResponseClient

授权码模式下AccessToken获取接口,默认实现是

DefaultAuthorizationCodeTokenResponseClient 使用RestOperations来交换授权码和AccessToken。提供了针对请求前预处理和响应定制处理扩展点来满足扩展。

  • setRequestEntityConverter方法设置请求的前置处理器。默认实现是:OAuth2AuthorizationCodeGrantRequestEntityConverter:用于构建一个标准的OAuth2.0 Accesss Token 请求。

  • SetRestOperation 方法可以定制RestOperation,对请求响应进行配置, 默认配置如下:

RestTemplate restTemplate = new RestTemplate(

                                   Arrays.asList(new FormHttpMessageConverter(),

 new OAuth2AccessTokenResponseHttpMessageConverter()));

                  restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

                  this.restOperations = restTemplate;

DefaultRefreshTokenTokenResponseClient

实现Access Token 刷新流程,类似OAuth2AccessTokenResponseClient

JwtBearerOAuth2AuthorizedClientProvider

支持通过JWT获取AccessToken的客户端

项目实施-统一登录客户端

上面介绍了Oauth2.0服务和Spring Security的基本原理,从项目的角度需要实现的以Oauth的客户端的角色接入到公司的统一认证平台。

方式一 基于Spring Security的Oauth标准流程进行接入

基于Spring Security的Oauth标准流程进行接入。针对上文解析中涉及到的各个扩展点,如

  1. ClientRegistrationReporisoty配置存取接口: 通常实际项目中的统一登录相关配置是保存在数据库或配置中心上。
  2. OAuth2AccessTokenResponseClient: 获取accessToken的接口,项目的accesssToken获取接口可能是所谓非标准的流程。
  3. 实现特定的AuthenticationSuccessHandler或AuthenticationFailureHandler来对认证结果进行后续处理。

优点:Spring Security框架成熟, 对于OAUTH2.0的标准协议可以迅速接入,稳定性高,扩展性强,目前Spring Security是主流的安全框架, 对OAUTH2后续可能的新增特性的可以保证持续更新。

缺点: Spring Security的Oauth2.0流程设计上存在一定复杂度,设计较多接口和类,有一定门槛, 实际项目中通常仅仅是为了完成OAUTH2的流程,如果深度集成到其流程中,需要较高的成本。

方式二 基于Spring Security 的认证流程接入。

这是方式一的一种取舍, 当项目中使用了Spring Security来进行认证后,需要补充OAuth2来完成统一登录的情况下, 可以直接基于AbstractAuthenticationProcessingFilter 自己实现OAuth2.0流程。

优点: 架构简单,实现代码集中,避免了Spring Security OAuth2.0的学习成本。

缺点:需要重复开发

方式三 不基于Spring Security实现OAuth2.0流程

这个方式是针对没有使用Spring Security的项目,比如使用了Shiro,直接按照OAuth2.0的流程实现响应的Http相关接口。

优点:避免引入过多依赖, 保持架构简洁。

缺点: 需要独立开发。

总结: 推荐使用方式二或方式三的实现统一登录, 毕竟统一登录对于企业数字化项目来说并不是一个经常改变的项目,通常所以一次性的。 另外OAUTH2.0本身的交互流程并不复杂,实现的难度不高,相对于引入新的框架的学习成本更低。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux精讲系列】——vim详解

​作者主页 📚lovewold少个r博客主页 ⚠️本文重点:c入门第一个程序和基本知识讲解 👉【C-C入门系列专栏】:博客文章专栏传送门 😄每日一言:宁静是一片强大而治愈的神奇海洋! 目录 目录 ​作者…

XML解析文档解析

1.首先是我的项目结构以及我所引入的依赖&#xff1a; 2.引入的依赖&#xff1a;jdk用的是17 <properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target> </properties> <dep…

【uniapp】通用列表封装组件

uniapp页面一般都会有像以下的列表页面&#xff0c;封装通用组件&#xff0c;提高开发效率&#xff1b; &#xff08;基于uView前端框架&#xff09; 首先&#xff0c;通过设计图来分析一下页面展示和数据结构定义 w-table组件参数说明 参数说明类型可选值默认值toggle列表是…

读者自荐的 4 个 GitHub 项目

本期推荐的 4 个开源项目&#xff0c;为读者在开源项目 Awesome-GitHub-Repo 的评论区自推的, 如果你开源了不错的项目&#xff0c;想让大家看到&#xff0c;也可以去 Awesome-GitHub-Repo 进行投稿。 本期推荐开源项目目录&#xff1a; 1. DB-GPT 2. 定制中国传统节日头像 3. …

零代码编程:用ChatGPT批量将Mp4视频转为Mp3音频

文件夹中有很多mp4视频文件&#xff0c;如何利用ChatGPT来全部转换为mp3音频呢&#xff1f; 在ChatGPT中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个批量将Mp4视频转为Mp3音频的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;…

Vue el-table序号与复选框hover切换

效果图下&#xff1a; <template><div class"container"><el-tableref"multipleTable"id"multipleTable":data"person.tableData"cell-mouse-enter"cellEnter"cell-mouse-leave"cellLeave"selecti…

网页【CSS】滚动条

前言 优化后的滚动条会提亮我们的网站页面。 例如&#xff1a;CSS-TRICKS这个网站如果采用的是浏览器默认的滚动条&#xff0c;不进行优化&#xff0c;页面会显得很不搭。 所以该网站的滚动条样式优化如下&#xff1a; html::-webkit-scrollbar {width: 30px;height: 30px; …

Leetcode---370周赛

题目列表 2923. 找到冠军 I 2924. 找到冠军 II 2925. 在树上执行操作以后得到的最大分数 2926. 平衡子序列的最大和 一、找到冠军I 第一题模拟题&#xff0c;简单来说是看每一行(列)是否全是1&#xff0c;当然不包括自己比自己强的情况&#xff0c;需要特判 代码如下 …

支持C#的开源免费、新手友好的数据结构与算法入门教程 - Hello算法

前言 前段时间完成了C#经典十大排序算法&#xff08;完结&#xff09;然后有很多小伙伴问想要系统化的学习数据结构和算法&#xff0c;不知道该怎么入门&#xff0c;有无好的教程推荐的。今天给大家推荐一个支持C#的开源免费、新手友好的数据结构与算法入门教程&#xff1a;He…

STM32Cube +VSCode开发环境搭建

STM32Cube VSCode开发环境搭建 0.前言一、各种方式对比1.STM32CubeMX CLion2.STM32CubeIDE VSCode STM32 VSCode Extension3.VSCode EIDE插件 二、STM32CubeIDE VSCode STM32 VSCode Extension环境搭建1.需要安装的软件2.相关配置3.编译测试 三、总结 0.前言 工欲善其事&…

Qt QtCreator调试Qt源码配置

目录 前言1、编译debug版Qt2、QtCreator配置3、调试测试4、总结 前言 本篇主要介绍了在麒麟V10系统下&#xff0c;如何编译debug版qt&#xff0c;并通过配置QtCreator实现调试Qt源码的目的。通过调试源码&#xff0c;我们可以对Qt框架的运行机制进一步深入了解&#xff0c;同时…

HTML_案例1_注册页面

用纯html页面&#xff0c;不用css画一个注册页面。 最终效果如下&#xff1a; html页面代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>注册页面</title> </head>…

【Git】Git安装入门使用常用命令Gitee远程仓库上传文件与下载

一&#xff0c;Git入门 1.1 Git是什么 Git是一款分布式版本控制系统&#xff0c;被广泛用于软件开发中的源代码管理。它由Linus Torvalds在2005年创造并发布&#xff0c;旨在解决传统版本控制系统&#xff08;如SVN&#xff09;的一些局限性。主要用于敏捷高效地处理任何或小或…

【解决方案】vue 项目 npm run dev 时报错:‘cross-env‘ 不是内部或外部命令,也不是可运行的程序

报错 cross-env 不是内部或外部命令&#xff0c;也不是可运行的程序 或批处理文件。 npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! estate1.0.0 dev: cross-env webpack-dev-server --inline --progress --config build/webpack.dev.conf.js npm ERR! Exit status 1 np…

Pytorch 里面torch.no_grad 和model.eval(), model.train() 的作用

torch.no_grad: 影响模型的自微分器&#xff0c;使得其停止工作&#xff1b;这样的话&#xff0c;数据计算的数据就会变快&#xff0c;内存占用也会变小&#xff0c;因为没有了反向梯度计算&#xff0c;当然&#xff0c;我哦们也无法做反向传播。 model.eval() 和model.train()…

Dockerfile搭建lnmp

目录 任务需求&#xff1a; 一、准备&#xff1a; 二、部署nginx容器&#xff08;172.18.0.10&#xff09;&#xff1a; 1.编写Dockerfile构建镜像&#xff1a; 2.准备nginx配置文件&#xff1a; 3.构建镜像&#xff0c;启动nginx容器&#xff1a; 三、部署mysql容器&#x…

Flutter学习:使用CustomPaint绘制路径

Flutter学习&#xff1a;认识CustomPaint组件和Paint对象 Flutter学习&#xff1a;使用CustomPaint绘制路径 Flutter学习&#xff1a;使用CustomPaint绘制图形 Flutter学习&#xff1a;使用CustomPaint绘制文字 Flutter学习&#xff1a;使用CustomPaint绘制图片 drawPath 绘制路…

矢量绘图软件Sketch 99 for mac

Sketch是一款为用户提供设计和创建数字界面的矢量编辑工具。它主要用于UI/UX设计师、产品经理和开发人员&#xff0c;帮助他们快速设计和原型各种应用程序和网站。 Sketch具有简洁直观的界面&#xff0c;以及丰富的功能集&#xff0c;使得用户可以轻松地创建、编辑和共享精美的…

C++ vector 动态数组的指定元素删除

文本旨在对 C 的容器 vector 进行肤浅的分析。 文章目录 Ⅰ、vector 的指定元素删除代码结果与分析 Ⅱ、vector 在新增元素后再删除指定元素代码结果与分析 Ⅲ、vector 在特定条件下新增元素代码结果与分析 参考文献 Ⅰ、vector 的指定元素删除 代码 #include <iostream&g…

Python语言:经典例题分析讲解

题1&#xff1a; 通过观察我们可以得出以下结论&#xff1a; 代码实现&#xff1a; """ &#xff08;3&#xff09;输入整数n&#xff0c;输出n行的字符图案。如n5时输出以下图案&#xff1a;* *** ***** ******* *********""""" for…