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. CANoe _ DBC 的创建过程

    在Canoe中创建DBC(Database Container)文件,用于描述和定义CAN总线上的节点.消息和信号,遵循以下步骤: 1.打开Canoe 启动Canoe软件. 2.创建新项目 在Cano ...

  2. 2023-06-20:给定一个长度为N的数组arr,arr[i]表示宝石的价值 你在某天遇到X价值的宝石, X价值如果是所有剩余宝石价值中的最小值,你会将该宝石送人 X价值如果不是所有剩余宝石价值中的

    2023-06-20:给定一个长度为N的数组arr,arr[i]表示宝石的价值 你在某天遇到X价值的宝石, X价值如果是所有剩余宝石价值中的最小值,你会将该宝石送人 X价值如果不是所有剩余宝石价值中的 ...

  3. 逍遥自在学C语言 | 多级指针探秘

    前言 多级指针在C语言中是一种特殊的指针类型,它可以指向其他指针的指针. 通过多级指针,我们可以间接地访问或修改存储在内存中的数据. 在本文中,我们将讨论多级指针的概念.使用方法.使用场景以及常见错误 ...

  4. 用postman模拟“授权代码授予”模式下获取Azure的用户信息(UserInfo)

    用postman模拟"授权代码授予"模式下获取Azure的用户信息(UserInfo) 1. 准备参数: 图1: 图2: 2. 调用: 点击按钮"Get New Acce ...

  5. python接口自动化封装导出excel方法和读写excel数据

    一.首先需要思考,我们在页面导出excel,用python导出如何写入文件的 封装前需要确认python导出excel接口返回的是一个什么样的数据类型 如下:我们先看下不对返回结果做处理,直接接收数据 ...

  6. eclipse在主题商城下载安装黑色主题

    Eclipse配置黑色主题方法: 1. 借用国外一个Elipse主题网站分享的主题配置文件来配置一个黑色的主题. 主题网址 2. 在这个网站下载自己喜欢的主题,单击主题进入下载页面,建议大家选择EPF ...

  7. bitfield

    bitfield 作用 位域修改 溢出控制 原理 通过对redis字符串二进制形式进行操作,通过改变其值的作用 更具体 将一个Redis字符串看作是一个由二进制位组成的数组. 并能对变长位宽和任意没有 ...

  8. 2022-1-11 控件学习4 ItemControl、ListBox、ComboBox

    ItemControl itemControl前台 ItemControl后台 ItemControl一般是竖直排列的,如果需要很想排列需要使用,也可以使用 UniformGrid Columns=& ...

  9. error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/

    解决办法: python3 是用 VC++ 14 编译的, python27 是 VC++ 9 编译的, 安装 python3 的包需要编译的也是要 VC++ 14 以上支持的.可以下载安装这个:vi ...

  10. 【pandas小技巧】--拆分列

    拆分列是pandas中常用的一种数据操作,它可以将一个包含多个值的列按照指定的规则拆分成多个新列,方便进行后续的分析和处理.拆分列的使用场景比较广泛,以下是一些常见的应用场景: 处理日期数据:在日期数 ...