【密码学】MD5、UUID,加盐,JWT的理解与使用范例


在这里插入图片描述

加密

1、MD5加密

  • Message Digest Algorithm MD5(中文名为消息摘要算法第五版)
  • 应用程序的密码通常不会明文保存,会使用各种各样的加密算法对密码进行加密
  • MD5算法相对来说较为安全。
  • 初始的MD5算法是由C语言实现
  • Java版本的MD5算法是根据C语言的MD5算法演变而来的
  • MD5加解密在线工具:http://www.cmd5.com/

安全访问认证

当用户登录的时候,系统把用户输入的密码进行MD5 Hash运算,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。这可以避免用户的密码被具有系统管理员权限的用户知道。

MD5将任意长度的“字节串”映射为一个128bit的大整数,并且是通过该128bit反推原始字符串是困难的,换句话说就是,即使你看到源程序和算法描述,也无法将一个MD5的值变换回原始的字符串,从数学原理上说,是因为原始的字符串有无穷多个,这有点象不存在反函数的数学函数。

正是因为这个原因,现在被黑客使用最多的一种破译密码的方法就是一种被称为"跑字典"的方法。有两种方法得到字典,一种是日常搜集的用做密码的字符串表,另一种是用排列组合方法生成的,先用MD5程序计算出这些字典项的MD5值,然后再用目标的MD5值在这个字典中检索。我们假设密码的最大长度为8位字节(8 Bytes),同时密码只能是字母和数字,共26+26+10=62个字节,排列组合出的字典的项数则是P(62,1)+P(62,2)….+P(62,8),那也已经是一个很天文的数字了,存储这个字典就需要TB级的磁盘阵列,而且这种方法还有一个前提,就是能获得目标账户的密码MD5值的情况下才可以。

示例代码:

md5加密工具类:

  • 对字符串md5加密(小写+数字)getMD5()
  • 对字符串md5加密(大写+数字)toMD5()
public class MD5Util {
  /**
    * 对字符串md5加密(小写+数字)
    *
    * @param str传入要加密的字符串
    * @return MD5加密后的字符串
    */
  public static String getMD5(String source) {
    try {
      // 生成一个MD5加密计算摘要
      MessageDigest md = MessageDigest.getInstance("MD5");
      // 计算md5函数
      md.update(source.getBytes());
      // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
      // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
      return new BigInteger(1, md.digest()).toString(16);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  /**
    * 对字符串md5加密(大写+数字)
    *
    * @param str传入要加密的字符串
    * @return MD5加密后的字符串
    */
  public static String toMD5(String source) {
    char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    try {
      byte[] btInput = source.getBytes();
      // 获得MD5摘要算法的 MessageDigest 对象
      MessageDigest mdInst = MessageDigest.getInstance("MD5");
      // 使用指定的字节更新摘要
      mdInst.update(btInput);
      // 获得密文
      byte[] md = mdInst.digest();
      // 把密文转换成十六进制的字符串形式
      int j = md.length;
      char str[] = new char[j * 2];
      int k = 0;
      for (int i = 0; i < j; i++) {
        byte byte0 = md[i];
        str[k++] = hexDigits[byte0 >>> 4 & 0xf];
        str[k++] = hexDigits[byte0 & 0xf];
      }
      return new String(str);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
}

自定义类-测试方法:模拟前端用户输入和后端数据库MD5加密比对

public static void main(String[] args) {
		//设置一个密码
		String password = "123456";
		System.out.println("MD5加密格式(大写+数字):" + MD5Util.toMD5(password);
		System.out.println("MD5加密格式(小写+数字):" + MD5Util.getMD5(password));
		
		//模拟后端数据库密码经过MD5加密
		String pwd_database = MD5Util.toMD5("123456");
		//模拟前端用户输入密码经过MD5加密
		String pwd_input = "123456";
		//打印
		System.out.println("前端用户输入和后端数据库MD5加密比对:" + MD5Util.toMD5(pwd_input).equals(pwd_database));
	}
}

在这里插入图片描述

可以看到密码明文一致的时候,加密的信息也是一致的,所以可以后端保存加密信息,然后将用户输入的密码明文进行MD5加密处理,来与后端数据库进行比对,作为一个简单的密码保护。

2、UUID

简介:

UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。最广泛应用的UUID,是微软公司的全局唯一标识符(GUID),而其他重要的应用,则有Linux ext2/ext3文件系统、LUKS加密分区、GNOME、KDE、Mac OS X等等。另外我们也可以在e2fsprogs包中的UUID库找到实现。
对UUID的详细认识:https://baike.baidu.com/item/UUID/5921266?fr=aladdin

使用:

在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。
而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)

public static void main(String[] args) {
    System.out.println(UUID.randomUUID());   //每次生成的编码不一样
}

在这里插入图片描述

结果串是用“-”来连接的,可以将它替换:

UUID uuid = UUID.randomUUID();   //生成结果串
String str = uuid.toString();   //结果串转换为String类型
String temp = str.replace("-","");   //将连接符“-”去掉
System.out.println(temp);

在这里插入图片描述

3、加盐

原理:

我们通常会将用户的密码进行 Hash 加密,如果不加盐,即使是两层的 md5 都有可能通过彩虹表的方式进行破译。彩虹表就是在网上搜集的各种字符组合的 Hash 加密结果。而加盐,就是人为的通过一组随机字符与用户原密码的组合形成一个新的字符,从而增加破译的难度。

原理:
简单来说:由原来的H( p) 变成了H(p+salt),相当于哈希函数H发生了变化,每次哈希计算使用的salt是随机的。
H如果发生了改变,则已有的彩虹表数据就完全无法使用,必须针对特定的H重新生成,这样就提高了破解的难度。

示例代码:

里面的静态方法包括:

  • 不加盐MD5:MD5WithoutSalt()
  • 生成盐:salt()
  • MD5加盐:MD5WithSalt()

运行文件的main方法:
在这里插入图片描述

package com.hz.springboot_crontab;

import java.security.MessageDigest;
import java.util.Random;

/**
     * 散列加密之32位哈希值的MD5算法,调用JDK里的API
     *ps:准确来说散列加密不是加密算法,因为它是不可逆的(只能加密,不能解密)
     */
    public class MyMD5 {

        private static char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

        public static void main(String[] args) throws Exception {
            String input = "123456789";
            System.out.println("MD5加密" + "\n"
                    + "明文:" + input + "\n"
                    + "无盐密文:" + MD5WithoutSalt(input));
            System.out.println("带盐密文:" + MD5WithSalt(input,0));
        }

        /**
         *@params: [inputStr] 输入明文
         *@Descrption: 不加盐MD5
         */
        public static String MD5WithoutSalt(String inputStr) {
            try {
            	//声明使用MD5算法,更改参数为"SHA"就是SHA算法了
                MessageDigest md = MessageDigest.getInstance("MD5");
                return byte2HexStr(md.digest(inputStr.getBytes()));//哈希计算,转换输出
            } catch (Exception e) {
                e.printStackTrace();
                return e.toString();
            }
        }

        /**
         *@params: [inputStr, type] inputStr是输入的明文;type是处理类型,0表示注册存hash值到库时,1表示登录验证时
         *@Descrption:  MD5加盐,盐的获取分两种情况;输入明文加盐;输出密文带盐(将salt存储到hash值中)
         */
        public static String MD5WithSalt(String inputStr, int type) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                
                String salt = null;
                if (type == 0) {//注册存hash值到库时,new salt
                    salt = salt();   
                } else if (type == 1) {//登录验证时,使用从库中查找到的hash值提取出的salt
                    String queriedHash=null;//从库中查找到的hash值
                    salt=getSaltFromHash(queriedHash);
                }

                String inputWithSalt = inputStr + salt;//加盐,输入加盐
                String hashResult = byte2HexStr(md.digest(inputWithSalt.getBytes()));//哈希计算,转换输出
                System.out.println("加盐密文:"+hashResult);

                /*将salt存储到hash值中,用于登陆验证密码时使用相同的盐*/
                char[] cs = new char[48];
                for (int i = 0; i < 48; i += 3) {
                    cs[i] = hashResult.charAt(i / 3 * 2);
                    cs[i + 1] = salt.charAt(i / 3);//输出带盐,存储盐到hash值中;每两个hash字符中间插入一个盐字符
                    cs[i + 2] = hashResult.charAt(i / 3 * 2 + 1);
                }
                hashResult = new String(cs);
                return hashResult;
            } catch (Exception e) {
                e.printStackTrace();
                return e.toString();
            }
        }


        /**
         * @return: salt
         * @params:
         * @Descrption: 自定义简单生成盐,是一个随机生成的长度为16的字符串,每一个字符是随机的十六进制字符
         */
        public static String salt() {
            Random random = new Random();
            StringBuilder sb = new StringBuilder(16);
            for (int i = 0; i < sb.capacity(); i++) {
                sb.append(hex[random.nextInt(16)]);
            }
            return sb.toString();
        }

        /**
         * @return: 十六进制字符串
         * @params: [bytes]
         * @Descrption: 将字节数组转换成十六进制字符串
         */
        private static String byte2HexStr(byte[] bytes) {
            /**
             *@Author: DavidHuang
             *@Time: 19:41 2018/5/10
             *@return: java.lang.String
             *@params:  * @param bytes
             *@Descrption:
             */
            int len = bytes.length;
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < len; i++) {
                byte byte0 = bytes[i];
                result.append(hex[byte0 >>> 4 & 0xf]);
                result.append(hex[byte0 & 0xf]);
            }
            return result.toString();
        }


        /**
         *@return: 提取的salt
         *@params: [hash] 3i byte带盐的hash值,带盐方法与MD5WithSalt中相同
         *@Descrption:  从库中查找到的hash值提取出的salt
         */
        public static String getSaltFromHash(String hash){
            StringBuilder sb=new StringBuilder();
            char [] h=hash.toCharArray();
            for(int i=0;i<hash.length();i+=3){
                sb.append(h[i+1]);
            }
            return sb.toString();
        }

}

4、jwt

【好文章要分享:https://zhuanlan.zhihu.com/p/433674847,有图片理解内部原理】

认知:

什么是jwt?

  • JWT只是缩写,全拼则是 JSON Web Tokens ,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。
    在这里插入图片描述

jwt原理:

  • jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。

JWT 使用:
1、服务端根据用户登录状态,将用户信息加密到token中,返给客户端
2、客户端收到服务端返回的token,存储在cookie中
3、客户端和服务端每次通信都带上token,可以放在http请求头信息中,如:Authorization字段里面
4、服务端解密token,验证内容,完成相应逻辑

JWT 特点:

  • JWT更加简洁,更适合在HTML和HTTP环境中传递
  • JWT适合一次性验证,如:激活邮件
  • JWT适合无状态认证
  • JWT适合服务端CDN分发内容
  • 相对于数据库Session查询更加省时
  • JWT默认不加密
  • 使用期间不可取消令牌或更改令牌的权限
  • JWT建议使用HTTPS协议来传输代码

JWT 结构:

  • 一个token分为3部分:头部(header)、载荷(payload)、签名(signature)
  • 三部分用点.隔开
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • header里面包含两部分信息,token的类型,算法的名称
    在这里插入图片描述
  • Payload
    在这里插入图片描述
  • Signature签名:将加密之后的上面两部分用点拼接的再次加密
    在这里插入图片描述

范例代码:

jar:jdk大于1.8的版本会报错,要导入其他依赖

<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
        <!--因为jdk的版本过高要加入的依赖,避免报错;  jdk1.8可以不加-->
<!--        <dependency>-->
<!--            <groupId>javax.xml.bind</groupId>-->
<!--            <artifactId>jaxb-api</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>com.sun.xml.bind</groupId>-->
<!--            <artifactId>jaxb-impl</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>com.sun.xml.bind</groupId>-->
<!--            <artifactId>jaxb-core</artifactId>-->
<!--            <version>2.3.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>javax.activation</groupId>-->
<!--            <artifactId>activation</artifactId>-->
<!--            <version>1.1.1</version>-->
<!--        </dependency>-->

JWTTest类:加密解密的方法

/**
 * jwt加密解密测试代码
 */
public class JWTTest {

    private long time = 1000*60*60*24;   //设置24小时之后失效
    private String signature = "admin";  //设置一个签名信息,加密解密都要通过他

    /**
     * 加密
     */
    @Test
    public void jwt() {
        //创建JwtBuilder对象
        JwtBuilder jwtBuilder = Jwts.builder();
        String jwtToken = jwtBuilder
                //header
                .setHeaderParam("typ","JWT")
                .setHeaderParam("alg","HS256")
                //payload
                .claim("username","Jules")
                .claim("role","admin")
                .setSubject("admin-test")   //主题
                .setExpiration(new Date(System.currentTimeMillis()+time))   //有效时间
                .setId(UUID.randomUUID().toString())
                //signature
                .signWith(SignatureAlgorithm.HS256,signature)
                //将上面三部分拼起来
                .compact();
        System.out.println(jwtToken);
    }

    /**
     * 解密
     */
   ……
}

加密后得到username和role的token
在这里插入图片描述

/**
 * 解密
 */
 @Test
    public void parse() {
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9………………";  //我省略了,复制自己的
        JwtParser jwtParser = Jwts.parser();
        Jws<Claims> claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();   //加密封装的东西都放到了claims里面
        //直接取:
        System.out.println(claims.get("username"));
        System.out.println(claims.get("role"));
        System.out.println(claims.getId());
        System.out.println(claims.getSubject());
        System.out.println(claims.getExpiration());

    }

将原来的数据解密:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朱尔斯Jules

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值