了解了一下扫码登录的原理,大致是网页传递一个唯一标识到二维码中,用户扫二维码的时候,会跳转带着这个唯一标识和移动端自己登录之后信息返回到服务器中,服务器根据唯一标识判定是否扫码登录、登录超时的问题,再根据登录信息到库里比对,成功即将页面进行跳转。
实现过程:
1.创建唯一标识存储CurrentTimeBean
package com.bean;
public class CurrentTimeBean {
private String uuid;
private String timestamp;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public CurrentTimeBean() {
}
@Override
public String toString() {
return "CurrentTimeBean [uuid=" + uuid + ", timestamp=" + timestamp
+ "]";
}
}
2.指定生成二维码的参数类QRCodeParams
package com.util;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class QRCodeParams {
private String txt; //二维码内容
private String qrCodeUrl; //二维码网络路径
private String filePath; //二维码生成物理路径
private String fileName; //二维码生成图片名称(包含后缀名)
private String logoPath; //logo图片
private Integer width = 300; //二维码宽度
private Integer height = 300; //二维码高度
private Integer onColor = 0xFF000000; //前景色
private Integer offColor = 0xFFFFFFFF; //背景色
private Integer margin = 1; //白边大小,取值范围0~4
private ErrorCorrectionLevel level = ErrorCorrectionLevel.L; //二维码容错率
public String getTxt() {
return txt;
}
public void setTxt(String txt) {
this.txt = txt;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Integer getWidth() {
return width;
}
public void setWidth(Integer width) {
this.width = width;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
public String getQrCodeUrl() {
return qrCodeUrl;
}
public void setQrCodeUrl(String qrCodeUrl) {
this.qrCodeUrl = qrCodeUrl;
}
public String getLogoPath() {
return logoPath;
}
public void setLogoPath(String logoPath) {
this.logoPath = logoPath;
}
public Integer getOnColor() {
return onColor;
}
public void setOnColor(Integer onColor) {
this.onColor = onColor;
}
public Integer getOffColor() {
return offColor;
}
public void setOffColor(Integer offColor) {
this.offColor = offColor;
}
public ErrorCorrectionLevel getLevel() {
return level;
}
public void setLevel(ErrorCorrectionLevel level) {
this.level = level;
}
/**
* 返回文件后缀名
* @return
*/
public String getSuffixName(){
String imgName = this.getFileName();
if(imgName != null && !"".equals(imgName)){
String suffix=fileName.substring(fileName.lastIndexOf(".")+1);
return suffix;
}
return "";
}
public Integer getMargin() {
return margin;
}
public void setMargin(Integer margin) {
this.margin = margin;
}
}
二维码生成工具类QRCodeUtil1
package com.util;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import javax.imageio.ImageIO;
import com.exception.QRParamsException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public class QRCodeUtil1 {
private static int width = 300; //二维码宽度
private static int height = 300; //二维码高度
private static int onColor = 0xFF000000; //前景色
private static int offColor = 0xFFFFFFFF; //背景色
private static int margin = 1; //白边大小,取值范围0~4
private static ErrorCorrectionLevel level = ErrorCorrectionLevel.L; //二维码容错率
/**
* 生成二维码
* @param params
* QRCodeParams属性:txt、fileName、filePath不得为空;
* @throws QRParamsException
*/
public static void generateQRImage(QRCodeParams params)throws QRParamsException{
if(params == null
|| params.getFileName() == null
|| params.getFilePath() == null
|| params.getTxt() == null){
throw new QRParamsException("参数错误");
}
try{
initData(params);
String imgPath = params.getFilePath();
String imgName = params.getFileName();
String txt = params.getTxt();
if(params.getLogoPath() != null && !"".equals(params.getLogoPath().trim())){
generateQRImage(txt, params.getLogoPath(), imgPath, imgName, params.getSuffixName());
}else{
generateQRImage(txt, imgPath, imgName, params.getSuffixName());
}
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 生成不带logo的二维码
* @param txt //二维码内容
* @param imgPath //二维码保存物理路径
* @param imgName //二维码文件名称
* @param suffix //图片后缀名
*/
public static void generateQRImage(String txt, String imgPath, String imgName, String suffix){
File filePath = new File(imgPath);
if(!filePath.exists()){
filePath.mkdirs();
}
File imageFile = new File(imgPath,imgName);
Hashtable hints = new Hashtable();
// 指定纠错等级
hints.put(EncodeHintType.ERROR_CORRECTION, level);
// 指定编码格式
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, margin); //设置白边
try {
MatrixToImageConfig config = new MatrixToImageConfig(onColor, offColor);
BitMatrix bitMatrix = new MultiFormatWriter().encode(txt,BarcodeFormat.QR_CODE, width, height, hints);
// bitMatrix = deleteWhite(bitMatrix);
MatrixToImageWriter.writeToPath(bitMatrix, suffix, imageFile.toPath(), config);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成带logo的二维码图片
* @param txt //二维码内容
* @param logoPath //logo绝对物理路径
* @param imgPath //二维码保存绝对物理路径
* @param imgName //二维码文件名称
* @param suffix //图片后缀名
* @throws Exception
*/
public static void generateQRImage(String txt, String logoPath, String imgPath, String imgName, String suffix) throws Exception{
File filePath = new File(imgPath);
if(!filePath.exists()){
filePath.mkdirs();
}
if(imgPath.endsWith("/")){
imgPath += imgName;
}else{
imgPath += "/"+imgName;
}
Hashtable hints = new Hashtable();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, level);
hints.put(EncodeHintType.MARGIN, margin); //设置白边
BitMatrix bitMatrix = new MultiFormatWriter().encode(txt, BarcodeFormat.QR_CODE, width, height, hints);
File qrcodeFile = new File(imgPath);
writeToFile(bitMatrix, suffix, qrcodeFile, logoPath);
}
/**
*
* @param matrix 二维码矩阵相关
* @param format 二维码图片格式
* @param file 二维码图片文件
* @param logoPath logo路径
* @throws IOException
*/
public static void writeToFile(BitMatrix matrix,String format,File file,String logoPath) throws IOException {
BufferedImage image = toBufferedImage(matrix);
Graphics2D gs = image.createGraphics();
int ratioWidth = image.getWidth()*2/10;
int ratioHeight = image.getHeight()*2/10;
//载入logo
Image img = ImageIO.read(new File(logoPath));
int logoWidth = img.getWidth(null)>ratioWidth?ratioWidth:img.getWidth(null);
int logoHeight = img.getHeight(null)>ratioHeight?ratioHeight:img.getHeight(null);
int x = (image.getWidth() - logoWidth) / 2;
int y = (image.getHeight() - logoHeight) / 2;
gs.drawImage(img, x, y, logoWidth, logoHeight, null);
gs.setColor(Color.black);
gs.setBackground(Color.WHITE);
gs.dispose();
img.flush();
if(!ImageIO.write(image, format, file)){
throw new IOException("Could not write an image of format " + format + " to " + file);
}
}
public static BufferedImage toBufferedImage(BitMatrix matrix){
int width = matrix.getWidth();
int height = matrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for(int x=0;x
3.对称加密工具类EncryptionUtil ,用于加密二维码中的唯一标识,
package com.security;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* 对称加密aes工具类
* @author sxycw
*
*/
public class EncryptionUtil {
// 16进制的字符数组
private final static String[] hexDigits =
{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
/**
* AES 加密
*
* @param content 需要加密的内容
* @param aesKey 加密密钥
* @return
*/
public static byte[] AESEncrypt(String content, String aesKey) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(aesKey.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(byteContent);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 解密
*
* @param content 待解密内容
* @param aesKey 解密密钥
* @return
*/
public static byte[] AESDecrypt(byte[] content, String aesKey) {
try {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(aesKey.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
byte[] result = cipher.doFinal(content);
return result; // 加密
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将二进制转换成16进制
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* 将16进制转换为二进制
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
//byte转16进制
private static String byteArrayToHexString(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (byte tem : bytes) {
stringBuilder.append(byteToHexString(tem));
}
return stringBuilder.toString();
}
//16进制转byte[]
private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n = 256 + n;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
/**
* 加密方法
* @param content 明文
* @param aesKey 密钥
* @return
*/
public static String FuncEncrypt(String content,String aesKey){
return parseByte2HexStr(AESEncrypt(content, aesKey));
}
/**
* 解密方法
* @param encrypt 密文
* @param aesKey 密钥
* @return
* @throws UnsupportedEncodingException
*/
public static String FuncDecode(String encrypt,String aesKey) throws UnsupportedEncodingException{
return new String(AESDecrypt(parseHexStr2Byte(encrypt),aesKey),"utf-8");
}
public static void main(String[] args) throws UnsupportedEncodingException {
String content = "你爸爸";
String aesKey = "key";
System.out.println("加密后的>>>"+FuncEncrypt(content, aesKey));
System.out.println("解密后的>>>"+FuncDecode(FuncEncrypt(content, aesKey), aesKey));
}
}
设定密钥级别枚举类Secretkey
package com.Enum;
import java.util.HashMap;
/**
*
* @author sxycw
* 密钥枚举
*/
public enum Secretkey {
SecurityLevel_Low(0),
SecurityLevel_Middle(1),
SecurityLevel_High(2),
SecurityLevel_Top(3);
private int code;
private static HashMap nameMap;
Secretkey(int code) {
this.code = code;
}
static {
nameMap = new HashMap();
nameMap.put(0, "admin");
nameMap.put(1, "huangxiaoming");
nameMap.put(2, "wuyanzu");
nameMap.put(3, "wenshao");
}
public static String getName(Secretkey key) {
return getName(key.code);
}
public static String getName(int code) {
if (nameMap.containsKey(code)) {
return nameMap.get(code);
} else {
return null;
}
}
public String getName() {
return getName(code);
}
public String toString() {
return String.valueOf(code);
}
public int getCode() {
return code;
}
}
4.二维码唯一标识缓存模型QrCodeCache
package com.cache;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 二维码缓存模型
* @author sxycw
* @param
*/
public final class QrCodeCache{
private final Lock lock = new ReentrantLock();
//设置最大缓存是
private final int maxCapacity;
//边界池
private final Map eden;
//持久池
private final Map longterm;
/**
* 构造方法
* @param maxCapacity
*/
public QrCodeCache(int maxCapacity) {
this.maxCapacity = maxCapacity;
this.eden = new ConcurrentHashMap(maxCapacity);
this.longterm = new WeakHashMap(maxCapacity);
}
/**
* 根据key取缓存值
* @param k
* @return
*/
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
lock.lock();
try{
v = this.longterm.get(k);
}finally{
lock.unlock();
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
}
/**
* 根据key存缓存值
* @param k
* @param v
*/
public void put(K k, V v) {
if (this.eden.size() >= maxCapacity) {
lock.lock();
try{
this.longterm.putAll(this.eden);
}finally{
lock.unlock();
}
this.eden.clear();
}
this.eden.put(k, v);
}
/**
* 根据key删除缓存
* @param key
* @return
*/
public boolean remove(K k){
try{
if(eden.containsKey(k)) {
eden.remove(k);
}
return true;
}
catch(Exception ex){
return false;
}
}
}
5.本地生成的二维码推流到浏览器ViewController
package com.controller;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller()
@RequestMapping("/view")
public class ViewController {
/**
* 得到本地二维码图片方法
* @param request
* @param response
*/
@RequestMapping("/getQrCodeView")
void getQrCodeView(HttpServletRequest request,HttpServletResponse response){
String imgUrl = request.getParameter("imgUrl");
//图片完整的名称
String baseUrl = imgUrl+".jpg";
//根目录
String basePath = "E:\\";
//最终的图片路径
String path = basePath+baseUrl;//E:\\1501033727694_f448a295-b2f1-4eb5-a10b-1d2069ed1a45.jpg
//开启io
FileInputStream fis;
OutputStream os;
try {
//读取图片
fis = new FileInputStream(path);
//读到图片
int i = fis.available();
//byte数组存储pic字节
byte[] buff = new byte[i];
String sb = new String(buff,"utf-8");
System.out.println(sb);
fis.read(buff);
fis.close();
//写到outputstream
response.setCharacterEncoding("utf-8");
os = new BufferedOutputStream(response.getOutputStream());
os.write(buff);
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.创建UserController,是否登录的方法判定
package com.controller;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.Enum.Secretkey;
import com.alibaba.fastjson.JSON;
import com.bean.CurrentTimeBean;
import com.cache.QrCodeCache;
import com.exception.QRParamsException;
import com.security.EncryptionUtil;
import com.util.QRCodeParams;
import com.util.QRCodeUtil1;
@Controller
@RequestMapping("/u")
public class UserController {
//登录超时时间
private static final int LONG_TIME_WAIT = 30000;//30s
//模拟登陆缓存
private QrCodeCache qrCache = new QrCodeCache(20);
//加密级别
private Secretkey scKey = Secretkey.SecurityLevel_High;
/**
* 二维码确认登录方法
* @param request
* @param response
* @return
* @throws UnsupportedEncodingException
*/
@RequestMapping(value="/qrCodeLogin",method = RequestMethod.POST,
produces = "text/json;charset=UTF-8")
@ResponseBody
String qrCodeLogin(HttpServletRequest request,HttpServletResponse response)
throws UnsupportedEncodingException{
//执行开始时间
long inTime = new Date().getTime();
//得到唯一标识
String uuid = EncryptionUtil.FuncDecode(request.getParameter("uuid"), scKey.getName());
//是否登录标识
String isLogin = request.getParameter("isLogin");
Boolean bool = true;
int i = 0;
while (bool) {
synchronized(this){
try {
i++;
Thread.sleep(500);
System.out.println(">>>正在进行"+i+"次轮询");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String uidCache = (String) qrCache.get(uuid);
if(uidCache!=null){//若存在
String resInfo = null;
System.out.println(">>>登录标识"+isLogin);
//是否登录
if("true".equals(isLogin)){
System.out.println("-------登录成功------");
resInfo = "登录成功,正在跳转...";
}else if("false".equals(isLogin)){
resInfo = "登录已取消";
}
//删除缓存
qrCache.remove(uuid);
return "{\"success\":\"true\",\"info\":\""+resInfo+"\"}";
}else{//不存在
if(new Date().getTime() - inTime > LONG_TIME_WAIT){//超时
bool = false;
//删除缓存
qrCache.remove(uuid);
System.out.println("-------请求超时------");
}
}
}
return "{\"success\":\"false\",\"info\":\"请求超时,请重试!\"}";
}
/**
* 开启二维码登录
* @return
* @throws QRParamsException
*/
@RequestMapping("/Login")
@ResponseBody
String Login() throws QRParamsException {
//生成随机编码
String uid = UUID.randomUUID().toString();
String ctm = String.valueOf(System.currentTimeMillis());
//设置二维码参数
QRCodeParams params = new QRCodeParams();
params.setFileName(ctm+"_"+uid+".jpg");
params.setFilePath("e:\\");
//跳转页面加入二维码中
params.setTxt("http://192.168.100.151:8080/SpMVC/rest/view/qrToLogin"
+ "?uuid="+EncryptionUtil.FuncEncrypt(uid, scKey.getName()));
//启用二维码工具类
QRCodeUtil1.generateQRImage(params);
//时间戳实体
CurrentTimeBean ctBean = new CurrentTimeBean();
ctBean.setTimestamp(ctm);
ctBean.setUuid(uid);
/*
* 添加缓存模拟登录状态
*/
qrCache.put(uid, uid);
return JSON.toJSONString(ctBean);
}
}
7.需要的两个页面
index.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%
String path = request.getContextPath();//
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path;
%>
主页
toQrCodeLogin.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path;
String uuid = request.getParameter("uuid");
%>
登录确认页面
8.测试
9.留存的问题
这个写的还有问题,一个问题是缓存模型的监控,需要再写一个计时缓存监控二维码标识的缓存模型,在超时后删除对应的键值对;另一个问题是在实际的生产过程中,如果是手机登录网页,用户扫码之后需要将用户的id和二维码唯一标识绑定传给服务器,再次传入该用户id前一个没有做登录的唯一标识删除;如果是网页登录手机,也应该是一样的,还需要将用户的信息一并写到二维码中;还有个轮询的问题,不想看了改天再研究(看心情)。
10.感慨
响应式页面是个好东西啊,有空看看h5把。
-end-