Spring Security验证码配置化开发

news2024/11/20 9:36:52

背景

在前两篇文章中给大家介绍了Spring Security的认证流程,包含对项目的定制化处理,可以明白security的设计还是比较强大的,通过一系列的过滤器注册的过滤器链,对流程进行链式的处理。
今天介绍一种以配置器的方式处理验证码生成、校验,流程可以参考Security中的FormLoginConfigurer表单登录配置器,在前两篇中提到的定制化UsernamePasswordAuthenticationFilter过滤器,就是表单登录配置器中的认证实现环节,而FormLoginConfigurer表单登录配置器的作用是定义了登录功能的入口、实现流程(从上文中可以看出我们所做的定制化是对于方法体,于配置器而言,我们的定制化是模板方法模式下的一种实现)。

配置器设计——模板方法模式

Security应用configurer配置器

Security的核心是HttpSecurity对象的应用,HttpSecurity配置完成后会通过build()方法生成SecurityFilterChain过滤器链应用到系统中。
SecurityFilterChain加入过滤器的两种方式

  • 通过HttpSecurity对象直接调用addFilterAt或者addFilterBefor方法,两种方法含义分别是:在某个过滤器的位置添加过滤器、在某个过滤器前面添加过滤器;
  • 通过配置器加入过滤器,HttpSecurity在build时会把所有的配置器应用到过滤器中

下面展示下模型图
在这里插入图片描述

configurer配置器设计

下面是对验证码配置器的设计,大致的骨架如图,自定义的验证码配置器的三个功能:加入自定义的验证码过滤器、校验码生成器对象、校验码处理器对象。骨架是模板,子类在不改变整体结构的情况,实现流程中的某些步骤细节。定义校验码接口,是为了方便扩展多种验证码校验例如:图片、短信、语音等。
![在这里插入图片描述](https://img-blog.csdnimg.cn/165b1f368fe94a28a956a48a8f637184.png

代码详解

ValidateCodeConfigurer配置器

配置器中添加了验证码处理的过滤器,声明了两个集合字段属性,key是验证码的接口地址,value是接口子类对象,分别是生成器接口子类和处理器接口子类,目的是兼容项目中多中验证方式。

public final class ValidateCodeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<ValidateCodeConfigurer<H>, H> {

    private AuthenticationFailureHandler failureHandler;

    private final Map<String, ValidateCodeGenerator> validateCodeGenerators = new HashMap<>();

    private final Map<String, ValidateCodeProcessor> validateCodeProcessors = new HashMap<>();

    public ValidateCodeConfigurer() {

    }

    @Override
    public void configure(H http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(this.validateCodeGenerators, this.validateCodeProcessors);
        validateCodeFilter.setFailureHandler(this.failureHandler);
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 
     * @param generateUrl 应用接口地址
     * @param validateCodeGenerator 生成器子类
     * @return
     */
    public ValidateCodeConfigurer<H> validateCodeGenerator(String generateUrl, ValidateCodeGenerator validateCodeGenerator) {
        Assert.isNull(this.validateCodeGenerators.get(generateUrl), StrUtil.format("{} has already exist", generateUrl));
        this.validateCodeGenerators.put(generateUrl, validateCodeGenerator);
        return ValidateCodeConfigurer.this;
    }
    
	/**
     * 
     * @param generateUrl 应用接口地址
     * @param validateCodeProcessor 处理器子类
     * @return
     */
    public ValidateCodeConfigurer<H> validateCodeProcessor(String processingUrl, ValidateCodeProcessor validateCodeProcessor) {
        Assert.isNull(this.validateCodeProcessors.get(processingUrl), StrUtil.format("{} has already exist", processingUrl));
        this.validateCodeProcessors.put(processingUrl, validateCodeProcessor);
        return ValidateCodeConfigurer.this;
    }

    public ValidateCodeConfigurer<H> failureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
        return ValidateCodeConfigurer.this;
    }

}

ValidateCodeFilter过滤器

验证码过滤器在doFilterInternal()方法中做了放行的校验,随即执行了验证码的生成或者处理。
放行的校验:生成和处理的子类是以Map的形式存在Filter中,key是具体的接口地址,所以如果不为获取验证码地址或登录接口地址,Filter会直接放行

@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private final Map<String, ValidateCodeGenerator> validateCodeGenerators;

    private final Map<String, ValidateCodeProcessor> validateCodeProcessors;

    private AuthenticationFailureHandler failureHandler;

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        // init
        Assert.isNull(this.failureHandler, "failureHandler cannot be null");
        // validateCodeGenerators 和 validateCodeProcessors 不能都为空
        Assert.isTrue(CollUtil.isEmpty(validateCodeGenerators) && CollUtil.isEmpty(validateCodeProcessors), "validateCodeGenerators and validateCodeProcessors must be init at least one");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!requiresValidateMethod(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        ValidateCodeGenerator validateCodeGenerator;
        ValidateCodeProcessor validateCodeProcessor;
        // generate
        if (null != (validateCodeGenerator = getValidateGenerator(request, response))) {
            try {
                // 判断认证状态
                if (validateCodeGenerator.needAuthenticated()
                        && null == SecurityContextHolder.getContext().getAuthentication()) {
                    throw new BadCredentialsException("Failed to generate since no authentication found");
                }
                validateCodeGenerator.send(request, response, validateCodeGenerator.create(request));
            } catch (AuthenticationException e) {
                if (e instanceof ValidateCodeException) {
                    this.failureHandler.onAuthenticationFailure(request, response, e);
                } else if (e instanceof BadCredentialsException) {// 透出未登录异常
                    this.failureHandler.onAuthenticationFailure(request, response, e);
                } else {
                    logger.error("validate code send error", e);
                    this.failureHandler.onAuthenticationFailure(request, response, new ValidateCodeException("验证码发送出错"));
                }
            }
            return;
        }
        // validate
        if (null != (validateCodeProcessor = getValidateProcessor(request, response))) {
            try {
                validateCodeProcessor.validate(request);
            } catch (AuthenticationException e) {
                if (e instanceof ValidateCodeException) {
                    this.failureHandler.onAuthenticationFailure(request, response, e);
                } else {
                    logger.error("validate code validate error", e);
                    this.failureHandler.onAuthenticationFailure(request, response, new ValidateCodeException("验证码校验出错"));
                }
                return;
            }
        }
        chain.doFilter(request, response);
    }

    private ValidateCodeGenerator getValidateGenerator(HttpServletRequest request, HttpServletResponse response) {
        return validateCodeGenerators.get(request.getRequestURI());// 简单直接获取,避免循环 match
    }

    private ValidateCodeProcessor getValidateProcessor(HttpServletRequest request, HttpServletResponse response) {
        return validateCodeProcessors.get(request.getRequestURI());// 简单直接获取,避免循环 match
    }

    private boolean requiresValidateMethod(HttpServletRequest request, HttpServletResponse response) {
        String method = request.getMethod();
        return HttpMethod.GET.matches(method) || HttpMethod.POST.matches(method);
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

}

ValidateCodeRepository验证码缓存操作接口

这个是将验证码的缓存拉出来做了设计,为了其它形式的缓存扩展。接口定义了保存、获取验证码方法。

public interface ValidateCodeRepository {

    /**
     * 保存验证码
     *
     * @param validateCode the code
     * @param expireIn     失效时间,单位秒
     */
    void save(ValidateCode validateCode, int expireIn);

    /**
     * 获取验证码
     *
     * @param uid 验证码唯一 ID
     * @return validate code
     */
    ValidateCode get(String uid);

    /**
     * 获取验证码
     *
     * @param uid 验证码唯一 ID
     * @return validate code
     */
    default ValidateCode getAndRemove(String uid) {
        ValidateCode validateCode = get(uid);
        remove(uid);
        return validateCode;
    }


    /**
     * 移除验证码
     *
     * @param uid 验证码唯一 ID
     */
    void remove(String uid);

}

@Slf4j
@RequiredArgsConstructor
public class RedissonValidateCodeRepository implements ValidateCodeRepository {

    private String keyPrefix = "VALIDATE:CODE";

    private final RedissonClient redissonClient;

    @Override
    public void save(ValidateCode validateCode, int expireIn) {
        RBucket<ValidateCode> bucket = getBucket(validateCode.getUid());
        bucket.set(validateCode, expireIn, TimeUnit.SECONDS);
    }

    @Override
    public ValidateCode get(String uid) {
        return getBucket(uid).get();
    }

    @Override
    public ValidateCode getAndRemove(String uid) {
        return getBucket(uid).getAndDelete();
    }

    @Override
    public void remove(String uid) {
        getBucket(uid).delete();
    }

    private RBucket<ValidateCode> getBucket(String uid) {
        if (StringUtils.isBlank(uid)) {
            throw new ValidateCodeException("验证码的id不能为空");
        }
        String key = getKeyPrefix() + ":" + uid;
        return redissonClient.getBucket(key);
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }
}

ValidateCode验证码对象封装

对验证码的一些属性做一层封装,包含了uid、code、type、expireIn、expireTime等,其子类对自己相关特征再添加其余信息,比如图片验证码,将Base64的图片放在子类中。

@Getter
@Setter
public class ValidateCode implements Serializable {

    /**
     * 验证码类型
     */
    private ValidateCodeType type;

    /**
     * 校验码唯一 ID
     */
    private final String uid;

    /**
     * 校验码
     */
    private String code;

    /**
     * 有效期,单位秒
     */
    private int expireIn;

    /**
     * 失效时间
     */
    private LocalDateTime expireTime;

    /**
     * @param type     类型
     * @param uid      校验码唯一 ID
     * @param code     校验码
     * @param expireIn 有效期, 单位秒
     */
    public ValidateCode(ValidateCodeType type, String uid, String code, int expireIn) {
        this.type = type;
        this.uid = uid;
        this.code = code;
        this.expireIn = expireIn;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    /**
     * 设置校验码有效期
     *
     * @param expireIn 有效期,单位秒
     */
    public void setExpireTime(int expireIn) {
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

    /**
     * 是否过期
     *
     * @return true/false
     */
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }

}


@Getter
@Setter
public class ImageCode extends ValidateCode {

    private String base64Str;

    public ImageCode(String uid, String code, int expireIn, String base64Str) {
        super(ValidateCodeType.IMAGE, uid, code, expireIn);
        this.base64Str = base64Str;
    }
}

ValidateCodeGenerator校验码生成器接口

定义了验证码生成的模板方法,方便子类以扩展和实现。目前使用的rediss做缓存操作。

public interface ValidateCodeGenerator {

    /**
     * 生成校验码
     *
     * @param request the request
     * @return validate code
     */
    ValidateCode generate(HttpServletRequest request);

    /**
     * 生成并保存校验码
     *
     * @param request the request
     * @return validate code
     */
    ValidateCode create(HttpServletRequest request);

    /**
     * 发送校验码
     *
     * @param request      the request
     * @param response     the response
     * @param validateCode the validateCode
     */
    void send(HttpServletRequest request, HttpServletResponse response, ValidateCode validateCode);

    /**
     * 是否需要认证
     *
     * @return boolean
     */
    default boolean needAuthenticated() {
        return false;
    }
}

@Getter
@Setter
@Slf4j
@Accessors(chain = true)
public class ImageCodeGenerator implements ValidateCodeGenerator {

/**
     * 基础字符,排除易混淆字符: o, O, i, I, l, L, p, P, q, Q
     */
    public static final String BASE_CHAR_WITHOUT = "abcdefghjkmnrstuvwxyz";

    public static final String BASE_CHAR = "abcdefghijklmnopqrstuvwxyz";

    public static final String BASE_NUMBER = "1234567890";

    private ValidateCodeRepository validateCodeRepository;
	/**
     * 验证码类型
     */
    private ValidateCodeType type;

    /**
     * 有效期, 单位秒
     */
    private int expireIn = 60;

    /**
     * 验证码位数,默认 4 位
     */
    private int count = 4;

    /**
     * 是否仅数字,默认否
     */
    private boolean numberOnly = false;

    /**
     * 是否需要认证
     */
    private boolean needAuthenticated = false;

    public ImageCodeGenerator(ValidateCodeRepository validateCodeRepository) {
    	this.validateCodeRepository = validateCodeRepository;
    }

    @Override
    public ImageCode generate(HttpServletRequest request) {
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(75, 30, getCount(), 4);
        lineCaptcha.setGenerator(new RandomGenerator(isNumberOnly() ? BASE_NUMBER : BASE_NUMBER + BASE_CHAR_WITHOUT, getCount()));
        return new ImageCode(IdUtil.fastUUID(), lineCaptcha.getCode(), getExpireIn(), lineCaptcha.getImageBase64Data());
    }

    @Override
    public ValidateCode create(HttpServletRequest request) {
        ImageCode imageCode = generate(request);
        getValidateCodeRepository().save(imageCode, imageCode.getExpireIn());
        return imageCode;
    }

    @Override
    public void send(HttpServletRequest request, HttpServletResponse response, ValidateCode validateCode) {
        if (!(validateCode instanceof ImageCode)) {
            throw new ValidateCodeException("发送验证码错误, 未生成图形");
        }
        ImageCode imageCode = (ImageCode) validateCode;
        if (StrUtil.isBlank(imageCode.getBase64Str())) {
            throw new ValidateCodeException("发送验证码错误, 未生成图形");
        }
        ImageCodeResponse responseData = new ImageCodeResponse().setUid(imageCode.getUid())
                .setCaptcha(imageCode.getBase64Str());
        log.info("发送图形验证码[{}]成功...", validateCode.getUid());
        HttpContextUtil.write(request, response, Rest.success(responseData));
    }

}


ValidateCodeProcessor校验码处理器

设计上和生成器类似,接口定义方法,不同类型去实现接口形成不同的子类。

public interface ValidateCodeProcessor {

    /**
     * 校验验证码(验证后删除)
     *
     * @param servletWebRequest the servlet web request
     */
    void validate(HttpServletRequest servletWebRequest);

    /**
     * 校验验证码(验证后不删除)
     *
     * @param servletWebRequest the servlet web request
     */
    void check(HttpServletRequest servletWebRequest);

}

@Slf4j
@RequiredArgsConstructor
public class DefaultValidateCodeProcessor implements ValidateCodeProcessor {

    private String keyParameter = "uid";

    private String valueParameter = "captcha";

    private final ValidateCodeRepository validateCodeRepository;

    protected String obtainKey(HttpServletRequest request) {
        String key = request.getParameter(keyParameter);
        if (StringUtils.isBlank(key)) {
            throw new ValidateCodeException("验证码的id不能为空");
        }
        return key;
    }

    protected String obtainValue(HttpServletRequest request) {
        String value = request.getParameter(valueParameter);
        if (StringUtils.isBlank(value)) {
            throw new ValidateCodeException("验证码的id不能为空");
        }
        return value;
    }

    /**
     * 验证后删除
     *
     * @param request the request
     */
    @Override
    public void validate(HttpServletRequest request) {
        String key = obtainKey(request);
        String value = obtainValue(request);
        ValidateCode validateCode = validateCodeRepository.getAndRemove(key);
        if (validateCode == null || validateCode.isExpired()) {
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equalsIgnoreCase(value, validateCode.getCode())) {
            throw new ValidateCodeException("验证码不匹配");
        }
        log.info("验证码[{}]校验通过...", key);
    }

    /**
     * 验证
     *
     * @param request the request
     */
    @Override
    public void check(HttpServletRequest request) {
        String key = obtainKey(request);
        String value = obtainValue(request);
        ValidateCode validateCode = validateCodeRepository.get(key);
        if (validateCode == null || validateCode.isExpired()) {
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equalsIgnoreCase(value, validateCode.getCode())) {
            throw new ValidateCodeException("验证码不匹配");
        }
    }

    public String getKeyParameter() {
        return keyParameter;
    }

    public ValidateCodeProcessor setKeyParameter(String keyParameter) {
        this.keyParameter = keyParameter;
        return DefaultValidateCodeProcessor.this;
    }

    public String getValueParameter() {
        return valueParameter;
    }

    public ValidateCodeProcessor setValueParameter(String valueParameter) {
        this.valueParameter = valueParameter;
        return DefaultValidateCodeProcessor.this;
    }
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/29182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Day11--配置tabBar效果

1.创建tabBar分支 我的操作&#xff1a; 1》在其根路径下按住“shift”和鼠标右键进入powerShell界面&#xff0c;然后输入创建一个分支tabBar. ****************************** ****************************** ****************************** ****************** 2.创建ta…

生产质量管理系统有哪些功能?

随着中国市场经济的快速发展&#xff0c;中国产品正经历着“中国制造”向“中国创造”的转变,在这个过程中&#xff0c;企业经营也逐渐从低成本生产管理向质量经营方向发展&#xff0c;企业已经认识到质量的重要性&#xff0c;企业对质量管理的有效性也提出了更高的要求。 作为…

【Servlet】5:详解响应对象 HttpServletResponse

目录 | 响应对象 HttpServletResponse接口 HttpServletResponse的基本概述 响应对象返回 String 给Browser & 中文乱码问题 响应对象返回 int 给Browser 响应对象 解析HTML标签后 返回给Browser 本文章属于后端全套笔记的第三部分 &#xff08;更新中&#xff09;【后…

UI和UI有什么不同,是如何协助的

UX(用户体验)和UI&#xff08;用户界面&#xff09;是一个常用的术语。然而&#xff0c;尽管有复杂的联系&#xff0c;但网页设计的两个领域是两个不同的东西。事实上&#xff0c;有可能有一个用户界面优秀但用户体验差的网站。因此&#xff0c;了解UX和UI良好的网页设计非常重…

只需要改造一下实体类,以后再也不用写SQL了

文章目录只需要改造一下实体类&#xff0c;以后再也不用写SQL了 现状分析现状示例1. 建立人员表M.T.Person表&#xff0c;包含三个字段名称&#xff0c;年龄&#xff0c;身份证。2. 新增一条数据&#xff0c;也就是插入的情况。3. 获取单条数据内容&#xff0c;也是就是根据ID获…

经济师十大专业通过人数分析!选专业有谱了!

最近&#xff0c;很多同学在后台咨询&#xff0c;哪个专业的好通过&#xff0c;自己要如何选专业…… 小编特别整理了已经公布的各省市2021年初、中级经济师各专业通过情况数据&#xff0c;给大家参考&#xff01; 2022年中级经济专业技术资格考试设《经济基础知识》和《专业知…

让学前端不再害怕英语单词(三)

前两章直通车↓↓↓ 让学前端不再害怕英语单词&#xff08;一&#xff09; 让学前端不再害怕英语单词&#xff08;二&#xff09; 由于前两章都反响强烈&#xff0c;都上了全站的热榜&#xff0c;所以今天打算把第三章也写出来 第三章我们主要讲一下css中的伪类的英语单词还有…

TResNet: ResNet改进,实现高精度的同时保持高 GPU 利用率

终于开题&#xff0c;抓紧发文&#xff0c;然后放飞​​​​​​&#xff0c;来由就是想搞一篇论文&#xff0c;但是增加了某个东西之后吧&#xff0c;速度变慢了&#xff0c;所以导师提议加个这玩意看看能不能快点。 论文题目&#xff1a;TResNet: High Performance GPU-Dedi…

深度剖析商业智能BI中的多维分析

数据在这些年的时间中&#xff0c;也逐渐成长为了个人、机构、企业乃至国家的战略资源&#xff0c;被很多人放到“新时代的石油”这一位置上。虽然这个说法也引起了一些争议&#xff0c;但更多只是讨论数据和石油的差异性&#xff0c;却并没有多少人否认数据的价值&#xff0c;…

开发速查表,一个值得每个程序员收藏的网站

在工作过程中&#xff0c;虽然我们程序员&#xff0c;主要是使用一门语言开发&#xff0c;但免不了会用到其他语言参与其他项目&#xff1b;或者很多全栈工程师&#xff0c;会参与前端的开发调试&#xff1b;总的来说&#xff0c;我们工作过程中&#xff0c;都会涉及到多门编程…

Fluent 嵌套网格(overset)功能讲解与实例操作

作者 | 张杨 在流体仿真中&#xff0c;我们经常会遇到边界运动的问题&#xff0c;如&#xff1a; 生物医疗行业中血管的运动 航空航天行业中飞行器的分离 容积泵中齿轮的相对运动 在ANSYS Fluent 17.0之前的版本中&#xff0c;我们通常采用传统的MDM&#xff08;Moving/D…

【模型训练】YOLOv7车辆和行人检测

YOLOv7车辆和行人检测 1、车辆和行人检测模型训练2、模型评估3、模型和数据集下载网盘链接1、本项目采用YOLOv7算法实现对车辆和行人检测,在几千多张车辆和行人检测中能训练得到,我们训练了YOLOv7、,所有指标都是在同一个验证集上得到; 2、目标类别数:2 ;类别名:person、…

5、网络配置

文章目录5、网络配置5.1 VMware三种模式5.1.1 桥连模式5.1.2 NAT模式5.1.3 仅主机模式5.2 查看网络IP和网关5.2.1 查看虚拟网络编辑器5.2.2 修改虚拟网卡 Ip5.2.3 查看网关5.2.4 查看 windows 环境的中 VMnet8 网络配置5.3 配置网络ip地址5.3.1 ifconfig查看网络接口配置1 基本…

Windows内核--系统调用参数验证(5.1)

内核参数验证的重要性 内核模式之所以有别于用户模式&#xff0c;在于内核模式应该是安全、可信的。用户系统调用可以传入各式各样的参数&#xff0c;可能是代码无意写错或因不预期的内存覆盖"暗地修改"参数&#xff0c;也可能是Hack有意传入&#xff0c;内核都应当妥…

【强化学习论文合集】ICRA-2022 强化学习论文 | 2022年合集(六)

强化学习&#xff08;Reinforcement Learning, RL&#xff09;&#xff0c;又称再励学习、评价学习或增强学习&#xff0c;是机器学习的范式和方法论之一&#xff0c;用于描述和解决智能体&#xff08;agent&#xff09;在与环境的交互过程中通过学习策略以达成回报最大化或实现…

生成者(建造者)模式

思考生成者模式 生成者模式就是将对象构建和对象内部构建分离 对象构建&#xff1a;手机的构建 对象内部构建&#xff1a;手机中屏幕和电池的构建 1.生成者模式的本质 生成器模式的本质:分离整体对象构建算法和对象的部件构造。 构建一个复杂的对象&#xff0c;本来就有构建的过…

前端程序员辞掉朝九晚五工作成为独立开发者一年开发出6款软件的故事

一个前端程序员的梦想 作为一个程序员&#xff0c;陈明福的梦想是&#xff1a; 自主自由的工作内容和方式。在全球范围内发展个人品牌和影响力。学习技术和经验&#xff0c;成为 SaaS 软件方面的专家。对世界产生积极影响。财务自由&#xff0c;能提前退休。 他的故事 1、他…

JS 根据某个字段进行排序或分组

JS 数组中根据某个字段进行排序 const arr [ { name: "崔喻琪", age: 32 }, { name: " 王忱景", age: 18 }, { name: " 房真睿", age: 27 }, { name: "姬泉孝", age: 20 }, { name: "余嘉芳", age: 16 }, { na…

Deep Few-Shot Learning for Hyperspectral Image Classification-浅读

这里写目录标题Deep Few-Shot Learning for Hyperspectral Image ClassificationIntroductionMethodExperimentDeep Few-Shot Learning for Hyperspectral Image Classification 我看的第一篇 few-shot learning 文章&#xff0c;记录一下&#xff0c;看看能不能说明few-shot …

学生家乡网页设计作品静态HTML网页—— HTML+CSS+JavaScript制作辽宁沈阳家乡主题网页源码(11页)

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…