在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的认证过滤器和认证处理器实现短信认证。

短信验证码认证

验证码对象类设计

和图片验证码一样,需要自己封装一个验证码对象,用来生成手机验证码并发送给手机。因为图片验证码和手机验证码对象的区别就在于前者多了个图片对象,所以两者共同部分抽象出来可以设计成一个ValidateCode类,这个类里面只存放验证码和过期时间,短信验证码直接使用这个类即可:

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class ValidateCode { private String code; private LocalDateTime expireTime; public ValidateCode(String code, int expireIn){
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
} public boolean isExpried() {
return LocalDateTime.now().isAfter(getExpireTime());
} public ValidateCode(String code, LocalDateTime expireTime) {
super();
this.code = code;
this.expireTime = expireTime;
}
}

图片验证码承继此类:

import java.awt.image.BufferedImage;
import java.time.LocalDateTime; import org.woodwhales.king.validate.code.ValidateCode; import lombok.Data;
import lombok.EqualsAndHashCode; @Data
@EqualsAndHashCode(callSuper=false)
public class ImageCode extends ValidateCode { private BufferedImage image; public ImageCode(BufferedImage image, String code, int expireId) {
super(code, LocalDateTime.now().plusSeconds(expireId));
this.image = image;
} public ImageCode(BufferedImage image, String code, LocalDateTime localDateTime) {
super(code, localDateTime);
this.image = image;
} }

验证码生成类设计

由于图片和短信类均可以生成相应的验证码,所以直接设计一个验证码生成接口,具体实现类根据业务进行实现:

import org.springframework.web.context.request.ServletWebRequest;

public interface ValidateCodeGenerator {

	ValidateCode generate(ServletWebRequest request);

}

这里的传参设计成了ServletWebRequest是能够根据前端请求中的参数进行不同的业务实现

目前实现累只有图片生成器和验证码生成器:

// 图片验证码生成器
@Component("imageCodeGenerator")
public class ImageCodeGenerator implements ValidateCodeGenerator { /**
* 生成图形验证码
* @param request
* @return
*/
@Override
public ValidateCode generate(ServletWebRequest request) { …… return new ImageCode(image, sRand, SecurityConstants.EXPIRE_SECOND); }
} // 短信验证码生成器
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator { @Override
public ValidateCode generate(ServletWebRequest request) {
String code = RandomStringUtils.randomNumeric(SecurityConstants.SMS_RANDOM_SIZE);
return new ValidateCode(code, SecurityConstants.SMS_EXPIRE_SECOND);
} }

短信验证码发送接口设计

短信验证码生成之后,需要设计接口依赖短信服务提供商进行验证码发送,因此至少设计一个统一的接口,供短信服务提供商生成发送短信服务:

public interface SmsCodeSender {
// 至少需要手机号和验证码
void send(String mobile, String code); }

为了演示,设计一个虚拟的默认短信发送器,只在日志文件中打印一行log:

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

/**
* 短信发送模拟
* @author Administrator
*
*/
@Slf4j
@Service
public class DefaultSmsCodeSender implements SmsCodeSender { @Override
public void send(String mobile, String code) {
log.debug("send to mobile :{}, code : {}", mobile, code);
}
}

短信验证码请求Controller

所有验证码的请求都在统一的ValidateCodeController里,这里注入了两个验证码生成器ValidateCodeGenerator,后期可以利用 spring 的依赖查找/搜索技巧来重构代码,另外所有的请求也是可以做成动态配置,这里临时全部 hardCode 在代码里:

import java.io.IOException;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import org.woodwhales.king.core.commons.SecurityConstants;
import org.woodwhales.king.validate.code.ValidateCode;
import org.woodwhales.king.validate.code.ValidateCodeGenerator;
import org.woodwhales.king.validate.code.image.ImageCode;
import org.woodwhales.king.validate.code.sms.DefaultSmsCodeSender; @RestController
public class ValidateCodeController { @Autowired
private SessionStrategy sessionStrategy; @Autowired
private ValidateCodeGenerator imageCodeGenerator; @Autowired
private ValidateCodeGenerator smsCodeGenerator; @Autowired
private DefaultSmsCodeSender defaultSmsCodeSender; @GetMapping("code/image")
public void createImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = (ImageCode)imageCodeGenerator.generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SecurityConstants.SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
} @GetMapping("code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SecurityConstants.SESSION_KEY, smsCode);
String mobile = ServletRequestUtils.getStringParameter(request, "mobile");
defaultSmsCodeSender.send(mobile, smsCode.getCode());
}
}

从上述代码中可以看出图片验证码和短信验证码的生成请求逻辑是相似的:首先调用验证码生成接口生成验证码,然后将验证码放入 session 中,最后将验证码返回给前端或者用户。因此这个套路流程可以抽象成一个模板方法,以增强代码的可维护性和可扩展性。

用一张图来表述重构后的代码结构:

随机验证码过滤器设计

由于图片和手机都会产生验证码,后期还可以通过邮件发送随机验证码的方式进行随机验证码登录验证,因此将随机验证码的认证可以独立封装在一个随机验证码过滤器中,并且这个过滤器在整个 spring security 过滤器链的最前端(它是第一道认证墙)。

随机验证码过滤器只要继承 spring 框架中的OncePerRequestFilter即可保证这个过滤器在请求来的时候只被调用一次,具体代码实现参见文末源码。

这里重点解释一下如何将随机验证码过滤器配置到 spring security 过滤器认证最前端,需要重写SecurityConfigurerAdapter的configure()方法,并将自定义的过滤器放到AbstractPreAuthenticatedProcessingFilter过滤器之前即可:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.stereotype.Component; import javax.servlet.Filter; @Component
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired
private Filter validateCodeFilter; @Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
}
}

短信验证码认证

在自定义短信登录认证流程之前,建议可以移步到之前的文章:SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解,了解清除用户密码的认证流程才能更容易理解下面这张经典的流程图:

左侧是用户+密码的认证流程,整体的流程就是经过用户名+密码认证过滤器认证,将请求封装成 token 并注入到 AutheticationMananger 中,之后由默认的认证校验器进行校验,在校验的过程中会调用 UserDetailsService 接口进行 token 校验,当校验成功之后,就会将已经认证的 token 放到 SecurityContextHolder 中。

同理,由于短信登录方式只需要使用随机验证码进行校验而不需要密码登录功能,当校验成功之后就认为用户认证成功了,因此需要仿造左侧的流程开发自定义的短信登录认证 token,这个 token 只需要存放手机号即可,在token 校验的过程中,不能使用默认的校验器了,需要自己开发校验当前自定义 token 的校验器,最后将自定义的过滤器和校验器配置到 spring security 框架中即可。

注意:短信随机验证码的验证过程是在 SmsCodeAuthticationFIlter 之前就已经完成。

短信登录认证Token

仿造UsernamePasswordAuthenticationToken设计一个属于短信验证的认证 token 对象,为什么要自定义一个短信验证的 token,spring security 框架不只提供了用户名+密码的验证方式,用户认证是否成功,最终看的就是SecurityContextHolder对象中是否有对应的AuthenticationToken,因此要设计一个认证对象,当认证成功之后,将其设置到SecurityContextHolder即可。

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion; public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; public SmsCodeAuthenticationToken(Object mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
} public SmsCodeAuthenticationToken(Object mobile, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = mobile;
super.setAuthenticated(true); // must use super, as we override
} public Object getPrincipal() {
return this.principal;
} public Object getCredentials() {
return null;
} public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} super.setAuthenticated(false);
} @Override
public void eraseCredentials() {
super.eraseCredentials();
} }

从AuthenticationToken接口可以看到,现在框架中有我们自己定义短信登录的 token 了:

短信登录认证过滤器

短信验证码的过滤器设计思路同理,仿造UsernamePasswordAuthenticationFilter过滤器,这里再次提醒,短信随机验证码

import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import org.woodwhales.core.constants.SecurityConstants; public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /**
* 请求中的参数
*/
private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE; private boolean postOnly = true; public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST"));
} 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 mobile = obtainMobile(request); if (Objects.isNull(mobile)) {
mobile = "";
} mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
} /**
* 获取手机号
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
} protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
} public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
} public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
} public final String getMobileParameter() {
return mobileParameter;
} }

短信验证码过滤器也成为了AbstractAuthenticationProcessingFilter其中一个子类,后期需要注册到安全配置中,让它成为安全认证过滤链中的一环:

短信登录认证校验器

短信登录认证校验器的作用就是调用UserDetailsService的loadUserByUsername()方法对 authenticationToken 进行校验,所有校验器的根接口为:AuthenticationProvider,因此自定义的短信登录认证校验器实现这个接口,重写authenticate()即可:

import java.util.Objects;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import lombok.Data; @Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; /**
* 调用 {@link UserDetailsService}
*/
UserDetails user = userDetailsService.loadUserByUsername((String)authenticationToken.getPrincipal()); if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
} SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult;
} @Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
} }

注意,这里使用@Data注解生成 setter 和 getter 方法。

短信登录认证安全配置设计

设计一个封装好的短信登录认证配置类,以供外部调用者直接调用:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component; @Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired
private UserDetailsService userDetailsService; @Override
public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); // 获取验证码提供者
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // 将短信验证码校验器注册到 HttpSecurity, 并将短信验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前
http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }
}

当外部想要引用这个封装好的配置,只需要在自定义的AbstractChannelSecurityConfig安全认证配置中添加进去即可,注意这个配置对象使用了@Component注解,注册到了spring 中,所以可以直接通过@Autowired引用,如:

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import org.woodwhales.core.authentication.sms.AbstractChannelSecurityConfig;
import org.woodwhales.core.authentication.sms.SmsCodeAuthenticationSecurityConfig;
import org.woodwhales.core.validate.code.config.ValidateCodeSecurityConfig; @Component
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig { @Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig; @Autowired
protected AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired
protected AuthenticationFailureHandler authenticationFailureHandler; @Autowired
private UserDetailsService userDetailsService; @Autowired
private DataSource dataSource; @Override
protected void configure(HttpSecurity http) throws Exception { http.formLogin()
.loginPage("/authentication/require") // 登录页面回调
.successHandler(authenticationSuccessHandler)// 认证成功回调
.failureHandler(authenticationFailureHandler) // 以下验证码的校验配置
.and()
.apply(validateCodeSecurityConfig) // 以下短信登录认证的配置
.and()
.apply(smsCodeAuthenticationSecurityConfig) // 记住我的配置
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600) // 设置记住我的过期时间
.userDetailsService(userDetailsService) .and()
// 请求做授权配置
.authorizeRequests()
// 以下请求路径不需要认证
.antMatchers("/authentication/require",
"/authentication/mobile",
"/login",
"/code/*",
"/")
.permitAll()
.anyRequest() // 任何请求
.authenticated() // 都需要身份认证 // 暂时将防护跨站请求伪造的功能置为不可用
.and()
.csrf().disable();
} /**
* 配置TokenRepository
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 初始化记住我的数据库表,建议通过看源码直接创建出来
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
} }

这里的配置中有些代码出现了冗余配置,可以全部封装成抽象模板,完成一些基础的配置。

项目源码:https://github.com/woodwhales/spring-security

参考源码:https://github.com/imooc-java/security

SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能的更多相关文章

  1. SpringBoot + Spring Security 学习笔记(三)实现图片验证码认证

    整体实现逻辑 前端在登录页面时,自动从后台获取最新的验证码图片 服务器接收获取生成验证码请求,生成验证码和对应的图片,图片响应回前端,验证码保存一份到服务器的 session 中 前端用户登录时携带当 ...

  2. SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解

    用户认证流程 UsernamePasswordAuthenticationFilter 我们直接来看UsernamePasswordAuthenticationFilter类, public clas ...

  3. SpringBoot + Spring Security 学习笔记(一)自定义基本使用及个性化登录配置

    官方文档参考,5.1.2 中文参考文档,4.1 中文参考文档,4.1 官方文档中文翻译与源码解读 SpringSecurity 核心功能: 认证(你是谁) 授权(你能干什么) 攻击防护(防止伪造身份) ...

  4. SpringBoot + Spring Security 学习笔记(四)记住我功能实现

    记住我功能的基本原理 当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的 ...

  5. Spring Security实现短信验证码登录

    Spring Security默认的一个实现是使用用户名密码登录,当初我们在开始做项目时,也是先使用这种登录方式,并没有多考虑其他的登录方式.而后面需求越来越多,我们需要支持短信验证码登录了,这时候再 ...

  6. springboot +spring security4 自定义手机号码+短信验证码登录

    spring security 默认登录方式都是用户名+密码登录,项目中使用手机+ 短信验证码登录, 没办法,只能实现修改: 需要修改的地方: 1 .自定义 AuthenticationProvide ...

  7. Spring Security构建Rest服务-1203-Spring Security OAuth开发APP认证框架之短信验证码登录

    浏览器模式下验证码存储策略 浏览器模式下,生成的短信验证码或者图形验证码是存在session里的,用户接收到验证码后携带过来做校验. APP模式下验证码存储策略 在app场景下里是没有cookie信息 ...

  8. SpringSceurity(5)---短信验证码登陆功能

    SpringSceurity(5)---短信验证码登陆功能 有关SpringSceurity系列之前有写文章 1.SpringSecurity(1)---认证+授权代码实现 2.SpringSecur ...

  9. wepy 实现 用户名登录与短信验证码登录

    wepy 实现 用户名登录与短信验证码登录

随机推荐

  1. 从GitHub中整理出来的15个最受欢迎的Python开源框架,你喜欢哪个

    从GitHub中整理出的15个最受欢迎的Python开源框架.这些框架包括事件I/O,OLAP,Web开发,高性能网络通信,测试,爬虫等. Django: Python Web应用开发框架 Djang ...

  2. Zookeeper + Dubbo + SpringMVC + dubbo-admin

    第一步:在CentOS/Windows上安装Zookeeper[前提] A:CentOS     Zookeeper作为Dubbo服务的注册中心,Dubbo原先基于数据库的注册中心,没采用Zookee ...

  3. 机器学习,流式IoT和医疗设备互联

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 让我们来看一下机器学习是如何应用于医护行业以及如何借助Apache Spark对患者的监控数据进行处理 现如今,IoT数据,实时流式数据分析 ...

  4. Unix系统的常用信号

    编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号).不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信 ...

  5. sql优化总结

    在项目前期目标是确保功能能够正常运行,但是随着时间的推移,数据的增加,逻辑的复杂,导致数据查询会越来越慢,这个时候我们首先想到的应该就是尽量优化sql. sql优化常见注意点: 1.对查询进行优化,应 ...

  6. Zepto源码(2016)——Zepto模块(核心模块)

    // Zepto.js // (c) 2010-2016 Thomas Fuchs // Zepto.js may be freely distributed under the MIT licens ...

  7. python使用@property

    在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改: s = Student() s.score = 9999 这显然不合逻辑.为了限制score的 ...

  8. windows server 2012 R2汉化 -- 玩转Microsoft Azure

    Microsoft Azure 试用版小试牛刀 首先需要申请一个账号获得试用权 我这里是1元免费试用, 进入后就可以创建自己的虚拟机及数据库 在这里先说创建的windows server 2012 R ...

  9. 解锁 vmware esxi 6.7 并安装 mac os 10.13

    1.安装 esxi 6.7 2.下载 unlocker 2.1.1.zip 3.上传 unlocker 2.1.1.zip esxi的磁盘中 4.开启esxi的ssh登录 5.使用 ssh 登录 es ...

  10. bzoj2120 数颜色 分块

    分块大法好 orz 处理出每个点的前驱和后继位置. 暴力修改,查询就在每个整块里查询pre<l的,暴力跑两边就好了 #include<cstdio> #include<cstr ...