SAML2.0 笔记(二)
创始人
2024-02-04 18:35:50

文章目录

  • 一、前言
  • 二、共通内容
    • 1.1、引入依赖
    • 1.2、初始化SAML部分
      • 1.2.1、检查JCE环境
      • 1.2.2、初始化服务
    • 1.3、拦截器部分
      • 1.3.1、构建AuthnRequest
      • 1.3.2、AuthRequest解析
      • 1.3.3、SP模式选择
      • 1.3.4、IDP模式选择
    • 1.4、涉及的工具类
      • 1.4.1、OpenSAMLUtils工具类
      • 1.4.2、sp/idp Credentials
  • 三、SP redirect 模式 + IDP post响应模式
    • 1.1、SP拦截处理逻辑
      • 1.1.1、利用证书对上下文进行签名
      • 1.1.2、序列化和签名
      • 1.1.3、跳转至IDP
        • 1.1.3.1、解析SP的内容
    • 1.2、IDP处理SP的讯息内容
      • 1.2.1、通过URL获取Sp处理后的三个参数(可忽略)
      • 1.2.2、验证解析消息是否被撰改(可忽略)
      • 1.2.3、通过Post模式响应SP的请求
        • 1.2.3.1、设置参数 context
        • 1.2.3.2、设置参数 HttpServletResponse
        • 1.2.3.3、设置参数VelocityTemplateId
        • 1.2.3.4、设置参数VelocityEngine
        • 1.2.3.5、初始化编码器并发送讯息
    • 1.3、获取IDP返回的讯息

一、前言

通常而言,我们都是基于 ADFS 来进行接口对接,使用拦截器特定拦截即可。

若是整个系统都需要以其为基准,可以直接使用过滤器。

二、共通内容

其实三个模式大同小异,为了方便拆解,这里三个模式一致的地方这里会单独列出说明。

1.1、引入依赖

3.x是JDK 8 的唯一选择了,故我这里直接选择3.2.0

···3.2.0
···org.opensamlopensaml-core${opensaml.version}org.opensamlopensaml-saml-api${opensaml.version}org.opensamlopensaml-saml-impl${opensaml.version}org.opensamlopensaml-messaging-api${opensaml.version}org.opensamlopensaml-messaging-impl${opensaml.version}org.opensamlopensaml-soap-api${opensaml.version}org.opensamlopensaml-soap-impl${opensaml.version}

1.2、初始化SAML部分

为了在项目中正常使用 SAML服务,可以考虑以下几步。

1.2.1、检查JCE环境

整体上为什么我们需要检查JCE环境,是因为初始化服务的时候需要创建实例 AES/CBC/ISO10126Padding

方式一: 手动检查jvm内的JCE 的provider

for (Provider jceProvider : Security.getProviders()) {System.out.println(jceProvider.getInfo());
}
//通常我们能从打印中 SunJCE Provider (...AES..) 找到AES就基本符合要求。

方式二: 利用SAML自带的检测方法来测试是否符合 (最保险)

谨记,建议在确保这步没问题后再去执行 1.2.2 步骤

①点击 InitializationService.initialize() 方法 ,进入 org.opensaml.core.config.InitializationService 类。

②点击类中 initializer.init() 方法,进入接口类 org.opensaml.core.config.Initializer ,我们可以从中找到实现类 JavaCryptoValidationInitializer

③ 可以清晰看到 方法 Cipher.getInstance("AES/CBC/ISO10126Padding"); ,同时看到头标注 An initializer which validates the Java Cryptographic Architecture environment is usable.

// 由上面步骤可以得出,我们只需要调用如下就可以验证是否支持saml初始化javaCryptoValidationInitializer.init();

1.2.2、初始化服务

为了使得SAML服务更好的加载入虚拟机,建议保证 1.2.1步骤 的可靠性后在执行初始化

//注解描述: Service which initializes OpenSAML library modules using the Java Services API.
InitializationService.initialize();

1.3、拦截器部分

这里主要作拦截判断是否授权的作用。

整体上就是:

存在授权: 放行

不存在授权:

①通过对应方式存储跳转地址

②构建 SAMLRequest 内容,采用对应方式来进行交互。

1.3.1、构建AuthnRequest

构建请求权限内容,以便整合到后续SP采取的方式中,也就是相对应的模式。

	private AuthnRequest buildAuthnRequest() {AuthnRequest authnRequest = OpenSAMLUtils.buildSAMLObject(AuthnRequest.class);//请求时间:该对象创建的时间,以判断其时效性authnRequest.setIssueInstant(new DateTime());//目标URL:目标地址,IDP地址authnRequest.setDestination(getIPDSSODestination());//传输SAML断言所需要的绑定:也就是用何种协议使用Artifact来取回真正的认证信息,这里希望以POST返回讯息authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);//SP地址: 也就是SAML断言返回的地址authnRequest.setAssertionConsumerServiceURL(getAssertionConsumerEndpoint());//请求的ID:为当前请求设置ID,一般为随机数jauthnRequest.setID(OpenSAMLUtils.generateSecureRandomId());//Issuer: 发行人信息,也就是SP的ID,一般是SP的URLauthnRequest.setIssuer(buildIssuer());//NameID:IDP对于用户身份的标识;NameID policy是SP关于NameID是如何创建的说明authnRequest.setNameIDPolicy(buildNameIdPolicy());// 请求认证上下文(requested Authentication Context):// SP对于认证的要求,包含SP希望IDP如何验证用户,也就是IDP要依据什么来验证用户身份。authnRequest.setRequestedAuthnContext(buildRequestedAuthnContext());return authnRequest;}

1.3.2、AuthRequest解析

AuthnRequest :说明要要如何才能鉴别用户,提供给IDP使用(整个结构是XML)。

通常我们可以根据客户提供的 metadata 来配合构建这个对象

IDP地址: 我们通常可以从 metadata 中获取到所需的地址。




断言绑定: 也就是用何种协议来使用Artifact取回真正的认证信息。





SP地址: 我们鉴定应答的地址,说白了就是用于解析IDP处理后的应答讯息的路径地址。

Issuer标识: 发行人的标识(也有推荐使用SP的url)

//可以定义发行人的标识: demo
Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
issuer.setValue(getSPIssuerValue());

NameID: IDP对于用户身份的标识



urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
urn:oasis:names:tc:SAML:2.0:nameid-format:transient


NameIDPolicy nameIDPolicy = OpenSAMLUtils.buildSAMLObject(NameIDPolicy.class);
//IDP是否被允许当发现用户不存在时创建用户账号//主要针对
nameIDPolicy.setAllowCreate(true);
nameIDPolicy.setFormat(NameIDType.EMAIL);
//nameIDPolicy.setFormat(NameIDType.PERSISTENT); 持久标识
//nameIDPolicy.setFormat(NameIDType.TRANSIENT); 临时标识
//nameIDPolicy.setFormat(NameIDType.UNSPECIFIED); 根据URL标识
//整体可以参考类 : org.opensaml.saml.saml2.core.NameIDType 

构造认证上下文:

AuthnContextComparisonTypeEnumeration : 主要区分几个级别

① better: 比任意选定的内容都要严格。

② exact: 最少要有一个标识也就是 NameId 与指定的上下文完全匹配。

③ maximum: 建议IDP尽可能去与匹配讯息,但是不需要超过一个标识去匹配。

④ minimum: 建议IDP尽可能去与匹配讯息,没有数量限制。

AuthnContext: 内容校验部分,这里列举常用的几个,具体可以参考 org.opensaml.saml.saml2.core.AuthnContext

① UNSPECIFIED_AUTHN_CTX: 针对URL来进行上下文校验

② PASSWORD_AUTHN_CTX : 基于用户名密码来上下文校验(通常是IDP用户定义的账户)

RequestedAuthnContext requestedAuthnContext = OpenSAMLUtils.buildSAMLObject(RequestedAuthnContext.class);
//涉及AuthnContextComparisonTypeEnumeration ,参考注解
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM);AuthnContextClassRef passwordAuthnContextClassRef = OpenSAMLUtils.buildSAMLObject(AuthnContextClassRef.class);
//AuthnContext ,参考注解
passwordAuthnContextClassRef.setAuthnContextClassRef(AuthnContext.PASSWORD_AUTHN_CTX);
requestedAuthnContext.getAuthnContextClassRefs().add(passwordAuthnContextClassRef);

1.3.3、SP模式选择

其实可以直接参考抽象类: org.opensaml.saml.saml2.binding.encoding.impl.BaseSAML2MessageEncoder ,可以帮助我们来对于 AuthnRequest 进行序列化和签名

1、 HTTPArtifactEncoder : SAML 2 Artifact Binding encoder, support both HTTP GET and POST.

顾名思义,支持以 Artifact的模式绑定传输讯息给idp,可以是 HTTP 通过 `URL` 传输也可以通过  `post` 参数传输。
使用的时候,可以参考类详情参数,方便切换 GET 与 Post方式

2、 HTTPPostEncoder: SAML 2.0 HTTP Post binding message encoder.

3、 HTTPPostSimpleSignEncoder: SAML 2.0 HTTP-POST-SimpleSign binding message encoder.

4、 HTTPRedirectDeflateEncoder: This encoder only supports DEFLATE compression and DSA-SHA1 and RSA-SHA1 signatures.

在这里插入图片描述

1.3.4、IDP模式选择

有加密当然有解密,参考抽象类: org.opensaml.messaging.decoder.servlet.BaseHttpServletRequestXMLMessageDecoder ,可以帮我们来对序列化后的内容进行解密,其实大多数用到的,针对着SP模式切换即可。

1、 HTTPArtifactDecoder: SAML 2 Artifact Binding decoder, support both HTTP GET and POST.

2、 HTTPPostDecoder: SAML 2.0 HTTP Post binding message decoder.

3、 HTTPPostSimpleSignDecoder: SAML 2.0 HTTP-POST-SimpleSign binding message decoder.

4、 HTTPRedirectDeflateDecoder: SAML 2.0 HTTP Redirect decoder using the DEFLATE encoding method.

图内记得区分 saml1 还是 saml2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3uIRKxY-1668969858115)(C:\Users\1\OneDrive\Documents\笔记解决方案图片\image-20220814221237957.png)]

1.4、涉及的工具类

1.4.1、OpenSAMLUtils工具类

主要涉及方法

	public static  T buildSAMLObject(final Class clazz) {T object = null;try {XMLObjectBuilderFactory builderFactory = XMLObjectProviderRegistrySupport.getBuilderFactory();QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null);object = (T) builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName);} catch (IllegalAccessException e) {throw new IllegalArgumentException("Could not create SAML object");} catch (NoSuchFieldException e) {throw new IllegalArgumentException("Could not create SAML object");}return object;}

1.4.2、sp/idp Credentials

关于参数 KEY_ENTRY_ID 这里的参数,要与证书的别名一致,否则无法正确校验。

说白点就是无法生成 signtaure

KEY_STORE_PASSWORD / KEY_STORE_ENTRY_PASSWORD : 证书的密码

public class SPCredentials {private static Logger logger = LoggerFactory.getLogger(SPCredentials.class);private static final String KEY_STORE_PASSWORD = "ringo";private static final String KEY_STORE_ENTRY_PASSWORD = "ringo";private static final String KEY_STORE_PATH = "/coffeeandice.jks";private static final String KEY_ENTRY_ID = "coffeeandice";private static final Credential credential;static {try {KeyStore keystore = readKeystoreFromFile(KEY_STORE_PATH, KEY_STORE_PASSWORD);Map passwordMap = new HashMap();passwordMap.put(KEY_ENTRY_ID, KEY_STORE_ENTRY_PASSWORD);KeyStoreCredentialResolver resolver = new KeyStoreCredentialResolver(keystore, passwordMap);Criterion criterion = new EntityIdCriterion(KEY_ENTRY_ID);CriteriaSet criteriaSet = new CriteriaSet();criteriaSet.add(criterion);credential = resolver.resolveSingle(criteriaSet);} catch (ResolverException e) {throw new RuntimeException("Something went wrong reading credentials", e);}}private static KeyStore readKeystoreFromFile(String pathToKeyStore, String keyStorePassword) {try {KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());InputStream inputStream = SPCredentials.class.getResourceAsStream(pathToKeyStore);keystore.load(inputStream, keyStorePassword.toCharArray());inputStream.close();return keystore;} catch (Exception e) {throw new RuntimeException("Something went wrong reading keystore", e);}}public static Credential getCredential() {logger.info("相应的凭证值:{}", credential);return credential;}}

三、SP redirect 模式 + IDP post响应模式

构建一个 SAML Request内容,用于重定向至IDP的介面,待校验成功后,IDP将会以 post 的形式,通知SP,并将相关讯息推送至SP内。

1.1、SP拦截处理逻辑

可以根据自己的基本情况用作拦截,这里简单利用 过滤器 对所有请求进行判断。

由于为示例,则以简单标识存储在会话中判断。

结合标题分级,这块采用 HTTPRedirectDeflateEncoder 帮助传输

1.1.1、利用证书对上下文进行签名

private void redirectUserWithRequest(HttpServletResponse httpServletResponse, AuthnRequest authnRequest) {MessageContext context = new MessageContext();context.setMessage(authnRequest);//关于传输对端实体的信息,对于IDP就是SP,对于SP就是IDP;SAMLPeerEntityContext peerEntityContext =context.getSubcontext(SAMLPeerEntityContext.class, true);//端点信息;//getIPDEndpoint() ,就是IPD的对应校验端点。SAMLEndpointContext endpointContext =peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);endpointContext.setEndpoint(getIPDEndpoint());//数据签名环境消息上下文SignatureSigningParameters signatureSigningParameters = new SignatureSigningParameters();//获得证书,其中包含公钥signatureSigningParameters.setSigningCredential(SPCredentials.getCredential());//ALGO_ID_SIGNATURE_RSA_SHA256signatureSigningParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);context.getSubcontext(SecurityParametersContext.class, true).setSignatureSigningParameters(signatureSigningParameters);// OpenSAML提供了HTTPRedirectDefalteEncoder// 它将帮助我们来对于AuthnRequest进行序列化和签名HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder();encoder.setMessageContext(context);encoder.setHttpServletResponse(httpServletResponse);try {encoder.initialize();} catch (ComponentInitializationException e) {throw new RuntimeException(e);}try {//*encode*方法将会压缩消息,生成签名,添加结果到URL并从定向用户到Idp.//先使用RFC1951作为默认方法压缩数据,在对压缩后的数据信息Base64编码encoder.encode();} catch (MessageEncodingException e) {throw new RuntimeException(e);}}

1.1.2、序列化和签名

这里通过处理后,主要分为3个参数传递出去

①SAMLRequest

②SigAlg

③Signature

Tips: 若生成跳转的地址缺失了 ②、③参数,建议校验证书别名是否一致。

整体过程会发生在 HTTPRedirectDeflateEncoder 内的 buildRedirectURL

1.1.3、跳转至IDP

由于采取了 SP redirect 模式 ,所以,在到执行 encoder.encode() 后会重定向至,配置好的url地址,按照示例,默认值应该为

http://localhost:8088/idp/logon

1.1.3.1、解析SP的内容

理论上,我们应该对请求的内容进行解析,对其进行初步判断

当然理论上我们是需要自动解析,判断 SP模式 是什么内容,然后采用解码器解码。(但是由于我是demo,不管了~滑稽)

1.2、IDP处理SP的讯息内容

整体上,这里需要一个登陆的流程,但是登陆的流程并不重要,可以自己随意加,这里就跳过了~~

1.2.1、通过URL获取Sp处理后的三个参数(可忽略)

①SAMLRequest

②SigAlg

③Signature

结合标题分级,这块采用 HTTPRedirectDeflateDecoder 帮助解析内容

String samlRequest = request.getParameter("SAMLRequest");
String SigAlg = request.getParameter("SigAlg");
String Signature = request.getParameter("Signature");//解析
HTTPRedirectDeflateDecoder httpRedirectDeflateDecoder = new HTTPRedirectDeflateDecoder();
httpRedirectDeflateDecoder.setHttpServletRequest(request);//初始化解析器 & 解码
//-----初始化解析器-----
try {httpRedirectDeflateDecoder.initialize();
} catch (ComponentInitializationException e) {e.printStackTrace();
}
//-----解码-----
try {httpRedirectDeflateDecoder.decode();
} catch (MessageDecodingException e) {e.printStackTrace();
}
//解析sp传递的消息体
MessageContext messageContext = httpRedirectDeflateDecoder.getMessageContext();

1.2.2、验证解析消息是否被撰改(可忽略)

其实我们可以翻看源码来查出 HTTPRedirectDeflateEncoder 如何生成 Signature ,我们照搬即可验证。

源码路径: org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder#buildRedirectURL

String samlRequest = request.getParameter("SAMLRequest");
String SigAlg = request.getParameter("SigAlg");
String Signature = request.getParameter("Signature");//数据签名环境上线文
SignatureSigningParameters signatureSigningParameters1 = new SignatureSigningParameters();
//获得证书,其中包含公钥
signatureSigningParameters1.setSigningCredential(IdpCredentials.getCredential());
//ALGO_ID_SIGNATURE_RSA_SHA256
signatureSigningParameters1.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);String b64Signature = null;
try {byte[] rawSignature =XMLSigningUtil.signWithURI(signatureSigningParameters1.getSigningCredential(), SigAlg, sigMaterial.getBytes("UTF-8"));b64Signature = Base64Support.encode(rawSignature, Base64Support.UNCHUNKED);
} catch (final SecurityException e) {
} catch (final UnsupportedEncodingException e) {// UTF-8 encoding is required to be supported by all JVMs
}System.out.println(b64Signature == Signature);

1.2.3、通过Post模式响应SP的请求

响应标题内容,哈哈~

既然以post请求响应,就基本是从下面两个出手,我选1,就不走simple了。

1、 HTTPPostDecoder: SAML 2.0 HTTP Post binding message decoder.

2、 HTTPPostSimpleSignDecoder: SAML 2.0 HTTP-POST-SimpleSign binding message decoder.

在这里插入图片描述

1.2.3.1、设置参数 context

 HTTPPostEncoder httpPostEncoder = new HTTPPostEncoder();
//一、针对参数 context
// 完成最低标准讯息体构造 org.opensaml.saml.common.binding.SAMLBindingSupport
//(1)需要最低一个设置SAMLPeerEntityContext & 设置SAMLPeerEntityContext下的SAMLEndpointContext
//(2)需要最低一个传递绑定消息的SAMLBindingContext
MessageContext context = new MessageContext();
//1.1、设置SAMLPeerEntityContext
SAMLPeerEntityContext peerEntityContext =context.getSubcontext(SAMLPeerEntityContext.class, true);
//1.2、设置SAMLPeerEntityContext 下的 SAMLEndpointContext
SAMLEndpointContext endpointContext =peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
endpointContext.setEndpoint(getIPDEndpoint());
peerEntityContext.setEntityId(idpConfig.idp_entity_id);
SignatureSigningParameters signatureSigningParameters = new SignatureSigningParameters();
signatureSigningParameters.setSigningCredential(IdpCredentials.getCredential());
//ALGO_ID_SIGNATURE_RSA_SHA256
signatureSigningParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);//2.1、设置 SAMLBindingContext
SAMLBindingContext baseContexts = new SAMLBindingContext();
//用于给判断的时间而已
baseContexts.setRelayState("60");
baseContexts.setHasBindingSignature(true);
baseContexts.setAutoCreateSubcontexts(true);
baseContexts.setBindingUri(idpConfig.idp_sso_logon);
context.addSubcontext(baseContexts);//3.1、推送地址消息
//这里可以很多讯息 SAMLObject 下的实现类都可以
EmailAddress emailAddress = OpenSAMLUtils.buildSAMLObject(EmailAddress.class);
emailAddress.setAddress("demo@outlook.com");
ArtifactResponse artifactResponse = OpenSAMLUtils.buildSAMLObject(ArtifactResponse.class);
artifactResponse.setMessage(emailAddress);
//可通过#getOrderedChildren 设置更多的内容
//artifactResponse.getOrderedChildren()
context.setMessage(artifactResponse);//4、最终设置,将上述内容填充进入
httpPostEncoder.setMessageContext(context);

1.2.3.2、设置参数 HttpServletResponse

直接将请求参数置入即可。

httpPostEncoder.setHttpServletResponse(response);

1.2.3.3、设置参数VelocityTemplateId

一般我们走默认值即可, /templates/saml2-post-binding.vm

org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder

1.2.3.4、设置参数VelocityEngine

这里主要是一个问题,就是存在无法读取模板的问题。

包内 opensaml-saml-impl ,建议直接复制模版放入resources内,本例会将默认值内的模版复制一份到 resources/templates

VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
velocityEngine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
//一定要初始化
velocityEngine.init();
httpPostEncoder.setVelocityEngine(velocityEngine);

1.2.3.5、初始化编码器并发送讯息

这里存在三个对象 org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder#populateVelocityContext

① action

② binding

③ SAMLResponse

try {httpPostEncoder.initialize();
} catch (ComponentInitializationException e) {e.printStackTrace();
}
try {//*encode*方法将会压缩消息,生成签名,添加结果到URL并从定向用户到Idp.//先使用RFC1951作为默认方法压缩数据,在对压缩后的数据信息Base64编码httpPostEncoder.encode();
} catch (MessageEncodingException e) {throw new RuntimeException(e);
}

1.3、获取IDP返回的讯息

从上述流程,我们开天眼知道,返回的内容参数内容是 SAMLResponse ,用 HTTPPostDecoder 进行传递

String samlResponse = req.getParameter("SAMLResponse");HTTPPostDecoder httpPostDecoder = new HTTPPostDecoder();
httpPostDecoder.setHttpServletRequest(req);
try {httpPostDecoder.initialize();
} catch (ComponentInitializationException e) {e.printStackTrace();
}
try {httpPostDecoder.decode();
} catch (MessageDecodingException e) {e.printStackTrace();
}
final MessageContext messageContext = httpPostDecoder.getMessageContext();
System.out.println(messageContext);
List encryptedAssert = messageContext.getMessage().getOrderedChildren();
for (int i = 0, len = encryptedAssert.size(); i < len; i++) {XMLObject xmlObject = encryptedAssert.get(i);if (xmlObject instanceof EmailAddress) {EmailAddress emailAddress = ((EmailAddress) xmlObject);logger.info("内容:{}", emailAddress.getAddress());//附带的其他内容//                final List orderedChildren = emailAddress.getOrderedChildren();//                for (int j = 0, leng = encryptedAssert.size(); j < len; j++) {//                    XMLObject xmlObject1 = orderedChildren.get(i);//                }}
}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...