SpringSecurity+JWT认证流程解析

2023-11-18

楔子

本文适合: 对Spring Security有一点了解或者跑过简单demo但是对整体运行流程不明白的同学,对SpringSecurity有兴趣的也可以当作你们的入门教程,示例代码中也有很多注释。
大家在做系统的时候,一般做的第一个模块就是认证与授权模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。
市面上一般做认证授权的框架就是shiro和Spring Security,也有大部分公司选择自己研制。出于之前看过很多Spring Security的入门教程,但都觉得讲的不是太好,所以我这两天在自己鼓捣Spring Security的时候萌生了分享一下的想法,希望可以帮助到有兴趣的人。

Spring Security框架我们主要用它就是解决一个认证授权功能,所以我的文章主要会分为两部分:

  • 第一部分认证(本篇)
  • 第二部分授权(放在下一篇)

我会为大家用一个Spring Security + JWT + 缓存的一个demo来展现我要讲的东西,毕竟脑子的东西要体现在具体事物上才可以更直观的让大家去了解去认识。
学习一件新事物的时候,我推荐使用自顶向下的学习方法,这样可以更好的认识新事物,而不是盲人摸象。

:只涉及到用户认证授权不涉及oauth2之类的第三方授权。

1. SpringSecurity的工作流程


想上手 Spring Security 一定要先了解它的工作流程,因为它不像工具包一样,拿来即用,必须要对它有一定的了解,再根据它的用法进行自定义操作。

我们可以先来看看它的工作流程:
在Spring Security的官方文档上有这么一句话:

Spring Security’s web infrastructure is based entirely on standard servlet filters.

Spring Security 的web基础是Filters。

这句话展示了Spring Security的设计思想:即通过一层层的Filters来对web请求做处理。

放到真实的Spring Security中,用文字表述的话可以这样说:

一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。

用图片表述的话可以这样画,这是我在百度找到的一张图片:


如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要讲的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

图中的这两个绿色过滤器我们今天不会去说,因为这是Spring Security对form表单认证和Basic认证内置的两个Filter,而我们的demo是JWT认证方式所以用不上。

如果你用过Spring Security就应该知道配置中有两个叫formLogin和httpBasic的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。

  • formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。
  • httpBasic对应着Basic认证方式,即BasicAuthenticationFilter。

换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。

因为Spring Security自带的过滤器中是没有针对JWT这种认证方式的,所以我们的demo中会写一个JWT的认证过滤器,然后放在绿色的位置进行认证工作。

2. SpringSecurity的重要概念


知道了Spring Security的大致工作流程之后,我们还需要知道一些非常重要的概念也可以说是组件:

  • SecurityContext:上下文对象,Authentication对象会放在里面。
  • SecurityContextHolder:用于拿到上下文对象的静态工具类。
  • Authentication:认证接口,定义了认证对象的数据形式。
  • AuthenticationManager:用于校验Authentication,返回一个认证完成后的Authentication对象。

1.SecurityContext

上下文对象,认证后的数据就放在这里面,接口定义如下:

public interface SecurityContext extends Serializable {
 // 获取Authentication对象
 Authentication getAuthentication();

 // 放入Authentication对象
 void setAuthentication(Authentication authentication);
}

这个接口里面只有两个方法,其主要作用就是get or set Authentication。

2. SecurityContextHolder

public class SecurityContextHolder {

 public static void clearContext() {
  strategy.clearContext();
 }

 public static SecurityContext getContext() {
  return strategy.getContext();
 }
    
    public static void setContext(SecurityContext context) {
  strategy.setContext(context);
 }

}

可以说是SecurityContext的工具类,用于get or set or clear SecurityContext,默认会把数据都存储到当前线程中。

3. Authentication

public interface Authentication extends Principal, Serializable {
 
 Collection<? extends GrantedAuthority> getAuthorities();
 Object getCredentials();
 Object getDetails();
 Object getPrincipal();
 boolean isAuthenticated();
 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这几个方法效果如下:

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息
  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。
  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。
  • isAuthenticated: 获取当前 Authentication 是否已认证。
  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。


Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。

4. AuthenticationManager

public interface AuthenticationManager {
 // 认证方法
 Authentication authenticate(Authentication authentication)
   throws AuthenticationException;
}

AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。
接下来大家可以构思一下如何将这四个部分,串联起来,构成Spring Security进行认证的流程:
1. 先是一个请求带着身份信息进来
2. 经过AuthenticationManager的认证,
3. 再通过SecurityContextHolder获取SecurityContext,
4. 最后将认证后的信息放入到SecurityContext。

3. 代码前的准备工作


真正开始讲诉我们的认证代码之前,我们首先需要导入必要的依赖,数据库相关的依赖可以自行选择什么JDBC框架,我这里用的是国人二次开发的myabtis-plus。

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>


接着,我们需要定义几个必须的组件。
由于我用的Spring-Boot是2.X所以必须要我们自己定义一个加密器:

1. 定义加密器Bean

 @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

这个Bean是不必可少的,Spring Security在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。

2. 定义AuthenticationManager

@Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

这里将Spring Security自带的authenticationManager声明成Bean,声明它的作用是用它帮我们进行认证操作,调用这个Bean的authenticate方法会由Spring Security自动帮我们做认证。

3. 实现UserDetailsService

public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("开始登陆验证,用户名为: {}",s);

        // 根据用户名验证用户
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
        UserInfo userInfo = userService.getOne(queryWrapper);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名不存在,登陆失败。");
        }

        // 构建UserDetail对象
        UserDetail userDetail = new UserDetail();
        userDetail.setUserInfo(userInfo);
        List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
        userDetail.setRoleInfoList(roleInfoList);
        return userDetail;
    }
}

实现UserDetailsService的抽象方法并返回一个UserDetails对象,认证过程中SpringSecurity会调用这个方法访问数据库进行对用户的搜索,逻辑什么都可以自定义,无论是从数据库中还是从缓存中,但是我们需要将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。

UserDetails 也是一个定义了数据形式的接口,用于保存我们从数据库中查出来的数据,其功能主要是验证账号状态和获取权限,具体实现可以查阅我仓库的代码。

4. TokenUtil

由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:

  • 创建token
  • 验证token
  • 反解析token中的信息


在下文我的代码里面,JwtProvider充当了Token工具类的角色,具体实现可以查阅我仓库的代码。

4. ✍代码中的具体实现


有了前面的讲解之后,大家应该都知道用SpringSecurity做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放到绿色部分。
在我们编写这个过滤器之前,我们还需要进行一个认证操作,因为我们要先访问认证接口拿到token,才能把token放到请求头上,进行接下来请求。
如果你不太明白,不要紧,先接着往下看我会在这节结束再次梳理一下。

1. 认证方法

访问一个系统,一般最先访问的是认证方法,这里我写了最简略的认证需要的几个步骤,因为实际系统中我们还要写登录记录啊,前台密码解密啊这些操作。

@Override
    public ApiResult login(String loginAccount, String password) {
        // 1 创建UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
        // 2 认证
        Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
        // 3 保存认证信息
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 4 生成自定义token
        UserDetail userDetail = (UserDetail) authentication.getPrincipal();
        AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());

        // 5 放入缓存
        caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
        return ApiResult.ok(accessToken);
    }

这里一共五个步骤,大概只有前四步是比较陌生的:

  1. 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。
  2. 使用我们先前已经声明过的Bean-authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。
  3. 认证完成没有出现异常,就会走到第三步,使用SecurityContextHolder获取SecurityContext之后,将认证完成之后的Authentication对象,放入上下文对象。
  4. 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,然后创建token。
  5. 把UserDetails对象放入缓存中,方便后面过滤器使用。


这样的话就算完成了,感觉上很简单,因为主要认证操作都会由
authenticationManager.authenticate()帮我们完成。



接下来我们可以看看源码,从中窥得Spring Security是如何帮我们做这个认证的(省略了一部分):

// AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication){

  // 校验未认证的Authentication对象里面有没有用户名
  String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
    : authentication.getName();  
    
     boolean cacheWasUsed = true;
     // 从缓存中去查用户名为XXX的对象
  UserDetails user = this.userCache.getUserFromCache(username);

     // 如果没有就进入到这个方法
  if (user == null) {
   cacheWasUsed = false;

   try {
                // 调用我们重写UserDetailsService的loadUserByUsername方法
                // 拿到我们自己组装好的UserDetails对象
    user = retrieveUser(username,
      (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (UsernameNotFoundException notFound) {
    logger.debug("User '" + username + "' not found");

    if (hideUserNotFoundExceptions) {
     throw new BadCredentialsException(messages.getMessage(
       "AbstractUserDetailsAuthenticationProvider.badCredentials",
       "Bad credentials"));
    }
    else {
     throw notFound;
    }
   }

   Assert.notNull(user,
     "retrieveUser returned null - a violation of the interface contract");
  }
    
    try {
         // 校验账号是否禁用
   preAuthenticationChecks.check(user);
         // 校验数据库查出来的密码,和我们传入的密码是否一致
   additionalAuthenticationChecks(user,
     (UsernamePasswordAuthenticationToken) authentication);
  }


}

看了源码之后你会发现和我们平常写的一样,其主要逻辑也是查数据库然后对比密码。
登录之后效果如下:


我们返回token之后,下次请求其他API的时候就要在请求头中带上这个token,都按照JWT的标准来做就可以。

2. JWT过滤器

有了token之后,我们要把过滤器放在过滤器链中,用于解析token,因为我们没有session,所以我们每次去辨别这是哪个用户的请求的时候,都是根据请求中的token来解析出来当前是哪个用户。
所以我们需要一个过滤器去拦截所有请求,前文我们也说过,这个过滤器我们会放在绿色部分用来替代
UsernamePasswordAuthenticationFilter,所以我们新建一个JwtAuthenticationTokenFilter,然后将它注册为Bean,并在编写配置文件的时候需要加上这个:

@Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(jwtAuthenticationTokenFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }

addFilterBefore的语义是添加一个Filter到XXXFilter之前,放在这里就是把
JwtAuthenticationTokenFilter放在UsernamePasswordAuthenticationFilter之前,因为filter的执行也是有顺序的,我们必须要把我们的filter放在过滤器链中绿色的部分才会起到自动认证的效果。
接下来我们可以看看
JwtAuthenticationTokenFilter的具体实现了:

@Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
                                    @NotNull FilterChain chain) throws ServletException, IOException {
        log.info("JWT过滤器通过校验请求头token进行自动登录...");

        // 拿到Authorization请求头内的信息
        String authToken = jwtProvider.getToken(request);

        // 判断一下内容是否为空且是否为(Bearer )开头
        if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
            // 去掉token前缀(Bearer ),拿到真实token
            authToken = authToken.substring(jwtProperties.getTokenPrefix().length());

            // 拿到token里面的登录账号
            String loginAccount = jwtProvider.getSubjectFromToken(authToken);

            if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 缓存里查询用户,不存在需要重新登陆。
                UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);

                // 拿到用户信息后验证用户信息与token
                if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {

                    // 组装authentication对象,构造参数是Principal Credentials 与 Authorities
                    // 后面的拦截器里面会用到 grantedAuthorities 方法
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

                    // 将authentication信息放入到上下文对象中
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
                }
            }
        }

        chain.doFilter(request, response);
    }

代码里步骤虽然说的很详细了,但是可能因为代码过长不利于阅读,我还是简单说说,也可以直接去仓库查看源码:

  1. 拿到Authorization请求头对应的token信息
  2. 去掉token的头部(Bearer )
  3. 解析token,拿到我们放在里面的登陆账号
  4. 因为我们之前登陆过,所以我们直接从缓存里面拿我们的UserDetail信息即可
  5. 查看是否UserDetail为null,以及查看token是否过期,UserDetail用户名与token中的是否一直。
  6. 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了。

这样的话,每一个带有正确token的请求进来之后,都会找到它的账号信息,并放在上下文对象中,我们可以使用SecurityContextHolder很方便的拿到上下文对象中的Authentication对象。

完成之后,启动我们的demo,可以看到过滤器链中有以下过滤器,其中我们自定义的是第5个:

‍就酱,我们登录完了之后获取到的账号信息与角色信息我们都会放到缓存中,当带着token的请求来到时,我们就把它从缓存中拿出来,再次放到上下文对象中去。

结合认证方法,我们的逻辑链就变成了:

登录拿到token请求带上tokenJWT过滤器拦截校验token将从缓存中查出来的对象放到上下文中

这样之后,我们认证的逻辑就算完成了。

4. 代码优化


认证和JWT过滤器完成后,这个JWT的项目其实就可以跑起来了,可以实现我们想要的效果,如果想让程序更健壮,我们还需要再加一些辅助功能,让代码更友好。

1. 认证失败处理器


当用户未登录或者token解析失败时会触发这个处理器,返回一个非法访问的结果。

2. 权限不足处理器


当用户本身权限不满足所访问API需要的权限时,触发这个处理器,返回一个权限不足的结果。

3. 退出方法


用户退出一般就是清除掉上下文对象和缓存就行了,你也可以做一下附加操作,这两步是必须的。

4. token刷新


JWT的项目token刷新也是必不可少的,这里刷新token的主要方法放在了token工具类里面,刷新完了把缓存重载一遍就行了,因为缓存是有有效期的,重新put可以重置失效时间。

后记


这篇文我从上周日就开始构思了,为了能讲的老妪能解,修修改改了几遍才发出来。

Spring Security的上手的确有点难度,在我第一次去了解它的时候看的是尚硅谷的教程,那个视频的讲师拿它和Thymeleaf结合,这就导致网上也有很多博客去讲Spring Security的时候也是这种方式,而没有去关注前后端分离。
也有教程做过滤器的时候是直接继承
UsernamePasswordAuthenticationFilter,这样的方法也是可行的,不过我们了解了整体的运行流程之后你就知道没必要这样做,不需要去继承XXX,只要写个过滤器然后放在那个位置就可以了。


好了,认证篇结束后,下篇就是动态鉴权了,这是我在掘金的第一篇文,我的第一次知识输出,希望大家持续关注。
你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。
我是耳朵,一个一直想做知识输出的人,下期见。


作者:和耳朵
链接:
https://juejin.cn/post/6846687598442708999

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringSecurity+JWT认证流程解析 的相关文章

  • Eclipse 自动完成更改变量名称

    只是一个愚蠢的问题 但很难搜索 因为有很多关于 Eclipse 自动完成的主题 而且很难找到与我的问题匹配的内容 所以问题是 如果我写 MyClass MyVarName 然后按空格键 添加 new MyClass Eclipse 自动添加
  • 获取TextView的高度

    我有一些文字将被放入TextView 我这样做是使用setText 现在我需要找到文本在文本中占据的行数或高度TextView 我尝试使用getHeight 但它总是返回 0 无论如何 有没有办法获得文本中存在的文本的高度TextView
  • 我需要显式关闭连接吗?

    我持有一个实例MongoClient and DB在我的应用程序中 每次我想执行某些操作时 我都会调用getCollection 我想知道是否需要显式关闭连接 就像connection close 在 JDBC 中 强调一下 我只有一个Mo
  • 根据哈希值确认文件内容

    我需要 检查完整性 content文件数量 文件将写入 CD DVD 可能会被复制多次 这个想法是识别正确复制的副本 在从 Nero 等中删除它们之后 我对此很陌生 但快速搜索表明Arrays hashCode byte http down
  • JSON 对象数组转 Java POJO

    将此 JSON 对象转换为 java 中的类 您的 POJO 类中的映射将如何 ownerName Robert pets name Kitty name Rex name Jake This kind of question is ver
  • Java 反射:如何检索匿名内部类?

    我在另一个类中有一个匿名内部类 SomeClass Both SomeClass class getClasses and SomeClass class getDeclaredClasses 返回空数组 我在中找不到一些关于此的提示Cla
  • 在Java中使用BufferedWriter写入文件时监视文件大小?

    我正在将一个可能很长的项目列表写入文件 我正在写的项目的长度是可变的 如果生成的文件大小大于10M 则应将其分成多个文件 为了提高性能 我目前使用 BufferedWriter 如下所示 final FileOutputStream fos
  • 在 JavaFX 中拖动未装饰的舞台

    我希望将舞台设置为 未装饰 使其可拖动且可最小化 问题是我找不到这样做的方法 因为我遇到的示例是通过插入到主方法中的方法来实现的 我想通过控制器类中声明的方法来完成此操作 就像我如何使用下面的 WindowClose 方法来完成此操作 这是
  • java.lang.UnsupportedOperationException:无法解析索引 13 处的属性:TypedValue{t=0x2/d=0x7f010046 a=-1}

    我在 android attrs xml 文件中添加了一个用于不同色调的属性 在 styles xml 文件中 我为这些属性指定了颜色 因此每种样式的它们都不同 Attrs xml
  • Spring使用实体管理器实现分页

    如何在 Spring hibernate 项目中实现分页 以下是代码 我将获得 PageRequest 对象 并且我想返回项目页面 Repository public class ItemRepository PersistenceCont
  • perl 和 java 正则表达式功能之间有什么区别?

    perl 和 java 在支持哪些正则表达式术语方面有什么区别 这个问题仅涉及正则表达式 并且特别排除了how可以使用正则表达式 即使用正则表达式的可用函数 方法 以及语言之间的语法差异 例如java要求转义反斜杠等 特别令人感兴趣的是 j
  • Java中无参数的for循环

    我在看别人的代码 发现了这段代码 for 我不是 Java 专家 这行代码在做什么 起初 我认为这会创建一个无限循环 但在该程序员使用的同一个类中 while true 其中 如果我错了 请纠正我 是一个无限循环 这两个相同吗 为什么有人会
  • 如何使用 AWS CodeCommit 作为 Spring Cloud Config 的存储库

    我正在尝试将 AWS CodeCommit 存储库与 Spring Cloud 配置结合使用 我已经设法让它与 SSH 一起工作 但我想使用 https 而不是 SSH AWS 建议使用凭证助手 有谁知道如何配置 spring config
  • java 1.8下无法启动eclipse

    java 1 8 升级后我无法启动 eclipse 附上错误截图 这是我的 eclipse 配置设置 我该如何解决 startup plugins org eclipse equinox launcher 1 3 0 v20120522 1
  • 处理照片上传的最佳方式是什么?

    我正在为一个家庭成员的婚礼制作一个网站 他们要求的一个功能是一个照片部分 所有客人都可以在婚礼结束后前往并上传他们的照片 我说这是一个很棒的想法 然后我就去实现它 那么只有一个问题 物流 上传速度很慢 现代相机拍摄的照片很大 2 5 兆 我
  • 使用链接列表插入优先级队列的方法

    首先 我觉得我应该提到这是一项作业 我并不是在寻找直接的代码答案 只是为了指出正确的方向 我们被要求在链表中实现优先级队列 我正在努力编写 insert 函数的第一部分 在代码中我尝试检查是否head包含任何内容 如果没有则设置为head
  • Apache Kafka 是否提供异步订阅回调 API?

    我的项目正在将 Apache Kafka 视为老化的基于 JMS 的消息传递方法的潜在替代品 为了让这个过渡尽可能的顺利 如果替代的排队系统 Kafka 有一个异步订阅机制那就更理想了 类似于我们当前项目使用的JMS机制MessageLis
  • 如何使用 Nimbus LookAndFeel 更改 JToolTip 的背景颜色?

    在使用 Nimbus LookAndFeel 的基于 Swing 的 Java 应用程序中 我尝试设置工具提示的背景颜色 因此 我创建了 JToolTip 的子类 并通过重写 createToolTip 在我的组件中使用它 到目前为止一切正
  • 将带有时区的 Joda-Time `DateTime` 转换为没有时区的 DateTime?

    Given a DateTime http www joda org joda time apidocs org joda time DateTime html例如2015 07 09T05 10 00 02 00 using 乔达时间 h
  • 升级到 Tomcat 8 时出现 ClassNotFoundException

    我最近将 NetBeans IDE 从 v7 3 升级到 v8 突然我的应用程序在连接到数据库时在服务器启动时抛出异常 这两个版本的 IDE 之间的唯一区别是后者使用 Tomcat 8 异常日志 javax naming NamingExc

随机推荐

  • Debian10常用命令

    文章目录 1 常用文件路径 2 用户和组管理 3 网络配置 4 主机名修改 5 vim编辑器 6 文件的操作 7 关机命令 1 常用文件路径 网卡配置文件 etc network interfaces dns配置文件 etc resolv
  • IP多播概念和实现

    目录 基本概念 硬件多播 IGMP和多播路由选择协议 基本概念 IP多播 曾译为组播 的目的是更好地支持一对多通信 即一个源点发送到多个终点 例如实时信息的交付 新闻 股市行情 软件更新 交互式会议及其他多媒体通信 下图中的多播组有90个成
  • SQL基础 读书笔记 第一章

    SQL基础教程 笔记 第一章 表格的新建 修改和删除 1 DDL DML DCL DDL 数据定义语言 CREATE DROP ALTER ALTER 修改数据库和表等对象的结构 比如增加 修改约束条件 比如 修改字段属性 INT 改为CH
  • 以OpenGL/ES视角介绍gfx-hal(Vulkan) Shader/Program接口使用

    文档列表见 Rust 移动端跨平台复杂图形渲染项目开发系列总结 目录 背景 The right way to tackle this in Vulkan is to use resource descriptors A descriptor
  • java关于通用分页参数的传递方式

    1 第一种 通过一个基础类 基础类中是所有类共有的属性 把pageSize和pageNum都放在基础类中 其他类全部继承基础类 如下 QuestionActivitiQueryReq类继承BasePage类 BasePage类就是专门用来放
  • LVM逻辑卷与条带卷管理

    逻辑卷管理器LVM Logical Volume Manager 在硬盘分区和文件系统之间添加了一个逻辑层 提供了一个抽象的卷组 允许多块硬盘进行卷组合并 不必关注物理硬盘设备的底层架构和布局 实现对硬盘资源的动态调整 条带化逻辑卷在物理卷
  • 组网学习之可扩展性设计(一)

    可扩展性设计策略 可靠性 1 设计冗余链路 为了保证可靠性 问题 会有环路 生成广播风暴 利用树协议解决广播风暴 增加网络带宽 聚合链路 采用分层的网络协议 rip 小型网络 与ospf 大规模 area用来划区域 0为骨干层 采用ap 无
  • 关于Visual Studio编译scanf报错问题以及一劳永逸的解决方法!

    前言 相信很多使用vs编译器初学C语言的同学 刚开始都能碰到这个问题 当你在代码使用了scanf vs编译器就会报错 不管你是2013还是2022以及其他版本都会报错 如上图所示 注意 C语言当中本身scanf是不会错的 怎么解决呢 先来看
  • in _create_inference_session raise ValueError(“This ORT build has {} enabled

    问题 line 363 in create inference session raise ValueError This ORT build has enabled format available providers ValueErro
  • Linux宝塔面板搭建Discuz论坛, 并公网远程访问【内网穿透】

    文章目录 前言 1 安装基础环境 2 一键部署Discuz 3 安装cpolar工具 4 配置域名访问Discuz 5 固定域名公网地址 6 配置Discuz论坛 前言 Crossday Discuz Board 以下简称 Discuz 是
  • jupyter notebook导入模块时报错:ModuleNotFoundError: No module named

    背景 基于anaconda创建虚拟环境后 在虚拟环境中打开jupyter notebook 创建python文件在文件中导入模块时报错 原因 1 虚拟环境中未安装此模块 2 安装完模块后 没有添加核 kernel 解决办法 1 激活虚拟环境
  • IIS7解析漏洞复现

    首先我们在win7上安装IIS7 控制面板 程序 打开或关闭windows功能 勾选如下信息 尽量勾选多一点防止实验失败 点击确定 稍等 在windows7虚拟机安装phpstudy2018版本 可先在物理机下载压缩包 然后上传到虚拟机 然
  • 工程职业伦理_Mooc_2018_期末考试答案

    1 直接在浏览器中在此页面按下ctrl F打开搜索框 2 复制原题目的部分 全部 3 在此页面的搜索框中粘贴复制的内容 即可直接找到此题目 加粗字体为正确答案 直接复制过来的 格式不太好看 单选题 1 1分数 当工程风险产生的不利后果比较严
  • delphi listview动态添加图片_南通启东【长泰海滨城】售楼处电话-位置-价格-开盘时间-最新动态【官网】...

    南通启东 长泰海滨城 售楼处电话 位置 价格 开盘时间 最新动态 官网 2020 10 29 14 38 来源 搜狐焦点淮北站 原标题 南通启东 长泰海滨城 售楼处电话 位置 价格 开盘时间 最新动态 官网 南通启东 长泰海滨城 售楼中心
  • 05笔趣阁小说爬取--爬取作者所有小说

    前面的程序已经实现了从笔趣阁自动爬取整部小说 接下来在之前的基础上 将功能进一步扩展 实现自动爬取某一作者在笔趣阁中的所有小说 继续以方想为例 下图是方想作品列表的页面 https www 52bqg com modules article
  • 伺服怎么接单相220伏_220v电表怎么接线?如何接单相电表?普通家用电表怎么接线...

    以下文章来源于电工电气学习 作者 电工电气学习 单相有功电度表 简称 单相电度表 由接线端子 电流线圈 电压线圈 计量转盘 计数器构成 只要电流线圈通过电流 同时电压线圈加有电压 转盘就受到电磁力而转动 单相电度表共有5个接线端子 其中有两
  • 如何把桌面显示的文件都放在D盘

    1 改目录 具体如下 先在d盘建一个文件夹 然后点开始 运行 regedit 点击HKEY CURRENT USER Software Microsoft Windows CurrentVersion Explorer user Shell
  • leetcode 34. 在排序数组中查找元素的第一个和最后一个位置

    虽然只是一道中等难度的题目 但是看了官方的解答 有很精彩的地方 但是总体代码写得不算优雅 class Solution returns leftmost or rightmost index at which target should b
  • 使用 mock 模拟登录接口数据

    mock js模拟登录接口 第一步 打开apipost 通过mock服务来进行编写登录接口 选择为mock环境 第二步 编写自己的登录接口 apipost支持 req body对象 当 post 请求以 x www form urlenco
  • SpringSecurity+JWT认证流程解析

    楔子 本文适合 对Spring Security有一点了解或者跑过简单demo但是对整体运行流程不明白的同学 对SpringSecurity有兴趣的也可以当作你们的入门教程 示例代码中也有很多注释 大家在做系统的时候 一般做的第一个模块就是