springboot-sample

介绍

springboot简单示例 跳转到发行版 查看发行版说明

软件架构(当前发行版使用)

  1. springboot
  2. hutool-all 非常好的常用java工具库 官网 maven
  3. bcprov-jdk18on 一些加密算法的实现 官网 maven

安装教程

git clone --branch 5.使用JWT进行授权认证 git@gitee.com:simen_net/springboot-sample.git
 

功能说明

WebSecurityConfig中配置自定义的JWT认证

/**
* 用户验证服务 {@link JwtUserDetailsService}
*/
private final UserDetailsService userDetailsService; /**
* 身份验证成功处理程序 {@link JwtAuthenticationSuccessHandler}
*/
private final AuthenticationSuccessHandler authenticationSuccessHandler; /**
* 身份验证失败的处理程序 {@link JwtAuthenticationFailureHandler}
*/
private final AuthenticationFailureHandler authenticationFailureHandler; /**
* 登出成功处理程序 {@link JwtLogoutSuccessHandler}
*/
private final LogoutSuccessHandler logoutSuccessHandler; /**
* JWT认证入口点 {@link JwtAuthenticationEntryPoint}
*/
private final AuthenticationEntryPoint authenticationEntryPoint; /**
* JWT请求过滤
*/
private final JwtRequestFilter jwtRequestFilter;
 

发行版说明

  1. 完成基本WEB服务 跳转到发行版
  2. 完成了KEY初始化功能和全局错误处理 跳转到发行版
  3. 完成了基本登录验证 跳转到发行版
  4. 完成了自定义加密进行登录验证 跳转到发行版
  5. 完成了自定义加密进行登录验证 跳转到发行版 查看发行版说明

使用JWT进行授权认证

配置Config

  • WebSecurityConfig.java中加入“注册验证成功/失败处理器”JwtAuthenticationSuccessHandler.javaJwtAuthenticationFailureHandler.java

    // 注册验证成功处理器
    httpSecurityFormLoginConfigurer.successHandler(authenticationSuccessHandler);
    // 注册验证失败处理器
    httpSecurityFormLoginConfigurer.failureHandler(authenticationFailureHandler);
     
  • WebSecurityConfig.java中加入“JWT认证入口点”JwtAuthenticationEntryPoint,请求无认证信息时在此处理

    // 加入异常处理器
    httpSecurity.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
    // 加入JWT认证入口点
    httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint)
    );
     
  • WebSecurityConfig.java中加入“登出成功处理器”JwtLogoutSuccessHandler.java注销用户登录信息等

    // 自定义登出成功处理器
    httpSecurityLogoutConfigurer.logoutSuccessHandler(logoutSuccessHandler);
     
  • WebSecurityConfig.java登出过滤器之前加入“JWT请求过滤器”JwtRequestFilter.java对所有请求进行鉴权

    // 在登出过滤器之前加入JWT请求过滤器
    httpSecurity.addFilterBefore(jwtRequestFilter, LogoutFilter.class);
     
  • WebSecurityConfig.java中强制session无效

    // 强制session无效,使用jwt认证时建议禁用,正常登录不能禁用session
    httpSecurity.sessionManagement(httpSecuritySessionManagementConfigurer->
    httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );
     

全局说明

  1. JwtUserDetails.java中增加private Map<String, Object> mapProperties,用于保存登录用户的扩展信息,录入用户分组、用户单位等等

  2. JwtUserDetailsService.java中模拟注入用户权限及扩展信息

    listGrantedAuthority.add(new SimpleGrantedAuthority("file_read"));
    mapProperties.put("扩展属性", username + " file_read");
    log.info("读取到已有用户[{}],默认密码123456,file_read权限,扩展属性:[{}]", username, mapProperties); return new JwtUserDetails(username, SecurityUtils.signByUUID("123456"), false, listGrantedAuthority, mapProperties);`
     
  3. SecurityUtils.java中定义全局登录信息MAP,保存用户的token和验证对象。一是防止用户伪造token,二是缓存用户验证对象

    /**
    * 【系统】用户名与JWT Token对应的map
    * key: 用户登录名
    * value: JWT Token
    */
    public static Map<String, String> MAP_SYSTEM_USER_TOKEN = new ConcurrentHashMap<>(8); /**
    * 【系统】用户名与 UsernamePasswordAuthenticationToken 对应的map
    * key: 用户登录名
    * value: UsernamePasswordAuthenticationToken
    */
    public static Map<String, UsernamePasswordAuthenticationToken> MAP_SYSTEM_USER_AUTHENTICATION = new ConcurrentHashMap<>(8);
     
  4. SystemErrorController中重写BasicErrorControllerpublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request),将包括JWT处理在内的各类服务异常进行统一处理

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = this.getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
    return new ResponseEntity<>(status);
    } else {
    Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
    log.info("非HTML请求返回错误:{}", body); // 获取http返回状态码
    Integer intStatus = MapUtil.getInt(body, "status");
    // 获取http返回的异常字符串
    String strException = MapUtil.getStr(body, "exception");
    // 返回对象的消息
    String strMsg = LOGIN_ERROR;
    // 返回对象的内容
    String strData = null; // 直接从request中获取STR_JAKARTA_SERVLET_ERROR_EXCEPTION对象
    Object objErrorException = request.getAttribute(STR_JAKARTA_SERVLET_ERROR_EXCEPTION); // 1. 使用request的STR_JAKARTA_SERVLET_ERROR_EXCEPTION值获取错误消息
    // 判断异常对象是否为空
    if (ObjUtil.isNotNull(objErrorException)) {
    List<String> lisErrorException = StrUtil.splitTrim(objErrorException.toString(), ":");
    if (lisErrorException.size() == 2) {
    String strTemp = MAP_EXCEPTION_MESSAGE.get(lisErrorException.get(0));
    if (StrUtil.isNotBlank(strTemp)) {
    strMsg = strTemp;
    strData = lisErrorException.get(1);
    }
    }
    } // 2. 使用request的exception字符串获取错误消息
    // 判断replyVO.getData()为空,且http返回的异常字符串是否为空
    if (StrUtil.isBlank(strData) && StrUtil.isNotBlank(strException)) {
    strData = MAP_EXCEPTION_MESSAGE.get(strException);
    } // 3. 使用request的exception字符串获取错误消息
    // 判断replyVO.getData()为空,且错误代码有效
    if (StrUtil.isBlank(strData) && intStatus > 0) {
    ReplyEnum replyEnum = EnumUtil.getBy(ReplyEnum.class,
    re -> re.getCode().equals(intStatus));
    // 判断错误代码获取到的枚举类是否存在
    if (ObjUtil.isNotNull(replyEnum)) {
    strData = replyEnum.getMsg();
    }
    } // 4. 使用默认错误消息
    // 判断replyVO.getData()为空
    if (StrUtil.isBlank(strData)) {
    // 默认返回的错误内容
    strData = LOGIN_ERROR_UNKNOWN;
    } return new ResponseEntity<>(JSON.toMap(new ReplyVO<>(strData, strMsg, intStatus)), HttpStatus.OK);
    }
    }
     
  5. 测试流程:访问 http://localhost:8080/login

登录流程

  1. 无权限访问时,转到JWT认证入口点JwtAuthenticationEntryPoint,根据request头Accept判断请求类型是html还是json,html请求跳转到登录页面,json请求返回异常接送代码【该功能主要为演示,使用JWT时实际很少出现需要同时处理html和json请求的情况】

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    // 从request头中获取Accept
    String strAccept = request.getHeader("Accept");
    if (StrUtil.isNotBlank(strAccept)) {
    // 对Accept分组为字符串数组
    String[] strsAccept = StrUtil.splitToArray(strAccept, ",");
    // 判断Accept数组中是否存在"text/html"
    if (ArrayUtil.contains(strsAccept, "text/html")) {
    // 存在"text/html",判断为html访问,则跳转到登录界面
    response.sendRedirect(STR_URL_LOGIN_URL);
    } else {
    // 不存在"text/html",判断为json访问,则返回未授权的json
    SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
    new ReplyVO<>(ReplyEnum.ERROR_TOKEN_EXPIRED));
    }
    }
    }
     
  2. 登录成功时,调用处理器JwtAuthenticationSuccessHandler.java,其中使用Sm2JwtSigner.java进行签名和校验。更新该用户的MAP_SYSTEM_USER_TOKEN,删除该用户的MAP_SYSTEM_USER_AUTHENTICATION

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
    if (!response.isCommitted() && authentication != null && authentication.getPrincipal() != null
    // 获取登录用户信息对象
    && authentication.getPrincipal() instanceof JwtUserDetails userDetails) { // 获取30分钟有效的token编码
    String strToken = jwtTokenUtils.getToken30Minute(
    userDetails.getUsername(),
    CollUtil.join(userDetails.getAuthorities(), ","),
    userDetails.getMapProperties()
    ); // 更新系统缓存的用户JWT Token
    MAP_SYSTEM_USER_TOKEN.put(userDetails.getUsername(), strToken);
    // 删除系统缓存的用户身份验证对象
    MAP_SYSTEM_USER_AUTHENTICATION.remove(userDetails.getUsername()); // 包装返回的JWT对象
    ReplyVO<JwtResponseData> replyVO = new ReplyVO<>(
    new JwtResponseData(strToken, DateUtil.date()), "用户登录成功"); // 将返回字符串写入response
    SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, replyVO); log.info("[{}]登录成功,已缓存该用户Token", userDetails.getUsername());
    }
    }
     
  3. 登录失败时,调用处理器JwtAuthenticationFailureHandler,根据抛出的异常返回对应的json

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
    String strData = LOGIN_ERROR_UNKNOWN;
    String strMessage = "LOGIN_ERROR_UNKNOWN"; if (exception instanceof LockedException) {
    strData = LOGIN_ERROR_ACCOUNT_LOCKING;
    strMessage = exception.getMessage();
    } else if (exception instanceof CredentialsExpiredException) {
    strData = LOGIN_ERROR_PASSWORD_EXPIRED;
    strMessage = exception.getMessage();
    } else if (exception instanceof AccountExpiredException) {
    strData = LOGIN_ERROR_OVERDUE_ACCOUNT;
    strMessage = exception.getMessage();
    } else if (exception instanceof DisabledException) {
    strData = LOGIN_ERROR_ACCOUNT_BANNED;
    strMessage = exception.getMessage();
    } else if (exception instanceof BadCredentialsException) {
    strData = LOGIN_ERROR_USER_CREDENTIAL_EXCEPTION;
    strMessage = exception.getMessage();
    } else if (exception instanceof UsernameNotFoundException) {
    strData = LOGIN_ERROR_USER_NAME_NOT_FOUND;
    strMessage = exception.getMessage();
    } // exception.printStackTrace();
    SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
    new ReplyVO<>(strData, strMessage, ReplyEnum.ERROR_USER_HAS_NO_PERMISSIONS.getCode()));
    }
     
  4. 正常请求json时,使用过滤器JwtRequestFilter.java,对每个JSON请求进行鉴权(其中使用MAP_SYSTEM_USER_AUTHENTICATION进行缓存处理),并将相应信息放入SpringSecurity的上下文身份验证中SecurityContextHolder.getContext().setAuthentication(authenticationToken);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    // 如果不是访问登出url,且通过认证
    if (!StrUtil.equals(URLUtil.getPath(request.getRequestURL().toString()), STR_URL_LOGOUT_URL) &&
    SecurityContextHolder.getContext().getAuthentication() == null) {
    // 获取请求头Authorization
    final String strAuthorization = request.getHeader(HttpHeaders.AUTHORIZATION);
    // 判断请求Authorization非空且以STR_AUTHENTICATION_PREFIX开头
    if (StrUtil.isNotBlank(strAuthorization) && strAuthorization.startsWith(STR_AUTHENTICATION_PREFIX)) {
    // 获取JWT Token
    String strJwtToken = strAuthorization.replace(STR_AUTHENTICATION_PREFIX, "");
    // 验证凭证,失败则抛出错误
    jwtTokenUtils.verifyToken(strJwtToken);
    // 从JWT Token中获取用户名
    String strUserName = jwtTokenUtils.getAudience(strJwtToken); // 从系统MAP中获取该用户的身份验证对象
    UsernamePasswordAuthenticationToken authentication = MAP_SYSTEM_USER_AUTHENTICATION.get(strUserName); // 判断身份验证对象非空
    if (ObjUtil.isNotEmpty(authentication)) {
    // 放入安全上下文中
    SecurityContextHolder.getContext().setAuthentication(authentication);
    log.info(String.format("检测到[%s]访问,从系统MAP中直接获取身份验证对象", strUserName));
    } else {
    // 从JWT Token中获取权限字符串
    String strAuthorities = jwtTokenUtils.getAuthorities(strJwtToken); // 将用户权限放入权限列表
    List<GrantedAuthority> listGrantedAuthority = new ArrayList<>();
    if (StrUtil.isNotBlank(strAuthorities)) {
    String[] strsAuthority = StrUtil.splitToArray(strAuthorities, ",");
    for (String strAuthority : strsAuthority) {
    listGrantedAuthority.add(new SimpleGrantedAuthority(strAuthority.trim()));
    }
    } // 构建用户登录信息实现
    JwtUserDetails userDetails = new JwtUserDetails(
    strUserName, // 获取用户名
    "[PROTECTED]", // 屏蔽密码
    jwtTokenUtils.isToRefresh(strJwtToken), // 从token获取jwt认证是否需要刷新
    listGrantedAuthority, jwtTokenUtils.getUserPropertiesMap(strJwtToken));
    // 构建用户认证token
    UsernamePasswordAuthenticationToken authenticationToken =
    new UsernamePasswordAuthenticationToken(userDetails, null, listGrantedAuthority);
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    // 放入安全上下文中
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    // 将身份验证对象放入系统MAP
    MAP_SYSTEM_USER_AUTHENTICATION.put(strUserName, authenticationToken);
    log.info(String.format("检测到[%s]访问,具有[%s]权限,缓存至系统MAP", userDetails.getUsername(), strAuthorities));
    }
    }
    }
    // 使用过滤链进行过滤
    filterChain.doFilter(request, response);
    }
     
  5. 登出成功时,调用处理器JwtLogoutSuccessHandler,并清空该用户的MAP_SYSTEM_USER_TOKENMAP_SYSTEM_USER_AUTHENTICATION缓存

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
    // 从Request中取出授权字符串
    final String strAuthorization = request.getHeader(HttpHeaders.AUTHORIZATION);
    // 判断授权字符串是否以STR_AUTHENTICATION_PREFIX开头
    if (StrUtil.startWith(strAuthorization, STR_AUTHENTICATION_PREFIX)) {
    // 获取认证的JWT token
    String strJwtToken = strAuthorization.replace(STR_AUTHENTICATION_PREFIX, "");
    // 判断token是否为空
    if (StrUtil.isNotBlank(strJwtToken)) {
    // 验证凭证,失败则抛出错误
    try {
    jwtTokenUtils.verifyToken(strJwtToken);
    // 从token中获取用户名
    String strUserName = jwtTokenUtils.getAudience(strJwtToken);
    // 断言用户名非空
    Assert.notBlank(strUserName, "当前用户不存在"); // 删除系统缓存的用户JWT Token
    MAP_SYSTEM_USER_TOKEN.remove(strUserName);
    // 删除系统缓存的用户身份验证对象
    MAP_SYSTEM_USER_AUTHENTICATION.remove(strUserName); log.info("[{}]登出成功,已清除该用户登录缓存信息", strUserName);
    } catch (Exception ignored) {
    log.info("登出失败");
    }
    }
    }
    // 返回登出成功信息
    SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK, new ReplyVO<>(LOGOUT_SUCCESS));
    }
     

JWT处理

  1. Sm2JwtSigner.java签名和校验时,将headerBase64payloadBase64使用STR_JWT_SIGN_SPLIT组合成字符串进行签名和校验

    /**
    * 返回签名的Base64代码
    *
    * @param headerBase64 JWT头的JSON字符串的Base64表示
    * @param payloadBase64 JWT载荷的JSON字符串Base64表示
    * @return 签名结果Base64,即JWT的第三部分
    */
    @Override
    public String sign(String headerBase64, String payloadBase64) {
    // 将headerBase64和payloadBase64使用STR_JWT_SIGN_SPLIT组合在一起之后进行签名
    return SecurityUtils.signByUUID(headerBase64 + STR_JWT_SIGN_SPLIT + payloadBase64);
    } /**
    * 验签
    *
    * @param headerBase64 JWT头的JSON字符串Base64表示
    * @param payloadBase64 JWT载荷的JSON字符串Base64表示
    * @param signBase64 被验证的签名Base64表示
    * @return 签名是否一致
    */
    @Override
    public boolean verify(String headerBase64, String payloadBase64, String signBase64) {
    // 将headerBase64和payloadBase64使用STR_JWT_SIGN_SPLIT组合在一起之后进行签名校验
    return SecurityUtils.verifyByUUID(headerBase64 + STR_JWT_SIGN_SPLIT + payloadBase64, signBase64);
    }
     
  2. 生成的JWT代码和解密内容

    • JWT Tokens 编码

      eyJ0eXAiOiJKV1QiLCJhbGciOiLlm73lr4ZTTTLpnZ7lr7nnp7Dnrpfms5XvvIzln7rkuo5CQ-W6kyJ9.eyJhdWQiOlsic2ltZW4iXSwiaWF0IjoxNjk1MDIwMzUzLCJleHAiOjE2OTUwMzgzNTMsIlVTRVJfQVVUSE9SSVRZIjoiZmlsZV9yZWFkIiwiTUFQX1VTRVJfUFJPUEVSVElFUyI6eyLmianlsZXlsZ7mgKciOiJzaW1lbiBmaWxlX3JlYWQifX0.MEQCIBr7QHoMdgqt53AM+hlVJfDfSrj8Pdi+dAJ9hg3QMBQuAiAhcFbV26ESehhylWewr467GNWncKruz86NfD68CU105Q==
       
    • 解码后HEADER

      {
      "typ": "JWT",
      "alg": "国密SM2非对称算法,基于BC库"
      }
       
    • 解码后PAYLOAD

      {
      "aud": [
      "simen"
      ],
      "iat": 1695020353,
      "exp": 1695038353,
      "USER_AUTHORITY": "file_read",
      "MAP_USER_PROPERTIES": {
      "扩展属性": "simen file_read"
      }
      }

Springboot简单功能示例-5 使用JWT进行授权认证的更多相关文章

  1. [转]三分钟学会.NET Core Jwt 策略授权认证

    [转]三分钟学会.NET Core Jwt 策略授权认证 一.前言# 大家好我又回来了,前几天讲过一个关于Jwt的身份验证最简单的案例,但是功能还是不够强大,不适用于真正的项目,是的,在真正面对复杂而 ...

  2. JWT实现授权认证

    目录 一. JWT是什么 二. JWT标准规范 三. 核心代码简析 四. 登录授权示例 五. JWT 使用方式 六. JWT注意事项 一. JWT是什么 JSON Web Token(JWT)是目前最 ...

  3. 三分钟学会.NET Core Jwt 策略授权认证

    一.前言 大家好我又回来了,前几天讲过一个关于Jwt的身份验证最简单的案例,但是功能还是不够强大,不适用于真正的项目,是的,在真正面对复杂而又苛刻的客户中,我们会不知所措,就现在需要将认证授权这一块也 ...

  4. dubbo+zookeeper+springboot简单示例

    目录 dubbo+zookeeper+springboot简单示例 zookeeper安装使用 api子模块 生产者producer 消费者consumer @(目录) dubbo+zookeeper ...

  5. 记录一次简单的springboot发送邮件功能

    场景:经常在我们系统中有通过邮件功能找回密码,或者发送生日祝福等功能,今天记录下springboot发送邮件的简单功能 1.引入maven <!-- 邮件开发--><dependen ...

  6. SpringBoot整合SpringSecurity示例实现前后分离权限注解

    SpringBoot 整合SpringSecurity示例实现前后分离权限注解+JWT登录认证 作者:Sans_ juejin.im/post/5da82f066fb9a04e2a73daec 一.说 ...

  7. 【java】org.apache.commons.lang3功能示例

    org.apache.commons.lang3功能示例 package com.simple.test; import java.util.Date; import java.util.Iterat ...

  8. 【java开发系列】—— spring简单入门示例

    1 JDK安装 2 Struts2简单入门示例 前言 作为入门级的记录帖,没有过多的技术含量,简单的搭建配置框架而已.这次讲到spring,这个应该是SSH中的重量级框架,它主要包含两个内容:控制反转 ...

  9. html5本地存储之localstorage 、本地数据库、sessionStorage简单使用示例

    这篇文章主要介绍了html5本地存储的localstorage .本地数据库.sessionStorage简单使用示例,需要的朋友可以参考下 html5的一个非常cool的功能,就是web stora ...

  10. springboot简单介绍

    1.springboot简单介绍 微服务架构 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程. 该框架使用了特定的方 ...

随机推荐

  1. 五年磨一剑——Sealos 云操作系统正式发布!

    这是个宏伟的计划 这是一个宏伟的计划,漫长且有趣. 2018 年的某个夜晚,夜深人静,我挥舞键盘,敲下了 Sealos 的第一行代码.当时仓库命名为 "kubeinit",后来觉得 ...

  2. 一文了解 io.Copy 函数

    1. 引言 io.Copy 函数是一个非常好用的函数,能够非常方便得将数据进行拷贝.本文我们将从io.Copy 函数的基本定义出发,讲述其基本使用和实现原理,以及一些注意事项,基于此完成对io.Cop ...

  3. Open LLM 排行榜近况

    Open LLM 排行榜是 Hugging Face 设立的一个用于评测开放大语言模型的公开榜单.最近,随着 Falcon 的发布并在 Open LLM 排行榜 上疯狂屠榜,围绕这个榜单在推特上掀起了 ...

  4. @Import :Spring Bean模块装配的艺术

    本文分享自华为云社区<Spring高手之路8--Spring Bean模块装配的艺术:@Import详解>,作者: 砖业洋__. 本文将带你深入探索Spring框架的装配机制,以及它如何使 ...

  5. Abaqus添加初始缺陷

    主要介绍通过施加节点位移的方法 步骤一: 复制model,新建Step,static linear perturbation Tools->Analytical Field 定义场函数,例如:A ...

  6. 《最新出炉》系列入门篇-Python+Playwright自动化测试-9-页面(page)

    1.简介 通过前边的讲解和学习,细心认真地小伙伴或者童鞋们可能发现在Playwright中,没有Element这个概念,只有Page的概念,Page不仅仅指的是某个页面,例如页面间的跳转等,还包含了所 ...

  7. 如何修改电脑的BIOS密码?

      本文介绍设置.修改Windows电脑BIOS模式密码的具体方法.   一般的,电脑默认都是不含有BIOS密码的,可以直接在开机时不输入任何密码进入BIOS模式:而在某些特定的场合,我们可能需要对其 ...

  8. 模型部署 — PaddleNLP 基于 Paddle Serving 快速使用(服务化部署 - Docker)— 图像识别 + 信息抽取(UIE-X)

    目录 流程 版本 安装 Docker 安装 PaddleNLP 安装 环境准备 模型准备 压缩模型 下载模型 模型部署 环境配置 启动服务 测试 -- 暂时还没通过 重启 图像识别 + 信息抽取(UI ...

  9. Blazor实战——Known框架多表增删改查

    多表增删改查示例 本章介绍学习多张表增.删.改.查功能如何实现,下面以销货出库单作为示例,该业务栏位如下: 销货出库单栏位 销货单号.销货日期.状态.客户.备注 销货出库单明细栏位 商品编码.商品名称 ...

  10. Inpaint Anything:一键进行多种图像修补

    本文分享自华为云社区<绘制一切>,作者: 雨落无痕 . 绘制一切-Inpaint Anything 相关链接: Notebook案例地址:绘制一切 AI Gallery:https://d ...