Sa-Token
创始人
2024-02-14 23:05:45

介绍

类似于Security的认证授权的解决方案,但是使用起来非常方便,1.支持登录认证,授权权限验证,踢人;2.支持自定义Token,并且能够结合Redis完成前后端分离认证方案;3.支持单点登录(前端同域,后端同Redis+前端不同域,后端不同Redis+两者都不同)

请添加图片描述

2.快速上手

1.demo所用依赖

cn.dev33sa-token-spring-boot-starter1.33.0org.springframework.bootspring-boot-starter-data-redisorg.apache.commonscommons-pool2io.springfoxspringfox-swagger22.9.2io.swaggerswagger-annotationsio.swaggerswagger-modelsio.springfoxspringfox-swagger-ui2.9.2io.swaggerswagger-annotations1.5.21io.swaggerswagger-models1.5.21org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testtestorg.junit.vintagejunit-vintage-engineorg.springframework.bootspring-boot-dependencies${spring-boot.version}pomimport

2.登录相关测试

1.登录接口

这里是直接进行username和password比对,然后将id传入登录参数得到token返回——>我们一样可以结合数据库,通过用户名密码得到用户信息,存在就将用户唯一标识传入StpUtil.login()

// 1.测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456@ApiOperation(value = "登录测试")@RequestMapping("doLogin")public SaResult doLogin(String username, String password) {// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if ("zhang".equals(username) && "123456".equals(password)) {//1.这里我们可以数据库判断业务后,StpUtil.login()用户的idStpUtil.login(10001,false);SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return SaResult.ok().data(tokenInfo).setMsg("登录成功");}return SaResult.error("登录失败");}

在这里插入图片描述

2.查询登录状态

// 2.查询登录状态,浏览器访问: http://localhost:8081/user/isLogin@RequestMapping("isLogin")@ApiOperation(value = "登录状态查询")public String isLogin() {return "当前会话是否登录:" + StpUtil.isLogin();}

3.退出登录
单体架构一般就是删除session中相关信息,结合token的话就是将redis中的token删除,如果是token放在cookie中保存,就删除cookie即可

这里注销的是当前用户

  @RequestMapping("logout")@ApiOperation(value = "退出登录")public SaResult logout() {StpUtil.logout();return SaResult.ok();}

源码

1.我们来看下Sa-Token中注销用户的核心代码:
它这里是引入了一个Storage存储器的概念,这个存储器每个用户都会有一个类似Session——>这里是由请求进行封装的

	/** * 会话注销 */public void logout() {// 如果连 Token 都没有,那么无需执行任何操作String tokenValue = getTokenValue();if(SaFoxUtil.isEmpty(tokenValue)) {return;}// 如果打开了 Cookie 模式,则把 Cookie 清除掉if(getConfig().getIsReadCookie()){SaCookieConfig cookie = getConfig().getCookie();SaHolder.getResponse().deleteCookie(getTokenName(), cookie.getPath(), cookie.getDomain());}// 从当前 [Storage存储器] 里删除 TokenSaHolder.getStorage().delete(splicingKeyJustCreatedSave());// 清除当前上下文的 [临时有效期check标记]SaHolder.getStorage().delete(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY);// 清除这个 Token 的相关信息logoutByTokenValue(tokenValue);}

2.会话注销token

	/*** 会话注销,根据指定 Token * * @param tokenValue 指定token*/public void logoutByTokenValue(String tokenValue) {// 1. 清理 token-last-activityclearLastActivity(tokenValue); 	// 2. 注销 Token-Session deleteTokenSession(tokenValue);// 3. 清理 token -> id 索引String loginId = getLoginIdNotHandle(tokenValue);if(loginId != null) {deleteTokenToIdMapping(tokenValue);}// if. 无效 loginId 立即返回if(isValidLoginId(loginId) == false) {return;}// $$ 发布事件:某某Token注销下线了 SaTokenEventCenter.doLogout(loginType, loginId, tokenValue);// 4. 清理User-Session上的token签名 & 尝试注销User-Session SaSession session = getSessionByLoginId(loginId, false);if(session != null) {session.removeTokenSign(tokenValue); session.logoutByTokenSignCountToZero();}}

3.可以看出来我们的storage就是由request进行封装的,并且可以进行封装kv

    public SaStorage getStorage() {return new SaStorageForServlet(SpringMVCUtil.getRequest());}

3.查询用户权限

首先我们赋予用户所有权限

这里我们可以给每个用户调用getPermissionList(Object id,String Type)方法,给指定用户传入权限,重写StpInterface接口即可得到权限

package com.wyhtoken.quicktoken.service;import java.util.ArrayList;
import java.util.List;import org.springframework.stereotype.Component;import cn.dev33.satoken.stp.StpInterface;/*** 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码 * * @author kong* @since 2022-10-13*/
@Component	// 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展 
public class StpInterfaceImpl implements StpInterface {/*** 1.添加权限集合* @param loginId:账户id* @param loginType:账户类型* @return*/@Overridepublic List getPermissionList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限List list = new ArrayList();	list.add("101");list.add("user.add");list.add("user.update");list.add("user.get");// list.add("user.delete");list.add("art.*");return list;}/*** 2.返回指定id的用户权限* @param loginId* @param loginType* @return*/@Overridepublic List getRoleList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色List list = new ArrayList();	list.add("admin");list.add("super-admin");return list;}}

1.用户所有权限

 @RequestMapping("checkPermission")@ApiOperation(value = "用户所有权限")public SaResult getPermissionList() {List permissionList = StpUtil.getPermissionList();return SaResult.ok().setData(permissionList).setMsg("所有权限返回成功");}

2.判断用户是否有某权限
这里我们可以跟上述结合起来一起用,查询用户所有权限后,判断用户是否有某权限返回状态,进行兜底服务

//1.返回boolean
StpUtil.hasPermission("user.addd");
//2.抛出异常
StpUtil.checkPermission("user.add");

3.检验用户是否有指定的所有权限

    /*** 4.该账号判断是否有指定的多个权限(必须全部通过)** @return*/@ApiOperation(value = "判断用户是否有所有权限")@RequestMapping("checkAll")public SaResult checkPermissionAnd() {StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");return SaResult.ok().setData("所有指定权限都有");}@ApiOperation(value = "判断用户是否有所有权限其中之一")@RequestMapping("checkOr")public SaResult checkPermissionOr(){StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");return SaResult.ok().setData("所有指定权限有其中之一");}

检验权限的源码

你会发现调用了getPermissionList(用户的标识)方法,而这个方法是StpInterface的实现类重写的方法,可以根据用户标识得到所有权限

 	/** * 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] * @param permissionArray 权限码数组*/public void checkPermissionOr(String... permissionArray){Object loginId = getLoginId();List permissionList = getPermissionList(loginId);for (String permission : permissionArray) {if(hasElement(permissionList, permission)) {// 有的话提前退出return;		}}if(permissionArray.length > 0) {throw new NotPermissionException(permissionArray[0], this.loginType).setCode(SaErrorCode.CODE_11051);}

串起来所有东西就能理解了

	/*** 获取:指定账号的权限码集合 * @param loginId 指定账号id* @return / */public List getPermissionList(Object loginId) {return SaManager.getStpInterface().getPermissionList(loginId, loginType);}

5.踢人下线

给指定用户踢出下线

@ApiOperation(value = "踢人下线")@RequestMapping("Kick")public SaResult KickOut(Integer id){StpUtil.kickout(10001);return SaResult.ok().setMsg("踢人下线成功");}

6.忽略权限能够访问接口的注解(游客访问)

 // 此接口加上了 @SaIgnore 可以游客访问 @SaIgnore@RequestMapping("getList")public SaResult getList() {// ... return SaResult.ok(); }

3.注解测试权限

package com.wyhtoken.quicktoken.controller;import cn.dev33.satoken.annotation.*;
import cn.dev33.satoken.util.SaResult;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Api(tags = "Sa-Token接口测试")
@RestController
@RequestMapping("/api/")
public class ApiController {// 登录校验:只有登录之后才能进入该方法@SaCheckLogin@RequestMapping("info")public String info() {return "查询用户信息";}// 角色校验:必须具有指定角色才能进入该方法@SaCheckRole("super-admin")@RequestMapping("add1")public String add1() {return "用户增加";}// 权限校验:必须具有指定权限才能进入该方法@SaCheckPermission("user-add")@RequestMapping("add2")public String add2() {return "用户增加";}// 二级认证校验:必须二级认证之后才能进入该方法@SaCheckSafe()@RequestMapping("add3")public String add3() {return "用户增加";}// Http Basic 校验:只有通过 Basic 认证后才能进入该方法@SaCheckBasic(account = "sa:123456")@RequestMapping("add4")public String add4() {return "用户增加";}// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法@SaCheckDisable("comment")@RequestMapping("send")public String send() {return "查询用户信息";}// 注解式鉴权:只要具有其中一个权限即可通过校验@RequestMapping("atJurOr")@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)public SaResult atJurOr() {return SaResult.data("用户信息");}// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验@RequestMapping("userAdd")@SaCheckPermission(value = "user.add", orRole = "admin")public SaResult userAdd() {return SaResult.data("用户信息");}
}

4.全局拦截器


@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns("/user/doLogin","/swagger-ui.html");}/*** 注册全局过滤器*/@Beanpublic SaServletFilter getSaServletFilter(){return new SaServletFilter().setAuth(obj->{System.out.println("--------进入Sa-Token全局认证--------");//1.登录认证 拦截所有路由,并且排除/user/doLogin用于开放登录SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());}).setError(e->{System.out.println("---------- 进入Sa-Token异常处理 -----------");return SaResult.error(e.getMessage());})// 前置函数:在每次认证函数之前执行.setBeforeAuth(r -> {// ---------- 设置一些安全响应头 ----------SaHolder.getResponse()// 服务器名称.setServer("sa-server")// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以.setHeader("X-Frame-Options", "SAMEORIGIN")// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面.setHeader("X-XSS-Protection", "1; mode=block")// 禁用浏览器内容嗅探.setHeader("X-Content-Type-Options", "nosniff");});}/*** 重写 Sa-Token 框架内部算法策略*/@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略SaStrategy.me.createToken = (loginId, loginType) -> {return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串};}}

5.重写算法策略

@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略SaStrategy.me.createToken = (loginId, loginType) -> {return SaFoxUtil.getRandomString(60);    // 随机60位长度字符串};}

5.前后端分离

常规 Web 端鉴权方法,一般由 Cookie模式 完成,而 Cookie 有两个特性:

可由后端控制写入。
每次请求自动提交。
这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)
而在app、小程序等前后台分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊?

我们也可以加一个验证是否登录

// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {// 第1步,先登录上 StpUtil.login(10001);// 第2步,获取 Token  相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第3步,返回给前端 return SaResult.data(tokenInfo);
}

6.记住我

Sa-Token默认StpUil.login(id,type)实现了记住我功能

实现原理:

Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:

临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。
持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。
利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式:

勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个持久Cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时Cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。

我们可以设置token的有效时间

// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));// ----------------------- 示例2:所有参数
// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginModel().setDevice("PC")                // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型.setIsLastingCookie(true)        // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在).setTimeout(60 * 60 * 24 * 7)    // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值).setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token .setIsWriteHeader(false)         // 是否在登录后将 Token 写入到响应头);

7.同端互斥功能实现

1.介绍: 请添加图片描述
如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。
同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。

首先在配置文件中,将 isConcurrent 配置为false,然后调用登录等相关接口时声明设备类型即可:
StpUtil.login(用户id标识,"PC");

2.查询当前登录的设备类型

// 返回当前token的登录设备类型
StpUtil.getLoginDevice();    

3.获取指定登录设备类型的tokenValue

// 获取指定loginId指定设备类型端的tokenValue 
StpUtil.getTokenValueByLoginId(10001, "APP");    

8.账号封禁

// 封禁指定账号 
StpUtil.disable(10001, 86400); 

参数含义:

参数1:要封禁的账号id。
参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)

1.我们一般采用先踢掉,再封禁的方法

// 先踢下线
StpUtil.kickout(10001); 
// 再封禁账号
StpUtil.disable(10001, 86400); 

2.然后我们再下次登录可以验证一下

// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001); // 通过校验后,再进行登录:
StpUtil.login(10001); 

9.禁用用户的某个权限

// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);

1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。
2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。
3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。

参数释义:

参数1:要封禁的账号id。
参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。
参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。

/** 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识*/// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment` 
StpUtil.checkDisable(10001, "comment");// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能
StpUtil.checkDisable(10001, "place-order");// 现在我们再将其下单能力封禁一下,期限为 7天 
StpUtil.disable(10001, "place-order", 86400 * 7);// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常)
StpUtil.checkDisable(10001, "place-order");// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码)
StpUtil.checkDisable(10001, "open-shop");

10.密码加密

Sa-Token支持非对称和对称加密两种方式

1.对称加密:AES加密

// 定义秘钥和明文
String key = "123456";
String text = "Sa-Token 一个轻量级java权限认证框架";// 加密 
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);// 解密 
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);

2.非对称加密:RSA加密

// 定义私钥和公钥 
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2); 

11.全局侦听器

实现SaTokenListener接口即可
思路:我们可以每次下线将日志写入文件中,就可以用这个全局监听器

/*** 自定义侦听器的实现 */
@Component
public class MySaTokenListener implements SaTokenListener {/** 每次登录时触发 */@Overridepublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {System.out.println("---------- 自定义侦听器实现 doLogin");}/** 每次注销时触发 */@Overridepublic void doLogout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doLogout");}/** 每次被踢下线时触发 */@Overridepublic void doKickout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doKickout");}/** 每次被顶下线时触发 */@Overridepublic void doReplaced(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doReplaced");}/** 每次被封禁时触发 */@Overridepublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {System.out.println("---------- 自定义侦听器实现 doDisable");}/** 每次被解封时触发 */@Overridepublic void doUntieDisable(String loginType, Object loginId, String service) {System.out.println("---------- 自定义侦听器实现 doUntieDisable");}/** 每次二级认证时触发 */@Overridepublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {System.out.println("---------- 自定义侦听器实现 doOpenSafe");}/** 每次退出二级认证时触发 */@Overridepublic void doCloseSafe(String loginType, String tokenValue, String service) {System.out.println("---------- 自定义侦听器实现 doCloseSafe");}/** 每次创建Session时触发 */@Overridepublic void doCreateSession(String id) {System.out.println("---------- 自定义侦听器实现 doCreateSession");}/** 每次注销Session时触发 */@Overridepublic void doLogoutSession(String id) {System.out.println("---------- 自定义侦听器实现 doLogoutSession");}/** 每次Token续期时触发 */@Overridepublic void doRenewTimeout(String tokenValue, Object loginId, long timeout) {System.out.println("---------- 自定义侦听器实现 doRenewTimeout");}
}

相关内容

热门资讯

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