本文欢迎转载,转载前请联系作者,经允许后方可转载。转载后请注明出处,谢谢! http://blog.csdn.net/colton_null 作者:喝酒不骑马 Colton_Null from CSDN


终于终于,用正确姿势搭建了一个Spring框架下整合Shiro的登录注册的DEMO。

因为还有其他事情要忙,所以有关这个shiro的demo前前后后鼓捣了一周多。为啥多花这么多时间?

按照平常,我在研究新框架的时候,都是看看框架介绍,找找样例几天就能搞出来了。但这次最气的是,目前能够讲清楚Spring + Shiro整合方法认真写的博客几乎没有。照着几个博客跟着配置,越配越生气的。给的配置文件不全;夹杂着好多没用的配置文件;没有注释;代码不全;还有的用法直接就是错的。耽误了太多的时间。

所以,就自己整理下正确实现Spirng + Shiro整合的姿势。分享给大家,希望别再走弯路。文末有demo源码,大家可以自行下载。

本文主要基于SpringBoot + Maven + Mybatis实现对Shiro的整合。对于Mabatis的配置在这里就不多做介绍了。而对于SpringMVC,也是SSM框架,只不过在对Shiro的配置方式上有所不同,原理都是一样的。我也会把SpringMVC下Shiro的配置方法贴出来。

在这里推荐两个个人觉得受益颇多的教程链接:
《跟我学shiro》
腾讯课程视频《Shiro安全框架》

一、准备好Maven + SpringBoot + MyBatis开发环境

1.准备一个user_t表,用户存放账户信息

-- ----------------------------
-- Table structure for user_t
-- ----------------------------
DROP TABLE IF EXISTS `user_t`;
CREATE TABLE `user_t` (`id` varchar(32) NOT NULL,`username` varchar(64) NOT NULL,`password` varchar(64) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS=1;

2.编写两个DAO接口
分别用于根据username查询信息以及插入一条数据。
UserMapper.java

@Repository
public interface UserMapper {/*** 根据用户名查询用户信息* @param username 用户名* @return 将数据封装到Map类型中*/public Map<String, Object> queryInfoByUsername(String username);/*** 插入一条数据* @param data Map中包含id,username,password*/public void insertData(Map<String, String> data);
}

userMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myz.shirodemo.dao.UserMapper"><select id="queryInfoByUsername" parameterType="java.lang.String" resultType="java.util.Map">SELECT id, username, password FROM user_t WHERE username = #{username,jdbcType=VARCHAR}</select><insert id="insertData"  parameterType="java.util.Map">INSERT INTO user_t ( id, username,password )VALUES ( #{id, jdbcType=VARCHAR}, #{username, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR});</insert>
</mapper>

二、引入Shiro依赖

pom中有关shiro的依赖如下

<!-- shiro -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-all</artifactId><version>1.2.2</version>
</dependency>
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.2.2</version>
</dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.2.2</version>
</dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.2.2</version>
</dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.2.2</version>
</dependency>
<!-- shiro END-->

三、配置ShiroConfig类
ShiroConfig.java

@Configuration
public class ShiroConfig {@Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 拦截器。匹配原则是最上面的最优先匹配Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();// 配置不会被拦截的链接filterChainDefinitionMap.put("/login", "anon");filterChainDefinitionMap.put("/doLogin", "anon");filterChainDefinitionMap.put("/doRegister", "anon");filterChainDefinitionMap.put("/register", "anon");// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了filterChainDefinitionMap.put("/doLogout", "logout");// 剩余请求需要身份认证filterChainDefinitionMap.put("/**", "authc");// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面shiroFilterFactoryBean.setLoginUrl("/login");// 未授权界面;
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}@Bean(name = "myShiroRealm")public ShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){ShiroRealm myShiroRealm = new ShiroRealm();myShiroRealm.setCredentialsMatcher(matcher);return myShiroRealm;}@Beanpublic SecurityManager securityManager(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher){DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();securityManager.setRealm(myShiroRealm(matcher));return securityManager;}/*** 密码匹配凭证管理器** @return*/@Bean(name = "hashedCredentialsMatcher")public HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();// 采用MD5方式加密hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 设置加密次数hashedCredentialsMatcher.setHashIterations(1024);return hashedCredentialsMatcher;}
}

1.shirFilter(SecurityManager securityManager)方法,是设置shiro的过滤规则。用于控制哪些请求需要身份认证后才能继续执行,哪些不需要认证等。
从http://blog.csdn.net/u010092167/article/details/52372811这个博主的博文中找到拦截器汇总表。

身份验证相关:

  • authc:基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure);
  • authcBasic:Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
  • logout:退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
  • user:用户拦截器,用户已经身份验证/记住我登录的都可;示例“/**=user”
  • anon:匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”

授权相关的:

  • roles:角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
  • perms:权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
  • port:端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
  • rest:rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll);
  • ssl:SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;

其他:

  • noSessionCreation:不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果subject.getSession(true)将抛出 DisabledSessionException异常;

2.myShiroRealm(HashedCredentialsMatcher matcher)用于配置自定义的Realm。在Shiro中,所有有关身份认证及授权管理数据源的获取与管理,都在Realm中进行。

3.hashedCredentialsMatcher()用于生成加密规则。这里采用MD5加密1024次的方式对密码进行加密处理。

4.securityManager(HashedCredentialsMatcher matcher)将加密规则属性设置到自定义的ShiroRealm中,并将这个Realm加载到SecurityManager中。

四、配置自定义Realm

ShiroRealm.java

public class ShiroRealm extends AuthenticatingRealm {@Autowiredprivate BaseService baseService;private SimpleAuthenticationInfo info = null;/*** 1.doGetAuthenticationInfo,获取认证消息,如果数据库中没有数,返回null,如果得到了正确的用户名和密码,* 返回指定类型的对象** 2.AuthenticationInfo 可以使用SimpleAuthenticationInfo实现类,封装正确的用户名和密码。** 3.token参数 就是我们需要认证的token* @param authenticationToken* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 将token装换成UsernamePasswordTokenUsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;// 获取用户名即可String username = upToken.getUsername();// 查询数据库,是否查询到用户名和密码的用户Map<String, Object> userInfo = baseService.queryInfoByUsername(username);if(userInfo != null) {// 如果查询到了,封装查询结果,返回给我们的调用Object principal =  userInfo.get("username");Object credentials = userInfo.get("password");// 获取盐值,即用户名ByteSource salt = ByteSource.Util.bytes(username);String realmName = this.getName();// 将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理info = new SimpleAuthenticationInfo(principal, credentials, salt,realmName);}else {// 如果没有查询到,抛出一个异常throw new AuthenticationException();}return info;}
}

1.这里我只做了身份认证。新建一个ShiroRealm类继承AuthenticatingRealm类,实现doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法。

2.这个方法主要就是用于获取数据库中的账户信息,以便用于和用户登录时从前台传过来的账户密码进行对比。

3.根据用户名到用户表中查询账户名密码,并设置好盐值。这里的盐值要和ShiroConfig中的盐值规则一样。将账户名,密码,盐值,realmName实例化到SimpleAuthenticationInfo中交给Shiro来管理。

4.如果账户不存在,则抛出AuthenticationException异常。

5.这样,每次用户进行login操作时,就会调用doGetAuthenticationInfo方法。Shiro就自动帮我们校验了账户密码是否匹配。

五、实现登录

这里先贴上Controller的全部代码
MyController.java

@Controller
public class MyController {@Autowiredprivate BaseService baseService;private final Logger logger = LoggerFactory.getLogger(MyController.class);@RequestMapping("/doLogin")public String doLogin(@RequestParam("username") String username,@RequestParam("password") String password) {// 创建Subject实例Subject currentUser = SecurityUtils.getSubject();// 将用户名及密码封装到UsernamePasswordTokenUsernamePasswordToken token = new UsernamePasswordToken(username, password);try {currentUser.login(token);// 判断当前用户是否登录if (currentUser.isAuthenticated() == true) {return "/index.html";}} catch (AuthenticationException e) {e.printStackTrace();System.out.println("登录失败");}return "/loginPage.html";}@RequestMapping("/doRegister")public String doRegister(@RequestParam("username") String username,@RequestParam("password") String password) {boolean result = baseService.registerData(username,password);if(result){return "/login";}return "/register";}@RequestMapping(value = "/login")public String login() {logger.info("login() 方法被调用");return "loginPage.html";}@RequestMapping(value = "/register")public String register() {logger.info("register() 方法被调用");return "registerPage.html";}@RequestMapping(value = "/hello")public String hello() {logger.info("hello() 方法被调用");return "helloPage.html";}
}

1.在doLogin方法中,实现登录认证过程。

2.首先获取当前Subject实例

3.将用户名和密码封装到UsernamePasswordToken中

4.用当前Subject实例执行login方法,传入参数为刚刚封装的token。执行login方法后,shiro框架最终就会调用刚刚自定义ShiroRealm中的doGetAuthenticationInfo方法。

5.用isAuthenticated()方法判断用户是否已经登录,如果是则跳转到登录后的页面(这里我跳转到的是index.html)。如果登录失败,则走报异常,最后还是跳转到登录界面。

6.这里我只catch了AuthenticationException异常。然而在AuthenticationException下有多个子异常,用于各种登录失败的场景,比如账户名不存在,密码不对,登录次数过多等等。大家针对不同的情况做不同的处理。但有一点建议,就是对于前台用户来说,不要暴露过多的错误信息,只是报一个登录失败即可,提高安全性。

六、实现注册

在Controller的doRegister方法中,调用service层的registerData方法,完成注册功能。

在service中对DAO进行封装,实现信息查询以及信息注册。
接口BaseService.java

public interface BaseService {/*** 根据用户名查询用户信息* @param username 用户名* @return 将数据封装到Map类型中*/public Map<String, Object> queryInfoByUsername(String username);/*** 注册功能* @param username 用户名* @param password 密码* @return*/public boolean registerData(String username, String password);
}

BaseService接口的实现类
BaseServiceImpl.java

@Service
public class BaseServiceImpl implements BaseService {@Autowiredprivate UserMapper userMapper;@Overridepublic Map<String, Object> queryInfoByUsername(String username) {return userMapper.queryInfoByUsername(username);}@Overridepublic boolean registerData(String username, String password) {// 生成uuidString id = UUIDUtil.getOneUUID();// 将用户名作为盐值ByteSource salt = ByteSource.Util.bytes(username);/** MD5加密:* 使用SimpleHash类对原始密码进行加密。* 第一个参数代表使用MD5方式加密* 第二个参数为原始密码* 第三个参数为盐值,即用户名* 第四个参数为加密次数* 最后用toHex()方法将加密后的密码转成String* */String newPs = new SimpleHash("MD5", password, salt, 1024).toHex();Map<String, String> dataMap = new HashMap<>();dataMap.put("id", id);dataMap.put("username", username);dataMap.put("password", newPs);// 看数据库中是否存在该账户Map<String, Object> userInfo = queryInfoByUsername(username);if(userInfo == null) {userMapper.insertData(dataMap);return true;}return false;}
}

1.注册时注意,由于之前配置了盐值规则及加密规则,所以这里要对用户输入的密码也做相同的处理之后再存入数据库中。

2.使用SimpleHash类完成密码的加密。最后用toHex()将加密后的密码转成String。

七、大功告成

到此,有关SpringBoot与Shiro的整合就基本完成了。有关静态页的代码这里就不贴了,在下面的github源码中有,大家可以自行下载。

最终效果就是,在http://localhost/register界面中,完成账户的注册。在http://localhost/login界面中进行登录操作。如果登录成功,则跳转到index页。如果登录失败或者没有登录,则访问index时会自动跳转到login界面。

八、有关SpringMVC环境下的配置

在SpringMVC下,有关Shiro的配置就要在Spring配置文件中来完成。
配置原理和SpringBoot中的一样。
spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager"/><property name="loginUrl" value="/login"/><property name="successUrl" value="/views/success.jsp"/><property name="unauthorizedUrl" value="/views/test.jsp"/><!-- 配置拦截策略 --><property name="filterChainDefinitions"><value>/login = anon/doLogin = anon/doLogout = logout/** = authc</value></property></bean><!-- 自定义Realm --><bean id="myRealm" class="com.myz.bean.ShiroRealm"><property name="credentialsMatcher"><bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"><property name="hashAlgorithmName" value="MD5"/><property name="hashIterations" value="1024"/></bean></property></bean><!-- 安全管理器 --><bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><!-- 缓存管理器 --><property name="cacheManager" ref="cacheManager" /><property name="realm" ref="myRealm" /></bean><!-- 配置ehcache --><bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"><property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/></bean><!-- 用来管理Spring容器中的Shiro常见的对象 --><bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/><bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"depends-on="lifecycleBeanPostProcessor"/><bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"><property name="securityManager" ref="securityManager"/></bean><!-- 网络方面 --><bean id="secureRemoteInvocationExecutor" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationExecutor"><property name="securityManager" ref="securityManager"/></bean></beans>

shiro缓存配置
ehcache-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false"  name="shirocache"><diskStore path="java.io.tmpdir"/><!-- 登录记录缓存 锁定10分钟 --><cache name="passwordRetryCache"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache><cache name="authorizationCache"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache><cache name="authenticationCache"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache><cache name="shiro-activeSessionCache"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="3600"timeToLiveSeconds="0"overflowToDisk="false"statistics="true"></cache><cache name="shiro_cache"maxElementsInMemory="2000"maxEntriesLocalHeap="2000"eternal="false"timeToIdleSeconds="0"timeToLiveSeconds="0"maxElementsOnDisk="0"overflowToDisk="true"memoryStoreEvictionPolicy="FIFO"statistics="true"></cache>
</ehcache>

九、源码

有关SpringBoot + Maven + Mybatis + Shiro的登录注册demo源码:ShiroDemo

SpringBoot/SpringMVC整合Shiro:实现登录与注册(MD5加盐加密)相关推荐

  1. 【SpringSSM项目】搏击俱乐部 使用邮箱进行登录注册 密码加盐加密

    在注册页面使用邮箱进行注册,注册后发送带有确认码的邮件到邮箱中,通过邮件确认注册 编写数据库 登录注册需要使用到用户表 table userinfo 包含 账号状态 用户名 邮箱 密码 头像 过期时间 ...

  2. 密码MD5加盐加密----注册、校验、修改模块

    思路:     单纯的MD5加密容易被碰撞破解,考虑将密码加上一个随机字符串(盐),再一同进行MD5加密,提高安全性. 此时,盐相当于另一半秘钥,需将盐一同存入数据库,用以验证. 实现过程:      ...

  3. Java使用MD5加盐对密码进行加密处理,附注册和登录加密解密处理

    前言 在开发的时候,有一些敏感信息是不能直接通过明白直接保存到数据库的.最经典的就是密码了.如果直接把密码以明文的形式入库,不仅会泄露用户的隐私,对系统也是极其的不厉,这样做是非常危险的. 那么我们就 ...

  4. 基于java注册登录MD5算法加盐加密颁发 Token身份令牌使用各种邮箱发送验证码详解雪花算法

    目的作用 == 在项目中,为了防止别人窥视我们的密码通常我们会采取一些加密方式.这里简单介绍一下MD5 加盐加密方法,MD5叫做信息-摘要算法,严格来说不是加密方式,而是信息摘要. 对于可以接触到数据 ...

  5. SpringBoot整合Shiro实现登录认证和授权CHCache

    文章目录 一. springboot实现普通登录 1 添加依赖 2 编写配置文件 3 新建实体类和mapper 4 编写业务层代码 5 编写控制器 6 编写启动类 7 编写登录页面和主页面 二. sp ...

  6. 对于AES和RSA算法的结合使用以及MD5加盐注册登录时的密码加密

    RSA和AES结合使用 接上篇的RSA和AES算法加密之后,AES对称算法对数据量大的加密比较快,而RSA公私钥加密的话会影响加密效率,但是AES的加密与解密的密钥是一致的,导致密钥不能外泄,密钥在网 ...

  7. SpringBoot Security 自定义登录验证逻辑+密码加盐

    密码加盐思路 JAVA 加盐加密方法_Teln_小凯的博客-CSDN博客 盐加密方法 @ApiOperation(value = "002-加密")@PreAuthorize(&q ...

  8. Shiro认证及加盐加密

    目录 今天的知识是与上次所分享的知识相关联的,在Shiro入门的基础进行编写,上次之前的数据是死数据(放在Shiro.ini)而这次是活数据,可以连接到数据库,运用域Relam知识.同时出于维护用户的 ...

  9. SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计

    加密算法参考: 浅谈常见的七种加密算法及实现 加密算法参考: 加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用 目的: 通过对API接口请求报文签名,后端进行验签处理, ...

  10. SpringBoot整合Shiro搭建登录注册认证授权权限项目模板

    主要内容: 1 SpringBoot整合Shiro安全框架; 2 Shiro主要学习内容总结;(执行流程.主要对象接口.注意事项等) 3 Redis实现对权限信息缓存; ! 温馨提示: 想要快速搭Sh ...

最新文章

  1. c++ template笔记(1)模板函数
  2. 2021某宝上的千月五级分润源码影视小说源码
  3. 中班音乐活动 机器人_幼儿园中班音乐活动教案《机器人》
  4. 检测php源码函数版本,PHP通用检测函数集合
  5. System.Data.SqlClient.SqlError: 尚未备份数据库的日志尾部
  6. 如何借助大数据进行社交媒体营销
  7. 微信公众号教程(8)用微信开发模式做欢迎词
  8. C#:Access数据库的连接、读取,字段的增加、删除、更新、查询操作
  9. 灰色预测的MATLAB代码
  10. 国赛培训——规划论——线性规划
  11. 设备 naa.6006016004102900751132ac8de3e211 性能降低。
  12. 人月神话(二)外科手术队伍、贵族专制、民主政治和系统设计
  13. hive 神盾特工局_《神盾局特工》沃德身份确认 九头蛇反派—蜂巢
  14. SSL/TLS - 什么是SSL?
  15. 骨架屏-css实现方式
  16. win11打不开 浏览器
  17. 武大博士后出站后应聘在社区做社工引热议,内卷还是人各有志?
  18. 逻辑代数的基本定理,布尔代数中的反律,摩根定律
  19. 密码-正则校验(数字、字母、特殊字符-任意组合)
  20. 极客日报:苹果承认从2019年开始扫描用户邮件寻找虐童资料;新浪回应“花钱买热搜”传闻;李沐斯坦福《机器学习》课程上线

热门文章

  1. 单出口双防火墙双核心冗余_王术芳/海关缴款书抵扣和出口退税操作变化要点解析...
  2. php的seeder是什么,Laravel框架使用Seeder实现自动填充数据功能
  3. python斗地主游戏源码_Python实现的斗地主引擎
  4. 计算机房应该配哪种灭火器,机房应选用哪种灭火器?
  5. matlab 三维立方体,使用matlab函数构建三维立方体的几种方法
  6. NW.js开发环境搭建
  7. python 元类理解
  8. 用sqlyog打开.sql文件
  9. 举个栗子!Tableau 技巧(109):用 LOD 计算产品销售周期
  10. 林大计算机科学考研分数线,2018年北京林业大学考研复试分数线已公布