最近的一个项目需要实现双因素强认证,平常我们都是采用 静态密码+动态短信这样的方式来实现,但用户侧并没有相应的短信接口。 后来决定采用 google身份验证器来实现。在网上找了一些资料和代码片段,经过梳理和改造,目前已上线使用了,效果还是比较好的,记录一下,也给需要的朋友做个参考。

首先简述一下双因素认证:双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到一起才能发挥作用的身份认证系统。双因素认证是一种采用时间同步技术的系统,采用了基于时间、事件和密钥三变量而产生的一次性密码来代替传统的静态密码。每个动态密码卡都有一个唯一的密钥,该密钥同时存放在服务器端,每次认证时动态密码卡与服务器分别根据同样的密钥,同样的随机参数(时间、事件)和同样的算法计算了认证的动态密码,从而确保密码的一致性,从而实现了用户的认证。----引自百度百科

实现原理可以参考:http://www.zhihu.com/question/20462696                            http://blog.seetee.me/archives/73.html

主要实现思路:

1: 当用户 登录系统,先经过用户名和密码验证后,再进行动态验证,查询用户的Google密钥是否启用(密钥信息在数据库中保存);

2:如果没有Google密钥,则将页面重定向到启用密钥的页面;

2.1  : 调用 GoogleAuthenticator生成google密钥,并将密钥写入二维码中,提示用户使用Google验证器扫描二维码;

2.2  : 用户使用Google身份验证器扫描二维码,用户的手机中即可生成动态验证码;

2.3  : 为了确保二维码密钥和用户手机上时间与服务器基本一致,需要让用户验证生成的动态验证码是否可以通过系统的验证(验证过程同登录过程,暂且不表);

3:如果用户已有Google密钥,用户打开手机上的Google身份验证器,找到对应的帐号生成的动态码,输入系统;

4: 调用 GoogleAuthenticator 密钥验证接口,判断是否验证通过;

4.1: 验证通过,执行系统登录的相关流程;

4.2: 验证不通过,提示用户:查验手机时间 与服务器的时间是否一致或者帐号与动态验证码的对应关系是否准确(如果用户手机上有多个帐号时,经常出现这种问题)

待完善功能:如果用户手机丢失,可信用户重置google密钥;

以上,仅是我的一个实现思路,如果大家有好的想法,欢迎交流。

以下为两个重要类的代码,供参考。

如果需要完整的项目代码,请移步:基于Google 验证器 实现内网的双因素认证项目   ,由于本代码为实际项目的代码片段,经过删减后的版本,比较混乱(但可以正常运行),所以不建议大家下载,

谷歌身份验证的Java服务器端:

package com.google.module.authenticator.utils;import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;import com.google.framework.QuickResponse.QRUtil;/*** 谷歌身份验证的Java服务器端*/
public class GoogleAuthenticator {// 来自谷歌文档,不用修该public static final int SECRET_SIZE = 10;public static final String SEED = "g8GjEvTbW5oVSV7avLBdwIHqGlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";//安全哈希算法(Secure Hash Algorithm)int window_size = 3; //默认 3 - 最大值17 (from google docs)多可偏移的时间--3*30秒的验证时间(手机客户端验证为30秒变化次)/*** 设置偏移量,最大值17* 默认 3 - 最大值17 (from google docs)多可偏移的时间--3*30秒的验证时间(手机客户端验证为30秒变化次)* @param s*/public void setWindowSize(int s) {if (s >= 1 && s <= 17)window_size = s;}/**秘钥* 随机生成1个秘钥,这个秘钥必须在服务器上保存,用户在手机Google身份验证器上配置账号时也要这个秘钥* @return secret key*/public static String generateSecretKey() {SecureRandom sr = null;try {sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);byte[] seedBytes = SEED.getBytes();sr.setSeed(Base64.decodeBase64(seedBytes));byte[] buffer = sr.generateSeed(SECRET_SIZE);Base32 codec = new Base32();byte[] bEncodedKey = codec.encode(buffer);String encodedKey = new String(bEncodedKey);return encodedKey;}catch (NoSuchAlgorithmException e) {// should never occur... configuration error}return null;}/***返回一个URL生成并显示二维码。用户扫描这个二维码*谷歌身份验证应用程序的智能手机注册身份验证代码*他们还可以手动输入秘钥key* @param user* @param host * @param secret 之前为用户生成的秘钥* @return 二维码的url*//*** @Definition: * @author: TangWenWu* @Created date: 2014-11-24* @param user* @param host* @param secret* @return*/public static String getQRBarcodeURL(String user, String host, String secret) {/* *   由于是内网系统,无法访问google,所以注释*    String format = "https://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s";*    return String.format(format, user, host, secret);*///采用com.google.zxing 自己生成 二维码图片,保存在项目某个目录下// 二维码内容String content = "otpauth://totp/"+user+"@"+host+"?secret="+secret;// 二维码宽度int width = 300;// 二维码高度int height = 300;// 二维码存放地址String imageName = "googleAuthCode_"+user+"_"+host+".png";return QRUtil.generateImageInBorderQR(content, width, height, imageName);}public static void main(String[] args) {String path = getQRBarcodeURL("tangww","ROOT","WERRWEIU23424U");System.out.println("path======================="+path);}/*** 查用户输入的6位码是否有效* @param secret 秘钥* @param code 6位码* @param t 偏移时间* @return*/public boolean check_code(String secret, long code, long timeMsec) {Base32 codec = new Base32();byte[] decodedKey = codec.decode(secret);// convert unix msec time into a 30 second "window" // this is per the TOTP spec (see the RFC for details)long t = (timeMsec / 1000L) / 30L;// window是用来检验之前生成的6位码// 可以用这个window_size来调整允许6位码生效的时间for (int i = -window_size; i <= window_size; ++i) {long hash;try {hash = verify_code(decodedKey, t + i);}catch (Exception e) {// Yes, this is bad form - but// the exceptions thrown would be rare and a static configuration probleme.printStackTrace();throw new RuntimeException(e.getMessage());//return false;}if (hash == code) {return true;}}// The validation code is invalid.return false;}/*** 生成验证码* @param key* @param t* @return* @throws NoSuchAlgorithmException* @throws InvalidKeyException*/private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {byte[] data = new byte[8];long value = t;for (int i = 8; i-- > 0; value >>>= 8) {data[i] = (byte) value;}SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");Mac mac = Mac.getInstance("HmacSHA1");mac.init(signKey);byte[] hash = mac.doFinal(data);int offset = hash[20 - 1] & 0xF;// We're using a long because Java hasn't got unsigned int.long truncatedHash = 0;for (int i = 0; i < 4; ++i) {truncatedHash <<= 8;// We are dealing with signed bytes:// we just keep the first byte.truncatedHash |= (hash[offset + i] & 0xFF);}truncatedHash &= 0x7FFFFFFF;truncatedHash %= 1000000;return (int) truncatedHash;}
}

生成二维码密钥类

package com.google.framework.QuickResponse;import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;import javax.imageio.ImageIO;import com.google.framework.utils.FilePathUtil;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;/*** @Description:二维码图片工具类                使用了两张工具图    名称是 heihei.png  qr_bg.png* @Copyright (C) 2014 BOCO All Right Reserved.* @createDate:2014-11-24* @author:TangWenWu* @version 1.0* * * * */
public class QRUtil {/*** 编码(将文本生成二维码)* * @param content*            二维码中的内容* @param width*            二维码图片宽度* @param height*            二维码图片高度* @param imagePath*            二维码图片存放位置* @return 图片地址*/private static String encode(String content, int width, int height,String imagePath) {Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();// 设置编码类型为utf-8hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");// 设置二维码纠错能力级别为H(最高)hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);BitMatrix byteMatrix = null;try {// 生成二维码byteMatrix = new MultiFormatWriter().encode(content,BarcodeFormat.QR_CODE, width, height, hints);File file = new File(imagePath);MatrixToImageWriter.writeToFile(byteMatrix, "png", file);} catch (IOException e) {e.printStackTrace();} catch (WriterException e) {e.printStackTrace();}return imagePath;}/*** 解码(读取二维码图片中的文本信息)* * @param imagePath*            二维码图片路径* @return 文本信息*/private static String decode(String imagePath) {// 返回的文本信息String content = "";try {// 创建图片文件File file = new File(imagePath);if (!file.exists()) {return content;}BufferedImage image = null;image = ImageIO.read(file);if (null == image) {return content;}// 解码LuminanceSource source = new BufferedImageLuminanceSource(image);BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));Hashtable hints = new Hashtable();hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");Result rs = new MultiFormatReader().decode(bitmap, hints);content = rs.getText();} catch (IOException e) {e.printStackTrace();} catch (ReaderException e) {e.printStackTrace();}return content;}/*** 图片打水印* * @param bgImage*            背景图* @param waterImg*            水印图* @param uniqueFlag*            生成的新图片名称中的唯一标识,用来保证生成的图片名称不重复,如果为空或为null,将使用当前时间作为标识* @return 新图片路径*/private static String addImageWater(String bgImage, String waterImg,String uniqueFlag) {int x = 0;int y = 0;String newImgPath = "";if (null == uniqueFlag) {uniqueFlag = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());} else if (uniqueFlag.trim().length() < 1) {uniqueFlag = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());}try {File file = new File(bgImage);String fileName = file.getName();Image image = ImageIO.read(file);int width = image.getWidth(null);int height = image.getHeight(null);BufferedImage bufferedImage = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);Graphics2D g = bufferedImage.createGraphics();g.drawImage(image, 0, 0, width, height, null);Image waterImage = ImageIO.read(new File(waterImg)); // 水印文件int width_water = waterImage.getWidth(null);int height_water = waterImage.getHeight(null);g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP,1));int widthDiff = width - width_water;int heightDiff = height - height_water;x = widthDiff / 2;y = heightDiff / 2;g.drawImage(waterImage, x, y, width_water, height_water, null); // 水印文件结束g.dispose();if (bgImage.contains(fileName)) {newImgPath = bgImage.replace(fileName, uniqueFlag + fileName);}File newImg = new File(newImgPath);ImageIO.write(bufferedImage, "png", newImg);File waterFile = new File(waterImg);if (file.exists()) {file.delete();}if (waterFile.exists()) {waterFile.delete();}} catch (IOException e) {e.printStackTrace();}return newImgPath;}/*** 图片缩放* * @param filePath*            图片路径* @param height*            缩放到高度* @param width*            缩放宽度* @param fill*            比例足时是否填白 true为填白,二维码是黑白色,这里调用时建议设为true* @return 新图片路径*/private static String resizeImg(String filePath, int width, int height,boolean fill) {String newImgPath = "";try {double ratio = 0; // 缩放比例File f = new File(filePath);String fileName = f.getName();BufferedImage bi = ImageIO.read(f);Image itemp = bi.getScaledInstance(width, height,BufferedImage.SCALE_SMOOTH);if (height != 0 && width != 0) {// 计算比例if ((bi.getHeight() > height) || (bi.getWidth() > width)) {if (bi.getHeight() > bi.getWidth()) {ratio = (new Integer(height)).doubleValue()/ bi.getHeight();} else {ratio = (new Integer(width)).doubleValue()/ bi.getWidth();}AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio),null);itemp = op.filter(bi, null);}}if (fill) {BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);Graphics2D g = image.createGraphics();g.setColor(Color.white);g.fillRect(0, 0, width, height);if (width == itemp.getWidth(null)) {g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2,itemp.getWidth(null), itemp.getHeight(null),Color.white, null);} else {g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0,itemp.getWidth(null), itemp.getHeight(null),Color.white, null);}g.dispose();itemp = image;}String now = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());if (filePath.contains(fileName)) {newImgPath = filePath.replace(fileName, now + fileName);}File newImg = new File(newImgPath);ImageIO.write((BufferedImage) itemp, "png", newImg);} catch (IOException e) {e.printStackTrace();}return newImgPath;}/*** 图片添加边框* * @param mainImgPath*            要加边框的图片* @param bgImgPath*            背景图(实际上是将图片放在背景图上,只利用背景图的边框效果)* @return 制作完成的图片路径*/private static String addWaterBorder(String mainImgPath, String bgImgPath) {String borderImgPath = "";try {File f = new File(mainImgPath);BufferedImage bi;bi = ImageIO.read(f);// 背景图长宽都比主图多4像素,这是因为我画的背景图的边框效果的大小正好是4像素,// 主图周边比背景图少4像素正好能把背景图的边框效果完美显示出来int width = bi.getWidth();int height = bi.getHeight();int bgWidth = width + 4;int bgHeight = height + 4;String now = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());borderImgPath = QRUtil.addImageWater(QRUtil.resizeImg(bgImgPath,bgHeight, bgWidth, true), mainImgPath, now);if (f.exists()) {f.delete();}} catch (IOException e) {e.printStackTrace();}return borderImgPath;}/*** @Definition: 生成常规二维码* @author: TangWenWu* @Created date: 2014-11-25* @param content  二维码中的内容* @param width    二维码宽度  单位:像素  建议300* @param height   二维码高度  单位:像素  建议300* @param imageName  二维码图片名称     生成路径为:appName/WebRoot/指定的目录/imageName * @return*/public static String generateCommonQR(String content,int width,int height,String imageName){String appPath = FilePathUtil.getResourcePath();/** 部分一开始***********生成常规二维码 *************/// 二维码存放地址imageName = appPath+"templates/qr/"+imageName;// 生成二维码,返回的是生成好的二维码图片的所在路径String qrImgPath = QRUtil.encode(content, width, height, imageName);/** 部分一结束***********如果生成不带图片的二维码,到这步已经完成了 *************/return qrImgPath;}/*** @Definition: 生成带图片但图片不带边框的二维码* @author: TangWenWu* @Created date: 2014-11-25* @param content  二维码中的内容* @param width    二维码宽度  单位:像素  建议300* @param height   二维码高度  单位:像素  建议300* @param imageName  二维码图片名称     生成路径为:appName/WebRoot/指定的目录/imageName * @return*/public static String generateOnlyImageQR(String content,int width,int height,String imageName){String appPath = FilePathUtil.getResourcePath();String qrImgPath = generateCommonQR(content,width,height,imageName);// 缩放水印图片,为保证二维码的读取正确,图片不超过二维码图片的五分之一,这里设为六分之一String waterImgPath = QRUtil.resizeImg(appPath+"images/boco_big.png", width/6,height/6, true);//生成带有图片的二维码,返回的是生成好的二维码图片的所在路径String qrImage = QRUtil.addImageWater(qrImgPath,waterImgPath,"BOCO");return qrImage;}/*** @Definition: 生成带图片且图片带边框的二维码* @author: TangWenWu* @Created date: 2014-11-25* @param content  二维码中的内容* @param width    二维码宽度  单位:像素  建议300* @param height   二维码高度  单位:像素  建议300* @param imageName  二维码图片名称     生成路径为:appName/WebRoot/指定的目录/imageName * @return*/public static String generateImageInBorderQR(String content,int width,int height,String imageName){String appPath = FilePathUtil.getResourcePath();String qrImgPath = generateCommonQR(content,width,height,imageName);// 缩放水印图片,为保证二维码的读取正确,图片不超过二维码图片的五分之一,这里设为六分之一// d:/qr/heihei.png 这图片是要加在二维码中间的那张图String waterImgPath = QRUtil.resizeImg(appPath+"images/boco_big.png", width / 6,height / 6, true);// d:/qr/qr_bg.png这种图片是自己画好边框光晕效果的边框底图String tempImg = QRUtil.addWaterBorder(waterImgPath, appPath+"images/qr_bg.png");// 生成带有边框图片的二维码,返回的是生成好的二维码图片的所在路径String qrImage = QRUtil.addImageWater(qrImgPath, tempImg, "BOCO");return qrImage;}public static void main(String[] args) {String path = FilePathUtil.getResourcePath();System.out.println(path);/** 部分一开始***********生成常规二维码 *************/// 二维码内容String content = "http://blog.csdn.net/tangwwk";// 二维码宽度int width = 300;// 二维码高度int height = 300;// 二维码存放地址String imagePath = path+"templates/qr/"+"Site.png";// 生成二维码,返回的是生成好的二维码图片的所在路径String qrImgPath = QRUtil.encode(content, width, height, imagePath);/** 部分一结束***********如果生成不带图片的二维码,到这步已经完成了 *************//** 部分二开始***********如果生成带图片但图片不带边框的二维码,解开这部分注释 *************/// 缩放水印图片,为保证二维码的读取正确,图片不超过二维码图片的五分之一,这里设为六分之一// String waterImgPath = QRUtil.resizeImg("d:/qr/heihei.jpg", width/6,// height/6, true);//  // //生成带有图片的二维码,返回的是生成好的二维码图片的所在路径// String qrImage = QRUtil.addImageWater(qrImgPath,// waterImgPath,"thatway");/** 部分二结束***********如果生成带图片但图片不带边框的二维码,解开这部分注释 *************//** 部分三开始(部分三不能和部分二共存)***********如果生成带图片且图片带边框的二维码,解开这部分注释 ****/// 缩放水印图片,为保证二维码的读取正确,图片不超过二维码图片的五分之一,这里设为六分之一// d:/qr/heihei.png 这图片是要加在二维码中间的那张图String waterImgPath = QRUtil.resizeImg(path+"images/boco_big.png", width / 6,height / 6, true);// d:/qr/qr_bg.png这种图片是自己画好边框光晕效果的边框底图String tempImg = QRUtil.addWaterBorder(waterImgPath, path+"images/qr_bg.png");// 生成带有边框图片的二维码,返回的是生成好的二维码图片的所在路径String qrImage = QRUtil.addImageWater(qrImgPath, tempImg, "tangwwk");/** 部分三结束***********如果生成带图片且图片带边框的二维码,解开这部分注释 *************//******* 测试一下解码 ******/System.out.println(QRUtil.decode(qrImage));;}
}

基于Google 验证器 实现内网的双因素认证相关推荐

  1. 基于windows server的简单内网渗透

    基于windows server的简单内网渗透 一.内网发现 1) 探测存活IP 2) 扫端口 3) 探测端口信息 4) 设置DNS,绑定网关 5) 挖掘子域名 6) 寻找并利用网站漏洞,进入网站后台 ...

  2. Laravel项目+Google验证器

    1.首先要在你的Laravel项目中安装Google验证器插件.二维码生成器插件,执行命令如下: # Google验证器插件安装命令: composer require "earnp/lar ...

  3. 电脑同时访问外网和内网?双路由的详细配置及讲解

    电脑同时上外网和内网?双路由的详细配置讲解 一.准备工作 1.要有两张网卡 电脑要有两张网卡. 一般笔记本电脑都有有线网卡和无线网卡:当然外置接usb的有线/无线网卡也是可以的.有些台式机可能会只有1 ...

  4. Linux 之 利用Google Authenticator实现用户双因素认证

    一.介绍:什么是双因素认证 双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到一起才能发挥作用的身份认证系统.双因素认证是一种采用时间同步技术的系统,采用了基于时间.事件和密钥三变量而产 ...

  5. 谷歌Google Authenticator实现双因素认证

    参考: https://www.cnblogs.com/hanyifeng/p/kevin4real.html 介绍:什么是双因素认证 双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到 ...

  6. 结合企业微信/钉钉/飞书/AAD/Google等账号实现内网802.1X准入

    随着企业微信.钉钉.飞书等办公应用被广泛使用,越来越多的企业衍生出新的需求:结合企微/钉钉/飞书/AAD/Google等社交身份.云身份,如何快速部署内网802.1x准入,实现账密.证书认证?本文将为 ...

  7. 基于个人服务器的P2P内网穿透

    前言 作为一个重度桌游爱好者,最近和小伙伴沉迷TTS(桌游模拟器),但是TTS是基于P2P进行连接的,如果小伙伴都不在一个网络节点上就会非常卡顿,为了更好的玩游戏,最后使用了基于zerotier的内网 ...

  8. 4G模块 | 基于4G Cat.1的内网穿透实践

    1024G 嵌入式资源大放送!包括但不限于C/C++.单片机.Linux等.关注微信公众号[嵌入式大杂烩],回复1024,即可免费获取! 上一篇分享了:<小熊派4G开发板初体验>,对小熊派 ...

  9. 基于nodejs与花生壳内网穿透工具的调查问卷

    一.制作调查问卷 先去网上找一张调查问卷的html页面(直接复制网页源代码,包括link链接里的css文件),所有文件放在一个文件夹下面,就叫'调查问卷'吧,然后修改问卷中问题,变成你的调查问卷,关于 ...

最新文章

  1. php post数据丢失
  2. 如何在到处是“雷”的系统中「明哲保身」?这是第一招
  3. 有关项目实施【老男孩】的经验分享
  4. [ASP.NET Core 3框架揭秘] 依赖注入:依赖注入模式
  5. 从MySQL导入导出大量数据的程序实现方法
  6. mac pycharm安装设置_Mac系统Python、PyCharm安装及使用方法详解
  7. 眼控科技 实习算法工程师面试
  8. Linux内存管理 brk(),mmap()系统调用源码分析1:基础部分
  9. Java 动态代理机制详解
  10. Matlab Tricks(十三)—— 提取矩阵的对角线元素
  11. git配置取消代理_「高手」如何优雅的解决 git 超时
  12. java 程序打包成jar_把Java程序打包成jar文件包并执行的方法
  13. MATLAB秦九韶算法
  14. 计算机音乐谱大全极乐净土,极乐净土maria曲谱
  15. 解决contenteditable内自动生成font标签问题
  16. 如何使用QT实现左右滑动的按钮
  17. python切片逆序_python 中倒序切片
  18. Android OpenGL ES(十一):绘制一个20面体
  19. 大学计算机系考英语四六级吗,大学英语四六级没过,这些福利只能眼睁睁看着别人有,后悔也没用...
  20. 腾讯地图标记点击事件

热门文章

  1. 樱花未开(更新完毕)
  2. HTC手机官解、S-ON/S-OFF与超级CID的关系
  3. 【mac】macos苹果系统终端如何进入ROOT及退出问题
  4. QIIME 2教程. 09数据导入Importing data(2021.2)
  5. mqtt 传文件断开连接的原因_mqtt服务器连上就断开
  6. 『政善治』Postman工具 — 3、补充:restful风格接口的项目说明
  7. Michael Scofield in Break Prison(越狱)
  8. [福建]福建企业的现实与渴望
  9. 学生成绩预测模型_逻辑回归实战练习——根据学生成绩预测是否被录取
  10. 我与世界杯的故事——达利奇:铜牌闪耀着金光