关于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,模拟一次登录,终于豁然开朗。

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

1.无状态场景下的shiro整合。

这是自己练手的产物,github:https://github.com/Arsenolite/com.qhs.blog

分析思路:

  • 由于是无状态API,只有带Token访问,因此把session相关的全部禁用。
  • 而Realm起到的是userService的作用,也禁用掉,毕竟我不可能每次用户请求API就去查数据库吧。
  • 再加上当时使用了JWT作为Token规范,因此业务需求就成了检验JWT有效性。
  • Filter中直接注入redisDAO和tokenService,对URL中的Token参数检验,有效就放行。

具体写法:

首先我们要有一个maven+ssm的环境,并且导入shiro-web,shiro-core和shiro-spring三个依赖。
由于我们需要将shiro纳入Spring的管理周期,在Web.xml中这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- shiro -->
<!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

这里因为要和Spring集成,所以具体Filter的代码不是写在这里,这里只是将shiro的Filter纳入Spring的Filter链。

接下来是重头戏:Spring配置文件,既然有SSM环境,肯定有一份或者多份xml。
定义Filter,每一个都指向具体的类(稍后在后文贴出)
1
2
3
4
5
6
7
<!--验证码Filter-->
<bean id="mailFilter" class="com.qhs.blog.util.shiro.filter.mailFilter"/>
<bean id="captchaFilter" class="com.qhs.blog.util.shiro.filter.captchaFilter"/>
<!--认证Filter-->
<bean id="loginFilter" class="com.qhs.blog.util.shiro.filter.loginFilter"/>
<!--授权Filter-->
<bean id="adminFilter" class="com.qhs.blog.util.shiro.filter.adminFilter"/>
组装Filter工厂,之前web.xml中的shiroFilter实际上是一个Filter工厂。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- shiroFilter工厂 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 构建securityManager环境 -->
<property name="securityManager" ref="securityManager"/>
<property name="filters">
<util:map>
<entry key="mail" value-ref="mailFilter"/>
<entry key="captcha" value-ref="captchaFilter"/>
<entry key="admin" value-ref="adminFilter"/>
<entry key="login" value-ref="loginFilter"/>
</util:map>
</property>
<!-- 设计哪些URL用哪个过滤器 -->
<!--由于具体用户权限还没细化成接口,先定义两个验证码的过滤器-->
<property name="filterChainDefinitions">
<value>
<!-- 邮箱验证码要求网页验证码认证通过 -->
/api/captcha/** = noSessionCreation,anon
/api/mail/** = noSessionCreation,captcha
<!-- 用户模块 -->

</value>
</property>
</bean>
接下来将shiro的会话相关功能全部禁用掉,StatelessDefaultSubjectFactory需要手动撰写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Subject工厂,写一个禁用会话的subjectFactory -->
<bean id="subjectFactory" class="com.qhs.blog.util.shiro.StatelessDefaultSubjectFactory"/>

<!-- 禁用掉会话调度器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>
</bean>

<!--解决报错,组装默认的subjectDAO-->
<bean id="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO">
<property name="sessionStorageEvaluator" ref="sessionStorageEvaluator"/>
</bean>

<bean id="sessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator">
<property name="sessionStorageEnabled" value="false"/>
</bean>
将配置为禁用Session的subjectDAO、subjectFactory和sessionManager注入安全管理器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--<property name="realm" ref="userRealm"/>-->
<property name="subjectDAO" ref="subjectDAO"/>
<property name="subjectFactory" ref="subjectFactory"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>

<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>

最后构建一个迭代7次的SHA-512 Hash服务,用来在类里调用。
1
2
3
4
5
<!--构建一个Hash服务,在此指定叠加方式和采用的算法-->
<bean id="defaultHashService" class="org.apache.shiro.crypto.hash.DefaultHashService">
<property name="hashAlgorithmName" value="SHA-512"/>
<property name="hashIterations" value="7"/>
</bean>
前文提到的StatelessDefaultSubjectFactory:
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qhs.blog.util.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
接下来就是编写具体Filter了。

先贴一段代码,逐步讲解作用。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.qhs.blog.util.shiro.filter;


import com.qhs.blog.serviceImpl.tokenServiceImpl;
import net.minidev.json.JSONObject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
* Created by QHS on 2017/5/31.
*/
public class captchaFilter extends AccessControlFilter {
@Autowired
private tokenServiceImpl tokenService;

JSONObject resp = new JSONObject();

@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
boolean flag = false;
HttpServletRequest req = WebUtils.toHttp(servletRequest);
String token = req.getParameter("token");
Map<String, Object> resultMap = tokenService.validToken(token);
switch ((String) resultMap.get("state")) {
case "EXPIRED":
resp.put("warning", "授权已过期");
break;
case "VALID":
JSONObject jo = (JSONObject) resultMap.get("data");
String value = (String) jo.get("value");
if (value.equals("captchaAuthed") && value.equals("getMailCode")) {
flag = true;
}
break;
case "INVALID":
resp.put("msg", "请输入验证码");
break;
}

return flag;
}

@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/json; charset=utf-8");
servletResponse.getWriter().write(resp.toJSONString());
return false;
}

}

这个Filter继承了AccessControllFilter,shiro有几个内置Filter,有的用于认证,有的用于鉴权。而在这里,我只需要一个验证码,没必要处理用户登录,因此直接用了AccessControllFilter。

继承了类就需要继承方法,AccessControllFilter带了isAccessAllowed和onAccessdenied方法。

前者如果return了True,就允许用户访问Filter保护下的url,如果return False,那就会进入后面的方法,如果后面的方法Return True也允许用户访问(意义不明啊)。

因此我在类里定义变量JSONObject,在isAccessAllowed方法中对传入Token进行验证,验证不通过则在JSONObject内写入相应的错误信息并返回false。

进入onAccessDenied方法,在方法体内将JSONObject写入Servlet流输出到网页上,这样就完成了一个基本的无状态应用集成。
(其实我的做法和手写Filter差不多,只是引入shiro作为练手,和后续功能扩展。)

2.普通Web应用中使用shiro

分析思路:

现在在单位做的项目,计划中是一个由服务端渲染JSP的项目,因此可以使用Session和Cookie。

这时候引入shiro,就可以使用realm的相关特性,将userService注入realm中,由shiro接管用户认证、授权的成功/失败后的场景。可以说是充分发挥了功能。
Web.xml内容不变,Spring配置中去掉那些关于Session的设置,并且也不用写StatelessDefaultSubjectFactory。

具体写法:

修改XML配置
1
2
3
4
5
6
7
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
</bean>

<bean id="userRealm" class="com.shengting.store.util.shiro.realm.userRealm">
<property name="cachingEnabled" value="false"/>
</bean>
新建userRealm

由于这个Realm类需要同时完成认证/鉴权工作(认证(Authc)指的是判断用户是否能登录,而鉴权(Authz)指的是用户有没有权限执行某项操作。),我们让它继承AuthorizingRealm类。

继承了类就要继承方法,我们的类看起来是这样子的:

1
2
3
4
5
6
7
8
9
10
public class userRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}

后面的大体思路,就是调用userMapper在数据库中查找信息(也可以使用封装好的userService)
由于工作进度问题,Controller还没有写完,暂时先更新到这里;w;

关于数据库删除数据的同时保持完整性的个人做法。

进了单位,要我从头写一个商城出来。
完成了初步的数据库表格设计之后,发现如下一个问题:

我的address表记录了用户的收货地址,而订单中采用aid指向address表的某行。

那么如果用户后期删除了这个aid对应的收货地址,或者修改了地址,会引发bug:早期订单对应的地址会改变或者无法找到。

一开始考虑用外键约束,但是外键约束只能达成这样的目标:

  • 如果存在采用这个aid的订单,就拒绝删除aid
  • 删除aid同时删除所有带这个aid的订单
    这明显不符合常识和业务需求。

我想出的解决办法是:给address表加一个valid的int字段,长度为1(实际上就是boolean,记录这个地址有没有效),删除地址的时候不直接从数据库删除,而是将valid改为0。

修改地址的时候,也不直接改,而是创建一个新的地址,再把老的地址valid值改成0。

这样,如果订单采用了valid为0的address,也能够正常显示,但是用户在选择自己收货地址的时候是看不到这条“被删除”的地址的(WHERE valid =1)。

类似的业务需求也是这样,例如product也加一个valid字段,出现一个包含了已删除产品的订单时,能正常显示,但是这个产品不能被加入购物车和订单。

这样业务需求倒是实现了,但是service感觉会异常复杂……需要重新梳理。

幸好Mapper层不需要太多修改,把删除方法全去掉,再在某些条件里加上valid=1就行了。

【转】斐讯k2固件更新后功能被精简的问题

转自http://www.allonautilus.cn:81/blog/2017/04/09/%E6%96%90%E8%AE%AFk2%E5%9B%BA%E4%BB%B6%E9%97%AE%E9%A2%98/。
原博客已经关闭,用Google快照看到的这篇文章_(:з」∠)_

斐讯K2之前刷的固件是潘多拉PandoraBox 16.12 2016-12-04的版本,前几天出了点问题,就直接升级为PandoraBox 17.01 2017-01-03的版本了。
此前是有一台局域网的电脑作为服务器的,可以外网访问。结果升级固件后,网址不能访问了,按照之前的套路来一步一步来进行,结果傻眼了,发现这个版本的固件精简了很多功能,网络下面都不带防火墙和端口转发的选项,原来2017年1月3号版的pandorabox默认安装插件没有防火墙的luci插件以及IP/MAC地址绑定的插件,还有虚拟wan口的插件从1月1号版也没了。
解决方法也简单:系统-软件包,找到firewall插件luci-app-firewall和语言包luci-i18n-firewall-zh-cn,安装后,重启(可能不需要)刷新即可在网路下面找到防火墙,里面即可对WAN-LAN之间的权限进行设置,端口转发也可以一并设置完成。
最后还需要进入WinSCP,etc-config,打开uhttpd,将option rfc1918_filter ‘1’,改为0,既可用IP地址(或域名)从外网访问!