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<String> 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标签页添加的效果代码也分享一下,感觉自己设计的用起来蛮舒服的。