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. js如何操作video标签

    一.简介 在做web ui自动化时,遇到操作视频的时候有时比较让人头疼,定位时会发现只有一个<video>标签,用selenium来实现的话比较麻烦,使用js后我们只需定位到video标签 ...

  2. 手写数字识别系统Python+CNN卷积神经网络算法【完整代码】

    一.介绍 手写数字识别系统,使用Python语言,基于TensorFlow搭建CNN卷积神经网络算法对数据集进行训练,最后得到模型,并基于FLask搭建网页端界面,基于Pyqt5搭建桌面端可视化界面. ...

  3. RPA自动化如何帮助企业提高业务业务洞察力

    目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.3 相关技术比较 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成 ...

  4. React后台管理系统09 菜单组件的抽取

    修改Home.tsx的内容:将主菜单的内容进行抽离,然后单独引入=>MainMenu import { Breadcrumb, Layout, Menu } from 'antd'; impor ...

  5. RSA 加密签名验签解密

    import javax.crypto.Cipher; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PSo ...

  6. iOS select标签适配去掉iOS默认select样式

    iOS端对select标签有独特的适配,如何取消这些默认样式呢 css样式表 加上 1 select { 2 -webkit-appearance: none; 3 } 博客同步更新:地址

  7. Unity的Console的控制类LogEntries:深入解析与实用案例

    使用Unity Console窗口的LogEntries私有类实现自定义日志系统 在Unity开发过程中,我们经常需要使用Console窗口来查看程序运行时的日志信息.Unity内置的日志系统提供了基 ...

  8. 从Excel 电子表格中读取数据并插入到数据库的简单方式

    using (FileStream fileStreamRead = new FileStream("new.xls" , FileMode.Open )) { //创建工作簿 I ...

  9. Hello-FPGA CoaXPress 2.0 FPGA HOST IP Core Demo User Manual

    目录 Hello-FPGA CoaXPress 2.0 Host FPGA IP Core Demo 4 1 说明 4 2 设备连接 5 3 VIVADO FPGA工程 6 4 SDK工程 9 图 1 ...

  10. 即构SDK5月迭代:新增声道选择、网络探测、智能消噪等功能,打造更优的视听体验

    即构SDK5月份的迭代更新如期而至,本月互动视频(LiveRoom).实时语音(AudioRoom)两大SDK以及录制插件(PlayRecord)均有新功能上线.新增的声道选择.变调控制.智能消噪.枚 ...