检查出⾮⽤户登录,或者账号被盗窃等潜在⻛险挖掘。通过对⽤户登录⾏为进⾏分析,提⾼了预测的准确性;可以应⽤于互联⽹⾦融⻛控技术中,也可应⽤于普通⽹站的⽤户恶意登录识别技术中。
⼀种基于⽤户登录⾏为分析的⻛控⽅法,其特征在于:所述的⽅法包括 ⽤户按键⻛险识别
、 ⽤户登录地⻛险识别
、 密码重试⻛险识别
、 设备来源⻛险识别
、 ⽤户登录习惯⻛险识别
、 累计登录多次⻛险识别
、 ⽤户登录的瞬时位移速度⻛险识别
七种模型;本系统仅仅负责根据⽤户的登录数据产⽣⻛险评估报告。报告的格式为:
并不负责对⽤户的登录做出定性的分析,仅仅当系统发送⽤户登录数据过来,由⼤数据评估系统对⽤户数据进⾏评估,然后产⽣评估报告。由业务系统⾃⾏根据评估报告,由⽤户采取相关的奖惩措施。介于以上评估报告系统需要⽤户发送以下数据格式,以辅助系统完成验证:
INFO 2020-03-31 10:12:00 QQ EVALUATE [张三] 6ebaf4ac780f40f486359f3ea6934620 "12355421"
Beijing "116.4,39.5"
[1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
登录成功产⽣数据:登录成功的数据 ⽤来作为下⼀次评估登录时的历史数据,系统留存最近的⼀些历史登录数据集,作为下⼀次登录评估的标准。
INFO 2020-03-31 10:12:00 QQ SUCCESS [张三] 6ebaf4ac780f40f486359f3ea6934620 "12355421"
Beijing "116.4,39.5"
[1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
借助:https://regex101.com
INFO 2020-03-31 10:12:00 Q1Q应⽤1 success [张三] 6ebaf4ac780f40f486359f3ea6934620
"123456" Beijing "116.4,39.5" [1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS
X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
public class EvaluateUtil {
public static final String LEGAL_REGEX="^INFO\\s(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2})\\s([a-z0-9\\u4e00-\\u9fa5]*)\\s(EVALUATE|SUCCESS)\\s\\[([a-z0-9\\u4e00-\\u9fa5]*)\\]\\s([a-z0-9]{32})\\s\\\"([a-z0-9\\.\\-\\,]{6,12})\\\"\\s([a-z\\u4e00-\\u9fa5]*)\\s\\\"([0-9\\.\\,]*)\\\"\\s\\[([0-9\\,\\.]*)\\]\\s\\\"(.*)\\\"";
public static final String EVALUATE_REGEX="^INFO\\s(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2})\\s([a-z0-9\\u4e00-\\u9fa5]*)\\s(EVALUATE)\\s\\[([a-z0-9\\u4e00-\\u9fa5]*)\\]\\s([a-z0-9]{32})\\s\\\"([a-z0-9\\.\\-\\,]{6,12})\\\"\\s([a-z\\u4e00-\\u9fa5]*)\\s\\\"([0-9\\.\\,]*)\\\"\\s\\[([0-9\\,\\.]*)\\]\\s\\\"(.*)\\\"";
public static final String SUCCESS_REGEX="^INFO\\s(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2})\\s([a-z0-9\\u4e00-\\u9fa5]*)\\s(SUCCESS)\\s\\[([a-z0-9\\u4e00-\\u9fa5]*)\\]\\s([a-z0-9]{32})\\s\\\"([a-z0-9\\.\\-\\,]{6,12})\\\"\\s([a-z\\u4e00-\\u9fa5]*)\\s\\\"([0-9\\.\\,]*)\\\"\\s\\[([0-9\\,\\.]*)\\]\\s\\\"(.*)\\\"";
public static final Pattern LEGAL_PATTERN = Pattern.compile(LEGAL_REGEX, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
public static final Pattern EVALUATE_PATTERN = Pattern.compile(EVALUATE_REGEX, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
public static final Pattern SUCCESS_PATTERN = Pattern.compile(SUCCESS_REGEX, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
final static String regex = "^INFO\\s(\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}:\\d{2})\\s([a-z0-9\\u4e00-\\u9fa5]*)\\s(EVALUATE|SUCCESS)\\s\\[([a-z0-9\\u4e00-\\u9fa5]*)\\]\\s([a-z0-9]{32})\\s\\\"([a-z0-9\\.\\-\\,]{6,12})\\\"\\s([a-z\\u4e00-\\u9fa5]*)\\s\\\"([0-9\\.\\,]*)\\\"\\s\\[([0-9\\,\\.]*)\\]\\s\\\"(.*)\\\"";
final static String string = "INFO 2020-03-31 10:12:00 Q1Q应用1 success [张三] 6ebaf4ac780f40f486359f3ea6934620 \"123456\" Beijing \"116.4,39.5\" [1200,15000,2100] \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36\"";
public static Boolean isLegal(String input){
Matcher matcher = LEGAL_PATTERN.matcher(input);
return matcher.matches();
}
public static Boolean isEvaluate(String input){
Matcher matcher = EVALUATE_PATTERN.matcher(input);
return matcher.matches();
}
public static Boolean isLoginSuccess(String input){
Matcher matcher = SUCCESS_PATTERN.matcher(input);
return matcher.matches();
}
public static EvaluateData parseEvaluateData(String input) throws ParseException {
//指定一个验证数据对象
EvaluateData evaluateData = new EvaluateData();
//获取匹配体
Matcher matcher = EvaluateUtil.EVALUATE_PATTERN.matcher(input);
//如果配配到了
if(matcher.find()){
//遍历
for (int i = 1; i <= matcher.groupCount(); i++) {
switch (i){
case 1:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = simpleDateFormat.parse(matcher.group(i));
//传递时间参数
evaluateData.setEvaluateTime(date.getTime());
break;
case 2:
//设置应用名
evaluateData.setApplicationName(matcher.group(i));
break;
case 4:
//设置用户标识
evaluateData.setUserIdentify(matcher.group(i));
break;
case 5:
//设置应用序列
evaluateData.setLoginSequence(matcher.group(i));
break;
case 6:
//设置密码
evaluateData.setOrdernessPassword(matcher.group(i));
break;
case 7:
//设置城市
evaluateData.setCityName(matcher.group(i));
break;
case 8:
//设置经纬度
String geoparams = matcher.group(i);
String[] geos = geoparams.split(",");
//指定一个经纬度对象
GeoPoint geoPoint = new GeoPoint(Double.parseDouble(geos[0]),Double.parseDouble(geos[1]));
//设置位置对象
evaluateData.setGeoPoint(geoPoint);
break;
case 9:
//设置输入特性
String featrues = matcher.group(i);
String[] featureGroup = featrues.split(",");
Double[] doubleFertrue = {Double.parseDouble(featureGroup[0]),Double.parseDouble(featureGroup[1]),Double.parseDouble(featureGroup[2])};
evaluateData.setInputFeatures(doubleFertrue);
break;
case 10:
//设置设备信息
evaluateData.setDeviceInformation(matcher.group(i));
break;
}
}
}
return evaluateData;
}
public static LoginSuccessData parseLoginSuccessData(String input) throws ParseException {
//指定一个验证数据对象
LoginSuccessData loginSuccessData = new LoginSuccessData();
//获取匹配体
Matcher matcher = EvaluateUtil.SUCCESS_PATTERN.matcher(input);
//如果配配到了
if(matcher.find()){
//遍历
for (int i = 1; i <= matcher.groupCount(); i++) {
switch (i){
case 1:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = simpleDateFormat.parse(matcher.group(i));
//传递时间参数
loginSuccessData.setLoginTime(date.getTime());
break;
case 2:
//设置应用名
loginSuccessData.setApplicationName(matcher.group(i));
break;
case 4:
//设置用户标识
loginSuccessData.setUserIdentify(matcher.group(i));
break;
case 5:
//设置应用序列
loginSuccessData.setLoginSequence(matcher.group(i));
break;
case 6:
//设置密码
loginSuccessData.setOrdernessPassword(matcher.group(i));
break;
case 7:
//设置城市
loginSuccessData.setCityName(matcher.group(i));
break;
case 8:
//设置经纬度
String geoparams = matcher.group(i);
String[] geos = geoparams.split(",");
//指定一个经纬度对象
GeoPoint geoPoint = new GeoPoint(Double.parseDouble(geos[0]),Double.parseDouble(geos[1]));
//设置位置对象
loginSuccessData.setGeoPoint(geoPoint);
break;
case 9:
//设置输入特性
String featrues = matcher.group(i);
String[] featureGroup = featrues.split(",");
Double[] doubleFertrue = {Double.parseDouble(featureGroup[0]),Double.parseDouble(featureGroup[1]),Double.parseDouble(featureGroup[2])};
loginSuccessData.setInputFeatures(doubleFertrue);
break;
case 10:
//设置设备信息
loginSuccessData.setDeviceInformation(matcher.group(i));
break;
}
}
}
return loginSuccessData;
}
final static Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
final static Matcher matcher = pattern.matcher(string);
public static void main(String[] args) throws ParseException {
String input="INFO 2020-03-31 10:12:00 Q1Q应用1 success [张三] 6ebaf4ac780f40f486359f3ea6934620 \"123456\" Beijing \"116.4,39.5\" [1200,15000,2100] \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36\"";
if (matcher.find()) {
System.out.println("Full match: " + matcher.group(0));
for (int i = 1; i <= matcher.groupCount(); i++) {
System.out.println("Group " + i + ": " + matcher.group(i));
}
}
LoginSuccessData loginSuccessData = parseLoginSuccessData(input);
System.out.println(loginSuccessData);
}
}
Full match: INFO 2020-03-31 10:12:00 QQ SUCCESS [张三] 6ebaf4ac780f40f486359f3ea6934620
"123456" Beijing "116.4,39.5" [1200,15000,2100] "Mozilla/5.0 (Macintosh; Intel Mac OS
X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
Group 1: 2020-03-31 10:12:00
Group 2: QQ
Group 3: SUCCESS
Group 4: 张三
Group 5: 6ebaf4ac780f40f486359f3ea6934620
Group 6: 123456
Group 7: Beijing
Group 8: 116.4,39.5
Group 9: 1200,15000,2100
Group 10: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/80.0.3987.149 Safari/537.36
评估数据:当前登陆城市
beijing
历史数据 : 留存用户的历史登陆城市一个Set集合[beijing
,shanghai
,zhengzhou
]
package com.baizhi.evaluate.impl;
import com.baizhi.entities.EvaluateData;
import com.baizhi.entities.EvaluateReport;
import com.baizhi.entities.HistoryData;
import com.baizhi.entities.RiskFactor;
import com.baizhi.evaluate.Evaluate;
import com.baizhi.evaluate.EvaluateChain;
import java.util.Set;
public class AreaEvaluate extends Evaluate {
public AreaEvaluate() {
super(RiskFactor.AREA);
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getCityName(),historyData.getHistoryCities()));
//驱动调用下一个评估
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
public boolean doEval(String cityName, Set<String> historyCities){
if(historyCities==null || historyCities.size()==0){//说明是第一次使用
return false;
}else{//如果登录过该城市就没有风险
return !historyCities.contains(cityName);
}
}
}
评估数据:当前用户的设备信息
历史数据 : 仅仅保存近期的3个有效设备信息去重
package com.baizhi.evaluate.impl;
import com.baizhi.entities.EvaluateData;
import com.baizhi.entities.EvaluateReport;
import com.baizhi.entities.HistoryData;
import com.baizhi.entities.RiskFactor;
import com.baizhi.evaluate.Evaluate;
import com.baizhi.evaluate.EvaluateChain;
import java.util.Set;
public class DeviceEvaluate extends Evaluate {
public DeviceEvaluate() {
super(RiskFactor.DEVICE);
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getDeviceInformation(),historyData.getHistoryDeviceInformations()));
//驱动调用下一个评估
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
public boolean doEval(String deviceInformation, Set<String> historyDeviceInformations){
if(historyDeviceInformations==null || historyDeviceInformations.size()==0){//说明是第一次使用
return false;
}else{//如果没有使用该设备
return !historyDeviceInformations.contains(deviceInformation);
}
}
}
评估数据:-
历史数据:当天上一次累积评估次数
package com.baizhi.evaluate.impl;
import com.baizhi.entities.EvaluateData;
import com.baizhi.entities.EvaluateReport;
import com.baizhi.entities.HistoryData;
import com.baizhi.entities.RiskFactor;
import com.baizhi.evaluate.Evaluate;
import com.baizhi.evaluate.EvaluateChain;
import java.util.Set;
public class TotalEvaluate extends Evaluate {
private Integer threshold=0;
/**
* 允许用户最大的登录次数
* @param threshold
*/
public TotalEvaluate(Integer threshold) {
super(RiskFactor.TOTAL);
this.threshold=threshold;
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
evaluateReport.signReport(getRiskFactor(),doEval(historyData.getCurrentDayLoginCount()));
//驱动调用下一个评估
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
public boolean doEval(Integer currentDayLoginCount){
if(currentDayLoginCount==null){//说明是第一次使用
return false;
}else{//如果登录超过限定次数
return currentDayLoginCount >= threshold;
}
}
}
评估数据:当前登陆时间,提取
dayOfWeek
hourOfDay
历史数据:留存用户所有的历史登陆信息dayOfWeek
hourOfDay
次数
public class TimeSlotEvaluate extends Evaluate {
private int threshold;
public TimeSlotEvaluate(int threshold) {//设定多累计登录多少次以后再对用户进行评估
super(RiskFactor.TIMESLOT);
this.threshold=threshold;
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getEvaluateTime(),historyData.getHistoryLoginTimeSlot()));
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
/**
* @param evaluateTime
* @param historyLoginTimeSlot
* @return
*/
public boolean doEval(long evaluateTime, Map<String, Map<String,Integer>> historyLoginTimeSlot){
String[] WEEKS={"星期日","星期一","星期二","星期三","星期四","星期五","星期六"};
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(evaluateTime);
String dayOfWeek = WEEKS[calendar.get(Calendar.DAY_OF_WEEK) - 1];
DecimalFormat decimalFormat=new DecimalFormat("00");
String hourOfDay= decimalFormat.format(calendar.get(Calendar.HOUR_OF_DAY));//01 02 ... 24
//用户第一次登录
if(historyLoginTimeSlot==null || historyLoginTimeSlot.size()==0){
return false;
}
//用户是否达到评估计算阈值标准,如果登录总次小于阈值,不做评估
if(!historyLoginTimeSlot.containsKey(dayOfWeek)){
Integer totalCount = historyLoginTimeSlot.entrySet()
.stream()// 星期几 Map<小时,次数>
.map(t -> t.getValue().entrySet().stream().map(v -> v.getValue()).reduce((v1, v2) -> v1 + v2).get()) // 每天登录总数
.reduce((v1, v2) -> v1 + v2)
.get();
return totalCount >= threshold;
}else{
//获取当天的登录时段数据
Map<String, Integer> historyTimeSlot = historyLoginTimeSlot.get(dayOfWeek);
if(!historyTimeSlot.containsKey(hourOfDay)){//该天登录过,但是在该时段没有登录
return true;
}else{//该天登录过,但是在该时段也登录过
//判断当前的登录时段是否使用户的登录习惯
Integer currentHourLoginCount = historyTimeSlot.get(hourOfDay);
//升序登录时间段集合
List<Integer> sortedList = historyTimeSlot.entrySet()
.stream()
.map(t -> t.getValue()) //每个时段登录总和
.sorted().collect(Collectors.toList());
//获取用户登录时段的阈值,大于或者等于该值都是习惯
Integer thresholdTimeSlotCount=sortedList.get((sortedList.size()*2)/3);
return currentHourLoginCount<thresholdTimeSlotCount;
/*
//计算出所有登录总次数大于thresholdTimeSlotCount值所有hourOfDay集合-习惯时段
List habbitTimeSlot = historyTimeSlot.entrySet()
.stream()
.filter(t -> t.getValue() >= thresholdTimeSlotCount)
.map(t -> t.getKey())
.collect(Collectors.toList());
return !habbitTimeSlot.contains(hourOfDay);
*/
}
}
}
}
密码错误评估,成分评估(☆☆☆)
此评估的依据需要借助余弦相似度来进行评估,因为普通的java代码不能够满足评估的需求,故需要进行建模,将用户的历史密码以及当前输入的密码进行提取特征,构建成特征向量,然后根据两两向量之间夹角的余弦值进行评定,余弦值越小说明相似度越高。
余弦相似度的具体介绍请看:https://blog.csdn.net/weixin_45607513/article/details/105245121
评估数据:乱序明文密码
历史数据:历史明文密码乱序集合Set
public class SimilarityEvaluate extends Evaluate {
private Double thresholdSimilarity;
public SimilarityEvaluate(Double thresholdSimilarity) {
super(RiskFactor.SIMILARITY);
this.thresholdSimilarity=thresholdSimilarity;
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
//生成评估报告结果
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getOrdernessPassword(),historyData.getHistoryOrdernessPasswords()));
//驱动调用下一个评估
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
/**
* 乱序密码评估结果
* */
public boolean doEval(String ordernessPassword, Set<String> historyOrdernessPasswords){
if(historyOrdernessPasswords==null || historyOrdernessPasswords.size()==0){
return false;
}
//构建词袋-密码维度
Set<Character> chars=new HashSet<>();
historyOrdernessPasswords.stream().forEach(historyOrdernessPassword-> {
for (char c : historyOrdernessPassword.toCharArray()) {
chars.add(c);
}
});
//将现行密码加入词袋,富化维度
for (char c : ordernessPassword.toCharArray()) {
chars.add(c);
}
//对词袋进行排序
List<Character> wordBag = chars.stream().sorted().collect(Collectors.toList());
System.out.println("wordBag:"+wordBag);
//将所有的历史密码装转为特征向量
List<Integer[]> verctors=historyOrdernessPasswords.stream()
.map(historyOrdernessPassword-> converString2Vector(wordBag,historyOrdernessPassword))
.collect(Collectors.toList());
//当前输入密码的特征向量
Integer[] currentVector = converString2Vector(wordBag, ordernessPassword);
List<Double> resultList = verctors.stream()
.map(v1 -> {
double similarity = calculateSimilarity(v1, currentVector);
System.out.println(similarity);
return similarity;
})
.filter(similarity -> similarity >= thresholdSimilarity)
.collect(Collectors.toList());
return resultList.size()==0;
}
/**
* 计算相似度
* */
private double calculateSimilarity(Integer[] v1,Integer[] v2){
Double sum=0.0;
//计算分子
for (int i = 0; i < v1.length; i++) {
sum+=v1[i]*v2[i];
}
//计算平方和
Integer powSum1 = Arrays.stream(v1).map(i -> i * i).reduce((i1, i2) -> i1 + i2).get();
Integer powSum2 = Arrays.stream(v2).map(i -> i * i).reduce((i1, i2) -> i1 + i2).get();
return sum/(Math.sqrt(powSum1)*Math.sqrt(powSum2));
}
/**
* 构建特征向量
* */
private Integer[] converString2Vector(List<Character> wordBag,String ordernessPassword){
//特征向量
Integer[] vector=new Integer[wordBag.size()];
//计算特征向量中元素的值
HashMap<Character, Integer> charMap = new HashMap<>();
//如果包含字符,加1;不包含,置为1
for (Character c : ordernessPassword.toCharArray()) {
Integer count=1;
if(charMap.containsKey(c)){
count+=charMap.get(c);
}
charMap.put(c,count);
}
//将字符集合中的每个字符的值放到特征向量中
for (int i = 0; i < wordBag.size(); i++) {
Character c = wordBag.get(i);
vector[i]= charMap.containsKey(c)?charMap.get(c):0;
}
return vector;
}
评估数据:评估时间、GeoPoint
历史数据: 上⼀次登录的时间、上⼀次登录GeoPoint
public class SpeedEvaluate extends Evaluate {
//阈值
private Double thresholdSpeed;
//地球半径
private static final Double EARTH_RADIUS=6371.393;//千米
public SpeedEvaluate(Double thresholdSpeed) {
super(RiskFactor.SPEED);
this.thresholdSpeed=thresholdSpeed;
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getEvaluateTime(),evaluateData.getGeoPoint(),historyData.getLastLoginTime(),historyData.getLastLoginGeoPoint()));
//驱动调用下一个评估
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
public boolean doEval(long evaluateTime, GeoPoint currentGeoPoint,long lastLoginTime,GeoPoint lastLoginGeoPoint){
//第一次登录
if(lastLoginGeoPoint==null){
return false;
}
//两次登录的时间差
double diffTime=(evaluateTime-lastLoginTime)*1.0/(3600*1000);//小时时差
//两次登录的距离
Double distance = calculateDistance(currentGeoPoint, lastLoginGeoPoint);
//之间的速度
double speed=distance/diffTime;
System.out.println("distance:"+distance+",diffTime:"+diffTime+",speed:"+speed);
//速度大于阈值,有风险
return speed> thresholdSpeed;
}
//球面距离公式
private Double calculateDistance(GeoPoint point1,GeoPoint point2){
Double wA=toRadians(point1.getLatitude());//将角度转换为弧度
Double jA=toRadians(point1.getLongtitude());
Double wB=toRadians(point2.getLatitude());
Double jB=toRadians(point2.getLongtitude());
return EARTH_RADIUS * acos(cos(wA)*cos(wB)*cos(jB-jA)+sin(wA)*sin(wB));
}
//测试
public static void main(String[] args) throws ParseException {
SpeedEvaluate speedEvaluate = new SpeedEvaluate(600.0);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long evaluateTime=sdf.parse("2020-04-01 10:10:00").getTime();
GeoPoint p1=new GeoPoint();
p1.setLongtitude(116.2317);//北京
p1.setLatitude(39.5427);
long lastLoginTime=sdf.parse("2020-04-01 08:00:00").getTime();
GeoPoint p2=new GeoPoint();
p2.setLongtitude(114.14);//郑州
p2.setLatitude(34.16);
speedEvaluate.doEval(evaluateTime,p1,lastLoginTime,p2);
}
}
评估数据:获取⽤户输⼊特征 [a0,b0,c0]
历史数据: 留存⽤户最近10次输⼊特征[(a1,b1,c1),(a2,b2,c2),(a3,b3,c3),…]
public class InputFeatureEvaluate extends Evaluate {
public InputFeatureEvaluate() {
super(RiskFactor.INPUTFEATURE);
}
@Override
public void eval(EvaluateData evaluateData, HistoryData historyData, EvaluateReport evaluateReport, EvaluateChain evaluateChain) {
//填写报告
evaluateReport.signReport(getRiskFactor(),doEval(evaluateData.getInputFeatures(),historyData.getLatestInputFeatures()));
//路由请求
evaluateChain.doChain(evaluateData,historyData,evaluateReport);
}
/**
* 1.计算圆心中点
* 2.计算两两特征距离
* 3.对距离进行排序(升序),取2/3处作为评估距离阈值 - threshold
* 4.计算当前输入的特征距离中心点距离d
* @param inputFeatures
* @param latestInputFeatures
* @return
*/
public boolean doEval(Double[] inputFeatures, List<Double[]> latestInputFeatures){
//至少保证历史记录有2条(含)以上
if(latestInputFeatures==null || latestInputFeatures.size()<2){
return false;
}
// 1.计算圆心中点
Double[] sumInputs = latestInputFeatures.stream().reduce((v1, v2) -> {
//输入特征各个坐标对应的总和
Double[] sumInputFeatures = new Double[v1.length];
for (int i = 0; i < v1.length; i++) {
if(sumInputFeatures[i]==null){
sumInputFeatures[i]=0.0;
}
sumInputFeatures[i] += v1[i] + v2[i];
}
return sumInputFeatures;
}).get();
//计算圆心中点
Double[] centerVector=new Double[sumInputs.length];
for (int i = 0; i < sumInputs.length; i++) {
centerVector[i]=sumInputs[i]/latestInputFeatures.size();
}
System.out.println("中点:"+ Arrays.stream(centerVector).map(i->i+"").reduce((v1,v2)->v1+","+v2).get());
//2.计算两两特征距离
List<Double> distances=new ArrayList<>();
//一定可以拿到 n*(n-1)/2 【n= latestInputFeatures.size()】 如:5个点,一共有10个距离
for (int i = 0; i < latestInputFeatures.size() ; i++) {
Double[] currentFeature=latestInputFeatures.get(i);
for (int j = i+1; j < latestInputFeatures.size(); j++) {
Double[] nextFeature=latestInputFeatures.get(j);
//计算向量距离
Double distance= calculateDistance(currentFeature,nextFeature);
distances.add(distance);
}
}
//3.对距离进行排序(升序),取2/3处作为评估距离阈值 - threshold
distances.sort(new Comparator<Double>() {
@Override
public int compare(Double o1, Double o2) {
if(o1==o2) {
return 0;
}else{
return o1>o2?1:-1;
}
}
});
System.out.println("distances:"+distances);
Integer n= latestInputFeatures.size();
int position=(n*(n-1)/2)*2/3;
Double thresholdDistance = distances.get(position);
//4.计算当前输入的特征所在点与中心点的距离d
Double currentDistance = calculateDistance(inputFeatures, centerVector);
System.out.println("threshold:"+thresholdDistance+","+"currentDistance:"+currentDistance);
//如果大于阈值,说明有风险
return currentDistance> thresholdDistance;
}
//欧氏距离 n维空间 两个坐标点的距离
private Double calculateDistance(Double[] v1,Double[] v2){
Double sum=0.0;
for (int i = 0; i < v1.length; i++) {
sum+=(v1[i]-v2[i])*(v1[i]-v2[i]);
}
return Math.sqrt(sum);
}
//测试
public static void main(String[] args) {
InputFeatureEvaluate inputFeatureEvaluate = new InputFeatureEvaluate();
ArrayList<Double[]> latestInputFeatures = new ArrayList<>();
latestInputFeatures.add(new Double[]{1000.0,1100.0,1800.0});
latestInputFeatures.add(new Double[]{1100.0,1120.0,1750.0});
latestInputFeatures.add(new Double[]{950.0,1250.0,2000.0});
latestInputFeatures.add(new Double[]{1200.0,1050.0,1900.0});
latestInputFeatures.add(new Double[]{1400.0,800.0,2500.0});
inputFeatureEvaluate.doEval(new Double[]{1100.0,1000.0,1750.0},latestInputFeatures);
}
}
关于余弦相似度、欧式距离、球面距离公式的推断可以参考以前的博客:
余弦相似度:https://blog.csdn.net/weixin_45607513/article/details/105245121
欧氏距离:https://blog.csdn.net/weixin_45607513/article/details/105266407
球面距离公式:https://blog.csdn.net/weixin_45607513/article/details/105275046