netty-websocket 鉴权token及统一请求和响应头(鉴权控制器)
创始人
2024-05-31 14:45:20
0

在这里插入图片描述

自己想法和实现,如果有说错的或者有更好的简单的实现方式可以私信交流一下(主要是实现握手时鉴权)

需求实现

  1. 握手鉴权是基于前台请求头 Sec-WebSocket-Protocol的
  2. 本身socket并没有提供自定义请求头,只能自定义 Sec-WebSocket-Protocol的自协议

问题描述

socket握手请求是基于http的,握手成功后会升级为ws

前台传输了 token作为Sec-WebSocket-Protocol的值,后台接收到后总是断开连接,后来网上看了很多博客说的都是大同小异,然后就看了他的源码一步步走的(倔脾气哈哈),终于我看到了端倪,这个问题是因为前后台的Sec-WebSocket-Protocol值不一致,所以会断开,但是我记得websocket好像是不用自己设置请求头的,但是netty我看了源码,好像没有预留设置websocket的response的响应头(这只是我的个人理解)

具体实现

CustomWebSocketProtocolHandler

解释: 自定义替换WebSocketProtocolHandler,复制WebSocketProtocolHandler的内容即可,因为主要是WebSocketServerProtocolHandler自定义会用到

abstract class CustomWebSocketProtocolHandler extends MessageToMessageDecoder {@Overrideprotected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List out) throws Exception {if (frame instanceof PingWebSocketFrame) {frame.content().retain();ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content()));return;}if (frame instanceof PongWebSocketFrame) {// Pong frames need to get ignoredreturn;}out.add(frame.retain());}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.fireExceptionCaught(cause);ctx.close();}
}
 

CustomWebSocketServerProtocolHandler

解释: 自定义WebSocketServerProtocolHandler,实现上面自定义的WebSocketProtocolHandler,具体内容和WebSocketServerProtocolHandler保持一致,只需要将handlerAdded中的类ProtocolHandler改为自己定义的即可
注意:后面监听读写的自定义业务的handler需要实现相应的方法:异常或者事件监听,因为比如异常,如果抛出异常了,是不会有控制器去管的,因为当前的业务控制器就是最后一层,因为上面已经把默认实现改成了自己的实现(其他的控制器都是基于默认handler实现的,如果改了后,去初始化自己改后的handler那便是最后一层),所以要手动去关闭

ublic class CustomWebSocketServerProtocolHandler extends CustomWebSocketProtocolHandler {/*** Events that are fired to notify about handshake status*/public enum ServerHandshakeStateEvent {/*** The Handshake was completed successfully and the channel was upgraded to websockets.** @deprecated in favor of {@link WebSocketServerProtocolHandler.HandshakeComplete} class,* it provides extra information about the handshake*/@DeprecatedHANDSHAKE_COMPLETE}/*** The Handshake was completed successfully and the channel was upgraded to websockets.*/public static final class HandshakeComplete {private final String requestUri;private final HttpHeaders requestHeaders;private final String selectedSubprotocol;public HandshakeComplete(String requestUri, HttpHeaders requestHeaders, String selectedSubprotocol) {this.requestUri = requestUri;this.requestHeaders = requestHeaders;this.selectedSubprotocol = selectedSubprotocol;}public String requestUri() {return requestUri;}public HttpHeaders requestHeaders() {return requestHeaders;}public String selectedSubprotocol() {return selectedSubprotocol;}}private static final AttributeKey HANDSHAKER_ATTR_KEY =AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");private final String websocketPath;private final String subprotocols;private final boolean allowExtensions;private final int maxFramePayloadLength;private final boolean allowMaskMismatch;private final boolean checkStartsWith;public CustomWebSocketServerProtocolHandler(String websocketPath) {this(websocketPath, null, false);}public CustomWebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith) {this(websocketPath, null, false, 65536, false, checkStartsWith);}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols) {this(websocketPath, subprotocols, false);}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions) {this(websocketPath, subprotocols, allowExtensions, 65536);}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize) {this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false);}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false);}public CustomWebSocketServerProtocolHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {this.websocketPath = websocketPath;this.subprotocols = subprotocols;this.allowExtensions = allowExtensions;maxFramePayloadLength = maxFrameSize;this.allowMaskMismatch = allowMaskMismatch;this.checkStartsWith = checkStartsWith;}@Overridepublic void handlerAdded(ChannelHandlerContext ctx) {ChannelPipeline cp = ctx.pipeline();if (cp.get(CustomWebSocketServerProtocolHandler.class) == null) {// Add the WebSocketHandshakeHandler before this one.ctx.pipeline().addBefore(ctx.name(), CustomWebSocketServerProtocolHandler.class.getName(),new CustomWebSocketServerProtocolHandler(websocketPath, subprotocols,allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));}if (cp.get(Utf8FrameValidator.class) == null) {// Add the UFT8 checking before this one.ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),new Utf8FrameValidator());}}@Overrideprotected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List out) throws Exception {if (frame instanceof CloseWebSocketFrame) {WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel());if (handshaker != null) {frame.retain();handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame);} else {ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);}return;}super.decode(ctx, frame, out);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {if (cause instanceof WebSocketHandshakeException) {FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST, Unpooled.wrappedBuffer(cause.getMessage().getBytes()));ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);} else {ctx.fireExceptionCaught(cause);ctx.close();}}static WebSocketServerHandshaker getHandshaker(Channel channel) {return channel.attr(HANDSHAKER_ATTR_KEY).get();}public static void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) {channel.attr(HANDSHAKER_ATTR_KEY).set(handshaker);}public static ChannelHandler forbiddenHttpRequestResponder() {return new ChannelInboundHandlerAdapter() {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof FullHttpRequest) {((FullHttpRequest) msg).release();FullHttpResponse response =new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.FORBIDDEN);ctx.channel().writeAndFlush(response);} else {ctx.fireChannelRead(msg);}}};}
}
 

SecurityServerHandler

用SecurityServerHandler自定义的入站控制器替换原有默认的控制器WebSocketServerProtocolHandshakeHandler
这一步最关键了,因为在这一步就要将头设置进去,前面两步只是为这一步做铺垫,因为netty包中的类不能外部引用也没有提供修改方法,所以才有了上面的自定义类,此类中需要调整握手逻辑,添加握手响应头,然后将WebSocketServerProtocolHandler改为CustomWebSocketServerProtocolHandler,其他的实现类也是一样的去改

public class SecurityServerHandler extends ChannelInboundHandlerAdapter {private final String websocketPath;private final String subprotocols;private final boolean allowExtensions;private final int maxFramePayloadSize;private final boolean allowMaskMismatch;private final boolean checkStartsWith;/*** 自定义属性 token头key*/private final String tokenHeader;/*** 自定义属性 token*/private final boolean hasToken;public SecurityServerHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, String tokenHeader, boolean hasToken) {this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false,tokenHeader,hasToken);}SecurityServerHandler(String websocketPath, String subprotocols,boolean allowExtensions, int maxFrameSize,boolean allowMaskMismatch,boolean checkStartsWith,String tokenHeader,boolean hasToken) {this.websocketPath = websocketPath;this.subprotocols = subprotocols;this.allowExtensions = allowExtensions;maxFramePayloadSize = maxFrameSize;this.allowMaskMismatch = allowMaskMismatch;this.checkStartsWith = checkStartsWith;this.tokenHeader = tokenHeader;this.hasToken = hasToken;}@Overridepublic void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {final FullHttpRequest req = (FullHttpRequest) msg;if (isNotWebSocketPath(req)) {ctx.fireChannelRead(msg);return;}try {// 具体的鉴权逻辑HttpHeaders headers = req.headers();String token = Objects.requireNonNull(headers.get(tokenHeader));if(hasToken){// 开启鉴权 认证//extracts device information headersLoginUser loginUser = SecurityUtils.getLoginUser(token);if(null == loginUser){refuseChannel(ctx);return;}Long userId = loginUser.getUserId();//check ......SecurityCheckComplete complete = new SecurityCheckComplete(String.valueOf(userId),tokenHeader,hasToken);ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);ctx.fireUserEventTriggered(complete);}else {// 不开启鉴权 / 认证SecurityCheckComplete complete = new SecurityCheckComplete(null,tokenHeader,hasToken);ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);}if (req.method() != GET) {sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));return;}final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,allowExtensions, maxFramePayloadSize, allowExtensions);final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);if (handshaker == null) {WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());} else {// 此处将具体的头加入http中,因为这个头会传递个netty底层设置响应头的方法中,默认实现是传的nullHttpHeaders httpHeaders = new DefaultHttpHeaders().add(tokenHeader,token);// 此处便是构造握手相应头的关键步骤final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req,httpHeaders,ctx.channel().newPromise());handshakeFuture.addListener((ChannelFutureListener) future -> {if (!future.isSuccess()) {ctx.fireExceptionCaught(future.cause());} else {// Kept for compatibilityctx.fireUserEventTriggered(CustomWebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);ctx.fireUserEventTriggered(new CustomWebSocketServerProtocolHandler.HandshakeComplete(req.uri(), req.headers(), handshaker.selectedSubprotocol()));}});CustomWebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);ctx.pipeline().replace(this, "WS403Responder",CustomWebSocketServerProtocolHandler.forbiddenHttpRequestResponder());}}catch (Exception e){e.printStackTrace();}finally {req.release();}}public static final class HandshakeComplete {private final String requestUri;private final HttpHeaders requestHeaders;private final String selectedSubprotocol;HandshakeComplete(String requestUri, HttpHeaders requestHeaders, String selectedSubprotocol) {this.requestUri = requestUri;this.requestHeaders = requestHeaders;this.selectedSubprotocol = selectedSubprotocol;}public String requestUri() {return requestUri;}public HttpHeaders requestHeaders() {return requestHeaders;}public String selectedSubprotocol() {return selectedSubprotocol;}}private boolean isNotWebSocketPath(FullHttpRequest req) {return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);}private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {ChannelFuture f = ctx.channel().writeAndFlush(res);if (!isKeepAlive(req) || res.status().code() != 200) {f.addListener(ChannelFutureListener.CLOSE);}}private static String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {String protocol = "ws";if (cp.get(SslHandler.class) != null) {// SSL in use so use Secure WebSocketsprotocol = "wss";}String host = req.headers().get(HttpHeaderNames.HOST);return protocol + "://" + host + path;}private void refuseChannel(ChannelHandlerContext ctx) {ctx.channel().writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED));ctx.channel().close();}private static void send100Continue(ChannelHandlerContext ctx,String tokenHeader,String token) {DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);response.headers().set(tokenHeader,token);ctx.writeAndFlush(response);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("channel 捕获到异常了,关闭了");super.exceptionCaught(ctx, cause);}@Getter@AllArgsConstructorpublic static final class SecurityCheckComplete {private String userId;private String tokenHeader;private Boolean hasToken;}
}

initChannel方法去初始化自己的实现类

其他的类需要自己实现或者引用,其他的就是无关紧要的,不用去处理的类

@Overrideprotected void initChannel(SocketChannel ch){log.info("有新的连接");//获取工人所要做的工程(管道器==管道器对应的便是管道channel)ChannelPipeline pipeline = ch.pipeline();//为工人的工程按顺序添加工序/材料 (为管道器设置对应的handler也就是控制器)//1.设置心跳机制pipeline.addLast("idle-state",new IdleStateHandler(nettyWebSocketProperties.getReaderIdleTime(),0,0,TimeUnit.SECONDS));//2.出入站时的控制器,大部分用于针对心跳机制pipeline.addLast("change-duple",new WsChannelDupleHandler(nettyWebSocketProperties.getReaderIdleTime()));//3.加解码pipeline.addLast("http-codec",new HttpServerCodec());//3.打印控制器,为工人提供明显可见的操作结果的样式pipeline.addLast("logging", new LoggingHandler(LogLevel.INFO));pipeline.addLast("aggregator",new HttpObjectAggregator(8192));// 将自己的授权handler替换原有的handlerpipeline.addLast("auth",new SecurityServerHandler(// 此处我是用的yaml配置的,换成自己的即可nettyWebSocketProperties.getWebsocketPath(),nettyWebSocketProperties.getSubProtocols(),nettyWebSocketProperties.getAllowExtensions(),nettyWebSocketProperties.getMaxFrameSize(),//todofalse,nettyWebSocketProperties.getTokenHeader(),nettyWebSocketProperties.getHasToken()));pipeline.addLast("http-chunked",new ChunkedWriteHandler());// 将自己的协议控制器替换原有的协议控制器pipeline.addLast("websocket",new CustomWebSocketServerProtocolHandler(nettyWebSocketProperties.getWebsocketPath(),nettyWebSocketProperties.getSubProtocols(),nettyWebSocketProperties.getAllowExtensions(),nettyWebSocketProperties.getMaxFrameSize()));//7.自定义的handler针对业务pipeline.addLast("chat-handler",new ChatHandler());}

效果截图

在这里插入图片描述

源码跟踪

SecurityServerHandler 调整

调整为自定义请求头解析,但不去替换其他handler

package com.edu.message.handler.security;import com.edu.common.utils.SecurityUtils;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;import java.util.Objects;import static com.edu.message.handler.attributeKey.AttributeKeyUtils.SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY;/*** @author Administrator*/
@Slf4j
public class SecurityServerHandler extends ChannelInboundHandlerAdapter {private String tokenHeader;private Boolean hasToken;public SecurityServerHandler(String tokenHeader,Boolean hasToken){this.tokenHeader = tokenHeader;this.hasToken = hasToken;}private SecurityServerHandler(){}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if(msg instanceof FullHttpMessage){FullHttpMessage httpMessage = (FullHttpMessage) msg;HttpHeaders headers = httpMessage.headers();String token = Objects.requireNonNull(headers.get(tokenHeader));if(hasToken){// 开启鉴权 认证//extracts device information headersLong userId = 12345L;//SecurityUtils.getLoginUser(token).getUserId();//check ......SecurityCheckComplete complete = new SecurityCheckComplete(String.valueOf(userId),tokenHeader,hasToken);ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);ctx.fireUserEventTriggered(complete);}else {// 不开启鉴权 / 认证SecurityCheckComplete complete = new SecurityCheckComplete(null,tokenHeader,hasToken);ctx.channel().attr(SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY).set(complete);}}//other protocolssuper.channelRead(ctx, msg);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {System.out.println("channel 捕获到异常了,关闭了");super.exceptionCaught(ctx, cause);}@Getter@AllArgsConstructorpublic static final class SecurityCheckComplete {private String userId;private String tokenHeader;private Boolean hasToken;}
}

initChannel方法调整

改为使用默认实现

@Overrideprotected void initChannel(SocketChannel ch){log.info("有新的连接");//获取工人所要做的工程(管道器==管道器对应的便是管道channel)ChannelPipeline pipeline = ch.pipeline();//为工人的工程按顺序添加工序/材料 (为管道器设置对应的handler也就是控制器)//1.设置心跳机制pipeline.addLast("idle-state",new IdleStateHandler(nettyWebSocketProperties.getReaderIdleTime(),0,0,TimeUnit.SECONDS));//2.出入站时的控制器,大部分用于针对心跳机制pipeline.addLast("change-duple",new WsChannelDupleHandler(nettyWebSocketProperties.getReaderIdleTime()));//3.加解码pipeline.addLast("http-codec",new HttpServerCodec());//3.打印控制器,为工人提供明显可见的操作结果的样式pipeline.addLast("logging", new LoggingHandler(LogLevel.INFO));pipeline.addLast("aggregator",new HttpObjectAggregator(8192));pipeline.addLast("auth",new SecurityServerHandler(nettyWebSocketProperties.getTokenHeader(),nettyWebSocketProperties.getHasToken()));pipeline.addLast("http-chunked",new ChunkedWriteHandler());
//        pipeline.addLast("websocket",
//                new CustomWebSocketServerProtocolHandler(
//                nettyWebSocketProperties.getWebsocketPath(),
//                nettyWebSocketProperties.getSubProtocols(),
//                nettyWebSocketProperties.getAllowExtensions(),
//                nettyWebSocketProperties.getMaxFrameSize())
//        );pipeline.addLast("websocket",new WebSocketServerProtocolHandler(nettyWebSocketProperties.getWebsocketPath(),nettyWebSocketProperties.getSubProtocols(),nettyWebSocketProperties.getAllowExtensions(),nettyWebSocketProperties.getMaxFrameSize()));//7.自定义的handler针对业务pipeline.addLast("chat-handler",new ChatHandler());}

启动项目–流程截图

断点截图

在这里插入图片描述

1. SecurityServerHandler

第一步走到了自己定义的鉴权控制器(入站控制器),执行channelRead方法
在这里插入图片描述

2.userEventTriggered

自定义业务handler中的事件方法
在这里插入图片描述

3.WebSocketServerProtocolHandshakeHandler

此处便是走到了默认协议控制器的channelRead方法,需要注意handshaker.handshake(ctx.channel(), req) 这个方法,这是处理握手的方法,打个断点进去
在这里插入图片描述

4.WebSocketServerHandshaker

可以看到handshake 方法传的 HttpHeaders是null这里就是核心的握手逻辑可以看到并没有提供相应的头处理器
在这里插入图片描述

5. WebSocketServerHandshaker

newHandshakeResponse(req, responseHeaders) 就是构建响应结果,可以看到头是null
在这里插入图片描述

6. 最后的封装返回

可以看到有回到了自定义handler的业务控制器 中的时间监听方法
在这里插入图片描述
此时只要放行这一步便会在控制台打印出响应头,可以看出并没有设置我们自己的响应头,还是null
在这里插入图片描述
最后统一返回,连接中断,自协议头不一致所导致
在这里插入图片描述

相关内容

热门资讯

你的晚安是我的早安是什么歌曲 你的晚安是我的早安是什么歌曲苏天伦《你的早安是我的晚安》“你的晚安是我的早安”是单小源的歌曲《东京遇...
积极进取的小故事 积极进取的小故事现代的普通人,不要名人的。不能与别人重复,尽快回答   啄木鸟的故事       啄...
熊出没之探险日记3什么时候播出... 熊出没之探险日记3什么时候播出?熊出没之探险日记3,春节前播放。熊出没只是探险日记三2020年5月4...
谁知道所有有关“七”的歌?拜托... 谁知道所有有关“七”的歌?拜托了各位 谢谢就是歌曲名里有“七”这个字的!谢谢七月七迅谈日晴 看我七十...
求一本小说 女主穿越了三次 每... 求一本小说 女主穿越了三次 每次都在福临身边 后来怀孕了孩子被打掉了那个 女主叫什么雯?那个女主就是...
如果记忆不说话,流年也会开出花... 如果记忆不说话,流年也会开出花的基本信息书 名:《如果记忆不弯饥好说话,流年也会开出花》埋铅 作 者...
你好,旧时光漫画版在哪里可以看... 你好,旧时光漫画版在哪里可以看?暂时在绘心上连载
一首英文歌,男的组合唱的,MV... 一首英文歌,男的组合唱的,MV是一个婚礼的过程。求歌名。是不是darin的can'tstoplove...
为什么很多人喜欢用胶片相机? 为什么很多人喜欢用胶片相机?有一种情怀叫做“怀旧“吧,现在数码相机越来越普遍了,已经到了”全民摄影“...
女主先爱上男主,男主却不喜欢女... 女主先爱上男主,男主却不喜欢女主或者是另有所爱,最后女主男主还是在一起的穿越小说。有木有再生缘:我的...
爱情失恋伤感句子 爱情失恋伤感句子越是美好的从前,越幸福的曾经,现在只能带来锥心的疼痛,痛到撕心裂肺,肝肠寸断,终于痛...
24岁穿这个会不会显老 24岁穿这个会不会显老有点显老,这个颜色款式,颜色有点暗,没有活力,属于那种气质佳,长得高雅的女人,...
哈尔的移动城堡英语版 哈尔的移动城堡英语版可以发给我吗度盘~请查收~
秦时明月之万里长城什么时候播 秦时明月之万里长城什么时候播据说是今年暑假开播别急,官网什么的信他你就输了,12年之前底应该会出,杭...
孩子会得抽动症吗? 孩子会得抽动症吗?我天生的气性比较大,有时跟别人斗嘴时候就会手脚哆嗦,麻木,我问一下这是不是抽动症就...
亨德尔一生为音乐献出了怎样的贡... 亨德尔一生为音乐献出了怎样的贡献?亨德尔一生写了歌剧41部,清唱剧21部,以及大量的管乐器与弦乐器的...
礼仪起源和发展的经典故事? 礼仪起源和发展的经典故事?一、礼仪的起源;1、天神生礼仪;2、礼为天地人的统一体;3、礼产生于人的自...
描写桂林山水的句子有哪些? 描写桂林山水的句子有哪些?天下风光数桂林有杨万里的“梅花五岭八桂林,青罗带绕碧玉簪”;有邹应龙的“无...
避免与强敌正面对决的成语 避免与强敌正面对决的成语避免与强敌正面对决的成语避实就虚 【近义】避重就轻、避难就易、声东击西【反义...
多愁善感类的成语 多愁善感类的成语心细如发【解释】:极言小心谨慎,考虑周密。亦作“心细于发”。【出自】:吴梅《题天香石...