所謂短連接,就是把普通網址轉換成一個比較短的網址,而訪問獲得的內容不變。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…