文章

给博客添加文章访问验证码功能

之前在网上看别人的技术文章时,往往看到一些写的很不错的文章,但是都设置了验证码访问,需要扫码关注公众号获取验证码输入之后才能浏览完整的文章内容,感觉这个功能太棒了,简直就是引流神器,于是乎,在某个闲暇周末自己动手改造了一下本人的博客实现了这么一个功能,至于为什么写这篇文章呢?是因为有小伙伴也想要实现这个功能,希望我能出一篇教程。好啦,废话不多说,直接干!

干之前还是先看下成品吧!
文章验证码演示
后台操作非常简单,只需要发布文章的时候选择自定义模板validation即可,完全不影响原有的密码功能,并且密码验证成功之后session记录
文章验证码操作

本功能实现所依赖的环境:

  1. 博客程序:halo1.6
  2. 博客主题:Joe2.0

本次修改主要包涵一下几点:

  1. 后端新增接口:生成注册码、检查注册码,新增定时任务:清理过期注册码
  2. 前端验证码提示框

一、后端修改部分:

1、RegistrationCode:run.halo.app.model.entity.RegistrationCode

package run.halo.app.model.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.GenericGenerator;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.model.enums.RegistrationCodeType;

/**
 * 注册码
 *
 * @author lywq
 * @date 2023/06/14 09:33
 **/
@Data
@Entity
@Table(name = "registration_codes", indexes = {@Index(name = "code_index", columnList = "code")})
@ToString
@EqualsAndHashCode(callSuper = true)
public class RegistrationCode extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "custom-id")
    @GenericGenerator(name = "custom-id",
        strategy = "run.halo.app.model.entity.support.CustomIdGenerator")
    private Integer id;

    /**
     * 注册码
     */
    @Column(name = "code", nullable = false, columnDefinition = "VARCHAR(50) COMMENT '注册码'")
    private String code;

    /**
     * 类型 0-通用,1-售卖,2-激励广告,3-微信公众号,4-微信小程序
     */
    @Column(name = "type", nullable = false, columnDefinition = "TINYINT(0) COMMENT '类型:0-通用,1-售卖,2-激励广告,3-微信公众号,4-微信小程序'")
    private RegistrationCodeType type;

    /**
     * 状态 0-未使用,1-已使用,-1-已失效
     */
    @Column(name = "status", nullable = false, columnDefinition = "TINYINT(0) COMMENT '状态 0-未使用,1-已使用,-1-已失效'")
    @ColumnDefault("0")
    private RegistrationCodeStatus status;

    /**
     * 用户名
     */
    @Column(name = "user_name", columnDefinition = "VARCHAR(50) COMMENT '用户名'")
    private String userName;
}

2、RegistrationCodeDTO:run.halo.app.model.dto.RegistrationCodeDTO

package run.halo.app.model.dto;

import lombok.Data;
import run.halo.app.model.dto.base.OutputConverter;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.model.enums.RegistrationCodeType;

/**
 * 注册码
 *
 * @author lywq
 * @date 2023/01/31 17:37
 **/
@Data
public class RegistrationCodeDTO implements OutputConverter<RegistrationCodeDTO, RegistrationCode> {

    /**
     * 主键
     */
    private Integer id;

    /**
     * 注册码
     */
    private String code;

    /**
     * 类型 0-通用,1-售卖,2-激励广告,3-微信公众号,4-微信小程序
     */
    private RegistrationCodeType type;

    /**
     * 状态 0-未使用,1-已使用,-1-已失效
     */
    private RegistrationCodeStatus status;

    /**
     * 用户名
     */
    private String userName;

}

3、RegistrationCodeParam:run.halo.app.model.params.RegistrationCodeParam

package run.halo.app.model.params;

import lombok.Data;
import run.halo.app.model.dto.base.InputConverter;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.model.enums.RegistrationCodeType;

/**
 * RegistrationCode param
 *
 * @author lywq
 * @date 2023/02/09 09:51
 **/
@Data
public class RegistrationCodeParam implements InputConverter<RegistrationCode> {

    /**
     * 主键
     */
    private Integer id;

    /**
     * 注册码
     */
    private String code;

    /**
     * 类型 0-通用,1-售卖,2-激励广告,3-微信公众号,4-微信小程序
     */
    private RegistrationCodeType type = RegistrationCodeType.COMMON;

    /**
     * 状态 0-未使用,1-已使用,-1-已失效
     */
    private RegistrationCodeStatus status = RegistrationCodeStatus.NOT_USED;

    /**
     * 用户名
     */
    private String userName;

    @Override
    public void update(RegistrationCode domain) {
        InputConverter.super.update(domain);
    }
}

4、RegistrationCodeStatus:run.halo.app.model.enums.RegistrationCodeStatus

package run.halo.app.model.enums;

/**
 * 注册码状态 枚举
 *
 * @author lywq
 * @date 2023/06/14 10:03
 **/
public enum RegistrationCodeStatus implements ValueEnum<Integer> {


    /**
     * 已失效
     */
    INVALIDATED(-1),

    /**
     * 未使用
     */
    NOT_USED(0),

    /**
     * 已使用
     */
    USED(1);

    private final int value;

    RegistrationCodeStatus(int value) {
        this.value = value;
    }

    @Override
    public Integer getValue() {
        return value;
    }
}

5、RegistrationCodeType:run.halo.app.model.enums.RegistrationCodeType

package run.halo.app.model.enums;

/**
 * 注册码类型  枚举
 *
 * @author lywq
 * @date 2023/06/14 09:37
 **/
public enum RegistrationCodeType implements ValueEnum<Integer> {


    /**
     * 通用
     */
    COMMON(0),

    /**
     * 售卖
     */
    SALE(1),

    /**
     * 激励广告
     */
    INCENTIVE_ADVERTISING(2),

    /**
     * 微信公众号
     */
    WECHAT_OFFICIAL_ACCOUNT(3),

    /**
     * 微信小程序
     */
    WECHAT_APPLET(4);

    private final int value;

    RegistrationCodeType(int value) {
        this.value = value;
    }

    @Override
    public Integer getValue() {
        return value;
    }
}

6、RegistrationCodeRepository:run.halo.app.repository.RegistrationCodeRepository

package run.halo.app.repository;

import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.lang.NonNull;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.repository.base.BaseRepository;

/**
 * RegistrationCode repository
 *
 * @author lywq
 * @date 2023/01/31 18:13
 **/
public interface RegistrationCodeRepository
    extends BaseRepository<RegistrationCode, Integer>, JpaSpecificationExecutor<RegistrationCode> {

    @NonNull
    List<RegistrationCode> findAllByStatus(@NonNull RegistrationCodeStatus status);

}

7、RegistrationCodeService:run.halo.app.service.RegistrationCodeService

package run.halo.app.service;

import java.util.Collection;
import java.util.List;
import org.springframework.lang.NonNull;
import run.halo.app.model.dto.RegistrationCodeDTO;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.model.params.RegistrationCodeParam;
import run.halo.app.service.base.CrudService;

/**
 * 注册码接口
 *
 * @author lywq
 * @date 2023/06/14 10:20
 **/
public interface RegistrationCodeService extends CrudService<RegistrationCode, Integer> {

    @NonNull
    RegistrationCode createBy(@NonNull RegistrationCodeParam registrationCodeParam);

    RegistrationCode updateBy(@NonNull RegistrationCode registrationCode);

    RegistrationCode getBy(@NonNull String code);

    boolean existByCode(String code);

    @NonNull
    List<RegistrationCode> listAllBy(@NonNull RegistrationCodeStatus status);

    @NonNull
    List<RegistrationCode> removeByIds(@NonNull Collection<Integer> ids);

    RegistrationCodeDTO convertTo(@NonNull RegistrationCode registrationCode);

    boolean checkRegistrationCode(@NonNull String code);
}

8、RegistrationCodeServiceImpl:run.halo.app.service.impl.RegistrationCodeServiceImpl

package run.halo.app.service.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.model.dto.RegistrationCodeDTO;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.model.params.RegistrationCodeParam;
import run.halo.app.repository.RegistrationCodeRepository;
import run.halo.app.repository.base.BaseRepository;
import run.halo.app.service.RegistrationCodeService;
import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.RegistrationCodeUtil;

/**
 * 注册码接口
 *
 * @author lywq
 * @date 2023/06/14 10:57
 **/
@Service
public class RegistrationCodeServiceImpl extends AbstractCrudService<RegistrationCode, Integer>
    implements RegistrationCodeService {
    private final RegistrationCodeRepository registrationCodeRepository;

    protected RegistrationCodeServiceImpl(
        BaseRepository<RegistrationCode, Integer> repository,
        RegistrationCodeRepository registrationCodeRepository) {
        super(repository);
        this.registrationCodeRepository = registrationCodeRepository;
    }


    @Override
    public @NotNull RegistrationCode createBy(
        @NotNull RegistrationCodeParam registrationCodeParam) {
        Assert.notNull(registrationCodeParam, "RegistrationCode param must not be null");

        RegistrationCode registrationCode = registrationCodeParam.convertTo();

        List<RegistrationCode> registrationCodes = listAll();
        List<String> list =
            registrationCodes.stream().map(RegistrationCode::getCode).collect(Collectors.toList());

        String code = RegistrationCodeUtil.createRegistrationCode(list);
        registrationCode.setCode(code);
        return create(registrationCode);
    }

    @Override
    public RegistrationCode updateBy(@NotNull RegistrationCode registrationCode) {
        Assert.notNull(registrationCode, "RegistrationCode must not be null");
        return update(registrationCode);
    }

    @Override
    public RegistrationCode getBy(@NotNull String code) {
        RegistrationCode registrationCode = new RegistrationCode();
        registrationCode.setCode(code);
        registrationCode.setStatus(RegistrationCodeStatus.NOT_USED);
        return registrationCodeRepository.findOne(Example.of(registrationCode)).orElse(null);
    }

    @Override
    public boolean existByCode(String code) {
        Assert.hasText(code, "registrationCode must not be blank");
        RegistrationCode registrationCode = new RegistrationCode();
        registrationCode.setCode(code);
        registrationCode.setStatus(RegistrationCodeStatus.NOT_USED);
        return registrationCodeRepository.exists(Example.of(registrationCode));
    }


    @Override
    public List<RegistrationCode> listAllBy(RegistrationCodeStatus status) {
        Assert.notNull(status, "RegistrationCode status must not be null");

        return registrationCodeRepository.findAllByStatus(status);
    }

    @Override
    public List<RegistrationCode> removeByIds(Collection<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return ids.stream().map(this::removeById).collect(Collectors.toList());
    }

    @Override
    public RegistrationCodeDTO convertTo(@NotNull RegistrationCode registrationCode) {
        Assert.notNull(registrationCode, "RegistrationCode must not be null");
        return new RegistrationCodeDTO().convertFrom(registrationCode);
    }

    @Override
    public boolean checkRegistrationCode(String code) {
        RegistrationCode registrationCode = this.getBy(code);
        if (Objects.nonNull(registrationCode)) {
            registrationCode.setStatus(RegistrationCodeStatus.USED);
            this.updateBy(registrationCode);
            return true;
        } else {
            return false;
        }
    }
}

9、RegistrationCodeUtil:run.halo.app.utils.RegistrationCodeUtil

package run.halo.app.utils;

import cn.hutool.core.util.RandomUtil;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 注册码工具类
 *
 * @author lywq
 * @date 2023/06/14 10:16
 **/
@Slf4j
@Component
public class RegistrationCodeUtil {

    /**
     * 生成唯一的注册码
     *
     * @param list
     * @return java.lang.String
     * @author lywq
     * @date 2023/06/14 15:28
     **/
    public static String createRegistrationCode(List<String> list) {
        String registrationCode = RandomUtil.randomStringUpper(12);
        boolean contains = list.contains(registrationCode);
        if (contains) {
            return createRegistrationCode(list);
        }
        return registrationCode;
    }

}

10、RegistrationCodeController:run.halo.app.controller.content.api.RegistrationCodeController

package run.halo.app.controller.content.api;

import io.swagger.annotations.ApiOperation;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import run.halo.app.model.dto.RegistrationCodeDTO;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.params.RegistrationCodeParam;
import run.halo.app.model.support.BaseResponse;
import run.halo.app.service.RegistrationCodeService;

/**
 * 注册码
 *
 * @author lywq
 * @date 2023/06/14 10:29
 **/
@Slf4j
@RestController("ApiContentRegistrationCodeController")
@RequestMapping("/api/content/registrationCode")
public class RegistrationCodeController {

    private final RegistrationCodeService registrationCodeService;

    public RegistrationCodeController(RegistrationCodeService registrationCodeService) {
        this.registrationCodeService = registrationCodeService;
    }


    /**
     * 生成注册码
     *
     * @param registrationCodeParam
     * @return run.halo.app.model.dto.RegistrationCodeDTO
     * @author lywq
     * @date 2023/06/14 10:45
     **/
    @PostMapping("create")
    @ApiOperation("Creates a registrationCode")
    public RegistrationCodeDTO createBy(
        @RequestBody @Valid RegistrationCodeParam registrationCodeParam) {
        RegistrationCode createdRegistrationCode =
            registrationCodeService.createBy(registrationCodeParam);
        return registrationCodeService.convertTo(createdRegistrationCode);
    }

    /**
     * 检查注册码
     *
     * @param code
     * @return run.halo.app.model.support.BaseResponse<java.lang.Object>
     * @author lywq
     * @date 2023/06/14 17:13
     **/
    @GetMapping("check/{code}")
    @ApiOperation("Check a registrationCode")
    public BaseResponse<Object> checkRegistrationCode(@PathVariable("code") String code) {
        boolean checkRegistrationCode = registrationCodeService.checkRegistrationCode(code);
        return BaseResponse.ok(checkRegistrationCode);
    }

}

11、CleanRegistrationCodeTask:run.halo.app.task.CleanRegistrationCodeTask

package run.halo.app.task;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import run.halo.app.model.entity.RegistrationCode;
import run.halo.app.model.enums.RegistrationCodeStatus;
import run.halo.app.service.RegistrationCodeService;
import run.halo.app.utils.DateTimeUtils;

/**
 * 清理过期注册码
 *
 * @author lywq
 * @date 2023/06/16 11:22
 **/
@Slf4j
@Component
public class CleanRegistrationCodeTask {

    private final RegistrationCodeService registrationCodeService;

    public CleanRegistrationCodeTask(RegistrationCodeService registrationCodeService) {
        this.registrationCodeService = registrationCodeService;
    }


    @Scheduled(cron = "0 */1 * * * ?")
    public synchronized void clean() {
        List<RegistrationCode> registrationCodes =
            registrationCodeService.listAllBy(RegistrationCodeStatus.NOT_USED);

        LocalDateTime now = LocalDateTime.now();
        List<RegistrationCode> registrationCodeUpdateList =
            registrationCodes.stream().filter(registrationCode -> {
                LocalDateTime createTime =
                    DateTimeUtils.toLocalDateTime(registrationCode.getCreateTime());
                long until = createTime.until(now, ChronoUnit.MINUTES);
                if (until >= 5) {
                    registrationCode.setStatus(RegistrationCodeStatus.INVALIDATED);
                    return true;
                } else {
                    return false;
                }
            }).collect(Collectors.toList());

        if (CollectionUtils.isEmpty(registrationCodeUpdateList)) {
            return;
        }

        log.info("开始清理过期注册码");
        registrationCodeService.updateInBatch(registrationCodeUpdateList);
    }
}

12、HtmlUtil:run.halo.app.utils.HtmlUtil

package run.halo.app.utils;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.util.StringUtils;

/**
 * @author yadong.zhang email:yadong.zhang0415(a)gmail.com
 * @version 1.0
 * @website https://docs.zhyd.me
 * @date 2018/1/19 10:32
 * @since 1.0
 */
@Slf4j
public class HtmlUtil {

    /**
     * 获取Element
     *
     * @param htmlDocument
     * @param id
     * @return
     */
    public static Element getElementById(Document htmlDocument, String id) {
        if (htmlDocument == null || id == null || id.equals("")) {
            return null;
        }
        return htmlDocument.getElementById(id);
    }

    /**
     * 替换所有标签
     *
     * @param content
     * @return
     */
    public static String html2Text(String content) {
        if (StringUtils.hasText(content)) {
            return "";
        }
        // 定义HTML标签的正则表达式
        String regEx_html = "<[^>]+>";
        content = content.replaceAll(regEx_html, "").replaceAll(" ", "");
        content = content.replaceAll("&quot;", "\"")
            .replaceAll("&nbsp;", "")
            .replaceAll("&amp;", "&")
            .replaceAll("\n", " ")
            .replaceAll("&#39;", "\'")
            .replaceAll("&lt;", "<")
            .replaceAll("&gt;", ">")
            .replaceAll("javascript:", "")
            .replaceAll("[ \\f\\t\\v]{2,}", "\t");

        String regEx = "<.+?>";
        Pattern pattern = Pattern.compile(regEx);
        Matcher matcher = pattern.matcher(content);
        content = matcher.replaceAll("");
        return content.trim();
    }


    /**
     * 获取html内容
     *
     * @param url
     * @return java.lang.String
     * @author WED
     * @date 2022/12/05 18:56
     **/
    public static String getHtml(String url) {
        String html = null;
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet(url);
        try {
            get.setHeader("Cache-Control", "no-cache, must-revalidate");
            get.addHeader("Accept",
                "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
            get.addHeader("User-Agent",
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.81");
            get.setHeader("cookie",
                "bid=37jvRVbkt74; douban-fav-remind=1; ll=\"108288\"; ap_v=0,6.0; _pk_ref.100001.3ac3=%5B%22%22%2C%22%22%2C1615123876%2C%22https%3A%2F%2Fwww.douban.com%2Fmisc%2Fsorry%3Foriginal-url%3Dhttps%253A%252F%252Fbook.douban.com%252Fpeople%252F220804943%252Fcollect%22%5D; _pk_id.100001.3ac3=30dea2f8cdcf0fe9.1615123876.1.1615123876.1615123876.; _pk_ses.100001.3ac3=*; regpop=1");
            CloseableHttpResponse response = client.execute(get);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                html = EntityUtils.toString(entity);
            }
            EntityUtils.consume(entity);
        } catch (IOException e) {
            log.error("html client.execute(get) error [" + e + "]");
        }
        return html;
    }


    /**
     * url重定向得到真实链接
     *
     * @param url
     * @return url
     */
    private String reUrl(String url) {

        String html = null;
        try {
            Connection.Response response =
                Jsoup.connect(url).timeout(3000).method(Connection.Method.GET)
                    .followRedirects(false).execute();
            html = response.header("Location");
            return html;
        } catch (Exception e) {
            log.error("html reUrl Jsoup.connect(url) error [" + e + "]");
        }
        return null;
    }


    /**
     * 截取指定长度的html代码并保证html标签完整
     *
     * @param html
     * @return java.lang.String
     * @author lywq
     * @date 2023/07/20 11:03
     **/
    public static String trimHtml(String html) {
        Document doc = Jsoup.parse(html);
        Element body = doc.body();

        // 递归遍历HTML节点,保留最多maxLength个字符
        int maxLength = (int) Math.ceil(body.text().length() * 0.2); // 获取前20%的长度
        trimElement(body, maxLength);

        return body.children().toString();
    }

    public static String trimString(String str) {
        int maxLength = (int) Math.ceil(str.length() * 0.2); // 获取前20%的长度
        return str.substring(0, maxLength);
    }


    /**
     * 移除html元素
     *
     * @param element
     * @param maxLength
     * @return void
     * @author lywq
     * @date 2023/07/20 11:04
     **/
    private static void trimElement(Element element, int maxLength) {
        int length = 0;
        int endIndex = 0;
        int currentIndex = 0;

        for (Element child : element.children()) {
            length += child.text().length();
            currentIndex += 1;

            if (length > maxLength && currentIndex - 1 > endIndex) {
                child.remove();
            } else if (length <= maxLength) {
                endIndex += 1;
            }
        }
    }

}

以上是需要新增的12个类,到这里恭喜你已经完成一小半啦,下面进行其他类源代码的修改

1、PostModel:run.halo.app.controller.content.model.PostModel

实现服务端限制文章内容,修改以下代码块,大概位置150-155

原代码:

        if (StringUtils.isNotBlank(token)) {
            postDetail = postRenderAssembler.convertToPreviewDetailVo(post);
            model.addAttribute("post", postDetail);
        } else {
            postDetail = postRenderAssembler.convertToDetailVo(post);
            model.addAttribute("post", postDetail);
        }

修改为:

        if (StringUtils.isNotBlank(token)) {
            postDetail = postRenderAssembler.convertToPreviewDetailVo(post);
        } else {
            postDetail = postRenderAssembler.convertToDetailVo(post);
        }

        //文章验证码校验
        String prefix = Objects.requireNonNull(model.getAttribute("prefix")).toString();
        if (optionService.getArchivesPrefix().equals(prefix) &&
            Objects.equals(post.getTemplate(), "validation") &&
            !postAuthentication.isAuthenticated(post.getId())) {
            //设置总文字的10%可见
            postDetail.setOriginalContent(HtmlUtil.trimString(postDetail.getOriginalContent()));
            postDetail.setContent(HtmlUtil.trimHtml(postDetail.getContent()));
            model.addAttribute("slug", post.getSlug());
            model.addAttribute("show_validation", "true");
        } else {
            model.addAttribute("show_validation", "false");
        }
        model.addAttribute("post", postDetail);

2、ContentAuthenticationManager:run.halo.app.controller.content.auth.ContentAuthenticationManager

实现验证码校验及校验结果存放session,新增以下代码块,不要忘了注入RegistrationCodeService

   public ContentAuthentication verifyPassword(ContentAuthenticationRequest authRequest) throws
        AuthenticationException {
        if (EncryptTypeEnum.POST.getName().equals(authRequest.getPrincipal())) {
            return verifyPostPassword(authRequest);
        }
        throw new NotFoundException(
            "Could not be found suitable authentication processor for ["
                + authRequest.getPrincipal() + "]");
    }
    /**
     * 校验文章验证码
     *
     * @param authRequest
     * @return run.halo.app.controller.content.auth.PostAuthentication
     * @author lywq
     * @date 2023/07/20 16:28
     **/
    private PostAuthentication verifyPostPassword(ContentAuthenticationRequest authRequest) {

        if (StringUtils.isNotBlank(authRequest.getPassword())) {
            boolean checkRegistrationCode =
                registrationCodeService.checkRegistrationCode(authRequest.getPassword());
            if (checkRegistrationCode) {
                postAuthentication.setAuthenticated(authRequest.getId(), true);
                return postAuthentication;
            } else {
                throw new AuthenticationException("密码不正确");
            }
        } else {
            throw new AuthenticationException("密码不正确");
        }
    }

3、PostAuthentication:run.halo.app.controller.content.auth.PostAuthentication

修改以下代码块,大概位置50-52

原代码:

        if (!isPrivate(post)) {
            return true;
        }

修改为:

        if (!isPrivate(post) && !Objects.equals(post.getTemplate(), "validation")) {
            return true;
        }

以上就是所有的后端代码修改,现在我们来修改前端代码

二、前端修改部分

1、新增文章验证码模板post_validation.ftl,位置放在主题文件根目录,与settings.yaml同目录

<!DOCTYPE html>
<html lang="zh-CN">
<#import "template/common/header.ftl" as headInfo>
<@headInfo.head title="${post.title!}" type="post"/>
<#import "template/macro/tail.ftl" as tailInfo>
<body>
<div id="Joe">
  <#include "template/common/navbar.ftl">
  <#include "template/module/post_bread.ftl">
  <div class="joe_container joe_main_container page-post${settings.enable_show_in_up?then(' animated fadeIn','')}">
    <div class="joe_main joe_post">
      <div class="joe_detail" data-status="${post.status!}" data-cid="${post.id?c}" data-clikes="${post.likes?c}"
           data-author="${user.nickname!}">
        <#assign use_raw_content = (metas?? && metas.use_raw_content?? && metas.use_raw_content?trim!='')?then(metas.use_raw_content?trim, 'false')>
        <#include "template/macro/post_status.ftl">
        <@post_status status=post.status />
        <#if post.status=='PUBLISHED' && post.categories?size gt 0>
          <div class="joe_detail__category">
            <#list post.categories as category>
              <a href="${category.fullPath}" class="item item-0"
                 title="${category.name!}">${category.name!}</a>
            </#list>
          </div>
        </#if>
        <#if use_raw_content=='true'>
          <span class="joe_raw" title="原始内容"></span>
        </#if>
        <div class="joe_detail-wrapper">
          <h1 class="joe_detail__title${settings.enable_title_shadow?string(' txt-shadow', '')}">${post.title!}</h1>
          <#assign enable_page_meta = (metas?? && metas.enable_page_meta?? && metas.enable_page_meta?trim!='')?then(metas.enable_page_meta?trim,'true')>
          <#if settings.enable_page_meta && enable_page_meta=='true'>
            <div class="joe_detail__count">
              <div class="joe_detail__count-information">
                <img width="35" height="35" class="avatar lazyload" src="${settings.lazyload_avatar!}"
                     data-src="${USER_AVATAR}" onerror="Joe.errorImg(this)" alt="${user.nickname!}">
                <div class="meta">
                  <div class="author">
                    <a class="link" href="${blog_url}/about"
                       title="${user.nickname!}">${user.nickname!}</a>
                  </div>
                  <div class="item">
                    <span class="text">${post.createTime?string('yyyy-MM-dd')}</span>
                    <span class="line">/</span>
                    <span class="text">${post.commentCount} 评论</span>
                    <span class="line">/</span>
                    <span class="text">${post.likes} 点赞</span>
                    <span class="line">/</span>
                    <span class="text">${post.visits} 阅读</span>
                    <span class="line">/</span>
                    <span class="text">${post.wordCount!0} 字</span>
                    <#assign enable_collect_check = (metas?? && metas.enable_collect_check?? && metas.enable_collect_check?trim!='')?then(metas.enable_collect_check?trim,'true')>
                    <#if post.status=='PUBLISHED' && settings.check_baidu_collect==true && enable_collect_check == 'true'>
                      <span class="line">/</span>
                      <#include "template/module/baidu_push.ftl">
                    </#if>
                  </div>
                </div>
              </div>
              <time class="joe_detail__count-created"
                    datetime="${post.createTime?string('MM/dd')}">${post.createTime?string('MM/dd')}</time>
            </div>
          </#if>
          <div class="joe_detail__overdue">
            <#assign enable_passage_tips = (metas?? && metas.enable_passage_tips?? && metas.enable_passage_tips?trim!='')?then(metas.enable_passage_tips?trim,'true')>
            <#if settings.enable_passage_tips && enable_passage_tips == 'true'>
              <div class="joe_detail__overdue-wrapper">
                <div class="title">
                  <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"
                       width="20" height="20">
                    <path d="M0 512c0 282.778 229.222 512 512 512s512-229.222 512-512S794.778 0 512 0 0 229.222 0 512z"
                          fill="#FF8C00" fill-opacity=".51"/>
                    <path d="M462.473 756.326a45.039 45.039 0 0 0 41.762 28.74 45.039 45.039 0 0 0 41.779-28.74h-83.541zm119.09 0c-7.73 35.909-39.372 62.874-77.311 62.874-37.957 0-69.598-26.965-77.33-62.874H292.404a51.2 51.2 0 0 1-42.564-79.65l23.723-35.498V484.88a234.394 234.394 0 0 1 167.492-224.614c3.635-31.95 30.498-56.815 63.18-56.815 31.984 0 58.386 23.808 62.925 54.733A234.394 234.394 0 0 1 742.093 484.88v155.512l24.15 36.454a51.2 51.2 0 0 1-42.668 79.48H581.564zm-47.957-485.922c.069-.904.12-1.809.12-2.73 0-16.657-13.26-30.089-29.491-30.089-16.214 0-29.474 13.432-29.474 30.089 0 1.245.085 2.491.221 3.703l1.81 15.155-14.849 3.499a200.226 200.226 0 0 0-154.265 194.85v166.656l-29.457 44.1a17.067 17.067 0 0 0 14.182 26.556h431.155a17.067 17.067 0 0 0 14.234-26.487l-29.815-45.04V484.882A200.21 200.21 0 0 0 547.26 288.614l-14.985-2.986 1.331-15.224z"
                          fill="#FFF"/>
                    <path d="M612.864 322.697c0 30.378 24.303 55.022 54.272 55.022 30.003 0 54.323-24.644 54.323-55.022 0-30.38-24.32-55.023-54.306-55.023s-54.306 24.644-54.306 55.023z"
                          fill="#FA5252"/>
                  </svg>
                  <span class="text">温馨提示:</span>
                </div>
                <div class="content">
                  <#if settings.passage_tips_content?? && settings.passage_tips_content!=''>
                    ${settings.passage_tips_content}
                  <#else>
                    本文最后更新于 ${post.updateTime?string('yyyy-MM-dd')},若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
                  </#if>
                </div>
              </div>
            </#if>
          </div>
          <#import "template/ads/ads_post.ftl" as adpost>
          <@adpost.ads_post type="top" />
          <#assign enable_copy = (metas?? && metas.enable_copy?? && metas.enable_copy?trim!='')?then(metas.enable_copy?trim,'true')>
          <#assign img_align = (metas?? && metas.img_align?? && metas.img_align?trim!='')?then(metas.img_align?trim,settings.post_img_align!'center')>
          <#assign enable_read_limit = (metas?? && metas.enable_read_limit?? && metas.enable_read_limit?trim!='')?then(metas.enable_read_limit?trim,'false')>
          <article
                  class="joe_detail__article animated fadeIn ${img_align+'-img'}${(enable_read_limit=='true')?then(' limited','')}${(enable_copy!='true' || settings.enable_copy!=true)?then(' uncopy', '')}${settings.enable_indent?then(' indent','')}${(settings.enable_code_line_number==true && settings.enable_code_newline!=true)?then(' line-numbers','')}${settings.enable_single_code_select?then(' single_code_select','')}">
            <div id="post-inner">
              <#if use_raw_content == 'false'>
                ${post.formatContent!}
              <#else>
                <joe-raw-content>
                  <div id="_raw">${post.formatContent!}</div>
                </joe-raw-content>
              </#if>
            </div>
            <#if show_validation == 'true'>
              <div class="lywq_post_read_limited">
                <form id="verifyForm" class="form" onsubmit="return false">
                  <span></span> <span></span> <span></span> <span></span>
                  <div class="form-inner">
                    <div class="content">
                      <label><p class="text">扫码获取密码</p></label>
                      <div class="image"></div>
                      <label>
                        <input class="input" id="password" name="password" type="text"
                               placeholder="请输入访问密码"/>
                      </label>
                      <button class="btn" type="submit" onclick="verifyPassword()">验证</button>
                    </div>
                  </div>
                </form>
              </div>

            </#if>
          </article>
          <#assign enable_like = (metas?? && metas.enable_like?? && metas.enable_like?trim!='')?then(metas.enable_like?trim,'true')>
          <#if enable_like=='true' && settings.enable_like==true && post.status!='DRAFT'>
            <#import "template/module/favorite.ftl" as nsp>
            <@nsp.favorite post=post type="bottom" />
          </#if>
        </div>
        <#include "template/module/post_operate.ftl">
        <#import "template/macro/post_copyright.ftl" as pc>
        <@pc.post_copyright post_url="${post.fullPath}"/>
        <@adpost.ads_post type="bottom" />
      </div>
      <#include "template/module/post_operate_aside.ftl">
      <ul class="joe_post__pagination">
        <#if prevPost??>
          <li class="joe_post__pagination-item prev"><a href="${prevPost.fullPath!}"
                                                        title="${prevPost.title!}">上一篇:<span>${prevPost.title!}</span></a>
          </li>
        </#if>
        <#if nextPost??>
          <li class="joe_post__pagination-item next"><a href="${nextPost.fullPath!}"
                                                        title="${nextPost.title!}">下一篇:<span>${nextPost.title!}</span></a>
          </li>
        </#if>
      </ul>
      <#assign enable_comment = (metas?? && metas.enable_comment?? && metas.enable_comment?trim!='')?then(metas.enable_comment?trim,'true')>
      <#if settings.enable_clean_mode!=true && settings.enable_comment==true && post.status!='DRAFT'>
        <div class="joe_comment">
          <#if post.disallowComment == true || enable_comment == 'false'>
            <div class="joe_comment__close">
              <svg class="joe_comment__close-icon" viewBox="0 0 1024 1024"
                   xmlns="http://www.w3.org/2000/svg" width="18" height="18">
                <path d="M512.307.973c282.317 0 511.181 201.267 511.181 449.587a402.842 402.842 0 0 1-39.27 173.26 232.448 232.448 0 0 0-52.634-45.977c16.384-39.782 25.293-82.688 25.293-127.283 0-211.098-199.117-382.157-444.621-382.157-245.555 0-444.57 171.06-444.57 382.157 0 133.427 79.514 250.88 200.039 319.18v107.982l102.041-65.127a510.157 510.157 0 0 0 142.49 20.122l19.405-.359c19.405-.716 38.758-2.508 57.958-5.427l3.584 13.415a230.607 230.607 0 0 0 22.323 50.688l-20.633 3.328a581.478 581.478 0 0 1-227.123-12.288L236.646 982.426c-19.66 15.001-35.635 7.168-35.635-17.664v-157.39C79.411 725.198 1.024 595.969 1.024 450.56 1.024 202.24 229.939.973 512.307.973zm318.464 617.011c97.485 0 176.794 80.435 176.794 179.2S928.256 976.23 830.77 976.23c-97.433 0-176.742-80.281-176.742-179.046 0-98.816 79.309-179.149 176.742-179.149zM727.757 719.002a131.174 131.174 0 0 0-25.754 78.182c0 71.885 57.805 130.406 128.768 130.406 28.877 0 55.552-9.625 77.056-26.01zm103.014-52.327c-19.712 0-39.117 4.557-56.678 13.312L946.33 854.58c8.499-17.305 13.158-36.864 13.158-57.395 0-71.987-57.805-130.509-128.717-130.509zM512.307 383.13l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072 67.072 67.072 0 0 1 66.662-67.43zm266.752 0l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072h-.051l.307-6.86a67.072 67.072 0 0 1 66.406-60.57zm-533.504 0l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072 67.072 67.072 0 0 1 66.662-67.43z"/>
              </svg>
              <span>博主关闭了当前页面的评论</span>
            </div>
          <#else>
            <#include "template/macro/comment.ftl">
            <@comment target=post type="post"/>
          </#if>
        </div>
      <#else>
        <div class="joe_comment">
          <div class="joe_comment__close">
            <svg class="joe_comment__close-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"
                 width="18" height="18">
              <path d="M512.307.973c282.317 0 511.181 201.267 511.181 449.587a402.842 402.842 0 0 1-39.27 173.26 232.448 232.448 0 0 0-52.634-45.977c16.384-39.782 25.293-82.688 25.293-127.283 0-211.098-199.117-382.157-444.621-382.157-245.555 0-444.57 171.06-444.57 382.157 0 133.427 79.514 250.88 200.039 319.18v107.982l102.041-65.127a510.157 510.157 0 0 0 142.49 20.122l19.405-.359c19.405-.716 38.758-2.508 57.958-5.427l3.584 13.415a230.607 230.607 0 0 0 22.323 50.688l-20.633 3.328a581.478 581.478 0 0 1-227.123-12.288L236.646 982.426c-19.66 15.001-35.635 7.168-35.635-17.664v-157.39C79.411 725.198 1.024 595.969 1.024 450.56 1.024 202.24 229.939.973 512.307.973zm318.464 617.011c97.485 0 176.794 80.435 176.794 179.2S928.256 976.23 830.77 976.23c-97.433 0-176.742-80.281-176.742-179.046 0-98.816 79.309-179.149 176.742-179.149zM727.757 719.002a131.174 131.174 0 0 0-25.754 78.182c0 71.885 57.805 130.406 128.768 130.406 28.877 0 55.552-9.625 77.056-26.01zm103.014-52.327c-19.712 0-39.117 4.557-56.678 13.312L946.33 854.58c8.499-17.305 13.158-36.864 13.158-57.395 0-71.987-57.805-130.509-128.717-130.509zM512.307 383.13l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072 67.072 67.072 0 0 1 66.662-67.43zm266.752 0l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072h-.051l.307-6.86a67.072 67.072 0 0 1 66.406-60.57zm-533.504 0l6.861.358a67.072 67.072 0 0 1 59.853 67.072l-.307 6.86a67.072 67.072 0 0 1-66.407 60.57l-6.81-.358a67.072 67.072 0 0 1-59.852-67.072 67.072 67.072 0 0 1 66.662-67.43z"/>
            </svg>
            <span>${(post.status=='DRAFT')?then('预览状态下不可评论','博主关闭了所有页面的评论')}</span>
          </div>
        </div>
      </#if>
    </div>
    <#assign enable_aside = (metas?? && metas.enable_aside?? && metas.enable_aside?trim!='')?then(metas.enable_aside?trim,'true')>
    <#if settings.enable_post_aside == true && enable_aside == 'true'>
      <#include "template/common/aside_post.ftl">
    </#if>
  </div>
  <#if settings.enable_progress_bar!true>
    <div class="joe_progress_bar" ${(settings.progress_bar_bgc?? && settings.progress_bar_bgc!='')?then('style="background:${settings.progress_bar_bgc}"','')}></div>
  </#if>
  <#include "template/common/actions.ftl">
  <#include "template/common/footer.ftl">
</div>
<@tailInfo.tail type="post"/>
<script type="text/javascript">
  function verifyPassword() {

    const password = $('#password').val();
    if (password) {
      Utils.request({
        url: `/api/content/posts/verifyPassword/${slug!}`,
        method: "POST",
        returnRaw: true,
        data: $('#verifyForm').serialize(),
      }).then((res) => {
        if (res.data && res.data === true) {
          Qmsg.success(res.message);
          window.location.reload();
        } else {
          Qmsg.error("验证失败!");
        }
      }).catch((err) => {
        console.log(err);
      });
    } else {
      Qmsg.error("请输入密码!");
    }

  }
</script>
</body>
</html>

2、编写样式代码,可以放在后台主题设置自定义css里面,也可以自己新建一个css文件引入即可

/*文章验证*/
.lywq_post_read_limited {
    position: absolute;
    bottom: 0;
    z-index: 5;
    width: 100%;
    height: 180px;
    text-align: center;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    line-height: 146px;
    background: linear-gradient(0deg, var(--background) 30px, transparent 180px)
}

.lywq_post_read_limited p {
    display: inline-block;
    padding: 26px;
    line-height: 1.5;
    color: var(--minor);
    background: rgba(255, 255, 255, .76);
    border-radius: 5px;
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    border: 1px solid var(--classA);
    -webkit-box-shadow: 13px 16px 8px -3px var(--shadow-code);
    box-shadow: 13px 16px 8px -3px var(--shadow-code)
}

.lywq_post_read_limited p:hover {
    -webkit-animation: dung 1s ease;
    animation: dung 1s ease
}

.lywq_post_read_limited__button {
    position: relative;
    font-style: normal;
    cursor: pointer;
    color: var(--theme)
}

我的github地址放在这里啦,感兴趣的也可以去看看我的其他修改。

License:  CC BY 4.0