java实现App扫码登录客户端

  • 大致流程
  • 手机需登录
  • PC端二维码的生成
  • 以下为生成二维码的代码
  • APP手机端进行扫码
  • 以下是接口实现代码
  • PC端确认登录
  • 以下是接口实现代码


大致流程

使用redies进行临时储存信息,可生成镶嵌logo图片的功能,可调至二维码即logo图的大小。

手机需登录

app手机端进行扫码前一定是登录状态的,用户输入账号和密码后,手机端相当于获取到了用户的身份识别信息

PC端二维码的生成

1.pc端向服务器发送请求,获取二维码id,将二维码id储存到redies中,60秒有效,给前端返回二维码的名称–即id
2.为了及时知道二维码的状态,客户端在展现二维码后,PC端的js定时器带着二维码ID和跳转地址url不断的发送ajax请求轮询服务端,可以每隔两秒轮询一次,请求服务端告诉当前二维码的状态及相关信息,查看是否登录成功

以下为生成二维码的代码

导入依赖

<dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>core</artifactId>
        <version>3.3.3</version>
    </dependency>

    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>javase</artifactId>
        <version>3.3.3</version>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.10</version>
        <scope>provided</scope>
    </dependency>

图片生成类

import com.google.zxing.LuminanceSource;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;

/**
 * 图片生成类
 */
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;
	}




	@Override
	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;
	}

	@Override
	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;
	}
	@Override
	public boolean isCropSupported() {
		return true;
	}

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

	@Override
	public boolean isRotateSupported() {
		return true;
	}
	@Override
	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);
	}

}

二维码生成工具类

import com.google.zxing.*;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.OutputStream;
import java.util.Hashtable;
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<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
	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;
}

/**
 * 插入LOGO
 *
 * @param source
 *   二维码图片
 * @param imgPath
 *   LOGO图片地址
 * @param needCompress
 *   是否压缩
 * @throws Exception
 */
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();
}

/**
 * 生成二维码(内嵌LOGO)
 *
 * @param content
 *   内容
 * @param imgPath
 *   LOGO地址
 * @param destPath
 *   存放目录
 * @param needCompress
 *   是否压缩LOGO
 * @throws Exception
 */
public static File encode(String content, String imgPath, String destPath,
						  boolean needCompress) throws Exception {
	BufferedImage image = QRCodeUtil.createImage(content, imgPath,
		needCompress);
	mkdirs(destPath);
	File file = new File(String.format("%s/%s.jpg", destPath , content));
	ImageIO.write(image, FORMAT_NAME, file);
	return file;
}

/**
 * 当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)
 * @author lanyuan
 * Email: mmm333zzz520@163.com
 * @date 2013-12-11 上午10:16:36
 * @param destPath 存放目录
 */
public static void mkdirs(String destPath) {
	File file =new File(destPath);
	//当文件夹不存在时,mkdirs会自动创建多层目录,区别于mkdir.(mkdir如果父目录不存在则会抛出异常)
	if (!file.exists() && !file.isDirectory()) {
		file.mkdirs();
	}
}

/**
 * 生成二维码(内嵌LOGO)
 *
 * @param content
 *   内容
 * @param imgPath
 *   LOGO地址
 * @param destPath
 *   存储地址
 * @throws Exception
 */
public static void encode(String content, String imgPath, String destPath)
	throws Exception {
	QRCodeUtil.encode(content, imgPath, destPath, false);
}

/**
 * 生成二维码
 *
 * @param content
 *   内容
 * @param destPath
 *   存储地址
 * @param needCompress
 *   是否压缩LOGO
 * @throws Exception
 */
public static void encode(String content, String destPath,
						  boolean needCompress) throws Exception {
	QRCodeUtil.encode(content, null, destPath, needCompress);
}

/**
 * 生成二维码
 *
 * @param content
 *   内容
 * @param destPath
 *   存储地址
 * @throws Exception
 */
public static void encode(String content, String destPath) throws Exception {
	QRCodeUtil.encode(content, null, destPath, false);
}

/**
 * 生成二维码(内嵌LOGO)
 *
 * @param content
 *   内容
 * @param imgPath
 *   LOGO地址
 * @param output
 *   输出流
 * @param needCompress
 *   是否压缩LOGO
 * @throws Exception
 */
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);
}

/**
 * 生成二维码
 *
 * @param content
 *   内容
 * @param output
 *   输出流
 * @throws Exception
 */
public static void encode(String content, OutputStream output)
	throws Exception {
	QRCodeUtil.encode(content, null, output, false);
}

/**
 * 解析二维码
 *
 * @param file
 *   二维码图片
 * @return
 * @throws Exception
 */
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<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
	hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
	result = new MultiFormatReader().decode(bitmap, hints);
	String resultStr = result.getText();
	return resultStr;
}

/**
 * 解析二维码
 *
 * @param path
 *   二维码图片地址
 * @return
 * @throws Exception
 */
public static String decode(String path) throws Exception {
	return QRCodeUtil.decode(new File(path));
}

public static void main(String[] args) throws Exception {
	String text = "http://www.yihaomen.com";
	QRCodeUtil.encode(text, "c:/me.jpg", "c:/barcode", true);
}

controller层返回页面请求信息的

public ResponseEntity<Resource> checkCityq() {
	String logPath = String.format("%shome%sfiles%s%s", File.separator, File.separator,File.separator,"logo2.png");
	//生成唯一码
	String uuid = UuidUtil.getIdStr();
	//将二维码id存到redies中
	bladeRedis.setEx(uuid, "", 600L);
	if (!bladeRedis.exists(uuid)) {
		return null;
	}
	//生成二维码,给前端返回二维码名称--id
	File file = null;
	try {
		file = QRCodeUtil.encode(uuid, logPath, "d:/ceshi", true);

		FileSystemResource fileSystemResource = new FileSystemResource(file);
		if (!fileSystemResource.exists()) {
			return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
		}
		// inline表示播放,attachment为下载到本地
		String contentDisposition = ContentDisposition
			.builder("attachment")
			.filename(file.getName())
			.build().toString();

		return ResponseEntity.ok()
			.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
			.contentType(MediaType.TEXT_PLAIN)
			.body(fileSystemResource);
	} catch (Exception e) {
		return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
	}
}

APP手机端进行扫码

用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID和跳转地址url,

再调用服务端接口将手机端的身份信息与电脑端二维码ID和跳转的url一起发送给服务端,

服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成已扫描成功提示,然后返回给手机端,也就是确认登录页面

这里我将获取到的二维码id作为key,用户名和密码作为value存入到redirs中,并设置60秒有效期。

因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

以下是接口实现代码

public R updatePerSion(@RequestParam("id") String id,
						   @RequestParam("name") String name,
						   @RequestParam("password") String password, HttpServletResponse response
	) {
		if (!bladeRedis.exists(id)) {
			return R.fail("二维码已经过期。");
		}
		Map<String, Object> map = new HashMap<>();
		map.put("name", name);
		map.put("password", EncryptionUtil.encryption32(password));
		bladeRedis.setEx(id, map, 600L);
		return R.success("认证成功");
	}

PC端确认登录

手机端在接收到APP端的已扫描提示后后会弹出确认登录界面,用户点击确认时,手机端携带临时token(用户身份信息与二维码ID绑定)用来调用服务端的接口,告诉服务端,我已经确认

服务端收到确认后,从redies中根据二维码的id去获取相应的用户信息,用户不存在即返回二维码过期,用户存在且用信心正确,则服务器生成对应的tocken返回给手机端,手机端此时告诉PC端二维码的已确认信息

这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认",并且从服务端可以获取到用户登录的token,并且去数据库取用户的信息

以下是接口实现代码

public Object updatePerSion2(@RequestParam("id") String id) {
		if (!bladeRedis.exists(id)) {
			return R.fail("二维码已经过期。");
		}
		Map<String, Object> maps =  bladeRedis.get(id);
		String headerKey = WebUtil.getRequest().getHeader(TokenUtil.HEADER_KEY);
		JsonNode jsonNode = HttpRequest.post("http://loclhost:8094/datacenter/api/" + AppConstant.APPLICATION_AUTH_NAME + "/oauth/token")
			.connectTimeout(Duration.ofSeconds(2000))
			.query("tenantId", TokenUtil.DEFAULT_TENANT_ID)
			.query("username", maps.get("name"))
			.query("password", maps.get("password"))
			.setHeader("Authorization", headerKey)
			.execute()
			.onFailed(((request, e) -> log.error("获取用户token失败,错误原因:{}", e.getMessage())))
			.onSuccess(ResponseSpec::asJsonNode);
		return R.data(jsonNode);
	}