删除业务与鉴权

前往原站点查看

2022-04-16 00:55:54

    今天主要完成的是动态、专辑与图片的基本删除能力,其中包含了前端右键菜单设计以及导出功能的完善。(专辑=相册)

后端的业务实现

删除动态

    这个的实现最为简单,没有上面特别需要注意的地方,所以不多赘述。

删除专辑

    删除专辑中,由于有设定数据库中前三个专辑id的默认匹配规则(自动、博客、动态),所以在删除的时候给了id>3的约束,以免误删。同时,在删完该专辑确认成功后,才能继续将该专辑内的图片记录也都删除,否则除非事务回退,不然先删除图片记录是不可逆的,所以必须先进行专辑删除状态的确认才能删除图片。

删除图片

    删除图片的业务也相对没有什么特别的,只需要注意不要删除id=1的记录即可,因为id=1的图片记录将作为专辑的封面。

参数的注意点

    今天在控制器的一个方法参数中,使用Integer作为请求参数类型获取id,但是发现此时如果用户没有传递id值,Integer作为引用类型也会默认赋值null,而不会被报错,需要注意!所以如果该参数是必须的,还是应该设置为int类型的(仅控制器,因为控制器能通过说明必然有值了)。

前端的业务实现

事件对象函数的进一步说明

    昨天有说到需要获取事件的对象时,调用函数不带(),那么默认的第一个参数即为事件对象。

    但是今天遇到了既要传递参数又要对象的情况,这种情况的方式也很简单,传递参数的时候,第一个参数设置为$event即可。

动态页上图

    想来想去,最后还是决定动态也用博客页的编辑模板进行编辑,自我约束动态页格式不要太花里胡哨的就好了,原本其实是打算像QQ空间一样的上图设计方式,但是,嗯,感觉那样的方式不如直接用博客页编辑模板来的方便和自由,所以最后动态页上图也就简单的改一下模板就完成了。

前台回忆页图片展示

    前台回忆页的图片细节展示也摸了,直接利用后台查看图片的模板,稍作修改就完成了,这叫提高开发效率,绝对不是偷懒!一寸光阴一寸金,寸金难买寸光阴!

编辑页的文本导出

    为什么要导出文本编辑的内容?那当然是防止突然断网啊、突然缺少灵感啊什么不得不停笔的情况了。

    你绝对想象不到这篇博客之前版本的我是怎么导出备份的!(输出到控制台,手动复制粘贴到文件QAQ)

    经过一些资料的查询后,得到了最终的解决方式,那就是a链接下载,a链接有download属性,表示的是下载的文件名,href则是目标地址。不过对于则个目标地址,学问可太大了。

const myBlob = new Blob([this.editor.txt.html()])

const toDownload = document.createElement('a')

toDownload.download = 'log' + this.$time() + '.txt'
toDownload.href = URL.createObjectURL(myBlob)
toDownload.click()
document.body.removeChild(myBlob)

    首先是将需要保存的数据内容封装成blob类型,需要传递的是数组,所以要用[]括起来。然后利用URL.createObjectURL(Blob)将数据对象创建仅限当前页的URL路径,可以给a链接href赋值,之后调用a链接的click函数触发点击事件。最后将这个url创建出来的blob节点给删除掉。

后台动态删除

    后台动态的删除和后端的一样非常简单,不多赘述。

图片与专辑的删除

    这个稍微设计了一活儿,主要就是设计了一个右键弹出菜单的效果,利用@click.right.prevent来禁用原来的效果。目前设计的比较简单,或者说后台也就只有我能看,所以我能看的懂就行,当然以后从业时不能有这种想法,因为那时,我做出来的就是给别人的了。

    先来上图(实现了上传图片功能就是拽啊,随手上图( ̄y▽, ̄)╭ )[我是绝对不会说我在上传这张图时遇到了bug的,哼哧哼哧]

    怎样,是不是很简单的菜单实现,当完美右击指定专辑或者图片的时候,就获取clientX/Y并且记录是专辑类型还是图片类型以及其id号,然后将这个绝对定位的菜单的top和left对应设置就好了。然后全局加个click监听,当全局收到click时,这个菜单的v-show就设置为false,用来灵活的进行唤出。

html

<div class="menu" ref="menu" v-show="showMenu">
  <p>编辑</p>
  <p @click="delTarget">删除</p>
  <p>取消</p>
</div>

scss

.menu{
  position:absolute;
  width:100px;
  background-color:white;
  text-align: center;
  border-radius: 6px;
  cursor: default;
  p{
    border-bottom: 1px solid gray;
    padding: 2px;
  }
}

js

targetDeal (e, id, isAlbum) {
  this.targetMenuAlbum = isAlbum
  this.targetMenuId = id
  const node = this.$refs.menu
  node.style.top = e.clientY + 'px'
  node.style.left = e.clientX + 'px'
  this.showMenu = true
}

鉴权

    拖更到今天终于到鉴权了,之前没写是因为那时候这个博客栏目还没有做好,现在做好了所以补录一下。

    鉴权当然有着很多个板块,大的方向来说就是前端管理员页面鉴权。后端对一些需要鉴权的api请求拦截,以及图床请求需要AccessToken。下面来一个一个说。

图床鉴权

1. 签名

    签名的算法如下 signStr = 请求URI + 请求参数 + "\n" + 请求体(没有请求体也要 "" 貌似,看图床的api这样说的)

    签名 (sign)16 = hmac_sha1(signStr, <YOUR_SECRET_KEY>)

    注意:请求体千万注意每个字节都要,如果直接getBytes()然后转String,那么一些隐藏的字符被忽略,并且结果数据内容也完全不对,这样是无法通过签名验证的!

2. accessToken

    结果 accessToken = "TOKEN " + <YOUR_ACCESS_KEY> + ":" + (sign)16

    实际的java算法示例:

=> (byte[] bytes,String secretKey,String apiPath)

String signStr = apiPath + "\n";
ByteArrayOutputStream bao = new ByteArrayOutputStream();
bao.write(signStr.getBytes());
bao.write(bytes);
byte[] res = bao.toByteArray();
bao.close();

String sign = "";
try {
	Mac mac = Mac.getInstance("HmacSHA1");
	mac.init(new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"));
	sign = new String(new Hex().encode(mac.doFinal(res)), StandardCharsets.UTF_8);
	// 这里 Hex 来自 org.apache.commons.codec.binary.Hex
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
	e.printStackTrace();
	throw new RuntimeException(e.getMessage());
}
String authorization = "TOKEN " + accessKey + ':' + sign;

    之后每当发送给服务方需要鉴权的api请求时,带上Authrization请求头即可达成通信(代码有借鉴与更改)。

后端请求鉴权

   后端请求鉴权的方式熟悉拦截器那就非常简单了。

首先是产生token

@Override
public RetResult&lt;String&gt; checkPassword(String username, String password) {
  ValueOperations op = redisTemplate.opsForValue();
  
  String admin = (String) op.get("admin");
  String pass = (String) op.get("password");
  
  if(admin == null || pass == null) return RetResult.notFound();
  else if (!admin.equals(username)) return RetResult.fail("用户不正确");
  else if (!pass.equals(password)) return RetResult.fail("密码不正确");
  else{
  	String token = Md5String.md5WithTime(username);
  	op.set("token",token,10, TimeUnit.DAYS);
  	return RetResult.success(token);
  }
}

其中Md5String是自己定义的工具类

package top.dreamcenter.dreamcenter.utils;

import org.apache.tomcat.util.security.MD5Encoder;
import org.springframework.util.DigestUtils;
import sun.security.provider.MD5;

public class Md5String {
    public static String md5WithTime(String src){
        src = src + System.currentTimeMillis();
        return DigestUtils.md5DigestAsHex(src.getBytes());
    }
}


拦截器如下

package top.dreamcenter.dreamcenter.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import top.dreamcenter.dreamcenter.ret.RetResult;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class AdminAuthInterceptor implements HandlerInterceptor {

    private RedisTemplate template;

    @Autowired
    public AdminAuthInterceptor(RedisTemplate template) {
        this.template = template;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        String token = (String) template.opsForValue().get("token");
        if(authorization!=null && authorization.equals(token)) return true;

        response.setContentType("application/json;charset=utf8");

        ObjectMapper mapper = new ObjectMapper();
        String res = mapper.writeValueAsString(RetResult.denied());
        response.getWriter().write(res);

        return false;
    }
}

之后给容器配置添加完美的拦截器即可

package top.dreamcenter.dreamcenter.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.dreamcenter.dreamcenter.interceptor.AdminAuthInterceptor;

@Configuration
public class ToolsConfig implements WebMvcConfigurer{

    private RedisTemplate redisTemplate;

    @Autowired
    public ToolsConfig(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AdminAuthInterceptor(redisTemplate))
                .addPathPatterns("/api/info/**")
                .addPathPatterns("/api/*/add")
                .addPathPatterns("/api/*/delete")
                .addPathPatterns("/api/image/upload");
    }

}


前端管理员页鉴权

   这个部分有一说一还是错综复杂的,因为需要考虑的东西相对来说比较多。

   假如我们登录通过鉴权获得token后,我默认的设置会保存在session中,也就是当前会话,但是如果选择了记住密码,那么肯定是要保存在cookie中的,这样,下一次打开浏览器,到我们的domain域下,就会自动的从cookie中获取保存的token并且放置到session中,当然是放到session中啦,因为session会比cookie来的更及时一点,比如说更改密码,也是session最先奏效,而cookie是否更改和续期则要取决于需求了,而且统一调用session会比一活儿调用session(无cookie有session时)一活儿调用cookie来的要合适,个人理解。之后我们已有token于session中后,可以选择性的请求头携带Authorization,也可以干脆每个请求头都带,需要注意的是,如果每个都带,那么无需鉴权的请求也带,从某些方面来说,有一部分数据的冗余,增加了流量消耗。

    好了,有了鉴权后如何前端如何拦截未通过用户进入管理员页面,当然可以用路由守卫,不过我则是在beforeMount也就是装载之前时期,请求一个需要鉴权的api,如果被拒绝那么就跳转到登录页或者模式。

    下面是一些前端鉴权代码的方向:

给每个请求带Authorization

axios.interceptors.request.use((config) => {
  const token = sessionStorage.getItem('token')
  if (token) config.headers.Authorization = token
  return config
})

借着在App.vue中初始化挂载节点时查看是否有token的cookie

beforeMount () {
  const token = this.$cookie.get('token')
  if (token) sessionStorage.token = token
}

    

   突然发现前端鉴权虽然描述了一大堆,但是实际上却只有一丁点需要分享的23333,而后端没啥描述却有一堆需要分享的代码,也许这也暗示了:前端如美丽动人的妹子,后端如无人问津的死肥宅了吧!(我自己说的,我决定把这句话记入我的名言之一!我可太牛了,随随便便就能说出名言不是)



    好啦!今天的分享也结束了,算是提起那完成了不少工作?明后两天需要完成各项编辑业务以及一则统计业务,不知道又会有什么面对着我呢!

    对了,明天把后台博客页tag标签页添加的效果代码也分享一下,感觉自己设计的用起来蛮舒服的。





上一篇: 优化图片存储并前台展示
下一篇: 访问记录和回忆相片修改