1. 程式人生 > >定製專屬二維碼思路——實現【微信,QQ】掃碼登入

定製專屬二維碼思路——實現【微信,QQ】掃碼登入

效果演示

1.先在natapp實現外網對映(https://natapp.cn)

2.natapp.exe

3.生成token連結(先開啟redis)

//生成二維碼token連結(生成二維碼)
http://p5gc9b.natappfree.cc/generateCode

//瀏覽器展示二維碼
http://p5gc9b.natappfree.cc/getCodeImg?token=736e39d6c1424f4294958353206c1e06

用QQ或者微信掃描網頁二維碼

 

手機QQ和微信掃碼登陸原理解密

  1. 使用java的二維碼框架生成二維碼 底層存放該連結地址

        /updateTokenState?token=uuid

    2.如何保證二維碼不允許重複問題,使用token不重複就可以了

        將該token存放在redis中,key:token value 狀態碼

       Key:token value 狀態碼 預設狀態0

       0表示沒有掃過,1表示已經掃過

二維碼是如何生成的

  1. 線上二維碼生成方式 (http://www.liantu.com/
  2. 使用Java語言生成google框架
  3. 前端方式生成二維碼jquery-qrcode

使用Java語言生成二維碼

Maven依賴

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <dependencies>
        <!-- sprinboot web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--java二維碼生成框架  -->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!-- 整合redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
    </dependencies>

核心方法

public class BufferedImageLuminanceSource extends LuminanceSource {

    private final BufferedImage image;
    private final int left;
    private final int top;

    public BufferedImageLuminanceSource(BufferedImage image) {
        this(image, 0, 0, image.getWidth(), image.getHeight());
    }

    public BufferedImageLuminanceSource(BufferedImage image, int left, int top, int width, int height) {
        super(width, height);

        int sourceWidth = image.getWidth();
        int sourceHeight = image.getHeight();
        if (left + width > sourceWidth || top + height > sourceHeight) {
            throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
        }

        for (int y = top; y < top + height; y++) {
            for (int x = left; x < left + width; x++) {
                if ((image.getRGB(x, y) & 0xFF000000) == 0) {
                    image.setRGB(x, y, 0xFFFFFFFF); // = white
                }
            }
        }

        this.image = new BufferedImage(sourceWidth, sourceHeight, BufferedImage.TYPE_BYTE_GRAY);
        this.image.getGraphics().drawImage(image, 0, 0, null);
        this.left = left;
        this.top = top;
    }

    public byte[] getRow(int y, byte[] row) {
        if (y < 0 || y >= getHeight()) {
            throw new IllegalArgumentException("Requested row is outside the image: " + y);
        }
        int width = getWidth();
        if (row == null || row.length < width) {
            row = new byte[width];
        }
        image.getRaster().getDataElements(left, top + y, width, 1, row);
        return row;
    }

    public byte[] getMatrix() {
        int width = getWidth();
        int height = getHeight();
        int area = width * height;
        byte[] matrix = new byte[area];
        image.getRaster().getDataElements(left, top, width, height, matrix);
        return matrix;
    }

    public boolean isCropSupported() {
        return true;
    }

    public LuminanceSource crop(int left, int top, int width, int height) {
        return new BufferedImageLuminanceSource(image, this.left + left, this.top + top, width, height);
    }

    public boolean isRotateSupported() {
        return true;
    }

    public LuminanceSource rotateCounterClockwise() {
        int sourceWidth = image.getWidth();
        int sourceHeight = image.getHeight();
        AffineTransform transform = new AffineTransform(0.0, -1.0, 1.0, 0.0, 0.0, sourceWidth);
        BufferedImage rotatedImage = new BufferedImage(sourceHeight, sourceWidth, BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = rotatedImage.createGraphics();
        g.drawImage(image, transform, null);
        g.dispose();
        int width = getWidth();
        return new BufferedImageLuminanceSource(rotatedImage, top, sourceWidth - (left + width), getHeight(), width);
    }

}
public class QRCodeUtil {
    private static final String CHARSET = "utf-8";
    private static final String FORMAT_NAME = "JPG";
    // 二維碼尺寸
    private static final int QRCODE_SIZE = 300;
    // LOGO寬度
    private static final int WIDTH = 60;
    // LOGO高度
    private static final int HEIGHT = 60;

    public static BufferedImage createImage(String content, String imgPath, boolean needCompress) throws Exception {
        Hashtable hints = new Hashtable();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
        hints.put(EncodeHintType.MARGIN, 1);
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE,
                hints);
        int width = bitMatrix.getWidth();
        int height = bitMatrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        if (imgPath == null || "".equals(imgPath)) {
            return image;
        }
        // 插入圖片
        QRCodeUtil.insertImage(image, imgPath, needCompress);
        return image;
    }

    private static void insertImage(BufferedImage source, String imgPath, boolean needCompress) throws Exception {
        File file = new File(imgPath);
        if (!file.exists()) {
            System.err.println("" + imgPath + "   該檔案不存在!");
            return;
        }
        Image src = ImageIO.read(new File(imgPath));
        int width = src.getWidth(null);
        int height = src.getHeight(null);
        if (needCompress) { // 壓縮LOGO
            if (width > WIDTH) {
                width = WIDTH;
            }
            if (height > HEIGHT) {
                height = HEIGHT;
            }
            Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
            BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics g = tag.getGraphics();
            g.drawImage(image, 0, 0, null); // 繪製縮小後的圖
            g.dispose();
            src = image;
        }
        // 插入LOGO
        Graphics2D graph = source.createGraphics();
        int x = (QRCODE_SIZE - width) / 2;
        int y = (QRCODE_SIZE - height) / 2;
        graph.drawImage(src, x, y, width, height, null);
        Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
        graph.setStroke(new BasicStroke(3f));
        graph.draw(shape);
        graph.dispose();
    }

    public static void encode(String content, String imgPath, String destPath, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        mkdirs(destPath);
        // String file = new Random().nextInt(99999999)+".jpg";
        // ImageIO.write(image, FORMAT_NAME, new File(destPath+"/"+file));
        ImageIO.write(image, FORMAT_NAME, new File(destPath));
    }

    public  BufferedImage encode(String content, String imgPath, boolean needCompress) throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        return image;
    }

    public static void mkdirs(String destPath) {
        File file = new File(destPath);
        // 當資料夾不存在時,mkdirs會自動建立多層目錄,區別於mkdir.(mkdir如果父目錄不存在則會丟擲異常)
        if (!file.exists() && !file.isDirectory()) {
            file.mkdirs();
        }
    }

    public static void encode(String content, String imgPath, String destPath) throws Exception {
        QRCodeUtil.encode(content, imgPath, destPath, false);
    }
    // 被註釋的方法
    /*
     * public static void encode(String content, String destPath, boolean
     * needCompress) throws Exception { QRCodeUtil.encode(content, null, destPath,
     * needCompress); }
     */

    public static void encode(String content, String destPath) throws Exception {
        QRCodeUtil.encode(content, null, destPath, false);
    }

    public static void encode(String content, String imgPath, OutputStream output, boolean needCompress)
            throws Exception {
        BufferedImage image = QRCodeUtil.createImage(content, imgPath, needCompress);
        ImageIO.write(image, FORMAT_NAME, output);
    }

    public static void encode(String content, OutputStream output) throws Exception {
        QRCodeUtil.encode(content, null, output, false);
    }

    public static String decode(File file) throws Exception {
        BufferedImage image;
        image = ImageIO.read(file);
        if (image == null) {
            return null;
        }
        BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        Result result;
        Hashtable hints = new Hashtable();
        hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
        result = new MultiFormatReader().decode(bitmap, hints);
        String resultStr = result.getText();
        return resultStr;
    }

    public static String decode(String path) throws Exception {
        return QRCodeUtil.decode(new File(path));
    }

}

手機QQ掃碼登陸原理

  1. 生成二維碼,連結存放該Token 該狀態是為登陸狀態
  2. 前端使用ajax定時傳送請求,檢查該token是否為登陸狀態
  3. 使用者掃碼的時候,獲取該連線將token狀態該為登陸狀態

核心程式碼

@Controller
public class QrCodeController {
    @Autowired
    private RedisUtil redisUtil;

    private String baseImgUrl = "http://p5gc9b.natappfree.cc/updateTokenState?token=";

    /**
     * 提供建立二維碼介面
     *
     * @return
     */
    @RequestMapping("/generateCode")
    @ResponseBody
    public String generateCode() throws Exception {
        // 1.生成token
        String token = UUID.randomUUID().toString().replace("-", "");
        // 2. 存放在redis中 0 表示沒有被掃過,1表示已經掃過
        redisUtil.setString(token, "0");
        // 3.生成二維碼
        // 存放在二維碼中的內容
        // 嵌入二維碼的圖片路徑
        String imgPath = "E:/code/code_bg.png";
        // 生成的二維碼的路徑及名稱
        String destPath = "E:/code/" + token + ".png";
        //生成二維碼
        QRCodeUtil.encode(baseImgUrl + token, imgPath, destPath, true);
        // 解析二維碼
        String str = QRCodeUtil.decode(destPath);
        // 打印出解析出的內容
        return token;
    }

    /**
     * 根據Token返回對應的二維碼
     *
     * @param token
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/getCodeImg", produces = MediaType.IMAGE_JPEG_VALUE)
    @ResponseBody
    public byte[] getCodeImg(String token) throws IOException {
        File file = new File("E:/code/" + token + ".png");
        FileInputStream inputStream = new FileInputStream(file);
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes, 0, inputStream.available());
        return bytes;
    }

    /**
     * 根據token展示頁面的二維碼
     *
     * @param token
     * @param request
     * @return
     */
    @RequestMapping("/")
    public String index(String token, HttpServletRequest request) {
        request.setAttribute("token", token);
        return "index";
    }
    /**
     * 使用者掃碼 將該token 的狀態改為1
     *
     * @param token
     * @return
     */
    @RequestMapping("/updateTokenState")
    @ResponseBody
    public String updateTokenState(String token) {
        if (StringUtils.isEmpty(token)) {
            return "token 不能為空!";
        }
        // 將該token的狀態改為1
        redisUtil.setString(token, "1");
        return "<h1>使用者掃碼成功!</h1>";
    }

    /**
     * 前端使用定時器檢查token狀態
     *
     * @param token
     * @return
     */
    @RequestMapping("/checkToken")
    @ResponseBody
    public Boolean checkToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        String redisValue = redisUtil.getString(token);
        if (StringUtils.isEmpty(redisValue)) {
            return false;
        }
        if (!redisValue.equals("1")) {
            return false;
        }
        return true;
    }

    @RequestMapping("/sweepCode")
    @ResponseBody
    public String sweepCode() {
        return "恭喜您,掃碼登陸成功!";
    }


}

Application相關配置

spring:
  http:
    encoding:
      force: true
      charset: UTF-8
  redis:
    host: 127.0.0.1
    port: 6379
    #    password: 123456
  freemarker:
    allow-request-override: false
    cache: false
    check-template-location: true
    charset: UTF-8
    content-type: text/html; charset=utf-8
    expose-request-attributes: false
    expose-session-attributes: false
    expose-spring-macro-helpers: false
    suffix: .ftl
    template-loader-path: classpath:/templates
server:
  port: 8080

RedisUtils

@Component
public class RedisUtil {
	@Autowired
	private StringRedisTemplate stringRedisTemplate;

	// 如果key存在的話返回fasle 不存在的話返回true
	public Boolean setNx(String key, String value, Long timeout) {
		Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
		if (timeout != null) {
			stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
		}
		return setIfAbsent;
	}

	public StringRedisTemplate getStringRedisTemplate() {
		return stringRedisTemplate;
	}

	public void setList(String key, List<String> listToken) {
		stringRedisTemplate.opsForList().leftPushAll(key, listToken);
	}

	/**
	 * 存放string型別
	 * 
	 * @param key
	 *            key
	 * @param data
	 *            資料
	 * @param timeout
	 *            超時間
	 */
	public void setString(String key, String data, Long timeout) {
		try {

			stringRedisTemplate.opsForValue().set(key, data);
			if (timeout != null) {
				stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
			}

		} catch (Exception e) {

		}

	}

	/**
	 * 開啟Redis 事務
	 */
	public void begin() {
		// 開啟Redis 事務許可權
		stringRedisTemplate.setEnableTransactionSupport(true);
		// 開啟事務
		stringRedisTemplate.multi();

	}

	/**
	 * 提交事務
	 */
	public void exec() {
		// 成功提交事務
		stringRedisTemplate.exec();
	}

	/**
	 * 回滾Redis 事務
	 */
	public void discard() {
		stringRedisTemplate.discard();
	}

	/**
	 * 存放string型別
	 * 
	 * @param key
	 *            key
	 * @param data
	 *            資料
	 */
	public void setString(String key, String data) {
		setString(key, data, null);
	}

	/**
	 * 根據key查詢string型別
	 * 
	 * @param key
	 * @return
	 */
	public String getString(String key) {
		String value = stringRedisTemplate.opsForValue().get(key);
		return value;
	}

	/**
	 * 根據對應的key刪除key
	 * 
	 * @param key
	 */
	public Boolean delKey(String key) {
		return stringRedisTemplate.delete(key);

	}
}

index.ftl

<h1>掃一下該二維碼,實現掃碼登陸</h1>

<img src="getCodeImg?token=${token}">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
    function checkToken() {
        // alert('1000');
        $.ajax({
            url: "checkToken?token=${token}",
            dataType: "json",
            type: "get",
            async: "false",
            success: function (data) {
                if (data == true) {
                    window.location.href = '/sweepCode';
                }
            }
            ,
            error: function () {

            }
        })
        ;

    }
    setInterval(checkToken, 2000);

</script>

啟動類

@SpringBootApplication
public class AppQrCode {
    public static void main(String[] args) {
        SpringApplication.run(AppQrCode.class);