前言:
关于这个微信扫码登录,在做这个功能的时候主要是在开发物业系统的时候,要实现所有用户的统一性, 实现通行证理念的基础上开发实现的,关于前后端分离实现微信登录主要分为两个环节,下面我详细的介绍下前后端分离微信登录如何使用
一:实现网页二维码打开
A: 在实现Web端微信扫码登录,首先需要在微信开发者平台中获取对应的Web应用的 AppID 和 AppScert
微信开放平台 :https://open.weixin.qq.com/
B: 实现Url 展示微信二维码:(对应的三个参数 WebAppId WebAppSecret WebRedirectUri 分别是对应的微信AppId 和AppScect 和 回调对应的返回Url)
private static final String WebAppId= "微信AppId";
private static final String WebAppSecret = "微信AppScret";
//对应的回调路径(域名为 本地 hosts 修改 127.0.0.1 => zlwj.jiajgou.com)
private static final String WebRedirectUri = "http://zlwj.jiajgou.com/wx/callback";
/**
* 获取 web端 微信登录二维码操作
* @return
*/
public static String getWxWebLoginQrUrl(){
String state = UUIDUtils.getUuid();
String url = "https://open.weixin.qq.com/connect/qrconnect?appid=" + WebAppId + "&redirect_uri=" + WebRedirectUri + "&response_type=code&scope=snsapi_login&state=" + state;
return url;
}
回调之后获取对应的微信 OpenId 以及可以使用的 UnionId 以及其他信息
@RestController
@RequestMapping("/wx")
public class WxController {
private static final String WebAppId= "微信对应的AppId";
private static final String WebAppSecret = "微信对应的AppSerct";
private static final String grantType = "authorization_code";
private static final String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
private static final String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo";
@RequestMapping(value = "/callback")
public void callBack(HttpServletRequest request, HttpServletResponse response) throws Exception{
String code = request.getParameter("code");
String state = request.getParameter("state");
if(code != null){
StringBuffer url = new StringBuffer();
url.append(requestUrl)
.append("?appid=")
.append(WebAppId)
.append("&secret=")
.append(WebAppSecret)
.append("&code=")
.append(code)
.append("&grant_type=")
.append(grantType);
System.out.println("获取tokenUrl:" + url.toString());
JSONObject jsonObject =
JSON.parseObject(HttpClientUtil.doGet(url.toString()));
System.out.println("token:" + jsonObject.toString());
//拿到openid和请求微信api使用的token
String openid =jsonObject.get("openid").toString();
String token = jsonObject.get("access_token").toString();
url = new StringBuffer();
url.append(userInfoUrl)
.append("?access_token=")
.append(token)
.append("&openid=")
.append(openid);
System.out.println("获取用户信息:" + url);
JSONObject userInfoJSON =
JSON.parseObject(HttpClientUtil.doGet(url.toString()));
System.out.println(userInfoJSON.toJSONString());
response.sendRedirect("https://www.jianshu.com/");
}
}
}
二:在回调之后这个时候如何实现后端通知前端实现动态展示:
在这里我推荐使用 WebSockt 实现前后端通讯,通讯结束之后就结束对应的 长连接 已达到节省资源,当然,如果有存在需要这个长连接就任做其他的处理,接下来就说说 Netty WebSockt 实现
注: 在此 Netty 实现 WebSockt 时做了一个小封装 , 封装代码如下:
1: NettyServer
public class NettyServer<Server extends NettyServerInitializer> { protected static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2; protected static final int BIZTHREADSIZE = 4; private Server server; private int port; public NettyServer(Server server, Integer port) { this.server = server; this.port = port; } @OperLog( operModul = "启动Netty服务器方法", operType = "run", operDesc = "启动Netty服务器方法" ) public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE); NioEventLoopGroup workerGroup = new NioEventLoopGroup(4); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)); serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true); serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); serverBootstrap.childHandler(this.server); ChannelFuture channelFuture = serverBootstrap.bind(this.port).sync(); channelFuture.channel().closeFuture().sync(); } catch (Exception var8) { var8.printStackTrace(); throw var8; } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
2:NettyServerHandler
public class NettyServerHandler<T extends NettyServerHandler> extends SimpleChannelInboundHandler<Object> implements NettyCallBack { private static final Logger log = LoggerFactory.getLogger(NettyServerHandler.class); private ServletContext context; public NettyServerHandler() { } public ServletContext getContext() { return this.context; } public void setContext(ServletContext context) { this.context = context; } protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { try { this.read(ctx, msg, this.getContext()); } catch (Exception var4) { throw var4; } } public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { try { this.readComplete(ctx, this.getContext()); ctx.flush(); } catch (Exception var3) { ctx.flush(); throw var3; } } public void channelActive(ChannelHandlerContext ctx) throws Exception { try { this.active(ctx, this.getContext()); } catch (Exception var3) { throw var3; } } public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { try { this.exc(ctx, cause, this.getContext()); closeChannel(ctx.channel()); } catch (Exception var4) { closeChannel(ctx.channel()); } } public static void closeChannel(Channel channel) { final String addrRemote = parseChannelRemoteAddr(channel); channel.close().addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { NettyServerHandler.log.info("closeChannel: close the connection to remote address[{}] result: {}", addrRemote, future.isSuccess()); } }); } public static String parseChannelRemoteAddr(final Channel channel) { if (null == channel) { return ""; } else { SocketAddress remote = channel.remoteAddress(); String addr = remote != null ? remote.toString() : ""; if (addr.length() > 0) { int index = addr.lastIndexOf("/"); return index >= 0 ? addr.substring(index + 1) : addr; } else { return ""; } } } public void read(ChannelHandlerContext ctx, Object msg, ServletContext context) throws Exception { } public void readComplete(ChannelHandlerContext ctx, ServletContext context) throws Exception { } public void active(ChannelHandlerContext ctx, ServletContext context) throws Exception { } public void exc(ChannelHandlerContext ctx, Throwable cause, ServletContext context) throws Exception { } }
3:NettyServerInitializer
public class NettyServerInitializer<Handler extends NettyServerHandler> extends ChannelInitializer<SocketChannel> implements PipelineCallBack { private Handler handler; public NettyServerInitializer(Handler handler, ServletContext context) { handler.setContext(context); this.handler = handler; } @OperLog( operModul = "netty编码解码处理以及添加handler处理", operType = "handler", operDesc = "netty编码解码处理以及添加handler处理" ) public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); this.channelPipeline(pipeline); pipeline.addLast(new ChannelHandler[]{this.handler}); } public void channelPipeline(ChannelPipeline pipeline) { } }
4:NettyCallBack
public interface NettyCallBack { void read(ChannelHandlerContext ctx, Object msg, ServletContext context) throws Exception; void readComplete(ChannelHandlerContext ctx, ServletContext context) throws Exception; void active(ChannelHandlerContext ctx, ServletContext context) throws Exception; void exc(ChannelHandlerContext ctx, Throwable cause, ServletContext context) throws Exception; }
ThreadPoolUtil 开启多线程线程池:
public class ThreadPoolUtil { private static volatile ExecutorService executorService; private ThreadPoolUtil() { } public static ExecutorService getExecutorService() { if (executorService == null) { Class var0 = ThreadPoolUtil.class; synchronized(ThreadPoolUtil.class) { ThreadFactory namedThreadFactory = (new ThreadFactoryBuilder()).setNameFormat("netty-pool-%d").build(); executorService = new ThreadPoolExecutor(10, 20, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(1024), namedThreadFactory, new AbortPolicy()); } } return executorService; } }
Netty WebSockt 实现
- 通过 @WebListener 实现 Netty 服务监听
/** * netty服务监听器 . <br> * @author xbin */ @WebListener public class NettyListener implements ServletContextListener { private Logger log = LoggerFactory.getLogger(getClass()); private Map<String, ChannelHandlerContext> map = new HashMap<>(); ServletContext context; @Override public void contextInitialized(ServletContextEvent sce) { //获取上下文 context = sce.getServletContext(); ExecutorService executorService= ThreadPoolUtil.getExecutorService(); executorService.execute(new WebServerThread()); } @Override public void contextDestroyed(ServletContextEvent sce) { } /** * 云+控制器 服务启动线程 C 版本 . <br> * @author hkb */ private class WebServerThread implements Runnable { @Override public void run(){ try { NettyServer nettyServer = new NettyServer<WebServerInitializer>(new WebServerInitializer(new WebHandler(), context), 4960); nettyServer.run(); }catch (Exception e){ log.info("启动服务失败"); } } } }
- 是先 Handler 通道逻辑处理
/** * @Sharable 表示它可以被多个channel安全地共享 C 版本 * @author xiaobin */ @ChannelHandler.Sharable public class WebHandler extends NettyServerHandler implements NettyCallBack { private WebSocketServerHandshaker handshaker; @Override public void read(ChannelHandlerContext ctx, Object msg, ServletContext context) throws Exception { try { if (msg instanceof FullHttpRequest){ //以http请求形式接入,但是走的是websocket handleHttpRequest(ctx, (FullHttpRequest) msg); }else if (msg instanceof WebSocketFrame){ //处理websocket客户端的消息 handlerWebSocketFrame(ctx, (WebSocketFrame) msg); } }catch (Exception e){ e.printStackTrace(); throw e; } } private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception{ // 判断是否关闭链路的指令 if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } // 判断是否ping消息 if (frame instanceof PingWebSocketFrame) { ctx.channel().write( new PongWebSocketFrame(frame.content().retain())); return; } // 本例程仅支持文本消息,不支持二进制消息 if (!(frame instanceof TextWebSocketFrame)) { System.out.println("本例程仅支持文本消息,不支持二进制消息"); throw new UnsupportedOperationException(String.format( "%s frame types not supported", frame.getClass().getName())); } // 返回应答消息 String request = ((TextWebSocketFrame) frame).text(); JSONObject jsonObject = JSON.parseObject(request); WebUtil.cmdXlt(ctx, jsonObject); } /** * 唯一的一次http请求,用于创建websocket * */ private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); String ip = insocket.getAddress().getHostAddress(); int port = insocket.getPort(); //要求Upgrade为websocket,过滤掉get/Post if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) { //若不是websocket方式,则创建BAD_REQUEST的req,返回给客户端 sendHttpResponse(ctx, req, new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( "ws://"+ ip +":" + port + "/xbin/xlt", null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory .sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); } } /** * 拒绝不合法的请求,并返回错误信息 * */ private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) { // 返回应答给客户端 if (res.status().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); } ChannelFuture f = ctx.channel().writeAndFlush(res); // 如果是非Keep-Alive,关闭连接 !isKeepAlive(req) if (res.status().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void readComplete(ChannelHandlerContext ctx, ServletContext context) throws Exception { } @Override public void active(ChannelHandlerContext ctx, ServletContext context) throws Exception { InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress(); String clientIp = insocket.getAddress().getHostAddress(); ChannelSupervise.addChannel(ctx.channel()); System.out.println("client -- [ip:" + clientIp + "]-- online"); } @Override public void exc(ChannelHandlerContext ctx, Throwable cause, ServletContext context) throws Exception { System.out.println("------------------error-----------------------" + cause + "---" + "ctx:" + ctx); ChannelSupervise.removeChannel(ctx.channel()); } }
import javax.servlet.ServletContext; public class WebServerInitializer extends NettyServerInitializer implements PipelineCallBack { public WebServerInitializer(NettyServerHandler handler, ServletContext context) { super(handler, context); } @Override public void channelPipeline(ChannelPipeline pipeline) { //设置log监听器,并且日志级别为debug,方便观察运行流程 pipeline.addLast("logging",new LoggingHandler("DEBUG")); //设置解码器 pipeline.addLast("http-codec",new HttpServerCodec()); //聚合器,使用websocket会用到 pipeline.addLast("aggregator",new HttpObjectAggregator(65536)); //用于大数据的分区传输 pipeline.addLast("http-chunked",new ChunkedWriteHandler()); } }
public class WebUtil { public static void cmdXlt(ChannelHandlerContext ctx, JSONObject jsonObject) throws Exception{ // 收到关于微信登录请求 { "cmd":"wx_login_qr_request" , "status":"1", "data":""} // 正常回复前端对应url 微信登录请求 { "cmd":"wx_login_qr_repose", "status":"1", "data":"" } String cmd = jsonObject.getString("cmd"); switch (cmd){ case "wx_login_qr_request": break; default: break; } } }
至此就可以实现 Netty WebSockt 通行, 通讯的协议规则可以自己自定义,按照具体的需求来进行处理。