一、spring security 简介

spring security 的核心功能主要包括:

  • 认证 (你是谁)
  • 授权 (你能干什么)
  • 攻击防护 (防止伪造身份)

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

比如,对于username password认证过滤器来说,

会检查是否是一个登录请求;

是否包含username 和 password (也就是该过滤器需要的一些认证信息) ;

如果不满足则放行给下一个。

下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic eHh4Onh4 的信息。中间可能还有更多的认证过滤器。最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。

注意:绿色的过滤器可以配置是否生效,其他的都不能控制。

二、入门项目

首先创建spring boot项目HelloSecurity,其pom主要依赖如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>

然后在src/main/resources/templates/目录下创建页面:

home.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><head><title>Spring Security Example</title></head><body><h1>Welcome!</h1>
​<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p></body>
</html>

我们可以看到, 在这个简单的视图中包含了一个链接: “/hello”. 链接到了如下的页面,Thymeleaf模板如下:

hello.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><head><title>Hello World!</title></head><body><h1>Hello world!</h1></body>
</html>

Web应用程序基于Spring MVC。 因此,你需要配置Spring MVC并设置视图控制器来暴露这些模板。 如下是一个典型的Spring MVC配置类。在src/main/java/hello目录下(所以java都在这里):

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/home").setViewName("home");registry.addViewController("/").setViewName("home");registry.addViewController("/hello").setViewName("hello");registry.addViewController("/login").setViewName("login");}
}

addViewControllers()方法(覆盖WebMvcConfigurerAdapter中同名的方法)添加了四个视图控制器。 两个视图控制器引用名称为“home”的视图(在home.html中定义),另一个引用名为“hello”的视图(在hello.html中定义)。 第四个视图控制器引用另一个名为“login”的视图。 将在下一部分中创建该视图。此时,可以跳过来使应用程序可执行并运行应用程序,而无需登录任何内容。然后启动程序如下:

@SpringBootApplication
public class Application {
​public static void main(String[] args) throws Throwable {SpringApplication.run(Application.class, args);}
}

2、加入Spring Security

假设你希望防止未经授权的用户访问“/ hello”。 此时,如果用户点击主页上的链接,他们会看到问候语,请求被没有被拦截。 你需要添加一个障碍,使得用户在看到该页面之前登录。您可以通过在应用程序中配置Spring Security来实现。 如果Spring Security在类路径上,则Spring Boot会使用“Basic认证”来自动保护所有HTTP端点。 同时,你可以进一步自定义安全设置。首先在pom文件中引入:

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

如下是安全配置,使得只有认证过的用户才可以访问到问候页面:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();}
​@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");}
}

WebSecurityConfig类使用了@EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还扩展了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。

configure(HttpSecurity)方法定义了哪些URL路径应该被保护,哪些不应该。具体来说,“/”和“/ home”路径被配置为不需要任何身份验证。所有其他路径必须经过身份验证。

当用户成功登录时,它们将被重定向到先前请求的需要身份认证的页面。有一个由 loginPage()指定的自定义“/登录”页面,每个人都可以查看它。

对于configureGlobal(AuthenticationManagerBuilder) 方法,它将单个用户设置在内存中。该用户的用户名为“user”,密码为“password”,角色为“USER”。

现在我们需要创建登录页面。前面我们已经配置了“login”的视图控制器,因此现在只需要创建登录页面即可:

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><head><title>Spring Security Example </title></head><body><div th:if="${param.error}">Invalid username and password.</div><div th:if="${param.logout}">You have been logged out.</div><form th:action="@{/login}" method="post"><div><label> User Name : <input type="text" name="username"/> </label></div><div><label> Password: <input type="password" name="password"/> </label></div><div><input type="submit" value="Sign In"/></div></form></body>
</html>

你可以看到,这个Thymeleaf模板只是提供一个表单来获取用户名和密码,并将它们提交到“/ login”。 根据配置,Spring Security提供了一个拦截该请求并验证用户的过滤器。 如果用户未通过认证,该页面将重定向到“/ login?error”,并在页面显示相应的错误消息。 注销成功后,我们的应用程序将发送到“/ login?logout”,我们的页面显示相应的登出成功消息。最后,我们需要向用户提供一个显示当前用户名和登出的方法。 更新hello.html 向当前用户打印一句hello,并包含一个“注销”表单,如下所示:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"><head><title>Hello World!</title></head><body><h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1><form th:action="@{/logout}" method="post"><input type="submit" value="Sign Out"/></form></body>
</html>

三、参数详解

1、注解 @EnableWebSecurity

在 Spring boot 应用中使用 Spring Security,用到了 @EnableWebSecurity注解,官方说明为,该注解和 @Configuration 注解一起使用, 注解 WebSecurityConfigurer 类型的类,或者利用@EnableWebSecurity 注解继承 WebSecurityConfigurerAdapter的类,这样就构成了 Spring Security 的配置。

2、抽象类 WebSecurityConfigurerAdapter

一般情况,会选择继承 WebSecurityConfigurerAdapter 类,其官方说明为:WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制。

3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)

Demo 中重写了 WebSecurityConfigurerAdapter 的两个方法:

   /*** 通过 {@link #authenticationManager()} 方法的默认实现尝试获取一个 {@link AuthenticationManager}.* 如果被复写, 应该使用{@link AuthenticationManagerBuilder} 来指定 {@link AuthenticationManager}.** 例如, 可以使用以下配置在内存中进行注册公开内存的身份验证{@link UserDetailsService}:** // 在内存中添加 user 和 admin 用户* @Override* protected void configure(AuthenticationManagerBuilder auth) {*     auth*       .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()*         .withUser("admin").password("password").roles("USER", "ADMIN");* }** // 将 UserDetailsService 显示为 Bean* @Bean* @Override* public UserDetailsService userDetailsServiceBean() throws Exception {*     return super.userDetailsServiceBean();* }**/protected void configure(AuthenticationManagerBuilder auth) throws Exception {this.disableLocalConfigureAuthenticationBldr = true;}/*** 复写这个方法来配置 {@link HttpSecurity}. * 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为:* * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();**/protected void configure(HttpSecurity http) throws Exception {logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
​http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();}

4、final 类 HttpSecurity

HttpSecurity 常用方法及说明:

方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应
cors() 配置跨域资源共享( CORS )
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterAt() 在指定的Filter类的位置添加过滤器

5、类 AuthenticationManagerBuilder

/**
* {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for
* easily building in memory authentication, LDAP authentication, JDBC based
* authentication, adding {@link UserDetailsService}, and adding
* {@link AuthenticationProvider}'s.
*/

意思是,AuthenticationManagerBuilder 用于创建一个 AuthenticationManager,让我能够轻松的实现内存验证、LADP验证、基于JDBC的验证、添加UserDetailsService、添加AuthenticationProvider。

四、原理讲解

1、校验流程图

2、源码分析

  • AbstractAuthenticationProcessingFilter 抽象类
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
​HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;
​if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);
​return;}
​if (logger.isDebugEnabled()) {logger.debug("Request is to process authentication");}
​Authentication authResult;
​try {authResult = attemptAuthentication(request, response);if (authResult == null) {// return immediately as subclass has indicated that it hasn't completed// authenticationreturn;}sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {logger.error("An internal error occurred while trying to authenticate the user.",failed);unsuccessfulAuthentication(request, response, failed);
​return;}catch (AuthenticationException failed) {// Authentication failedunsuccessfulAuthentication(request, response, failed);
​return;}
​// Authentication successif (continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}
​successfulAuthentication(request, response, chain, authResult);}

调用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。如果需要验证,则会调用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三种结果:

  1. 返回一个 Authentication 对象。配置的 SessionAuthenticationStrategy` 将被调用,然后 然后调用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
  2. 验证时发生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法将被调用。
  3. 返回Null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
  • UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子类)
public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}
​String username = obtainUsername(request);String password = obtainPassword(request);
​if (username == null) {username = "";}
​if (password == null) {password = "";}
​username = username.trim();
​UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
​// Allow subclasses to set the "details" propertysetDetails(request, authRequest);
​return this.getAuthenticationManager().authenticate(authRequest);}

attemptAuthentication () 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证(即 this.getAuthenticationManager().authenticate(authRequest) )。默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。

  • ProviderManager(AuthenticationManager的实现类)
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;Authentication result = null;boolean debug = logger.isDebugEnabled();
​for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}
​if (debug) {logger.debug("Authentication attempt using "+ provider.getClass().getName());}
​try {result = provider.authenticate(authentication);
​if (result != null) {copyDetails(authentication, result);break;}}catch (AccountStatusException e) {prepareException(e, authentication);// SEC-546: Avoid polling additional providers if auth failure is due to// invalid account statusthrow e;}catch (InternalAuthenticationServiceException e) {prepareException(e, authentication);throw e;}catch (AuthenticationException e) {lastException = e;}}
​if (result == null && parent != null) {// Allow the parent to try.try {result = parent.authenticate(authentication);}catch (ProviderNotFoundException e) {// ignore as we will throw below if no other exception occurred prior to// calling parent and the parent// may throw ProviderNotFound even though a provider in the child already// handled the request}catch (AuthenticationException e) {lastException = e;}}
​if (result != null) {if (eraseCredentialsAfterAuthentication&& (result instanceof CredentialsContainer)) {// Authentication is complete. Remove credentials and other secret data// from authentication((CredentialsContainer) result).eraseCredentials();}
​eventPublisher.publishAuthenticationSuccess(result);return result;}
​// Parent was null, or didn't authenticate (or throw an exception).
​if (lastException == null) {lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",new Object[] { toTest.getName() },"No AuthenticationProvider found for {0}"));}
​prepareException(lastException, authentication);
​throw lastException;
}

尝试验证 Authentication 对象。AuthenticationProvider 列表将被连续尝试,直到 AuthenticationProvider 表示它能够认证传递的过来的Authentication 对象。然后将使用该 AuthenticationProvider 尝试身份验证。如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。从代码中不难看出,由 provider 来验证 authentication, 核心点方法是:

Authentication result = provider.authenticate(authentication);

此处的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的实现,看看它的 authenticate(authentication) 方法:

// 验证 authentication
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));
​// Determine usernameString username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
​boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);
​if (user == null) {cacheWasUsed = false;
​try {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);}catch (AuthenticationException exception) {if (cacheWasUsed) {// There was a problem, so try again after checking// we're using latest data (i.e. not from the cache)cacheWasUsed = false;user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}else {throw exception;}}
​postAuthenticationChecks.check(user);
​if (!cacheWasUsed) {this.userCache.putUserInCache(user);}
​Object principalToReturn = user;
​if (forcePrincipalAsString) {principalToReturn = user.getUsername();}
​return createSuccessAuthentication(principalToReturn, authentication, user);}

AbstractUserDetailsAuthenticationProvider 内置了缓存机制,从缓存中获取不到的 UserDetails 信息的话,就调用如下方法获取用户信息,然后和 用户传来的信息进行对比来判断是否验证成功。

// 获取用户信息
UserDetails user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);

retrieveUser() 方法在 DaoAuthenticationProvider 中实现,DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子类。具体实现如下:

protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {UserDetails loadedUser;
​try {loadedUser = this.getUserDetailsService().loadUserByUsername(username);}catch (UsernameNotFoundException notFound) {if (authentication.getCredentials() != null) {String presentedPassword = authentication.getCredentials().toString();passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,presentedPassword, null);}throw notFound;}catch (Exception repositoryProblem) {throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);}
​if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}

可以看到此处的返回对象 userDetails 是由 UserDetailsService 的 #loadUserByUsername(username) 来获取的。

对于看本文比较困难的同学可以移步:https://www.lanqiao.cn/courses/3013

邀请码:STyLDQzM

其实内容和博客差不多,只不过更详细(就是一步一步的来,所有步骤有详细过程记录和截图,按照课程步骤最终能完整的操作完整个项目过程,适合小白),版本上也有所升级,还有就是有问题可以直接沟通,谢谢支持!

spring security——基本介绍(一)相关推荐

  1. Spring Security 基本介绍,初窥路径

    Spring Security 基本介绍和环境搭建 实验介绍 Spring Security 是一个非常强大的身份验证和授权控制框架.为了满足企业项目的不同需求,它提供了很多定制化开发的解决方案,通过 ...

  2. 翼支付门户架构之Spring Security框架介绍

    Spring Security3,其前身是"spring的acegi安全系统". 先来谈一谈Acegi的基础知识,Acegi的架构比较复杂.如果对Web资源进行保护,最好的办法莫过 ...

  3. 【Spring Security】基本功能介绍

    文章目录 1.spring security 简介 spring security 基本原理 2 入门项目 2.1 web工程配置 2.1 加入Spring Security 3. 参数详解 3.1. ...

  4. Spring Security:身份验证入口AuthenticationEntryPoint介绍与Debug分析

    ExceptionTranslationFilter ExceptionTranslationFilter(Security Filter)允许将AccessDeniedException和Authe ...

  5. SpringBoot + Spring Security 简单入门

    这篇文章主要介绍了SpringBoot + Spring Security 简单入门 Spring Security 基本介绍 这里就不对Spring Security进行过多的介绍了,具体的可以参考 ...

  6. SpringBoot 整合Spring Security(简单版)

    1 写在前面 关于spring security的介绍,网上一大堆,这里就不介绍了,这里直接使用springboot开始整合 2 整个流程 spring security授权和认证的流程大致和shir ...

  7. Spring Security简单的登陆验证授权

    Spring Security的介绍就省略了,直接记录一下登陆验证授权的过程. Spring Security的几个重要词 1.SecurityContextHolder:是安全上下文容器,可以在此得 ...

  8. Spring Security——OAuth2.0

    目录 1.OAuth2.0介绍 1.1.概念说明 1.2.使用场景 1.3.OAuth2.0中的四种授权方式 1.3.1.授权码模式(authorization code) 1.3.2.简化模式(im ...

  9. Spring Security入门(一) 导学与开发环境安装

    一.导学 课程目标 深入理解Spring Security及相关框架的原理.功能和代码 可以机遇Spring Security及相关框架独立开发认证授权相关功能 掌握抽象和封装的常见技巧,可以编写可重 ...

最新文章

  1. 3dmax2020安装失败原因及解决方法_电伴热带安装维护,线路断路器跳闸的原因及解决方法...
  2. Oracle RAC删除节点
  3. Android判断Service是否运行
  4. PHP生成唯一订单号
  5. docker之Dockerfile指令介绍
  6. 论基于candence的组装清单做法
  7. mycli mysql_MyCLI :易于使用的 MySQL/MariaDB 客户端
  8. 目标检测近5年发展历程概述(转)
  9. js数组获取index_通过事例重温一下常见的 JS 中 15 种数组操作(备忘清单)
  10. 重学数据结构——快速排序,二分法查找
  11. nim3取石子游戏 (威佐夫博弈)
  12. NYOJ954--N的阶乘的二进制表示最低位的1的位置
  13. 游戏代练平台源码打包+搭建教程
  14. C++经典程序代码大全
  15. Ubuntu server树莓派版本默认用户名密码及密码修改
  16. Pr 入门教程如何减少音频中的噪音和混响?
  17. windows server backup功能
  18. 三菱Q系列PLC数据采集随笔
  19. Facebook广告投放有哪些比较好的策略?
  20. 天耀18期 – 6.面向对象-类和对象【作业】.

热门文章

  1. 年薪都是二十多万,选华为还是选特斯拉
  2. Nokia E71: 3G, 3M pixels CMOS, Wi-Fi, AGPS, BT v2.0, QWERTY, microSD
  3. python利用pyperclip 模块使用计算机的剪贴板
  4. java monitor 用法_[作业解析]并发中的Monitor及在Java中的运用如notify(),synchronized等用法...
  5. mysql数据库导入操作_MySQL数据库的导入方法
  6. 平行志愿计算机检索原理,平行志愿有先后顺序吗怎样检索的
  7. js:面向对象编程,带你认识封装、继承和多态
  8. SpringMVC producesheadersconsumes
  9. 注解RequestMapping中produces属性
  10. 永生生物_指导:通往永生的道路