本文来说下AuthenticationManager验证原理

文章目录

  • AuthenticationManager概述
  • AuthenticationManager相关类图
  • security认证流程
  • AuthenticationManager初始化流程
  • AuthenticationManager的认证过程
  • AuthenticationMananger与ProviderMananger
  • AuthenticationProvider视角中的Authentication
  • DaoAuthenticationProvider
    • 使用UserDetailsService获取内部用户身份信息
    • 使用PasswordEncoder来进行密码的比对
  • 本文小结

AuthenticationManager概述

UsernamePasswordAuthenticationFilter源码分析 中,最后在类UsernamePasswordAuthenticationFilter 的验证方法 attemptAuthentication() 会将用户表单提交过来的用户名和密码封装成对象,委托给类 AuthenticationManager 的验证方法 authenticate() 进行身份验证。用户身份有关的信息UserDetails对象就是由AuthenticationManager认证管理器来管理的。


我们要做的,就是继承一个 UserDetailSevice 接口并且加入到容器中,他就是连接我们数据库和 Spring Security 的桥梁

UserDetailService 的要求也很简单,只需要重写 loadUserByUsername(String username) 方法,里面的逻辑通常是从数据库查找出对应用户名的密码然后构造一个org.springframework.security.core.userdetails.UserDetails对象,Spring Security 会根据返回的这个带有正确用户信息的对象和前台传过来的用户名密码进行比对来判断是否认证通过

因此,怎么设计数据库都可以,不管我们是用一个表还是两个表还是三个表,也不管我们是用户-授权,还是用户-角色-授权,还是用户-用户组-角色-授权,这些具体的东西 Spring Security 统统不关心,它只关心返回的那个User对象,至于怎么从数据库中读取数据,那就是我们自己的事了。

那么本文主要对 AuthenticationManager 的验证方法 authenticate() 验证原理进行源码分析。


AuthenticationManager相关类图

AuthenticationManager 验证过程涉及到的类和接口较多,先用一张类图说明各个类和接口之间的关系,如下

核心类和接口说明

  • AuthenticationManager 为认证管理接口类,其定义了认证方法 authenticate()。
  • ProviderManager 为认证管理类,实现了接口 AuthenticationManager ,并在认证方法 authenticate() 中将身份认证委托给具有认证资格的 AuthenticationProvider 进行身份认证。ProviderManager中的成员变量 providers [List] 存储了一个 AuthenticationProvider 类型的 List。
  • AuthenticationProvider 为认证接口类,其定义了身份认证方法 authenticate()。
  • AbstractUserDetailsAuthenticationProvider 为认证抽象类,实现了接口 AuthenticationProvider 定义的认证方法 authenticate()。AbstractUserDetailsAuthenticationProvider 还定义了虚拟方法 retrieveUser() 用作查询数据库用户信息,以及虚拟方法 additionalAuthenticationChecks() 用作身份认证。
  • DaoAuthenticationProvider 继承自类 AbstractUserDetailsAuthenticationProvider,实现该类的方法 retrieveUser() 和 additionalAuthenticationChecks()。DaoAuthenticationProvider 中还具有成员变量 userDetailsService [UserDetailsService] 用作用户信息查询,以及成员变量 passwordEncoder [PasswordEncoder] 用作密码的加密及验证。

security认证流程

本质上讲,Spring Security 是通过过滤器(Filter)和面向切面编程(AOP)实现应用安全控制。Spring Security 中定义和使用了很多的过滤器,针对认证过程重点讲解:AbstractAuthenticationProcessingFilter。AbstractAuthenticationProcessingFilter 用于拦截认证请求,它是基于浏览器和 HTTP 认证请求的处理器,可以理解为它就是 Spring Security 认证流程的入口。

整个认证流程如下

① AbstractAuthenticationProcessingFilter 收集用于认证的用户身份信息(通常是用户名和密码),并基于这些信息构造一个 Authentication请求对象,AbstractAuthenticationProcessingFilter 只是一个虚类,查看 Spring Security API 文档 可以看到 Spring Security 提供了几个实现类:

  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • OpenIDAuthenticationFilter
  • UsernamePasswordAuthenticationFilter

最常使用的应该是 UsernamePasswordAuthenticationFilter,其它类都应用于特定的场景。

② AbstractAuthenticationProcessingFilter 类将构造的 Authentication 请求对象呈现给 AuthenticationManager,AbstractAuthenticationProcessingFilter 类有以下方法设置和获取 AuthenticationManager:

protected AuthenticationManager getAuthenticationManager()public void setAuthenticationManager(AuthenticationManager authenticationManager)

③ AuthenticationManager 只是一个接口,Spring Security 提供了一个默认实现 ProviderManager。ProviderManager 在接收到 AbstractAuthenticationProcessingFilter 传递过来的 Authentication 请求对象后并不会执行认证处理,它持有一个 AuthenticationProvider 的列表,ProviderManager 委托列表中的 AuthenticationProvider 处理认证请求;

④ AuthenticationProvider 也只是接口,Spring Security 提供了很多此接口的实现,如 DaoAuthenticationProvider、LdapAuthenticationProvider、JaasAuthenticationProvider 等,现在暂时不关心这些具体实现。列表中的 AuthenticationProvider 会依次对 Authentication 请求对象进行认证处理,如果认证通过则返回一个完全填充的 Authentication 对象(后面会解释什么是“完全填充”),如果认证不通过则抛出一个异常(注意对抛出的异常有类型要求)或直接返回 null。如果列表中的所有 AuthenticationProvider 都返回 null,则 ProviderManager 会抛出 ProviderNotFoundException 异常;

⑤ 认证通过后 AuthenticationProvider 返回完全填充的 Authentication 对象给 ProviderManager,ProviderManager 继续向上返回给 AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter 会继续返回。

⑥ Spring Security 的“authentication mechanism”在接收到一个完全填充的 Authentication 对象返回后会认定认证请求有效,并将此 Authentication 对象放入 SecurityContextHolder

SecurityContextHolder 是 Spring Security 最基础的对象,用于存储应用程序当前安全上下文的详细信息,这些信息后续会被用于授权

至此,Spring Security 的认证流程已介绍完毕,但还缺少对两个十分常见的接口的说明:UserDetails 和 UserDetailsService

Spring Security API 文档 对 UserDetails 的说明如下

Implementations are not used directly by Spring Security for security purposes. They simply store user information which is later encapsulated into Authentication objects. This allows non-security related user information (such as email addresses, telephone numbers etc) to be stored in a convenient location.

从中可以看出 UserDetails 只是用于存储用户信息并最终封装到 Authentication 对象中。

Spring Security API 文档 对 UserDetailsService 的说明如下

Core interface which loads user-specific data.It is used throughout the framework as a user DAO and is the strategy used by the DaoAuthenticationProvider.The interface requires only one read-only method, which simplifies support for new data-access strategies.

从中可以看出 UserDetailsService 只定义了一个只读方法,返回一个 UserDetails 接口对象。

UserDetails loadUserByUsername(String username)

实际上,UserDetailsService 和 UserDetails 只是构造 Authentication 对象的一个过程。UserDetailsService 可以作为 AuthenticationProvider 的一个属性,在 AuthenticationProvider 执行请求认证时调用 UserDetailsService 的 loadUserByUsername 方法返回一个 UserDetails 对象,并使用此 UserDetails 对象封装最终的 Authentication 对象,事实上这也就是 Spring Security 预置的一些AuthenticationProvider 实现类使用的方法,如 DaoAuthenticationProvider,可以查看 Spring Security API 文档 进一步了解 DaoAuthenticationProvider 实现细节。

在大多数情况下,如果需要定制认证过程,建议直接实现 AuthenticationProvider,这样做更有意义,在 AuthenticationProvider 的 authenticate 方法中直接封装 Authentication 对象,这比引用一个 UserDetailsService 实现返回一个 UserDetails 对象后再封装成最终的 Authentication 对象更直观。


AuthenticationManager初始化流程

AuthenticationManager这个接口方法非常奇特,入参和返回值的类型都是Authentication。该接口的作用是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常AuthenticationException。

AuthenticationManager源码

package org.springframework.security.authentication;import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;public interface AuthenticationManager {Authentication authenticate(Authentication var1) throws AuthenticationException;
}

那么AbstractAuthenticationProcessingFilter中的 AuthenticationManager 是在哪里配置的呢? 看过Spring Security 实战干货系列应该知道WebSecurityConfigurerAdapter中的void configure(AuthenticationManagerBuilder auth)是配置AuthenticationManager 的地方, 我根据源码总结了一下AuthenticationManager 的初始化流程,相信可以帮助你去阅读相关的源码:

如何配置AuthenticationManager

  /*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}/*** 身份认证接口*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}

AuthenticationManager的认证过程

AuthenticationManager的实现ProviderManager管理了众多的AuthenticationProvider。每一个AuthenticationProvider都只支持特定类型的Authentication,如果不支持将会跳过。另一个作用就是对适配的Authentication进行认证,只要有一个认证成功,那么就认为认证成功,所有的都没有通过才认为是认证失败。认证成功后的Authentication就变成授信凭据,并触发认证成功的事件。认证失败的就抛出异常触发认证失败的事件。


从这里我们可以看出认证管理器AuthenticationManager针对特定的Authentication提供了特定的认证功能,我们可以借此来实现多种认证并存

AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。这样一来四不四就好理解多了?

如何从AuthenticationManager获得用户的数据信息,核心代码


// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));//获得用户信息,即UserDetails对象
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

AuthenticationMananger与ProviderMananger

AuthenticationMananger作为整个身份验证核心最外层的封装负责与外部使用者进行交互。 AuthenticationMananger接口有且仅有一个对外的服务便是“身份验证”。这样是整个身份验证服务对外提供的服务接口。

外部使用者通过将身份验证的必要信息,比如用户名和密码封装一个Authentication传递、调用AuthenticationMananger的authenticate方法。如果没有返回异常和null值,那么验证服务便是完成。完成身份验证的Authentication不仅包含了用户的身份验证信息,比如用户名,额外还会将该用户身份下所有对应的权限列表也一并封装返回。

身份信息交互的纽带:Authentication

在整个与外部使用交互的过程中Authentication的职责有两个,第一个是封装了验证请求的参数,第二个便是封装了用户的权限信息。结合Authentication的接口设计便更加清晰了这样的设计意图:principal用于存放用户的身份标识信息,比如用户名,credentials用于存放用户的验证凭证比如密码,authorities用于存放用户的权限列表。而details则存放了除了用户名和密码其他可能会被用于身份验证的信息,比如应用限定用户的使用ip范围场景下,ip信息可能便会被存放在details做辅助的验证信息使用。

唯一的实现类:ProviderMananger

为了向外部提供身份验证服务,Spring Security中通过ProviderMananger实现了AuthenticationManager的身份验证接口。作为实现类ProviderMananger便不能和AuthenticationManager一样只关心唯一的抽象核心服务authenticate。在ProviderMananger为了管理外部输入与像外部返回的Authentication,ProviderMananger内部大致的工序如下:

  • 首先,寻找可以进行验证当前外部输入Authentication形式的AuthenticationProvider;如果自身的providers中无法处理验证并且当前层次的Mananger还有父级的Mananger则向上传递,交由父层Mananger进行处理;
  • 然后,因为details的信息是外部传入的,内部身份验证后的Authentication并不会从持久化或者其他数据源中携带,在返回前将details写入返回给外部的Authentication;
  • 最后,如果有必要则将外部身份验证请求中的敏感擦除,比如讲请求验证的密码置空。

了解了ProviderMananger完成的三件工作,大致明白了虽然整个验证框架只有一个ProviderManager暴露在外部,但是其内部可能是有多个AuthenticationMananger和AuthenticationProvider组成的网络,并且最终进行核心身份验证的还是AuthenticationProvider。核心在叶子节点中依次寻找对验证当前Authentication形式的AuthenticationProvider。如果存在支持便将验证请求的Authentication传递给AuthenticationProvider,委托其进行验证。在处理输入的验证请求Authentication,ProviderMananger并不对其进行任何的处理,而是指在处理完后进行必要的加工和处理。


AuthenticationProvider视角中的Authentication

相对AuthenticationMananger而言AuthenticationProvider的工作更加明确:针对特定的验证数据,提供特定的验证行为。在这个语境下,Authentication的设计目的是解决验证什么(What)的问题,而authenticate方法更像是在回答怎么验证的问题(How)。

那么我们先对验证数据也就是Authentication的设计进行展开讨论。Authentication主要职责就是封装身份验证时候需要的信息数据,比如用户名场景下的用户名和密码,短信验证码下的手机号码和验证码,OAuth2场景下的ID和Code。总之每个不同验证协议使用的验证信息都需要被被封装成Authentication,更准确说在Spring Security把这种封装了用户身份验证信息的Authentication具体为了AuthenticationToken的概念,毕竟一说token更容易理解。所有Spring Security中提供的各种协议的身份验证数据的封装都继承AbstractAuthenticationToken

  • 基于用户名和密码的UsernamePasswordAuthenticationToken
  • 基于OAuth2的OAuth2AuthorizationCodeAuthenticationToken。
  • 基于CAS的CasAssertionAuthenticationToken。

通常我们使用用户名和密码的场景是最多,无论是使用基于数据库持久化的用户名密码方案还是基于LDAP的用户名和密码方法。虽然验证在验证协实现有细微差别,但是无论使用验证LDAP还是数据库进行身份验证比对,因为用户提交的验证身份信息几乎一致,我们便可以复用通用结构的AuthenticationToken——将username赋值到principal属性并将password赋值到cencredentials属性中。这样就意味着我们在AuthenticationToken设计上最需要考虑是数据的封装,而不是身份验证行为的实现。

我们已经解决了第一个问题,在AuthenticationProvider验证数据放都可以通过传入Authentication的各种实现类AuthenticationToken进行获取。下一个问题便是AuthenticationProvider是如何进行身份验证的。我们假设的场景是需要对使用用户名和密码的UsernamePasswordAuthenticationToken进行验证。在Spring Security针对UsernamePasswordAuthenticationToken进行身份验证的有主要有两个AuthenticationProvider:一个是基于Dao模型与数据层用户信息对比验证的DaoAuthenticationProvider,另外一个是虽然同样使用用户名和密码,但是验证流程更加复杂,且用户数据是通过与LDAP服务进行用户验证的LdapAuthenticationProvider。在这里使用最广泛使用的基于Dao的DaoAuthenticationProvider进行说明,如果有对Ldap实现有兴趣的相信在看完对DaoAuthenticationProvider的分析之后再阅读LdapAuthenticationProvider部分的代码就会轻松许多。


DaoAuthenticationProvider

DaoAuthenticationProvider为了实现外部的验证请求便需要对外部传递身份信息——用户名和密码进行验证。我们把这个任务进一步分解成两个独立的任务:

  • 从数据层获取对应用户名在数据层的数据记录;
  • 对外部的用户名、密码与数据层的用户名、密码进行比对。

为什么在DaoAuthenticationProvider会将验证任务再分解成这两个独立任务,最大原因便是,这两个任务一个感知外部资源,另一个感知验证算法,两种都是不同用户可能存在不同的使用场景,框架并无法控制具体的实现。第一个任务,从数据层获取对应用户名在数据层的数据记录,我们的目标是从数据层中查找到我们需要比对的用户身份数据,但是在这个场景下我们无非控制的是数据层的实现具体是什么?是通过JDBC访问Mysql还是通过JPA访问Oracle,更或是直接通过内存访问一个存储了用户信息键值对的Map?第二个任务,对外部的用户名、密码与数据层的用户名、密码进行比对,具体的加密算法是什么?如何实现的?
这两个问题在DaoAuthenticationProvider中都无法给出明确的实现。Spring Security便将这两种在DaoAuthenticationProvider无法确定、存在变化的行为分别委托给了DaoAuthenticationProvider两个重要组件去完成:

  • 通过用户名返回数据层中的用户信息的UserDetailsService;
  • 通过特定加密算法处理用户密码的PasswordEncoder。


使用UserDetailsService获取内部用户身份信息

UserDetailsService接口定位从他的接口方法就可以明白,就是向核心组件们提供数据层的用户信息,而用户信息在这里被封装成了UserDetails。


UserDetailsService中只有一个方法便是loadUserByUsername方法,通过传入用户名返回数据层的用户身份记录。


UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

当我们确定我们获取用户身份信息的方法之后,我们便可以自行扩展UserDetailsService方法,告知框架如何获取用户身份信息。通常这个步骤是使用Spring Security中是必须完成的工作。我们在第一个章节中曾经写过以下代码用于配置我们使用的UserDetailsService:

public class WebSecurityConfig  extends WebSecurityConfigurerAdapter {//注入新的UserDetailsServiceBean@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();return manager;}
}

那一次我们使用了基于内存键值对的形式来存储和获取用户信息记录。同样的我们也可以通过JDBC和JPA来获取用户身份信息,而Spring Security很贴心的已经提供了一个基于JDBC的UserServiceDetails实现和对应的模板DDL:

而通过UserDetailsService返回的数据类型是UserDetails,UserDetails中封装了用户名、密码和授权信息同时还额外包括了一些过期和锁定的标识属性。我们不难发现UserDetails封装的数据和Authentication非常的相似。没错,在身份验证成功后,DaoAuthenticationProvider便会将内部的UserDetails抽离必要的数据对应赋值到UsernamePasswordAuthenticationToken最终返回给外部调用者进行使用。我们可以简单的把UserDetails理解为用户身份信息在数据层的封装。在客制化的过程中,如果用户信息的数据结构是比较特殊的结构,比如Ldap,那么便可以自行扩展UserDetails客制化一个特殊的结构用于获取用户数据记录。
说到这里基本上我们已经了解了DaoAuthenticationProvider两个组件中用于获取用户身份记录的UserDetailsService部分,下面我们介绍下处理密码验证的PasswordEncoder部分。


使用PasswordEncoder来进行密码的比对

我们继续追踪上面的场景来说下关于验证算法部分。DaoAuthenticationProvider收到了外部提交的用户名和密码,同样的DaoAuthenticationProvider也查找到了对应用户名在数据库中的用户名和密码。通常情况下虽然都是密码,数据库中存储的密码通常会进行过一定的加密。DaoAuthenticationProvider便需要将外部提交的用户名和密码进行一次加密流程并进行比对。举个例子我们当前使用的算法比如是MD5,加密明文的样本是用户的用户名拼接用户名。如有当前需要身份验证的请求中用户名是admin,密码是password。同样的在数据库中加密后的密码是9b02edfbc208a538。我们便需要对外部传递的用户名和密码做一个MD5(“passwordadmin”)得到9b02edfbc208a538,再与数据库中的password字段进行对比,如果一致则认定验证成功。
在Spring Security中这种针对处理称为PasswordEncoder,PasswordEncoder接口主要的作用就是对明文密码进行加密与比对。



Spring Security中默认向DaoAuthenticationProvider提供的PasswordEncoder是BCryptPasswordEncoder。如果对BCrypt可以额外通过谷歌去了解加密流程。如果我们需要客制化自己的加密算法,只要实现PasswordEncoder接口,并重新通过Spring注入DaoAuthenticationProvider便可以了。

@Bean
public PasswordEncoder passwordEncoder() {//通过修改注入的实例,客制化自己的PasswordEncoderreturn new BCryptPasswordEncoder();
}

PasswordEncoder 部分的功能相对较少,通常情况下使用默认提供的BCryptPasswordEncoder就足够完成任务。


本文小结

在本期我们花了很大篇幅介绍了身份验证核心中最主要个几个组件和基于一个使用用户名和密码验证场景下对应接口的实现类的具体职责:

  1. AuthenticationManager负责核心验证前后的处理,并且负责与外部调用者进行交互;
  2. AuthenticationProvider是验证服务的核心实现,其验证的数据形式是AuthenticationToken其中封装了验证使用的用户标识和用户验证凭证信息;
  3. AuthenticationProvider中获取内部用户身份信息是通过UserDetailsService完成的。如需要进行密码处理,则引入了PasswordEncoder;
  4. UserDetails封装了用户信息在内部的结构,在向外部返回Authentication之前,AuthenticationProvider通常会将UserDetails必要的数据复制到向外部返回的AuthenticationToken中。

通过几期的说明,整个Spring Security关于身份验证的组件、流程和特定场景的实现基本我们都了解了一遍。从下一期开始,我们会开始讲解访问控制部分、框架配置部分的设计与概念。同时也将不定期通过一些场景的实战强相关框架概念和设计理念。

AuthenticationManager验证原理分析相关推荐

  1. oracle access manager token,AuthenticationManager验证原理

    AuthenticationManager相关类图 AuthenticationManager验证过程 AuthenticationManager验证过程涉及到的类和接口较多,我们就从这里开始逐一分析 ...

  2. 安全扫描失败无法上传_Apache Solr 未授权上传(RCE)漏洞的原理分析与验证

    漏洞简介 Apache Solr 发布公告,旧版本的ConfigSet API 中存在未授权上传漏洞风险,被利用可能导致 RCE (远程代码执行). 受影响的版本: Apache Solr6.6.0 ...

  3. Apache Solr 未授权上传(RCE)漏洞(CVE-2020-13957)的原理分析与验证

     聚焦源代码安全,网罗国内外最新资讯! 01 漏洞简介 Apache Solr 发布公告,旧版本的ConfigSet API 中存在未授权上传漏洞风险,被利用可能导致 RCE (远程代码执行). 受影 ...

  4. AD时间同步原理分析

    AD时间同步原理分析 对于加入域环境的客户端是与在父域中的权威服务器进行时间同步的.默认的同步时间的方法就是使用域层次,客户端会使用其所连接域中的域控来同步时间,而域控会反过来从整个林中的权威时间源来 ...

  5. HTTPS协议原理分析

    HTTPS协议原理分析 HTTPS协议需要解决的问题 HTTPS作为安全协议而诞生,那么就不得不面对以下两大安全问题: 身份验证 确保通信双方身份的真实性.直白一些,A希望与B通信,A如何确认B的身份 ...

  6. MyBatis版本升级引发的线上告警回顾及原理分析

    本文从一次MyBatis版本升级引发的线上告警开始讲起,然后针对告警定位过程.源码原理进行了深入的分析,并加入了不同版本的类比分析,最后结合实际工作做了一些经验总结,希望能对大家的工程实践有一定的帮助 ...

  7. Java NIO使用及原理分析

    http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一些Java方面的开发,其中最重要的一块 ...

  8. Java NIO使用及原理分析(二)

    2019独角兽企业重金招聘Python工程师标准>>> 转载自:李会军•宁静致远 在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是 ...

  9. AbstractQueuedSynchronizer的介绍和原理分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

最新文章

  1. 面试官:为什么 SpringBoot 的 jar 可以直接运行?
  2. linux下openssl编程
  3. 宝塔环境下配置PM2+NODE+VUE+WEBPACK环境
  4. .NET Framework 4.0源代码
  5. LeetCode 1249. 移除无效的括号(栈+set / deque)
  6. iOS5中 UIViewController新方法的使用
  7. JEECG V3版本组合查询实现方法
  8. 类的定义 java 1613806383
  9. centos安装apache_实战Linux部署Apache
  10. 数据结构研究 ----单链表的按序号查找
  11. python画猫咪老师_夏目友人帐 | 绘画小白怎样用水彩画一只圆滚滚的猫咪老师?...
  12. 读书感受 之 《好好说话2》
  13. Power Apps 应用实战|轻松用 Power Apps 开发员工休假考勤管理小程序
  14. bat脚本设置系统环境变量即时生效
  15. “辶”“廴”偏旁的字五笔拆字
  16. Linux系统安全加固指南(万字长文)
  17. 【渝粤题库】国家开放大学2021春2776兽医基础题目
  18. jetson-nano环境查询
  19. HTTP中的常用方法
  20. C#问题——CS1591 缺少对公共可见类型或成员的 XML 注释

热门文章

  1. 当大数据遇上“智慧园区”会怎样?
  2. spy-debugger 前端调试工具
  3. macosx下apache的默认用户为daemon
  4. 设计模式之禅之设计模式-组合模式
  5. [IOS地图开发系类]5、改变大头针MKPinAnnotationView的颜色
  6. oracle 数据库启动停止小结
  7. Express+Socket.IO 实现简易聊天室
  8. iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 中间件用法
  9. 路由器LED闪灯泄露数据
  10. MySQL性能调优my.cnf详解