Springboot如何使用filter对response内容进行加密方式
发表于:2024-11-28 作者:千家信息网编辑
千家信息网最后更新 2024年11月28日,这篇文章主要介绍Springboot如何使用filter对response内容进行加密方式,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!使用filter对response内容进
千家信息网最后更新 2024年11月28日Springboot如何使用filter对response内容进行加密方式
这篇文章主要介绍Springboot如何使用filter对response内容进行加密方式,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!
使用filter对response内容进行加密
编写加密类(AES)
/** * aes加密解密 */public class AesEncryptUtils { //参数分别代表 算法名称/加密模式/数据填充方式 private static String algorithmstr = "AES/ECB/PKCS5Padding"; public static String getAlgorithmstr() { return algorithmstr; } /** * 加密 * @param content 加密的字符串 * @param encryptKey key值 * @return * @throws Exception */ public static String encrypt(String content, String encryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); Cipher cipher = Cipher.getInstance(algorithmstr); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES")); byte[] b = cipher.doFinal(content.getBytes("utf-8")); return Base64.encodeBase64String(b); } /** * 解密 * @param encryptStr 解密的字符串 * @param decryptKey 解密的key值 * @return * @throws Exception */ public static String decrypt(String encryptStr, String decryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); Cipher cipher = Cipher.getInstance(algorithmstr); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES")); byte[] encryptBytes = Base64.decodeBase64(encryptStr); byte[] decryptBytes = cipher.doFinal(encryptBytes); return new String(decryptBytes); } public static void main(String[] args) throws Exception{ String str = "pp2bQLjabobRWp2T5Ro5/GlqWCigmkwHYnrOK11VZkTkIA2hSwnEi1sijfTV6Ozd/"; System.out.println(decrypt(str,"f8db034bda44rtkb")); }}
编写Filter类
/** * 过滤器拦截请求,实现加密解密功能 * * @Component 将此Filter交给Spring容器管理 * @WebFilter 通过WebFilter进行Filter声明,这样容器在进行部署的时候就会处理该Filter * */@Componentpublic class EncryptFilter implements Filter { Logger log = LoggerFactory.getLogger(this.getClass()); @Value("${admin.encrypt.excludeUrl}") private String ignoreStr; private String[] ignoreArr; @Override public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub } /** * 有错误相应返回-44 * * @param response * @throws IOException */ private void getFailResponse(HttpServletResponse response) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = null; out = response.getWriter();// out.write("{\n" +// " \"status\":"+ Constant.ENCRYPT_FAIL +",\n" +// " \"message\": null,\n" +// " \"data\": []\n" +// "}"); //加密后的错误消息 out.write("+D+JO8tuwkrNbxnTTLdqStifmQceT+LlYETnIG/JZKrbAn+gIiqIp3VbzBV1y6R8B7aY53VM2xHa7cY3Osbnqw=="); out.flush(); out.close(); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { if(ignoreArr==null){ ignoreArr = ignoreStr.split(","); } HttpServletRequest HttpRequest=(HttpServletRequest)request; HttpServletResponse HttpResponse=(HttpServletResponse)response; boolean flag=isIgnore(HttpRequest,ignoreArr); if(flag) { try { chain.doFilter(HttpRequest, HttpResponse); } catch (IOException e) { e.printStackTrace(); } catch (ServletException e) { e.printStackTrace(); } }else{ try{ //响应处理 包装响应对象 res 并缓存响应数据 ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response); //执行业务逻辑 交给下一个过滤器或servlet处理 chain.doFilter(request, responseWrapper); byte[] resData = responseWrapper.getResponseData(); //设置响应内容格式,防止解析响应内容时出错// responseWrapper.setContentType("text/plain;charset=UTF-8"); //加密响应报文并响应 String encryptBASE64 = AesEncryptUtils.encrypt(new String(resData),Constant.ENCRYPT_STR); PrintWriter out = response.getWriter(); out.print(encryptBASE64); out.flush(); out.close(); }catch(Exception e){ try { getFailResponse((HttpServletResponse)response); } catch (IOException ioException) { ioException.printStackTrace(); } e.printStackTrace(); } }} @Override public void destroy() { // TODO Auto-generated method stub } /** * 哪些路径不处理 * @param request * @param strArr * @return */ public boolean isIgnore(HttpServletRequest request,String[] strArr) { String path=request.getRequestURI(); for(String ignore:strArr) { if(path.contains(ignore)) { return true; } } return false; }}
下图是对应的application.properties中的配置
其中用到了两个工具类
RequestWrapper
/** * @Description: 请求包装类 * @Date: 2020/5/26 16:29 */public class RequestWrapper extends HttpServletRequestWrapper { private String requestBody = null; //请求体 private HttpServletRequest req = null; // private final byte[] body;//保存流的字节数组 private final MapreqHeaders=new HashMap<>(); public RequestWrapper(HttpServletRequest request) throws IOException { super(request); this.req = request;// this.reqHeaders = new HashMap ();// String sessionStream = getRequestBodyStr(request);//读取流中的参数// body = sessionStream.getBytes(Charset.forName("UTF-8")); } public RequestWrapper(HttpServletRequest request, String requestBody) { super(request); this.requestBody = requestBody; this.req = request;// this.reqHeaders = request.get; } /** * @Description: 获取请求body * @Date: 2020/5/26 10:31 * @Param: [request] * @Return: java.lang.String */ public String getRequestBodyStr(final ServletRequest request) throws IOException { StringBuilder sb = new StringBuilder(); InputStream inputStream = null; BufferedReader reader = null; try { inputStream = cloneInputStream(request.getInputStream()); reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line = ""; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { inputStream.close(); } if (reader != null) { reader.close(); } } return sb.toString(); } /** * @Description: 复制输入流 * @Param: [inputStream] * @Return: java.io.InputStream */ public InputStream cloneInputStream(ServletInputStream inputStream) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) > -1) { byteArrayOutputStream.write(buffer, 0, len); } byteArrayOutputStream.flush(); InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); return byteArrayInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody.getBytes(req.getCharacterEncoding())); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return bais.read(); } }; } /** * 添加header的名称和值 * * @param name * @param value */ public void addHeader(String name, String value) { reqHeaders.put(name, value); } @Override public String getHeader(String name) {// log.info("getHeader --->{}", name); String headerValue = super.getHeader(name); if (reqHeaders.containsKey(name)) { headerValue = reqHeaders.get(name); } return headerValue; } /** * 得到headers的名称 */ @Override public Enumeration getHeaderNames() { List names = Collections.list(super.getHeaderNames()); for (String name : reqHeaders.keySet()) { names.add(name); } return Collections.enumeration(names); } @Override public Enumeration getHeaders(String name) {// log.info("getHeaders name --->>>>>>{}", name); List values = Collections.list(super.getHeaders(name));// log.info("getHeaders value --->>>>>>{}", values); if (reqHeaders.containsKey(name)) { values = Arrays.asList(reqHeaders.get(name)); } return Collections.enumeration(values); }}
ResponseWrapper
/** * @Description: 响应包装类 * @Date: 2020/5/26 16:29 */public class ResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream buffer = null; private ServletOutputStream out = null; private PrintWriter writer = null; public ResponseWrapper(HttpServletResponse response) throws IOException { super(response); buffer = new ByteArrayOutputStream();// 真正存储数据的流 out = new WapperedOutputStream(buffer); writer = new PrintWriter(new OutputStreamWriter(buffer,this.getCharacterEncoding())); } /** 重载父类获取outputstream的方法 */ @Override public ServletOutputStream getOutputStream() throws IOException { return out; } /** 重载父类获取writer的方法 */ @Override public PrintWriter getWriter() throws UnsupportedEncodingException { return writer; } /** 重载父类获取flushBuffer的方法 */ @Override public void flushBuffer() throws IOException { if (out != null) { out.flush(); } if (writer != null) { writer.flush(); } } @Override public void reset() { buffer.reset(); } /** 将out、writer中的数据强制输出到WapperedResponse的buffer里面,否则取不到数据 */ public byte[] getResponseData() throws IOException { flushBuffer(); return buffer.toByteArray(); } /** 内部类,对ServletOutputStream进行包装 */ private class WapperedOutputStream extends ServletOutputStream { private ByteArrayOutputStream bos = null; public WapperedOutputStream(ByteArrayOutputStream stream) throws IOException { bos = stream; } @Override public void write(int b) throws IOException { bos.write(b); } @Override public void write(byte[] b) throws IOException { bos.write(b, 0, b.length); } @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { } }}
写配置类
@Configurationpublic class WebConfiguration { @Autowired private EncryptFilter encryptFilter; @Bean public FilterRegistrationBean registFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(encryptFilter); registration.addUrlPatterns("/*"); registration.setName("EncryptFilter"); registration.setOrder(1);// registration.setEnabled(false); return registration; } //做跨域处理,跟这个filter没关系 @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("*") .allowedHeaders("*") .maxAge(3600); } }; }}
Springboot数据加密传输
创建加解密注解注解
对于拦截路径上全部采用数据加解密处理,如果有部分接口不需要加解密处理的话,在方法上或者类上加上此注解即可不做加解密处理
package com.hars.common.infrastructure.validation.security;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 加解密注解 * * @author Huangbigao * @date 2020/8/29 11:02 */@Documented@Target({ElementType.METHOD, ElementType.TYPE,})@Retention(RetentionPolicy.RUNTIME)public @interface CryptoDecryptionSecurity { /** * 是否加解密,默认加解密 * * @return */ boolean cryptoDecryption() default true; /** * 是否进行request 解密,默认进行解密 * * @return */ boolean requestDecryption() default true; /** * 是否对输出结果进行加密,默认进行加密 * * @return */ boolean responseCrypto() default true;}
ps:注解使用
@CryptoDecryptionSecurity(responseCrypto = false) @ApiOperation(value = "微信公众号验证业务处理接口") @GetMapping(value = "/handle/{appid}", produces = "text/plain;charset=utf-8") public String authHandle(@PathVariable String appid, @RequestParam(name = "signature", required = false) String signature, @RequestParam(name = "timestamp", required = false) String timestamp, @RequestParam(name = "nonce", required = false) String nonce, @RequestParam(name = "echostr", required = false) String echostr, HttpServletRequest request) { return weChatMpService.authHandle(appid, signature, timestamp, nonce, echostr, request); }
创建request解密类
package com.hars.common.infrastructure.utils.filter;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.TypeReference;import com.hars.common.infrastructure.utils.aes.AesUtil;import com.hars.common.infrastructure.utils.http.HttpContextUtil;import com.hars.common.infrastructure.utils.string.StringUtil;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;import java.util.Collections;import java.util.Enumeration;import java.util.HashMap;import java.util.LinkedHashSet;import java.util.Map;import java.util.Set;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import org.springframework.util.Assert;/** * @author Huangbigao * @date 2020/8/29 10:12 */public class DecryptionRequestUtil extends HttpServletRequestWrapper { private static final String APPLICATION_JSON = "application/json"; /** * 所有参数的Map集合 */ private MapparameterMap; /** * 输入流 */ private InputStream inputStream; private final boolean valueValid = true; public DecryptionRequestUtil(HttpServletRequest request, String password) { super(request); String encrypt; String contentType = request.getHeader("Content-Type"); if (contentType != null && contentType.contains(APPLICATION_JSON)) { //json String bodyStr = HttpContextUtil.getBodyString(request); if (StringUtil.isBlank(bodyStr)){ return; } encrypt = (String) JSON.parseObject(bodyStr).get("encrypt"); } else { // url encrypt = request.getParameter("encrypt"); } String jsonData = AesUtil.decrypt(encrypt, password); if (StringUtil.isBlank(jsonData)){ return; } if (contentType != null && contentType.contains(APPLICATION_JSON)) { if (this.inputStream == null) { this.inputStream = new DecryptionInputStream(new ByteArrayInputStream(jsonData.getBytes())); } } parameterMap = buildParams(jsonData); } private Map buildParams(String src) { Map map = new HashMap<>(); Map params = JSONObject.parseObject(src, new TypeReference
创建response加密类
package com.hars.common.infrastructure.utils.filter;import java.io.ByteArrayOutputStream;import java.io.IOException;import javax.servlet.ServletOutputStream;import javax.servlet.WriteListener;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpServletResponseWrapper;/** * @author Huangbigao * @date 2020/8/29 13:11 */public class ResponseWrapperUtil extends HttpServletResponseWrapper { private ByteArrayOutputStream buffer; private ServletOutputStream out; public ResponseWrapperUtil(HttpServletResponse httpServletResponse) { super(httpServletResponse); buffer = new ByteArrayOutputStream(); out = new WrapperOutputStream(buffer); } @Override public ServletOutputStream getOutputStream() throws IOException { return out; } @Override public void flushBuffer() throws IOException { if (out != null) { out.flush(); } } public byte[] getContent() throws IOException { flushBuffer(); return buffer.toByteArray(); } private static class WrapperOutputStream extends ServletOutputStream { private ByteArrayOutputStream bos; WrapperOutputStream(ByteArrayOutputStream bos) { this.bos = bos; } @Override public void write(int b) throws IOException { bos.write(b); } @Override public boolean isReady() { // TODO Auto-generated method stub return false; } @Override public void setWriteListener(WriteListener arg0) { // TODO Auto-generated method stub } }}
创建AES加密工具类
package com.hars.common.infrastructure.utils.aes;import com.hars.common.infrastructure.utils.string.StringUtil;import java.nio.charset.StandardCharsets;import java.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import lombok.extern.slf4j.Slf4j;/** * AES 加解密 工具类 * * @author Huangbigao * @date 2020/8/28 15:17 */@Slf4jpublic class AesUtil { /** * AES解密 * * @param content 密文 * @param password 秘钥,必须为16个字符组成 * @return 明文 */ public static String decrypt(String content, String password) { try { if (StringUtil.isBlank(content) || StringUtil.isBlank(password)) { return null; } byte[] encryptByte = Base64.getDecoder().decode(content); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(password.getBytes(), "AES")); byte[] decryptBytes = cipher.doFinal(encryptByte); return new String(decryptBytes); } catch (Exception e) { log.error(e.getMessage(), e); return null; } } /** * AES加密 * * @param content 明文 * @param password 秘钥,必须为16个字符组成 * @return 密文 */ public static String encrypt(String content, String password) { try { if (StringUtil.isBlank(content) || StringUtil.isBlank(password)) { return null; } Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(password.getBytes(), "AES")); byte[] encryptStr = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encryptStr); } catch (Exception e) { log.error(e.getMessage(), e); return null; } }
创建加解密Filter类
package com.hars.user.infrastructure.filter;import com.alibaba.fastjson.JSON;import com.hars.common.infrastructure.utils.aes.AesUtil;import com.hars.common.infrastructure.utils.filter.DecryptionRequestUtil;import com.hars.common.infrastructure.utils.filter.ResponseWrapperUtil;import com.hars.common.infrastructure.validation.security.CryptoDecryptionSecurity;import com.hars.result.infrastructure.advice.Response;import java.io.IOException;import java.util.ArrayList;import java.util.List;import java.util.Map;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletOutputStream;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.beans.factory.BeanFactoryUtils;import org.springframework.context.ApplicationContext;import org.springframework.core.annotation.AnnotationAwareOrderComparator;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.HandlerExecutionChain;import org.springframework.web.servlet.HandlerMapping;/** * @author Huangbigao * @date 2020/8/28 16:26 */public class CryptoDecryptionFilter implements Filter { //方法映射集 private ListhandlerMappings; public CryptoDecryptionFilter(ApplicationContext applicationContext) { Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<>(matchingBeans.values()); AnnotationAwareOrderComparator.sort(this.handlerMappings); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; //判断方法上是否存在注解,如果不存在,默认加解密 //类上的注解 CryptoDecryptionSecurity classFlag = null; //方法上的注解 CryptoDecryptionSecurity methodFlag = null; try { HandlerExecutionChain handlerExecutionChain = getHandler(httpServletRequest); Object handler = handlerExecutionChain != null ? handlerExecutionChain.getHandler() : null; if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod) handler; classFlag = method.getBeanType().getAnnotation(CryptoDecryptionSecurity.class); methodFlag = method.getMethodAnnotation(CryptoDecryptionSecurity.class); //如果方法注解存在,且不加密,则直接返回 if (methodFlag != null && !methodFlag.cryptoDecryption()) { chain.doFilter(request, response); return; } //如果类注解存在,且不加密,则直接返回 if (classFlag != null && !classFlag.cryptoDecryption()) { chain.doFilter(request, response); return; } } } catch (Exception e) { response.setContentType("application/json; charset=UTF-8"); response.getWriter().write(JSON.toJSONString(Response.error("该请求无效", 601))); return; } CryptoDecryptionSecurity currentFlag = null; if (methodFlag != null) { currentFlag = methodFlag; } else if (classFlag != null) { currentFlag = classFlag; } //加解密密码 String password = "Hbg584782648!@hb"; ResponseWrapperUtil responseWrapper = null; //加解密处理 if (currentFlag == null || (currentFlag.requestDecryption() && currentFlag.responseCrypto())) { ServletRequest requestWrapper = new DecryptionRequestUtil(httpServletRequest, password); responseWrapper = new ResponseWrapperUtil(httpServletResponse); chain.doFilter(requestWrapper, responseWrapper); } else if (currentFlag.requestDecryption() && !currentFlag.responseCrypto()) { ServletRequest requestWrapper = new DecryptionRequestUtil(httpServletRequest, password); chain.doFilter(requestWrapper, response); } else if (!currentFlag.requestDecryption() && currentFlag.responseCrypto()) { responseWrapper = new ResponseWrapperUtil(httpServletResponse); chain.doFilter(request, responseWrapper); } else { chain.doFilter(request, response); } if (responseWrapper != null) { byte[] content = responseWrapper.getContent();//获取返回值 //判断是否有值 if (content.length > 0) { String result = new String(content, "UTF-8"); //加密 String encryptStr = AesUtil.encrypt(result, password); //把返回值输出到客户端 ServletOutputStream out = response.getOutputStream(); out.write(encryptStr.getBytes()); out.flush(); } } } /** * 获取访问目标方法 * * @param request * @return HandlerExecutionChain * @throws Exception */ private HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping hm : this.handlerMappings) { HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } } return null; }}
定义过滤器的拦截路径
@Autowired private ApplicationContext applicationContext; /** * 添加加解密过滤器 * * @return */ @Bean public FilterRegistrationBean encryptionDataFilterRegistration() { FilterRegistrationBeanregistration = new FilterRegistrationBean<>(); registration.setFilter(new CryptoDecryptionFilter(applicationContext)); registration.addUrlPatterns("/*"); registration.setName("cryptoDecryptionFilter"); registration.setOrder(2); return registration; }
以上是"Springboot如何使用filter对response内容进行加密方式"这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注行业资讯频道!
加密
注解
处理
方法
内容
数据
字符
过滤器
UTF-8
包装
方式
参数
名称
工具
路径
输出
业务
加密解密
字符串
容器
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
石家庄应用软件开发服务私人定做
软件开发三方付款协议范本
软件开发项目心得 ppt
查看db2数据库字符集
我的世界国际版诡异的服务器
长沙岳麓区软件开发师培训
网络安全工作 主持词
软件开发有哪些职业
大学网络安全大赛
网站服务器负载均衡
网络安全防护意识新闻稿
数据库中如何设置属性不隐藏
试述关系数据库的特点
互联网产业金融科技有限公司
西安世邦网络技术
月老为难打一网络技术
湖南惠普服务器维修维保多少钱
python什么软件开发
网络安全大家谈军人
阿里云服务器是不是和虚拟机一样
数据库默认约束设置的关键字是
上海思建网络技术
数据库两个表建立关联
网吧无盘服务器如何打驱动
服务器文件保存为bat
小型管理软件开发的语言选择
服务器安全专业
宝山区创新数据库服务电话多少
永定林业局网络安全管理
网络安全在我身边优秀视频展播