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. 如何在.net6webapi中实现自动依赖注入

    IOC/DI IOC(Inversion of Control)控制反转:控制反正是一种设计思想,旨在将程序中的控制权从程序员转移到了容器中.容器负责管理对象之间的依赖关系,使得对象不再直接依赖于其他 ...

  2. Java 实战介绍 Cookie 和 Session 的区别

    HTTP 是一种不保存状态的协议,即无状态协议,HTTP 协议不会保存请求和响应之间的通信状态,协议对于发送过的请求和响应都不会做持久化处理. 无状态协议减少了对服务压力,如果一个服务器需要处理百万级 ...

  3. Python 标准类库-并发执行之multiprocessing-基于进程的并行

    实践环境 Python3.6 介绍 multiprocessing是一个支持使用类似于线程模块的API派生进程的包.该包同时提供本地和远程并发,通过使用子进程而不是线程,有效地避开了全局解释器锁.因此 ...

  4. CF1832F Zombies

    简要题意 给定 \(n\) 个左闭右开的区间 \(A_i = [L_i, R_i)\),其中 \(0\le L_i < R_i \le x\),你可以自由选择 \(k\) 个长度为 \(m\) ...

  5. 自然语言处理 Paddle NLP - 基于预训练模型完成实体关系抽取

    自然语言处理 Paddle NLP - 信息抽取技术及应用 重点:SOP 图.BCEWithLogitsLoss 基于预训练模型完成实体关系抽取 信息抽取旨在从非结构化自然语言文本中提取结构化知识,如 ...

  6. 基于python+django的宠物商店-宠物管理系统设计与实现

    该系统是基于python+django开发的宠物商店-宠物管理系统.是给师妹开发的课程作业.现将源码开放给大家.大家学习过程中,如遇问题可以在github咨询作者. 演示地址 前台地址: http:/ ...

  7. AcWing 第87场周赛题解

    T1 移动棋子 算出数值为 \(1\) 的点离 \((3, 3)\) 的距离即可. #include <iostream> #include <cstring> #includ ...

  8. 现代C++(Modern C++)基本用法实践:零、概述&测试项目

    序言 习惯上,我们把C++11之前的C++语法特性称之为"传统C++(traditional c++)",而把c++11之后的语法特性称之为现代C++(modern c++).有一 ...

  9. Flask结合gunicorn和nginx反向代理的生产环境部署及踩坑记录

    前言 之前自己写的flask使用gunicorn上线生产环境没有什么问题,但是最近搭建了一个现成的flask项目,当使用python直接运行时不会有问题,而使用gunicorn时则会出现一些问题. 部 ...

  10. 云储存选择做Hexo博客图床(腾讯云、七牛云、网易云)

    前言 博客里需要添加很多图片作为内容的补充,但是把图片放在本地博客文件夹里,上传到网上后,加载这些图片就是一个很大的问题,他们会拖累网页加载的速度,所以建议把图片放图床里,通过外链来访问和加载这些图片 ...