所谓短连接,就是把普通网址转换成一个比较短的网址,而访问获得的内容不变。html
好比说对于一个这样的连接 juejin.im/post/5dece9… ,使用短连接服务的话就能够将它转换成相似这种 http://xxx/abc (因为某不可描述的缘由与某不可抗力的影响 这个URL实际上是我编的)。是否是感受简洁了许多 (。-`ω´-)前端
简单来讲,当咱们输入 http://xxx/abc
后,会通过以下过程 :java
http://xxx
的 IP 地址http://xxx
服务器上运行的服务会经过短码 abc 获取其本来的 URL其中,重定向又分 301(永久重定向)和 302(临时重定向)。因为短地址一经生成就不会变化的性质,使用永久重定向能够对服务器压力又必定减小,但也所以没法统计到经由短地址来访问该页面的次数。因此,当对这种数据存在分析需求的时候,可使用 302 进行跳转,以增长一点服务器压力的代价来实现对更多数据的收集。mysql
内容压缩算法(MD5压缩算法)git
使用算法直接对长链内容进行压缩,例如获取 hash 算法,或是采用 MD5 算法,将长连接生成一个 128 位的特征码,而后将特征码截取成 4 到 8 位用做短链码。github
优势 :生成简单,不须要创建对应关系就能够支持长连接重复查询。算法
缺点 :因为 MD5 为有损压缩算法,不可避免会出现重复的问题。spring
发号器算法(id自增算法)sql
给每一个请求过来的长连接分配一个 id 号,对该索引进行加密,并用其做为短码生成须要的短连接。因为 id 是自增的,因此理论上生成的短连接永远不会重复,所以也叫永不重复算法。数据库
优势 :避免了短连接重复问题。
缺点 :自增 id 暴露在外,会有很大的安全风险,形成连接信息泄露;须要创建对应关系才能够支持长连接重复查询。
这里咱们选用id自增算法进行实现。
首先建立一个 springboot 项目,技术栈 spring boot + spring-data-jpa + mysql
,项目结构以下 :
而后就要开始理需求啦 :咱们实际须要的其实只有两个功能
首先要解决的是 长连接重复查询问题
。上文提到须要创建对应关系,这里咱们就简单用一张表来存储。与其对应的实体类 TranslationEntity 以下 :
package com.demo.demosurl.demosurl.model.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/** * @author yuanyiwen * @create 2019-12-06 21:04 * @description */
@Entity
@Data
public class TranslationEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
/** * 真实url(长链) */
private String url;
/** * 转换url(短链) */
private String shortUrl;
}
复制代码
而后是 直接暴露自增主键形成的连接信息泄露问题
。简单来想的话,若是 id 变化的规律为 一、二、三、4,...,就很好推测下一个;但若是 id 变化的规律是 一、3二、1八、97,...,呢?是否是就猜不到下一个是啥啦!
隐藏递增规律的核心就是使用 Feistel 密码结构,引用大佬的话 :
Feistel 加密算法的输入是长为 2w 的明文和一个密钥 K=(K1,K2...,Kn)。将明文分组分红左右两半 L 和 R ,而后进行 n 轮迭代,迭代完成后,再将左右两半合并到一块儿以产生密文分组。
Feistel 加密算法可以产生一个很是好用的特色,那就是,在必定的数字范围内(2 的 n 次方),每个数字都能根据密钥找到惟一的一个匹配对象,这就达到了咱们隐藏递增规律的目的。
例如,咱们给定数字范围为 64(2 的 6 次方),其中,每一个数字都能找到惟一一个随机对应的数字对。这里的随机经过密钥来产生。
那么咱们将 1 和 2 使用算法进行计算,会发现对应到的数字就是 17 和 25。这就完美解决了咱们的数字递增问题,外部用户没法从数字表面看出是递增的。并且每一个数字的匹配模式都不同。
借助这种思想,咱们添加一个 id 混淆工具类,来将有序的 id 映射为无序 :
package com.demo.demosurl.demosurl.util.encrypt;
/** * @author yuanyiwen * @create 2019-12-07 22:02 * @description id混淆工具类 */
public abstract class NumberEncodeUtil {
/** * 对id进行混淆 * @param id * @return */
public static Long encryptId(Long id) {
Long sid = (id & 0xff000000);
sid += (id & 0x0000ff00) << 8;
sid += (id & 0x00ff0000) >> 8;
sid += (id & 0x0000000f) << 4;
sid += (id & 0x000000f0) >> 4;
sid ^= 11184810;
return sid;
}
/** * 对混淆的id进行还原 * @param sid * @return */
public static Long decodeId(Long sid) {
sid ^= 11184810;
Long id = (sid & 0xff000000);
id += (sid & 0x00ff0000) >> 8;
id += (sid & 0x0000ff00) << 8;
id += (sid & 0x000000f0) >> 4;
id += (sid & 0x0000000f) << 4;
return id;
}
}
复制代码
解决了数字递增问题后,接下来要作的是 数字压缩与加密
。对一个数字长度进行压缩而不改变其含义,最简单的方式就是将它变为更高的进制。对于一个符号位来讲,若是咱们采用不带符号的简单字符(0-9a-zA-Z),加起来正好 62 个经常使用字符。因此咱们能够选择将混淆后的十进制 id 转换为 62 进制 :
package com.demo.demosurl.demosurl.util.encrypt;
import java.util.Stack;
/** * @author yuanyiwen * @create 2019-12-07 22:00 * @description 进制转换工具类 */
public abstract class ScaleConvertUtil {
/** * 将10进制数字转换为62进制 * @param number 数字 * @param length 转换成的62进制长度,不足length长度的高位补0 * @return */
public static String convert(long number, int length){
char[] charSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
Long rest=number;
Stack<Character> stack=new Stack<Character>();
StringBuilder result=new StringBuilder(0);
while(rest!=0){
stack.add(charSet[new Long((rest-(rest/62)*62)).intValue()]);
rest=rest/62;
}
for(;!stack.isEmpty();){
result.append(stack.pop());
}
int result_length = result.length();
StringBuilder temp0 = new StringBuilder();
for(int i = 0; i < length - result_length; i++){
temp0.append('0');
}
return temp0.toString() + result.toString();
}
}
复制代码
而后就能够开始进行具体的实现了!ServiceImpl 包含两个方法 :
具体的实现以下,关键思路标在注释里了 :
package com.demo.demosurl.demosurl.service.impl;
import com.demo.demosurl.demosurl.common.CommonConstant;
import com.demo.demosurl.demosurl.dao.TranslationDto;
import com.demo.demosurl.demosurl.model.entity.TranslationEntity;
import com.demo.demosurl.demosurl.model.vo.TranlationVo;
import com.demo.demosurl.demosurl.service.TranslationService;
import com.demo.demosurl.demosurl.util.convertion.EntityVoUtil;
import com.demo.demosurl.demosurl.util.encrypt.NumberEncodeUtil;
import com.demo.demosurl.demosurl.util.encrypt.ScaleConvertUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/** * @author yuanyiwen * @create 2019-12-06 21:12 * @description */
@Service
public class TranslationServiceImpl implements TranslationService {
@Autowired
private TranslationDto translationDto;
@Override
public TranlationVo getShortUrlByUrl(String url) {
// 若不是URL格式,直接返回空
if(!isHttpUrl(url)) {
return null;
}
TranslationEntity translationEntity = translationDto.findByUrl(url);
// 若是该实体不为空,直接返回对应的短url
if(translationEntity != null) {
return EntityVoUtil.convertEntityToVo(translationEntity);
}
// 不然,从新生成转换实体并存入数据库 todo 存入缓存
translationEntity = new TranslationEntity();
// 获取当前id并生成短url尾缀
Long currentId = translationDto.count()+1;
String shortUrlSuffix = ScaleConvertUtil.convert(NumberEncodeUtil.encryptId(currentId), CommonConstant.LENGTH_OF_SHORT_URL);
// 短连接拼接
String shortUrl = CommonConstant.PRIFIX_OF_SHORT_URL + shortUrlSuffix;
translationEntity.setUrl(url);
translationEntity.setShortUrl(shortUrl);
translationDto.save(translationEntity);
return EntityVoUtil.convertEntityToVo(translationEntity);
}
@Override
public TranlationVo getUrlByShortUrl(String shortUrl) {
TranslationEntity translationEntity = translationDto.findByShortUrl(shortUrl);
if(translationEntity != null) {
return EntityVoUtil.convertEntityToVo(translationEntity);
}
return null;
}
/** * 判断一个字符串是否为URL格式 * @param url * @return */
private boolean isHttpUrl(String url) {
boolean isUrl = false;
if(url.startsWith("http://") || url.startsWith("https://")) {
isUrl = true;
}
return isUrl;
}
}
复制代码
对外暴露一个“根据URL获取对应短连接”的接口方法 :
@PostMapping("/surl")
public ResponseVo<TranlationVo> getShortUrlByUrl(String url) {
TranlationVo tranlationVo = translationService.getShortUrlByUrl(url);
if(tranlationVo == null) {
return ResponseUtil.toFailResponseVo("请检查上传的url格式");
}
return ResponseUtil.toSuccessResponseVo(tranlationVo);
}
复制代码
最后是一些能够集中配置的常量参数,这里单独抽出来方便进行一些自定义调配 :
package com.demo.demosurl.demosurl.common;
/** * @author yuanyiwen * @create 2019-12-07 22:13 * @description 保存项目中用到的常量 */
public interface CommonConstant {
/** * 默认生成的短连接后缀长度 */
Integer LENGTH_OF_SHORT_URL = 4;
/** * 默认生成的短连接前缀 */
String PRIFIX_OF_SHORT_URL = "http://localhost:8090/";
/** * 若输入的短连接不存在,默认跳转的页面 */
String DEFAULT_URL = "http://localhost:8090/default";
}
复制代码
这个就很是简单了,只须要经过短链获取到长链,而后使用 ModelAndView 的重定向进行跳转 :
@GetMapping("{shortUrl}")
public ModelAndView redirect(@PathVariable String shortUrl, ModelAndView mav){
// 获取对应的长连接(若短连接不存在,则跳转到默认页面)
TranlationVo tranlationVo = translationService.getUrlByShortUrl(CommonConstant.PRIFIX_OF_SHORT_URL + shortUrl);
String url = (tranlationVo == null) ? CommonConstant.DEFAULT_URL : tranlationVo.getUrl();
// 跳转
mav.setViewName("redirect:" + url);
return mav;
}
复制代码
启动项目,打开 postman ,输入咱们的URL :
能够看到,服务已经将长连接 juejin.im/post/5dece9… 转成对应的短连接 http://localhost:8090/kvgQ 返了回来。让咱们访问一下试试 :
跳转成功!这样一个简单的短连接服务就完成啦。
由于是一个自定义的很是简单的短连接服务,因此仍是有很是多地方能够继续改进的,好比说
最后悄悄放一个github地址 _(:3 」∠)_ : github.com/yuanLier/su…
参考文章 :
www.cnblogs.com/yuanjiangw/… my.oschina.net/u/2485991/b…