常见DES实现陷阱

DES要点说明

  • DES走的是分组加密,每次处理对象的是8位byte,所以对字符串加解密的时候,会涉及字符编码格式和补齐8位的问题。
  • DES的密钥是固定8位的byte的,其中前7位是加解密用的,最后一位是校验码。
  • 3DES的增强型的DES,带3个key,如果3个key一样,就是DES,也有一种变种是1、3是一样的。但都是固定8位的。
  • 3DES通常是EDE,就是先加密(k1)再解密(k2)再加密(k3)

目前,项目代码中有3个和DES实现相关的类,下面看看他们有哪些问题:

案例1

  • 从字符串到byte的转换,有指定编码格式GBK,这个是可以接受的。
  • 使用的是DESede,就是3DES的EDE加密方式,但是3个key是一样的,没有意义。
  • 加密时代码先自行进行了补齐操作(补\0),但是补齐是在字符串上操作的,不是在字节上操作,导致实际上可能没有对齐(中文情况)。
  • 调用加密API时,没有指定补齐方式,会采用默认补齐,造成重复补齐(当然也修复了上面的补齐操作)。
  • 解密指定NoPadding,和加密Padding方式不一样,造成解密结果最后会出现很多多余的字节。所以结果必须得trim一下才行。

参考代码如下:

补齐实现有误:

    public String encrypt(String in) throws Exception {
        String strIn = in;
        if (null == strIn || "".equals(strIn)) {
            return "";
        }

        int i = 0;
        i = strIn.length() % 8;

        if (0 == i) {
            for (i = 0; i < 8; i++) {
                strIn += "\0";
            }
        } else {
            while (i > 0) {
                strIn += "\0";
                i--;
            }
        }
        byte[] bytes = strIn.getBytes(CHARSET);
        byte[] enbytes = encryptCipher.doFinal(bytes);
        return byteArrToHexStr(enbytes);
    }

key是一样的,补齐方式没对应上:

    public DESedeEncrypt() {
        byte[] buffer = new byte[] {
                0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
                0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31
        };

        SecretKeySpec key = new SecretKeySpec(buffer, "DESede");

        try {
            encryptCipher = Cipher.getInstance(KEY_ALGORITHM);
            encryptCipher.init(Cipher.ENCRYPT_MODE, key);
            decryptCipher = Cipher.getInstance("DESede/ECB/NoPadding");
            decryptCipher.init(Cipher.DECRYPT_MODE, key);
        } catch (NoSuchAlgorithmException e) {
            Throwables.propagate(e);
        } catch (NoSuchPaddingException e) {
            Throwables.propagate(e);
        } catch (InvalidKeyException e) {
            Throwables.propagate(e);
        }
    }

案例2

  • 从字符串到byte的转换,采用了系统默认编码,存在平台移植性问题。
  • 密钥key的长度布置8位,有多余字符(虽然只取前8位避免出错),造成混乱。

key的格式不标准,有多余字符:

    private static String strDefaultKey = "mywebsite123456%";
    private Key getKey(byte[] arrBTmp) throws Exception {
        byte[] arrB = new byte[8];
        for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
            arrB[i] = arrBTmp[i];
        }

        Key key = new javax.crypto.spec.SecretKeySpec(arrB, "DES");

        return key;
    }

案例3

  • 从字符串到byte的转换,采用了系统默认编码,存在平台移植性问题。
  • 实现不是标准的DES,或3DES,是在DES基础上定义了一套加密。
  • 根据目前key的长度,比标准3DES都要慢很多,另外没有采用JDK带的API。

key的长度不标准:

public class DesUtil {
    public static final String firstKey = "com.xxx.xxxpro";
    public static final String secondKey = "xxx_web";
    public static final String thirdKey = "xxxservice";
}

实现方式是对每个key补齐8位,再切割形成每组多个8位的key,再采用EEE的方式进行处理:

                        for (x = 0; x < firstLength; x++) {
                            tempBt = enc(tempBt, (int[]) firstKeyBt.get(x));
                        }
                        for (y = 0; y < secondLength; y++) {
                            tempBt = enc(tempBt, (int[]) secondKeyBt.get(y));
                        }
                        for (z = 0; z < thirdLength; z++) {
                            tempBt = enc(tempBt, (int[]) thirdKeyBt.get(z));
                        }

java常见工具库培训

目前项目中常见的工具库有apache commons,google guava,再算上spring的话,需要自己从头开始写工具类的情况大大减少。 为了给广大童鞋普及一下工具库用法,减少无用功(还可能因为实现的不好留后遗症的),这里简单的介绍一下相关工具类。google guava大家应该比较陌生,这里先不介绍,:)

apache commons

官方地址: http://commons.apache.org/

apache commons历史悠久,涉及范围也是最广的,在官网上分了数十个模块,但有些模块是新开发的,就不要贸然使用啦。

这里只是介绍最最常用的commons库,排名不分先后,如下:

commons-codec

包括常见的编码、解码算法,例如MD5,Base64,举例如下:

  • Base64#encode 加密成base64串
  • Base64#decode 解密base64串
  • DigestUtils.md5Hex 进行MD5加密,注意得到的是小写的MD5(MD5标准不区分大小写),在比较的时候需要注意
  • DigestUtils.shaHex 进行SHA1加密 SHA256,512之类也是支持的,可以自行查阅

commons-collections

包括一堆增强的集合类(我了解不多,大家可以自行学习),各种和集合类相关的工具类,举例如下:

  • CollectionUtils.isEmpty 是否null或空集合,这一类的方法很多,看看有个大概印象
  • MapUtils.isEmpty 是否null或空Map
  • ListUtils.removeAll 从某个列表中删除存在于另外个列表的元素

同类型的还有SetUtils、IteratorUtils等,大体上是集合相关的操作,如过滤、是否相等、交集、差集、转换(变同步、变不可变)等,其实这个用到的机会也不是很大。

commons-net

实现了一些常见的网络协议,可能关系最大的要数ftp、smtp的实现了。而jdk带的sun.net.ftp,这个尽量就少用拉。

这套api的实现用法得google一下了,看官方文档的例子, 又或者别人的经验代码,例如这个http://my.oschina.net/hly3825/blog/33657

commons-httpclient

http客户端实现,貌似已经从commons独立出去了。3.x版本和4.x版本变化比较大,大家要使用的时候自行查阅资料。 尽量避免使用HttpURLConnection去直接搞。

commons-io

io方面的工具类,主要包括文件处理、流处理,常见的类有IOUtils、FileUtils、FilenameUtils。举例如下:

  • IOUtils.closeQuietly 安静关闭输出输出流,常用于finally关闭流的时候
  • IOUtils.copy 把某个输入流拷贝到某个输出流中去
  • IOUtils.toString 把某个输入流、URI的内容转换成字符串
  • IOUtils.readLines 按行读取流
  • Charset.UTF_8 有一些常见的、系统都会支持的字符集,已经定义成常量
  • FileUtils.readLines 按行读取文件
  • FileUtils.readFileToString 读取文件保存在一个字符串中

IOUtils针对的是stream,FileUtils针对的是File对象,相应的有文件拷贝、删除等操作。
注意的是,使用字符流格式的时候,务必指定编码

commons-lang

这个是使用最多的库了,有lang2.x和3.x版本,尽量使用3.x版本。

常见的有StringUtils、SystemUtils、RandomStringUtils、DateFormatUtils、DateUtils、各种Builder、Validate,举例如下:

  • StringUitls.isEmpty 判空,和isBlank的区别在于它不进行trim
  • StringUtils.join 按分隔符合并,这个很常用
  • StringUtils.repeat 重复某个字符或字符串,有些需要格式化的是会用到
  • StringUtils.startsWith 和endsWith那样,是增强版本,还有endsWithAny、endsWithIgnoreCase等
  • SystemUtils 主要是一些常见系统环境变量,如临时目录、用户目录、分隔符等
  • RandomStringUtils 用来生成各种随即字符串,例如全字母、全数字或混合型的
  • DateFormatUtils、DateUtils 一个是字符串变日期,一个是日期相关的操作
  • 各种Builder 主要用实现常见的toString、compareTo、equals、hashcode等常见类,例如ReflectionToStringBuilder就很方便实现toString方法。同理,CompareToBuilder、EqualsBuilder、HashCodeBuilder都很好理解。
  • Validate 实现一些assert,例如Validate.notNull可以用来做前置校验,和spring的Assert类是类似的。

其他commons库

  • commons-fileupload 仅限于在文件上传的类中使用,虽然它也有一些工具类,但是就不要在其他地方使用啦。
  • commons-dbcp 一个数据库连接池,现在就比较少用了
  • commons-pool 一个java对象池实现,通常用来缓存一些耗时较大的对象,dbcp也是基于它的,一般也少直接用。
  • commons-logging 日志包装实现,在开源项目中使用广泛,项目中一般直接用log4j等。

使用org.json库进行xml和json转换存在的问题

org.json库中提供一个xml和json进行转换的工具类,XML.java

使用方式如下:

  • xmlstr = XML.toString(jsonstr)
  • jsonstr = XML.toJSONObject(xmlstr).toString()

中间层原有代码使用这种方式进行格式转换,不过存在一些问题:

  • json转换为xml的时候,对带content字段的节点,是直接生成文本,而不是xx
  • xml转换为json的时候,会对指为整形(还有true/false/null等)的字符串尝试进行转换,变成原生类型

为了避免这两个问题,对org.json库的XML.java进行了一些修改:

  • 去掉content字段的特殊处理
  • 去掉整形字符串尝试转换的逻辑

见https://github.com/mccxj/JSON-java

经验教训: 以后引用第三方库的时候,要小心呀,避免触碰到一些特殊开关。

结合状态机的开发风格

本文主要以XXX的html5版本为蓝本,讨论结合状态机开发的思路和实践方式。状态机选型使用statechart.js

起步知识

特别是状态机介绍,内容非常好,强烈推荐。

适用场景

  • 主要用于某个具体业务的复杂页面流控制
  • 简单的业务流程是不需要的。例如只有一两个页面(列表+详情)
  • 适用于多步骤多页面(包括弹出框)、各种跳转的场景

如何定义状态?

根据页面流、步骤来定义状态。可以参考以下步骤:

  • 对照保真、流程图,划分每个独立页面 以个人营销活动为例,主要页面包括活动页面、选档次页面、奖品页面、奖品包选择页面、缴费页面、发票页面。 那么可以考虑定义为list、level、reward、giftpack、charge、invoice

  • 对于有多种弹出窗口的情况,可以考虑定义子状态 以推荐业务为例,在菜单页面上,可能会弹出反馈窗口,或者产品订购窗口。 那么可以考虑定义为menu/index、menu/feedback、menu/prod,这样的话,通过下面的页面控制,就可以让在值状态的情况下,菜单页面一直显示。

<div ng-show="fsm.isCurrent('/menu')" ng-include="'app/partials/recommended/recommended_menu.html'"></div>
<div ng-show="fsm.isCurrent('/menu/prod')" ng-include="'app/partials/recommended/recommended_orderprod.html'"></div>
<div ng-show="fsm.isCurrent('/menu/feedback')" ng-include="'app/partials/recommended/recommended_feedback.html'"></div>
  • 对于页面显示,有较多共性的页面,可以考虑定义子状态,方便共享逻辑和事件处理 以上述的个人营销活动为例,奖品页面、奖品包选择页面的页面很类似,功能操作也比较实现,可以定义成子状态,如order/reward、order/giftpack
<div ng-show="fsm.isCurrent('/list')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_list.html'"></div>
<div ng-show="fsm.isCurrent('/level')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_level.html'"></div>
<div ng-show="fsm.isCurrent('/order/reward')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_reward.html'"></div>
<div ng-show="fsm.isCurrent('/order/giftpack')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_giftpack.html'"></div>
<div ng-show="fsm.isCurrent('/invoice')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_invoice.html'"></div>
<div ng-show="fsm.isCurrent('/charge')" ng-include="'app/partials/personalMarketCamp/personalMarketCamp_charge.html'"></div>

如何控制页面的显示、如何响应页面操作?

  • 页面显示与否,通过状态机的状态,而不是数据的状态。这里用的是isCurrent方法
  • 页面操作,通过状态机的事件发送,而不是直接使用绑定在$scope的方法。这里用的是send方法
  • 页面跳转,通过状态机的状态变化来驱动。这里用的是goto方法,是在send方法之后的event逻辑中处理的。

页面显示与否,例子上面已经说了。而对于ng-click这种事件触发,直接用send方法即可。

<div class="Feedback-btn">
  <a ng-click="fsm.send('feedback', 'hesitate')" class="accept-btn"><span>考虑</span></a>
  <a ng-click="fsm.send('feedback', 'refuse')" class="refuse-btn"><span>拒绝</span></a>
</div>

可以看到,事件也可以捎带参数的,这样可以在该状态的event中进行处理,如下:

# 反馈
@state 'feedback', ->
  # 进行反馈操作
  @event 'feedback', (operationtype) ->
    product = $scope.viewModel.product
    if product.opertype == '1'
      new Toast(
        context: $('body')
        message: "该产品不可推荐"
      ).show();
      return

    qryfeedbackService.event
      "userseq": $scope.viewModel.product.userseq
      "servnumber": $scope.telnum
      "operationtype": operationtype
    .then (ok) =>
        @goto '/menu/index'
      , (err) ->
        new Toast(
          context: $('body')
          message: err
        ).show()

需要注意的是,event是挂靠在某个状态下的,如果你是子状态的话的,event会先在子状态中找,如果没有找到会在父状态上找。 通过这种方式,就可以实现多个子状态共享event,例如奖品页面、奖品包页面都有选择功能,就可以把这个操作放到父状态的event中去。

更多状态机的细节

很多状态机都实现了某些特殊状态,如进入状态,退出状态这种事件。statechart也实现了,对应的是enter和exit,代码大体上是:

@state 'menu', ->
  @enter ->
    #TODO

  @exit ->
    #TODO

但是需要注意的是,重复进入这个状态的话,是会重复执行的。所以对于A -> B -> C这样的业务流程,从A到B和C回退到B,都会执行这个enter, 就无法区分这种情况了。因为通常,从A到B是进行初始化,而从C回到B得保留原来B的数据状态。所以实际上我很少使用这些特殊事件,除非:

  • 无需区分的情况,这样写会让代码风格更统一。
  • 没有接口交互,本地操作的话,因为这种消耗小很多。
  • 存在直接跳转到该状态的情况(例如由另外的业务跳转过来),这种特殊情况下前面的步骤都被忽略。而且这种情况下,需要通常需要接口交互(例如补充某些必要信息),而为了区分回退的情况,我通常会根据业务特性考虑一些数据缓存处理。

json格式须知

着重介绍与项目使用相关的json知识。如果没有特别说明,环境是指Javascript下的json。

区分类型

  • 首先需要区分json字符串和json对象,不过通常根据上下文可以区分。
  • 协议关注的是json字符串,而代码中处理的是json对象,两者通过序列化(JSON.stringify)和反序列化相互转换(JSON.parse)。

常见格式

  • 主要有数组和key/value形式的object
  • 数组是有顺序的,可以不同类型,常用于顺序遍历操作。
  • object是无顺序的,key只能是字符串,常用于快速随机查找。
  • null是可以被序列化的,而undefined不可以(会消失)。
  • 其他的一些特殊值,如Nan,Infinity,会被序列化为null。

关于数组

  • 对于数组对象,虽然支持key/value的操作,但是序列化的时候设置的值会丢失。
  • 数组序列化的长度是根据length属性来的,没有赋值的位置是null。
  • 对数组遍历不应该采用for in语句,因为通过key/value设置的值也会被输出。

关于Object

  • 规范上规定key是带双引号的字符串(),但实际上很多反序列化工具能够支持数值、单引号字符串、字符串字面量(没有引号的字符串)。
  • 如果是一普通浮点数值,可以通过相应的数值作为key获取,或者通过对应的字符串来获取。如用2.2的话,可以用2.2或”2.2”。
  • 如果是一整型数值,可以用数值,但用字符串只能用整型的,如用2.0的话,可以用2.0或2或”2”,但”2.0”就不可以。
  • 如果使用字符串字面量的话,需要避免一些关键字使用。如delete
  • key不应该重复,如果重复的话,通常结果是后面的会覆盖前面的。
  • 可以用.后面加key来取值,或者用[]这样的操作符来获取,第一种方式更推荐,但只能支持非数字开头的字符串,unicode也是可以。
  • 对object遍历可以采用for in语句。

协议转换

  • 协议传输的是json字符串,但通常里边的类型都是字符串,不区分数值,因此做数值运算需要先转换。
  • 和xml一样,需要注意特殊字符如引号、回车、unicode等,尽量避免手动拼接,采用序列化工具。
  • object类型的json序列化/序列化的时候,都不应该预期他是有顺序的,虽然很多库都有带默认顺序,应该使用数组。