你的浏览器不支持canvas

做你害怕做的事情,然后你会发现,不过如此。

责任链模式实际应用,实现文档分享鉴权

时间: 作者: 黄运鑫

本文章属原创文章,未经作者许可,禁止转载,复制,下载,以及用作商业用途。原作者保留所有解释权。


应用场景

  • 用户生成文章的分享链接,可以把链接分享给其他人,其他人可以通过链接访问文章
  • 用户创建分享链接时可以设置访问权限:
    • 开启/关闭分享链接(关闭后链接失效)
    • 是否互联网公开(公开后不校验登录)
    • 是否需要访问密码(设置密码后,访问时需要输入密码)

实现逻辑

  • 用户打开分享链接后,前端调用接口对链接进行校验
  • 后端接口根据链接的权限设置,使用责任链模式校验,责任链的每一个Handler负责校验一项权限
  • 后端接口返回状态码,前端根据状态码执行相应的动作(跳登录页、提示输入密码、打开文章等)
  • 后端接口校验成功后返回token,前端携带此token访问文章信息接口,文章信息接口再对token二次校验是否有效,有效则返回文章信息

实现代码

  • 创建状态码常量ShareChainCode
/**
 * 状态码常量
 */
public interface ShareChainCode {

    /**
     * 分享未开启
     */
    Integer NOT_OPEN = 10001;
    /**
     * 请先登录
     */
    Integer TO_LOGIN = 10002;
    /**
     * 请输入密码
     */
    Integer PASSWORD_IMPORT = 10003;
    /**
     * 密码错误
     */
    Integer PASSWORD_ERROR = 10004;
}
  • 责任链参数类ShareChainDTO,便于以后扩展参数
import com.transnal.modules.share.entity.Share;
import lombok.Data;

import java.io.Serializable;

/**
 * 责任链参数dto
 */
@Data
public class ShareChainDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 分享的设置
     */
    private Share share;
    /**
     * 访问的用户id
     */
    private Long userId;
    /**
     * 用户输入的密码
     */
    private String userKey;
}
  • 创建责任链处理器抽象类ShareHandler,其他责任链处理器必须继承此类
import com.transnal.core.tool.api.R;
import com.transnal.modules.share.dto.ShareChainDTO;

/**
 * 责任链处理器抽象类,其他责任链处理器必须继承此类
 */
public abstract class ShareHandler {
    /**
     * 下一个处理器
     */
    protected ShareHandler nextHandler;

    /**
     * 设置下一个处理器
     */
    public void setNextHandler(ShareHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    /**
     * 执行下一个处理器
     */
    public R verify(ShareChainDTO dto) {
        if (nextHandler != null) {
            //执行下一个处理器
            return nextHandler.execute(dto);
        }
        
        //没有下一个处理器,则说明校验通过,返回成功
        return R.status(true);
    }

    /**
     * 处理器执行内容
     */
    abstract protected R execute(ShareChainDTO dto);
}
  • 校验分享链接是否开启的处理器
import com.transnal.core.tool.api.R;
import com.transnal.modules.share.chain.ShareChainCode;
import com.transnal.modules.share.chain.ShareHandler;
import com.transnal.modules.share.dto.ShareChainDTO;
import com.transnal.modules.share.entity.Share;
import org.springframework.stereotype.Component;

/**
 * 校验分享链接是否开启的处理器
 */
@Component
public class OpenHandler extends ShareHandler {

    /**
     * 第一个处理器覆写verify方法,执行自己的方法
     */
    @Override
    public R verify(ShareChainDTO dto) {
        return this.execute(dto);
    }

    @Override
    protected R execute(ShareChainDTO dto) {
        //分享的设置
        Share share = dto.getShare();
        //校验分享链接是否开启
        if (share == null || !share.getIsOpen()) {
            return R.fail(ShareChainCode.NOT_OPEN, "分享未开启");
        }
        //调用父类的verify,执行下一个责任链
        return super.verify(dto);
    }
}
  • 校验是否互联网公开的处理器
import com.transnal.core.tool.api.R;
import com.transnal.modules.share.chain.ShareChainCode;
import com.transnal.modules.share.chain.ShareHandler;
import com.transnal.modules.share.dto.ShareChainDTO;
import com.transnal.modules.share.entity.Share;
import org.springframework.stereotype.Component;

/**
 * 校验是否互联网公开的处理器
 */
@Component
public class OpenInNetHandler extends ShareHandler {

    @Override
    protected R execute(ShareChainDTO dto) {
        //分享的设置
        Share share = dto.getShare();
        //当前用户id
        Long userId = dto.getUserId();
        //如果不是互联网公开,需要校验登录状态
        if (!share.getOpenInNet()) {
            //校验用户是否登录
            if (userId == null || userId <= 0) {
                return R.fail(ShareChainCode.TO_LOGIN, "请先登录");
            }
        }
        //执行下一个责任链
        return this.verify(dto);
    }
}
  • 校验密码的处理器
import cn.hutool.core.util.StrUtil;
import com.transnal.core.tool.api.R;
import com.transnal.modules.share.chain.ShareChainCode;
import com.transnal.modules.share.chain.ShareHandler;
import com.transnal.modules.share.dto.ShareChainDTO;
import com.transnal.modules.share.entity.Share;
import org.springframework.stereotype.Component;

/**
 * 校验密码的处理器
 */
@Component
public class PasswordHandler extends ShareHandler {

    @Override
    protected R execute(ShareChainDTO dto) {
        //分享的设置
        Share share = dto.getShare();
        //用户输入的密码
        String userKey = dto.getUserKey();
        //校验密码
        if (share.getIsKey()) {
            //校验是否输入密码
            if (StrUtil.isBlank(userKey)) {
                return R.fail(ShareChainCode.PASSWORD_IMPORT, "请输入密码");
            }

            //校验密码是否正确
            if (!userKey.equals(share.getKey())) {
                return R.fail(ShareChainCode.PASSWORD_ERROR, "密码错误");
            }
        }
        //执行下一个责任链
        return this.verify(dto);
    }
}
  • 项目启动后,初始化责任链处理器执行顺序
import com.transnal.core.tool.utils.SpringUtil;
import com.transnal.modules.share.chain.handler.OpenHandler;
import com.transnal.modules.share.chain.handler.OpenInNetHandler;
import com.transnal.modules.share.chain.handler.PasswordHandler;
import lombok.Data;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
 * 项目启动后初始化责任链
 */
@Component
@Data
public class ShareChainPattern implements CommandLineRunner {

    /**
     * 第一个处理器
     */
    private ShareHandler shareHandler;

    @Override
    public void run(String... args) {
        //获取责任链处理器
        OpenHandler openHandler = SpringUtil.getBean(OpenHandler.class);
        OpenInNetHandler openInNetHandler = SpringUtil.getBean(OpenInNetHandler.class);
        PasswordHandler keyHandler = SpringUtil.getBean(PasswordHandler.class);

        //设置处理器的执行顺序
        openHandler.setNextHandler(openInNetHandler);
        openInNetHandler.setNextHandler(keyHandler);
        //设置第一个处理器
        shareHandler = openHandler;
    }
}
  • 接口直接调用ShareChainPattern即可
import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.transnal.core.boot.ctrl.GeccoController;
import com.transnal.core.secure.utils.AuthUtil;
import com.transnal.core.tool.api.R;
import com.transnal.modules.share.chain.ShareChainPattern;
import com.transnal.modules.share.dto.ShareChainDTO;
import com.transnal.modules.share.entity.Share;
import com.transnal.modules.share.service.IShareService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 资源分享 控制器
 */
@RestController
@AllArgsConstructor
@RequestMapping("/share/v1")
public class ShareController extends GeccoController {
    private IShareService shareService;
    private ShareChainPattern shareChainPattern;

    /**
     * 验证权限
     */
    @GetMapping("/verify")
    @ApiOperationSupport(order = 7)
    @ApiOperation(value = "验证权限", notes = "传入wikiid")
    public R verify(@ApiParam(value = "分享的url") @RequestParam String url,
                    @ApiParam(value = "用户输入的密码") String userKey) {
        //查询分享url的设置信息
        Share share = shareService.findOneByUrl(url);
        if (share == null) {
            return R.fail("分享未开启");
        }

        //使用dto封装参数,方便将来扩展参数
        ShareChainDTO dto = new ShareChainDTO();
        //分享设置信息
        dto.setShare(share);
        //当前用户id
        dto.setUserId(AuthUtil.getUserId());
        //用户输入的密码
        dto.setUserKey(StrUtil.trim(userKey));

        //通过责任链校验
        R verify = shareChainPattern.getShareHandler().verify(dto);
        //校验未通过,返回错误信息及状态码
        if (!verify.isSuccess()) {
            return verify;
        }

        //创建加密的token,token包含创建时间、访问的文章id等信息,前端需要携带此token访问文章
        String token = shareService.getToken(share);
        return R.data(token);
    }
}
  • 其中shareService.getToken方法封装了加密token代码;R类封装了接口返回值、返回对象和状态码;代码略

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。