热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

万字长文详解SpringSecurity验证码的生成

本文思维导图图1-1验证码生成概图概述总所周知,验证码方式的登录模式十分的普遍,不过SpringSecurity并没有提供比较好的原生解决方案&#x

本文思维导图

图1-1 验证码生成 概图

概述

总所周知,验证码方式的登录模式十分的普遍,不过 Spring Security 并没有提供比较好的原生解决方案,但是我们可以 do it by ourselves!,本文的篇幅相对比较长,因此分上下篇分别来介绍。上篇主要介绍:验证码的生成,下篇对自定义验证码登录的流程进行讲解。

我们比较常见的验证码主要有两种:图形验证码以及短信验证码,相对来说不是特别的复杂。可能会有人有疑惑:为什么简单的验证码生成需要花费一整篇幅来介绍呢?原因当然是:身为菜鸟的我也有一个架构师的梦!验证码的生成会结合模板方法模式一起讲解。

初探模板方法模式

模板方法模式属于一种行为型的设计模式,主要是用来解决复用和扩展两个问题。

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到某些子类中实现。该模式可以让子类在不改变算法整体结构的情况,重新定义算法中的某些步骤细节。

这里提到了一个算法骨架的概念,算法 并非是指数据结构中的“算法”,可以理解为广义上的业务逻辑;骨架 架子其实就是模板;总的来说:算法骨架 可以理解为包含广义业务逻辑的模板方法。

实践出真知

绝大部分的设计模式的原理都十分的简单,难得是将原理落实到实践中,解决实际问题。

我们知道模板方法模式主要是用来解决 复用 和 扩展 这两个问题,结合到实际情况中来分析;验证码生成有哪些地方需要 复用 和 扩展 呢?

让我们来梳理一下验证码登录模式的流程,无论是短信验证码还是图形验证码,大致上都有如下步骤:生成验证码、存储、发送、校验;既然流程上相同,那么就能做到复用。而扩展 并非是指代码的扩展性,而是指框架上的扩展性,模板方法模式可以让使用者在不修改骨架源码的情况下,定制化扩展功能。

废话不多说,接下来就来瞅瞅模板方法模式在验证码生成模块的落地情况吧!还是老规矩,先上图:

图1-2 验证码关系概览图

验证码的生成主要分3个模块:骨架模块、验证码生命周期模块、具体验证码模块(短信验证码和图形验证码)

  • 骨架模块主要包含 ValidateCodeProcessor 接口以及AbstractValidateCodeProcessor抽象类;封装了验证码相关的可复用的业务逻辑。

  • 验证码生命周期模块是指:验证码的生成、存储、发送。

  • 具体验证码模块涉及短信验证码和图形验证码,基于骨架重新定义自己的相关实现。


验证码骨架

无论是图形验证码还是短信验证码,验证码的相关业务逻辑(算法骨架)都是大同小异的;主要是验证码的 创建流程 和 验证流程。因此使用模板方法模式,对可复用的业务逻辑进行抽离,封装成一个骨架。

ValidateCodeProcessor.class

/*** 校验码处理器 封装不同验证码的处理逻辑*/
public interface ValidateCodeProcessor {/*** 创建验证码* 1.生成验证码  2.存储  3.发送** @param res http请求的request和response封装* @throws Exception*/void create(ServletWebRequest res) throws Exception;/*** 校验验证码** @param res*/void validate(ServletWebRequest res);
}

ValidateCodeProcessor 接口定义了2个方法:create() 方法,用于验证码的生成, validate() 方法用于验证码的校验。

AbstractValidateCodeProcessor.class

/*** 抽象方法模式——算法骨架* 对验证码的一些公有的业务逻辑进行抽离,做到复用**/
@Slf4j
public abstract class AbstractValidateCodeProcessor implements ValidateCodeProcessor {/*** 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。*/@Autowiredprivate Map validateCodeGeneratorMap;/*** 验证码的存储介质*/@Autowiredprivate ValidateCodeRepository validateCodeRepository;private static final String SMS = "sms", IMAGE = "image";@Overridepublic void create(ServletWebRequest res) throws Exception {// 生成C validateCode = generate(res);// 存储save(res, validateCode);// 发送 (抽象方法 由具体的子类实现各自的发送逻辑)send(res, validateCode);}@Overridepublic void validate(ServletWebRequest res) {// 根据请求获取验证码的类型,并且从repository存储层中寻找匹配的验证码ValidateCodeEnum codeEnum = getValidateCodeType(res);Optional codeOpt = validateCodeRepository.get(res, codeEnum);ValidateCode valCodeInStorage = codeOpt.orElseThrow(() -> new ValidateCodeException("验证码不存在"));// 从请求中获取验证码String codeInRequest;try {codeInRequest = ServletRequestUtils.getStringParameter(res.getRequest(),codeEnum.getType());} catch (ServletRequestBindingException e) {throw new ValidateCodeException("获取请求验证码的值失败");}if (StringUtils.isBlank(codeInRequest)) {throw new ValidateCodeException(codeEnum + "请求验证码的值不能为空");}// 对短信验证码做一个是否过期的判断if (ValidateCodeEnum.SMS.equals(codeEnum) && valCodeInStorage.checkExpired()) {validateCodeRepository.remove(res, codeEnum);throw new ValidateCodeException(codeEnum + "验证码已过期");}// 验证码校验if (!StringUtils.equals(valCodeInStorage.getCode(), codeInRequest)) {throw new ValidateCodeException(codeEnum + "验证码不匹配");}log.info("验证码校验成功");validateCodeRepository.remove(res, codeEnum);}/*** 生成验证码** @param res* @return C 验证码泛型*/@SuppressWarnings("unchecked")private C generate(ServletWebRequest res) {// 根据传入的res来做类型判断String type = getValidateCodeType(res).getType();// 获取具体的Generator的名字String generatorName = type.concat(ValidateCodeGenerator.class.getSimpleName());ValidateCodeGenerator codeGenerator = Optional.ofNullable(validateCodeGeneratorMap.get(generatorName)).orElseThrow(() -> new ValidateCodeException("验证码生成器:" + generatorName + "不存在"));return (C) codeGenerator.generate(res);}/*** 存储短信验证码* 可复用---抽象类中定义** @param res* @param validateCode*/private void save(ServletWebRequest res, C validateCode) {ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());validateCodeRepository.save(res, code, getValidateCodeType(res));}/*** 验证码的发送* 图形验证码和短线验证码的发送逻辑不一样,因此设计为抽象方法,由具体的子类实现各自的发送逻辑** @param res* @param validateCode* @throws ServletRequestBindingException* @throws IOException*/protected abstract void send(ServletWebRequest res, C validateCode) throws ServletRequestBindingException, IOException;/*** 根据请求的url获取校验码的类型** @param res* @return ValidateCodeType*/private ValidateCodeEnum getValidateCodeType(ServletWebRequest res) {String uri = res.getRequest().getRequestURI();if (StringUtils.contains(uri, SMS)) {return ValidateCodeEnum.SMS;}return ValidateCodeEnum.IMAGE;}
}

AbstractValidateCodeProcessor 抽象类实现 ValidateCodeProcessor 接口,主要功能是对验证码相关的共有逻辑进行一个抽离,达到功能的复用。

  • create流程可以细分为以下几个步骤:生成、存储、发送。

  • validate是做验证码的校验,无论是图形验证码or短信验证码;验证的逻辑是一致的。

类中有2个成员变量:

  • private Map validateCodeGeneratorMap 验证码生成器,不同的验证码生成逻辑不同,因此生成模块抽离出去由外部实现。需要提到的是:这里使用到Spring 的 定向查找 技巧进行注入,Spring 启动时,会查找容器中所有 ValidateCodeGenerator接口的实现,并把Bean的名字作为 Key,实体作为Value放到 Map中。

  • private ValidateCodeRepository validateCodeRepository 验证码存储层,生成的验证码code值需要存储到某个存储介质中,用以后续校验的时候取得(我这里使用的是Redis作为存储介质)。


验证码生命周期

验证码的生命周期可简单的划分为:生成、存储、发送。

生成验证码

生成和发送模块也相对简单,就是定义了验证码的具体生成器以及发送器。

ValidateCodeGenerator.class

/*** 验证码生成器*/
public interface ValidateCodeGenerator {/*** 生成验证码** @param res http请求中的request和response* @return ValidateCode*/ValidateCode generate(ServletWebRequest res);}

ValidateCodeGenerator接口定义了验证码生成方法generate(),具体的生成逻辑由对应的子类SmsValidatecodeGeneratorImageValidateCodeGenerator实现。

SmsValidateCodeGenerator.class

/*** 短信验证码生成器**/
public class SmsValidateCodeGenerator implements ValidateCodeGenerator {private final SecurityProperties securityProperties;public SmsValidateCodeGenerator(SecurityProperties securityProperties) {this.securityProperties = securityProperties;}/*** 短信验证码生成逻辑** @param res* @return ValidateCode*/@Overridepublic ValidateCode generate(ServletWebRequest res) {//随机生成指定长度的短信验证码String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());}
}

短信验证码的生成逻辑,代码相对简单,当然可以定义自己的生成逻辑,反正就是随你开心就行拉!

ImageValidateCodeGenerator.class

/*** 图形验证码生成器**/
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {private final SecurityProperties securityProperties;public ImageValidateCodeGenerator(SecurityProperties securityProperties) {this.securityProperties &#61; securityProperties;}/*** 图形验证码生成逻辑* todo 这里的生成逻辑可稍微优化一下成utils** &#64;param res* &#64;return ValidateCode*/&#64;Overridepublic ValidateCode generate(ServletWebRequest res) {// 这里是实现了验证码参数的三级可配&#xff1a;请求级>应用级>默认配置 从请求中获取width 如果没有则从 securityProperties的配置中获取int width &#61; ServletRequestUtils.getIntParameter(res.getRequest(), "width",securityProperties.getCode().getImage().getWidth());int height &#61; ServletRequestUtils.getIntParameter(res.getRequest(), "height",securityProperties.getCode().getImage().getHeight());BufferedImage image &#61; new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics g &#61; image.getGraphics();Random random &#61; new Random();g.setColor(getRandColor(200, 250));g.fillRect(0, 0, width, height);g.setFont(new Font("Times New Roman", Font.ITALIC, 20));g.setColor(getRandColor(160, 200));for (int i &#61; 0; i < 155; i&#43;&#43;) {int x &#61; random.nextInt(width);int y &#61; random.nextInt(height);int xl &#61; random.nextInt(12);int yl &#61; random.nextInt(12);g.drawLine(x, y, x &#43; xl, y &#43; yl);}String sRand &#61; "";for (int i &#61; 0; i < securityProperties.getCode().getImage().getLength(); i&#43;&#43;) {String rand &#61; String.valueOf(random.nextInt(10));sRand &#43;&#61; rand;g.setColor(new Color(20 &#43; random.nextInt(110), 20 &#43; random.nextInt(110), 20 &#43; random.nextInt(110)));g.drawString(rand, 13 * i &#43; 6, 16);}g.dispose();return new ImageValidateCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());}/*** 生成随机背景条纹** &#64;param fc* &#64;param bc* &#64;return Color*/private Color getRandColor(int fc, int bc) {Random random &#61; new Random();if (fc > 255) {fc &#61; 255;}if (bc > 255) {bc &#61; 255;}int r &#61; fc &#43; random.nextInt(bc - fc);int g &#61; fc &#43; random.nextInt(bc - fc);int b &#61; fc &#43; random.nextInt(bc - fc);return new Color(r, g, b);}}

ImageValidateCodeGenerator为图形验证码生成器&#xff0c;相对简单&#xff1b;当然里面有些类util的代码可以更好的封装&#xff0c;这里就不额外封装了。

存储验证码

验证码生成之后后端服务需要进行存储&#xff0c;方便后续校验的时候取得&#xff0c;相对比较简单&#xff0c;我这里使用的是Redis来存储。

ValidateCodeRepository.class

/*** 验证码存取器 接口*/
public interface ValidateCodeRepository {/*** 保存验证码** &#64;param res 请求HttpRequest 和HttpResponse的封装* &#64;param code 验证码* &#64;param validateCodeType 验证码类型*/void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum validateCodeType);/*** 获取验证码** &#64;param res* &#64;param validateCodeType* &#64;return Optional*/Optional get(ServletWebRequest res, ValidateCodeEnum validateCodeType);/*** 移除验证码** &#64;param request* &#64;param codeType*/void remove(ServletWebRequest request, ValidateCodeEnum codeType);}

ValidateCodeRepository 接口主要定义了三个方法save保存验证码&#xff0c; get获取验证码&#xff0c; remove移除验证码

RedisValidateCodeRepository.class

/*** 基于redis的验证码存取器*/
&#64;Slf4j
&#64;Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {&#64;Autowiredprivate RedisTemplate redisTemplate;/*** 设备id*/private static final String DEVICE_ID &#61; "deviceId";&#64;Overridepublic void save(ServletWebRequest res, ValidateCode code, ValidateCodeEnum codeEnum) {redisTemplate.opsForValue().set(buildKey(res, codeEnum), code, 30, TimeUnit.MINUTES);}&#64;Overridepublic Optional get(ServletWebRequest request, ValidateCodeEnum codeEnum) {Object value &#61; redisTemplate.opsForValue().get(buildKey(request, codeEnum));if (value &#61;&#61; null) {log.warn("不存在对应的验证码");return Optional.empty();}return Optional.of((ValidateCode) value);}&#64;Overridepublic void remove(ServletWebRequest request, ValidateCodeEnum codeEnum) {redisTemplate.delete(buildKey(request, codeEnum));}/*** 根据请求的设备生成验证码的key&#xff0c;如果同一个设备多次请求 则先前的验证码则被覆盖无效** &#64;param res* &#64;param codeEnum* &#64;return String redis存储的key*/private String buildKey(ServletWebRequest res, ValidateCodeEnum codeEnum) {String deviceId &#61; res.getHeader(DEVICE_ID);if (StringUtils.isBlank(deviceId)) {throw new ValidateCodeException("请在请求头中携带deviceId参数");}String codeKey &#61; SecurityConstant.DEFAULT_PARAMETER_NAME_CODE.concat(codeEnum.getType().toLowerCase()).concat(CommonConstant.COLON).concat(deviceId);log.info("本次请求生成的codeKey:{}", codeKey);return codeKey;}}

RedisValidateCodeRepository 类是接口的具体实现&#xff0c;使用Redis 作为存储媒介&#xff0c;代码相对比较简单&#xff0c;不做过多的赘述。

发送验证码

验证码经过生成&#xff0c;后端存储后&#xff0c;就要进入最后一步&#xff1a;发送。

ValidareCodeSender.class

/*** 短信验证码发送器*/
public interface SmsCodeSender {/*** 发送短线验证码** &#64;param code* &#64;param mobile*/void send(String code, String mobile);
}

DefaultSmsCodeSender.class

/*** 默认的短信验证码的发送器**/
&#64;Slf4j
public class DefaultSmsCodeSender implements SmsCodeSender {&#64;Overridepublic void send(String code, String mobile) {// 这里做简单的输出即可log.info("向手机号" &#43; mobile &#43; "发送短信验证码" &#43; code);}
}

真实的生产中发送验证码需要使用第三方的短信服务&#xff0c;由于这里是学习记录&#xff0c;就简单的log一下记录发送。

图形验证码

图形验证码继承于验证码骨架&#xff0c;实现图形验证码有关的自定义逻辑&#xff0c;诸如&#xff1a;生成、发送。

ImageValidateCodeProcessor.class

/*** 模板方法最底层 --- 基于各自的特定实现各自的发送行为**/
&#64;Component("imageValidateCodeProcessor")
public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor {private static final String JPEG &#61; "JPEG";&#64;Overrideprotected void send(ServletWebRequest res, ImageValidateCode validateCode) throws IOException {if (Objects.nonNull(res.getResponse())) {ImageIO.write(validateCode.getBufferedImage(), JPEG, res.getResponse().getOutputStream());}}
}

ImageValidateCodeProcessor类主要自定义图形验证码的发送逻辑&#xff0c;生成的逻辑已经封装在ImageValidateCodeGenerator类&#xff0c;由依赖查找的方式注入到验证码骨架中了。

短信验证码

短信验证码继承于验证码骨架&#xff0c;实现短信验证码有关的自定义逻辑&#xff0c;诸如&#xff1a;生成、发送。

SmsValidateCodeProcessor.class

/*** 短信验证码的处理器* 模板方法最底层 --- 基于各自的特定实现各自的发送行为**/
&#64;Component("smsValidateCodeProcessor")
public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor {&#64;Autowiredprivate SmsCodeSender smsCodeSender;private static final String MOBILE &#61; "mobile";&#64;Overrideprotected void send(ServletWebRequest res, ValidateCode validateCode) throws ServletRequestBindingException {smsCodeSender.send(validateCode.getCode(), ServletRequestUtils.getRequiredStringParameter(res.getRequest(), MOBILE));}
}

SmsValidateCodeProcessor类同样自定义短信验证码的发送逻辑&#xff0c;生成的逻辑已经封装在SmsValidateCodeGenerator类&#xff0c;由依赖查找的方式注入到验证码骨架中了。

其他模块

其他模块主要是一些配置类、枚举类、异常类以及是一些用以提升代码质量的封装&#xff0c;需要特别介绍的是ValidateCodeBeanConfig 配置类和ValidateCodeException 异常类。

ValidateCodeBeanConfig 配置了bean的生成规则&#xff0c;契合SpringBoot的默认实现原理&#xff1a;用户有自定义则使用自定义&#xff0c;没有则使用默认实现。

ValidateCodeBeanConfig.class

&#64;Configuration
public class ValidateCodeBeanConfig {&#64;Autowiredprivate SecurityProperties securityProperties;/*** 注册图形验证码生成器* 使用conditionalOnMissingBean是为了 如果业务方有自己的生成逻辑 则使用业务方的&#xff1b;否则使用该默认配置* 方法名就是bean的名字** &#64;return ValidateCodeGenerator*/&#64;Bean&#64;ConditionalOnMissingBean(name &#61; "imageValidateCodeGenerator")public ValidateCodeGenerator imageValidateCodeGenerator() {return new ImageValidateCodeGenerator(securityProperties);}/*** 短线验证码生成器** &#64;return ValidateCodeGenerator*/&#64;Bean&#64;ConditionalOnMissingBean(name &#61; "smsValidateCodeGenerator")public ValidateCodeGenerator smsValidateCodeGenerator() {return new SmsValidateCodeGenerator(securityProperties);}/*** 找到smsCodeSender接口的所有实现类* 默认实现是用来被覆盖的* 如果之前用户已经配置了 则不再装载Default的*/&#64;Bean&#64;ConditionalOnMissingBean(SmsCodeSender.class)public SmsCodeSender smsCodeSender() {return new DefaultSmsCodeSender();}
}

配置Bean的生成规则&#xff0c;例如&#xff1a;Generator模块&#xff0c;用户可通过实现ValidateCodeGenerator来达到自定义验证码生成&#xff0c;否则使用默认的生成器&#xff0c;也是一种编程技巧。

ValidateCodeException 异常类继承于 SpringSecurity的异常基类AuthenticationException&#xff0c;这是因为我们是基于SpringSecurity做扩展开发自定义验证码认证模式。

/*** AuthenticationException是整个security异常中的基类* 验证码异常属于认证过程中的一个特例&#xff0c;归属于该基类之下**/
public class ValidateCodeException extends AuthenticationException {/*** 验证码异常* &#64;param msg* &#64;return t*/public ValidateCodeException(String msg, Throwable t) {super(msg, t);}public ValidateCodeException(String msg) {super(msg);}
}

其他的一些可以根据类名大致猜出作用的类这里就不做过多的展示。

总结

本篇文章主要结合模板方法模式介绍了验证码的生成&#xff0c;并且介绍了2个比较常用的编程技巧&#xff1a;依赖查找 和 使用ConditionalOnMissingBean 契合SpringBoot默认实现思想。

本文如有错误或不妥指出&#xff0c;烦请指出&#xff01;

一套非常好的springboot学习教程分享给大家&#xff08;需要的链接自行观看&#xff09;&#x1f447;&#xff1a;

https://www.bilibili.com/video/BV1PZ4y1j7QK

SpringBoot最新教程-SpringBoot框架实战

 

 


推荐阅读
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • springboot项目引入jquery浏览器报404错误的解决办法
    本文介绍了在springboot项目中引入jquery时,可能会出现浏览器报404错误的问题,并提供了解决办法。问题可能是由于将jquery.js文件复制粘贴到错误的目录导致的,解决办法是将文件复制粘贴到正确的目录下。如果问题仍然存在,可能是其他原因导致的。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 开发笔记:spring boot项目打成war包部署到服务器的步骤与注意事项
    本文介绍了将spring boot项目打成war包并部署到服务器的步骤与注意事项。通过本文的学习,读者可以了解到如何将spring boot项目打包成war包,并成功地部署到服务器上。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文介绍了Linux Shell中括号和整数扩展的使用方法,包括命令组、命令替换、初始化数组以及算术表达式和逻辑判断的相关内容。括号中的命令将会在新开的子shell中顺序执行,括号中的变量不能被脚本余下的部分使用。命令替换可以用于将命令的标准输出作为另一个命令的输入。括号中的运算符和表达式符合C语言运算规则,可以用在整数扩展中进行算术计算和逻辑判断。 ... [详细]
  • 本文介绍了安全性要求高的真正密码随机数生成器的概念和原理。首先解释了统计学意义上的伪随机数和真随机数的区别,以及伪随机数在密码学安全中的应用。然后讨论了真随机数的定义和产生方法,并指出了实际情况下真随机数的不可预测性和复杂性。最后介绍了随机数生成器的概念和方法。 ... [详细]
author-avatar
卢启红
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有