白菜的开发日记3

许久没更博客了,写点白菜最近的进度吧。

整个Web版重构完成,我还在路由器上搭了一个mysql server,然后用mysqldump每天凌晨备份白菜的数据库。

顺手加上了凌晨把获取失败的ID以邮件形式发到自己邮箱,还加了清空当日生成的临时文件。

弱智啪啪啪提了一个req,!sleep 按小时为单位禁言自己,没想到做出来真的一堆弱智用……

日记2里提到的绘图类,被我从静态代码块改成了静态方法,然后在!sudo bg命令时调用它(否则会导致修改用户bg不能马上生效)。

顺手做了一个!fp功能,不过mp4和5没什么人用……

我还研究了一下HTTP Client,做了模拟登陆osu并且下图,还写了解析.osu的正则表达式,来从官网获取BG。

甚至还处理了0,0,”sb\bg.png”这种情况……

在这里吹自己一波,我居然想到了把官网下图时的InputStream包装为ZIPInputStream,直接在内存中解压osz……

写完才发现“卧槽我居然真的把这个想法实现了”。。可以说白菜现在的粗壮性又上了一个台阶(

Java中的值传递和引用传递

今天突然提到Java传递变量的方式,研究了一下值传递和引用传递。

我的结论是:Java是以值传递的方式传递对象的引用/基本数据类型本身。

可能听起来有点拗口,首先明确一下值传递和引用传递。

值传递,是在方法内无法通过赋值等手段改变这个变量本身,因为传递过程中这个变量被复制了一份传入方法内,方法内对这个形参的操作并不会影响方法外。

而引用传递,则是将对象本身的内存地址传入方法内,方法内操作这个变量会对这个内存地址上这个变量的本体进行操作。

首先Java的方法里,用赋值等号=是改变不了调用者里形参的值的,因此传递的方式是值传递无误。

当然,如果调用这个对象本身的方法去改变自身(例如StringBuilder的append方法,或者是Javabean的Setter方法)是可以做到的,因为这个引用依然引用着这个对象,只是这个对象本身变了而已。

所以给数组的某个索引赋值之后,是可以改变数组本身,因为数组本身也是对象,继承自Object……只不过是一个特殊的对象,可以参考
http://www.blogjava.net/flysky19/articles/92763.html?opt=admin
数组的类是运行时生成的,它没有构造方法。证明数组是对象的最简单方法是:用等号赋值时并没有克隆一份数组,而只是重新指定了引用,要克隆数组得用System.arraycopy()方法。

所以手写排序时递归调用本体,并不会内存爆炸,因为数组本身并没有被复制。

(以上仅仅是个人看法,欢迎指正)

白菜的开发日记2

白菜之前的代码基本已经稳定运行了,于是我准备把它改成一个Web项目,采用HTTP API来收发消息,同时将来可能扩展出网页什么的……

在重构的时候遇到一个问题:之前我在绘图类,用nio扫出结算界面需要的所有图片,然后用static代码块包裹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static {
final Path resultPath = Paths.get(rb.getString("path") + "\\data\\image\\resource\\result");
//使用NIO扫描文件夹
final List<File> resultFiles = new ArrayList<>();
Images = new ArrayList<>();
Nums = new ArrayList<>();
Mods = new ArrayList<>();
SimpleFileVisitor<Path> resultFinder = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
resultFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
};

try {
//将所有文件分为三个List
java.nio.file.Files.walkFileTree(resultPath, resultFinder);
for (int i = 0; i < 23; i++) {
Images.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 23; i < 37; i++) {
Nums.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 37; i < 48; i++) {
Mods.add(ImageIO.read(resultFiles.get(i)));
}
zPP = ImageIO.read(resultFiles.get(48));
zPPTrick = ImageIO.read(resultFiles.get(49));
} catch (IOException e) {
logger.error("读取result相关资源失败");
logger.error(e.getMessage());
}

这样这些对象只会在这个类第一次被加载的时候生成,避免了每次绘图重复扫描文件夹,降低效率的问题。

然后后面画图的时候,我只需要调用List中的BufferedImage对象

1
2
3
//右下角两个FPS
g2.drawImage(Images.get(1), 1300, 699, null);
g2.drawImage(Images.get(2), 1300, 723, null);

但是如果我要新加功能就会显得不便:我只能添加z开头的文件名,否则我得把所有的Images.get()后面的数字调一下。。

看了一眼它们的文件名,我觉得ppy应该是在osu启动的时候把皮肤全部加载到内存,然后根据文件名绘制……

看了一下nio,好像能生成文件名,那我把List换成Map好了……

白菜的开发日记1

emm。从16号早上出门前Initial Commit,到23号中午12点白菜正式上线,这一周的时间过的真是快啊……

https://github.com/Arsenolite/osubot/commits/master

43次commit之后,终于有时间坐下来写点开发日记了。严格的说这是第一个我写出来能用的程序啊(x

先介绍一下她吧,她是一个面向osu群的QQ机器人,具体用法可以参见github的readme(普通用户就两条指令你还写个readme真是凑不要脸x)

相对别的机器人的亮点就是她返回的是图片,样式主要由啊哇设计,我们从旁修改,当然最后是我实现的。(记得18号那天我画一行丢一个图在群里x

作为Java程序员,老本行就是折腾数据库,当然白菜也有这方面的功能。每天凌晨4点将所有登记了的玩家的数据爬取,然后存入,提供一个对比功能。

18号晚上写完用户名片绘制,19号上午写完凌晨的定时任务,20号做了一些sudo命令。

22号凌晨1点终于搞定BP绘制,然后白天研究maven打包,开通vps,关闭mysql占用空闲内存缓存等等。

22号晚上怕他突然提出来一个大BUG,之前数据库中我存入的是用户名,但是这样会导致无法识别改名玩家的问题。

于是晚上赶工重构代码+改表结构,由于发现操作系统的时区就是北京时间,而BP返回时间是根据API key对应玩家的国籍来的(也是北京时间),砍掉了所有的时区转换。

在vps上运行的时候查API失败率非常低,于是将api工具类中加上重试机制,砍掉了其他地方代码对网络错误的try-catch。

然后就是些小bug(之前改的时候没改彻底导致的各种问题),昨天中午正式上线,下午晚上研究了jsoup爬取网页指定元素,又把爬score rank改成了二分法。

顺手写了个自动欢迎新人,由于啊哇还没想好scorerank的呈现方式,把绘制scorerank的代码注释掉发布到了服务器上。

……挺后悔没有边开发边写博文的,现在有的遇到的bug都已经忘记了……

总之这个项目增强了我对SQL、前端(jsoup提供的是js的getElementById方法,和css的div.Class风格的选择器)的熟练度,顺便还达成了第一次使用WinServer系统的成就,勉强算是linux+WinServer都能干的运维?

复习了以前练手搞的多线程机制,IO流,各种包装类,日期处理,字符串处理之类的基础问题,初次尝试手写二分法(特别新鲜),可以说它弥补了我Java基础代码写得少的缺陷……

毕竟之前在单位搞的SSM框架更多是做填空题,把XML配好,crud和参数验证写好,甚至那个小项目都用不着组装复杂pojo……到处都是提供好的最佳实践(

……虽然这个项目用的也都是给好的类和方法,充其量起到了熟悉JDK本身的作用,毕竟不是和c一样要手写String的查找替换啥的(x

emm,白菜大概是不会弃坑的,以后尽量写出详细的遇到/解决bug的过程吧233

修改hexo-material主题的代码高亮风格

其实比较简单,做几个填空题而已。

根据主题官网:https://material.viosey.com/expert/ 的说明,
从 1.3.0 版本开始,您可以使用 hexo-prism-plugin 进行代码染色,具体文档请参阅 Hexo-Prism-Plugin 插件文档

转到插件文档:https://github.com/ele828/hexo-prism-plugin

运行npm安装,根据文档,在博客的_config.yml中加上:

1
2
3
4
prism_plugin:
mode: 'preprocess' # realtime/preprocess
theme: 'default'
line_number: false # default false

并且关闭自带的highlight即可。

另外所有主题的预览在这里:
https://github.com/PrismJS/prism-themes#available-themes

记一次代码重构

最近在编写Service层的时候,出现了这样的问题:

由于对数据合法性的要求比较高,光靠外键显然不能胜任,因此我全面抛弃外键,在Service层用大量代码校验数据的合法性。

于是就出现了这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public Map editXXX(Entity entity){

Map result = new Map<>;
boolean flag = true;
if(entity.getparam1() is invaild){
flag = false;
logger.log;
result.put("msg","param1 is invaild");
}
...(param2)

result.put("result",flag);
if(!flag){
return result;
}

...mapper.Update(entity);
logger.log;
result.put("msg","success");

return result;
}

这里具体检测是否有效的代码比较复杂,例如update方法的id肯定不能是null,积分明细的uid必须在user表里查得到。

这样写着写着,写了五个Service之后开始觉得迷茫,懵逼,姜硬,可读性、可维护性差,耦合度高。

于是萌生出重构的想法,某人一语惊醒我把这些if独立成一个方法

直接独立肯定不行,我Service是面向接口写的,难道在接口中先定义好这样的check方法?

本来想将它抽出来做切面,但是毕竟这是业务逻辑的核心代码,不是日志和性能这些边缘功能,AOP印象里是改变不了方法某个局部变量的值的……

看了一眼之前整合shiro的util包,决定在util包下建一个subpackage叫checker,然后定义baseChecker类,里面定义几个常用方法。

其他具体的实体检查器就继承它,然后将这些checker对象注入Service类进行调用即可。

在定义baseChecker类的时候,由于对Java基础语法不够熟悉,我写出了这样的代码:

1
2
3
4
5
6
public class baseChecker {

public boolean checkNull(Object object,boolean flag){
return flag;
};
}

然后我发现这里需要使用的是泛型,而不是Object,而且泛型声明应该放在类里,正确的写法应该是:

1
2
3
4
5
6
public class baseChecker<T> {

public boolean checkNull(T t,boolean flag){
return flag;
};
}

然后子类这么写:

1
2
3
4
5
6
public class userChecker extends baseChecker {
@Override
public boolean checkNull(Object o , boolean flag) {
return flag;
}
}

路由器上ngrok设置的一点笔记

鸟枪换炮,用k3把残疾的k2给换了。

k3除了千兆LAN口外,最大的好处就是补上了U口,而且还是3.0的。既然能挂载硬盘,那这不就成了一个小型的NAS吗?

目前我是在出租屋里蹭着房东的网,本来想通过阿里云DDNS,完成在外网访问路由器(aria2随时上车)的目的。

作为二级路由,要实现DDNS,必须要一级路由开启端口转发,但是房东的路由器管理权并不在我手中。

一开始想爆破,找不到太好的爆破机器,自己又懒得写,试了几个弱口令失败后放弃了。看样子房东家里应该有略懂一点的人在吧。

于是只好另辟蹊径,采用ngrok客户端完成内网映射。

我使用的是https://ngrok.cc/ 这家免费的ngrok服务提供者,注册账户之后会提供一个Token,直接开通免费的HTTP隧道。

先随便填写一个前置域名,用户名和密码也随便填写,本地端口写了127.1:80,用于访问路由器的Web管理页面。

保存,点击进入修改,将域名改为自定义域名,我直接在我买的域名上开了个二级域名,并且按照网站的提示,把CNAME解析到了server.ngrok.cc。

连上k3,在客户端里把令牌填好,下面的通道和网站上设置保持一致,保存。


这里遇到另一个问题,aria2是分为本体+web控制台的,而web控制台连接的是本体提供的RPC接口,端口是6800。

而目前的隧道只有80端口转发,因此会出现连不上的情况。

这里有一个坑,表面看起来aria2用的是http://xxx:6800/jsonrpc的地址,但是网站和路由器中一定要开TCP隧道,本地端口写6800,服务器端口随便选一个没被占用的。

接下来修改aria2Web控制台配置,把RPC地址改成隧道的地址即可。同时为了安全,将aria2的rpc连接修改为需要Token,保持Web控制台和路由器中的设置一致即可。

关于hexo标题问题

今天出于让hexo更优雅的目的,修改了一下配置,给默认模板加上了categories属性。

之前的新建文章,是用hexo new 2017-7-27这样的命令。由于默认情况下文件名=标题,因此会出现用日期命名的.md文件,再进行编辑。

本想直接在hexo的_config.yml中,把文件名改为用时间命名,这样就能用hexo new 标题的命令,简化流程。

然而在本地调试的时候出现了错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ValidationError: `slug` is required!
at ValidationError.WarehouseError (e:\hexo\blog\node_modules\warehouse\lib\error.js:17:11)
at new ValidationError (e:\hexo\blog\node_modules\warehouse\lib\error\validation.js:14:18)
at SchemaTypeString.SchemaType.validate (e:\hexo\blog\node_modules\warehouse\lib\schematype.js:107:11)
at SchemaTypeString.validate (e:\hexo\blog\node_modules\warehouse\lib\types\string.js:45:45)
at Array.<anonymous> (e:\hexo\blog\node_modules\warehouse\lib\schema.js:161:23)
at Schema._applySetters (e:\hexo\blog\node_modules\warehouse\lib\schema.js:305:13)
at Model._insertOne (e:\hexo\blog\node_modules\warehouse\lib\model.js:190:10)
at e:\hexo\blog\node_modules\warehouse\lib\model.js:214:17
at tryCatcher (e:\hexo\blog\node_modules\bluebird\js\release\util.js:16:23)
at e:\hexo\blog\node_modules\bluebird\js\release\using.js:185:26
at tryCatcher (e:\hexo\blog\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:512:31)
at Promise._settlePromise (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:569:18)
at Promise._settlePromise0 (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:614:10)
at Promise._settlePromises (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:693:18)
at Promise._fulfill (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:638:18)
at PromiseArray._resolve (e:\hexo\blog\node_modules\bluebird\js\release\promise_array.js:126:19)
at PromiseArray._promiseFulfilled (e:\hexo\blog\node_modules\bluebird\js\release\promise_array.js:144:14)
at Promise._settlePromise (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:574:26)
at Promise._settlePromise0 (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:614:10)
at Promise._settlePromises (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:693:18)
at Async._drainQueue (e:\hexo\blog\node_modules\bluebird\js\release\async.js:133:16)

这就很尴尬了……最后把_config.yml中new_post_name的值改为:year-:i_month-:day-:title.md # File name of new posts,问题解决。

应该是hexo不允许这样做(不会自动处理重复命名。)

关于密码加盐存储的笔记

  • 黑客入侵后,期望达到的目的:拿到原文去撞其他库。

  • 密码学的理论安全:就算你知道了整个具体算法,整个加解密的协议,以及密文保存的方法,所有源代码和数据库,只要不知道密钥是什么,就无法从密文破解出明文。(散列算法已经能做到知道密文拿不到原文。)

  • 密码学的应用安全:让破解成本超过获得的利益。

一开始的情况:MD5(原文)=密文

黑客拿到源码、密文之后:

使用记载大量原文+密文的彩虹表,加上可碰撞的特性(MD5(n原文)=1密文),获取大量弱口令用户的密码原文。

加盐:MD5(加盐(原文,盐))=密文

黑客拿到源码、数据库之后:
  • 没加盐的彩虹表失效了。足够长的盐,可以让加盐(原文,盐)的长度加长,不会出现在简单彩虹表里。

  • 每个用户的密文都不一样,无法通过出现频率高的密文猜出弱密码。

注意:如果盐值固定,那只需要对彩虹表进行修改,所有原文重新计算一次。相当于没有加盐

于是情况成了这样:

就算黑客拿到了密文、盐值、甚至加盐、加密算法,针对每个用户,都需要做一个加了这个用户专属的盐的彩虹表,去撞出原文,破解难度几何级增长。

疑惑:

在用户表中新增盐字段用来存盐,和直接用邮箱(邮箱是用户的唯一识别标识)做盐,在提高破解难度的角度上是否有区别?

关于shiro在SSM环境下的整合(2)

接上文

(题外话:好像hexo对Markdown里Java的渲染不是很好,注解会嵌到其他行去?)

编写Controller

做用户认证,首先得有一个Controller,生成一个User对象。由于研究对象是shiro,先不写实际页面,用一个简单的表单向Controller提交对象。

直接上Controller部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Controller
//省略具体URL路径代码
//此处直接传入表单,或者JSON也可以,总之要绑定User对象。
public String login(User user, BindingResult result, Model model, HttpServletRequest request) {

try {
//获取一个Subject,Subject在Shiro框架中是主体,一般场景中就是登陆的用户。
Subject subject = SecurityUtils.getSubject();
//如果是已经登录的(Session中有相应会话),跳转到上一个访问的页面
if (subject.isAuthenticated()) {
return "redirect:/";
}
//如果传入参数不正确,例如密码是空的
if (result.hasErrors()) {
model.addAttribute("error", "参数错误!");
return "login";
}

// 将表单传入的email和密码存为Shiro的Token
UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), user.getPwd());
//并且提交Realm验证
subject.login(token);
//如果认证通过,没有抛出异常
final User authUserInfo = um.getUserByEmail(user.getEmail());
request.getSession().setAttribute("userInfo", authUserInfo);
} catch (AuthenticationException e) {
// 身份验证失败
model.addAttribute("error", "用户名或密码错误 !");
return "login";
}
}

先实例化一个subject,校验后调用.login()方法,并传入包含用户名(可以是能用来登陆的其他东西)和密码。
之前在Spring配置中将Realm注入到SecurityManager,因此会跳转到Realm处理。

(出于业务需求的原因,本文暂时不涉及多个Realm的情况,可以参考http://blog.csdn.net/xiangwanpeng/article/details/54802509

编写Realm具体方法之认证

跳转之后,首先进入doGetAuthenticationInfo方法,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String email = String.valueOf(token.getPrincipal());
String password = new String((char[]) token.getCredentials());
//获取参数,并使用参数在数据库中验证,此处um即为Spring注入的UserMapper
final User authc = um.getIdPwdByEmail(email);
//接下来的逻辑可以自由发挥,总之登录失败就要抛出异常,并且由Controller捕捉
if (password.equals(authc.getPwd())){
//此处getname是获取该类的名字
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(email, password, getName());
return authenticationInfo;
}else{
throw new AuthenticationException("用户名或密码错误.");
}
}

这里使用了userMapper在数据库中取验证信息,当然也可以使用userService封装好的方法,随意发挥。

如果登陆失败,可以自己抛出并处理各种异常,shiro自带用户锁定、同IP尝试次数过多等非常丰富的异常。由于是小系统,不做复杂处理。

假如这个方法通过了,我们会return一个包含了用户名(确切讲是用户标识,根据业务场景不同而不同),密码(其他授权信息也可以)以及当前类名的authcInfo对象。

这个对象这里我们没有使用,但是可以用在JSP标签中,参考此文:http://www.sojson.com/blog/144.html

编写Realm具体方法之授权

如果上一个方法没有抛出异常,会进入doGetAuthorizationInfo方法,传入对象则是一个principalCollection。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String email = String.valueOf(principals.getPrimaryPrincipal());
final User user = um.getUserByEmail(email);
Set set = new HashSet<String>();
if (user.getAdmin() == 1){
set.add("Admin");
}else{
set.add("User");
}
authorizationInfo.setRoles(set);
return authorizationInfo;
}

这个方法的作用,是从数据源中,根据用户标识(邮箱),取出所对应的权限(无论是以字段形式存在于用户实体,或者是另起一张表,都只是形式,目的是要取出权限)。

并且以字符串的set形式,存入authzInfo对象中并且return,return的这个对象稍后可以在JSP中以tag形式调用。也可以直接在Spring配置中写上/admin = authz,使用shiro自带的Filter进行过滤。

至此,使用Shiro在SSM应用中的认证、授权已经完成。这里实现了最简单的功能,系统只有两个角色(用户、管理员),管理员能访问后台系统,用户不能访问,仅此而已。

事实上shiro的功能远远不止这些,多用户、多权限,甚至多数据源、多Realm都可以做到。甚至还提供了密码加盐存储的工具类,而这一切都能和Spring 完美整合。

使用shiro的目的,首先是无需手写Filter拦截每一个没有权限的请求,其次是不必手写代码去判断用户权限,不必手写代码实现每个权限能做的事,也不必手动处理登陆失败的情景,节省大量工作量。


题外话

在之前的学习过程中,我看到的demo为了实现全部功能,将用户、权限全部实现,起了五张表,分别存储用户、权限、角色,用户-角色关系,角色-权限关系。

更丧心病狂的是,权限表还有roleSign,roleName,roleDescription三个字段,而实际上放进authzInfo的只有roleSign一个字符串。

我尝试直接阅读代码,一个Realm三个Service三个实体,让我对功能一头雾水,今早突然醒悟,既然源码都开放了,我何必苦苦猜测功能,直接git clone 打breakpoint debug,模拟一次登录,终于豁然开朗。