shiro的单机版 和 集群版

摘要:
废话不多说了今天我们来讲讲shiro在我们设计库表时候:我们大多情况是这样的用户表:用户角色关系表:角色表:角色菜单关系表:菜单表:shiro单机集成spring下:/statics/**=anon/common/**=anon/error/**=anon˂!

在我们的开发当中 我们一般权限都是个 比较繁琐 但又必不可少的 一部分 【不管我们的 数据库设计 还是我们采用何种技术 我们的权限库表 大多都是大同小异 业务逻辑也是如此】 在我们不使用任何框架的时候 我们也是可以做到 但是细节过于麻烦 在很多时候 都是重复造轮子的过程 所以出现了 很多开源比较休息的额权限框架如:shiro Spring security。。。。。

废话不多说了 今天我们来讲讲shiro

在我们 设计库表时候 : 我们大多情况 是这样的

用户表 :

用户角色关系表:

角色表 :

角色菜单关系表:

菜单表:

shiro单机集成spring下:

<!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
<bean />

<!-- 用户授权信息Cache(本机内存实现)-->
<bean />

<!-- shiro 的自带 ehcahe 缓存管理器 -->
<bean class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
</bean>

<!--自定义Realm -->
<bean />

<!-- 凭证匹配器 -->
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm" />
<!-- redis 缓存 -->
<property name="cacheManager" ref="cacheManager" />
</bean>

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/index.jsp" />
<property name="successUrl" value="/loginSuccess.shtml" />
<property name="filterChainDefinitions">
<value>
<!-- 静态资源放行 -->
/statics/** = anon
/common/** = anon
/error/** = anon
<!-- 登录资源放行 -->
/toLogin/** = anon
/login/** = anon
<!-- shiro 自带登出 -->
/logout = logout
</value>
</property>
</bean>

最简单的shiro集成

我们只是需要自己写个 Realm 实现 AuthorizingRealm 实现它的两个方法 【认证 和 授权】 在授权里 我们要把角色集合 和 权限集合 放进去

在认证里 我们需要注意的是 我们可以自定义 密码加密方法覆写 setCredentialsMatcher方法

@PostConstruct //初始的时候 加载一次 在init 之前 运行
public void initCredentialsMatcher() {
//该句作用是重写shiro的密码验证,让shiro用自己的验证
setCredentialsMatcher(new CustomCredentialsMatcher());
}

/**
* 自定义shiro验证时使用的加密算法
*/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {

UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
Object tokenCredentials = encrypt(token);
Object accountCredentials = getCredentials(info);//数据库密码
//将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
return equals(tokenCredentials, accountCredentials);
}

private String encrypt(UsernamePasswordToken token) {
String password = PasswordUtil.encrypt(token.getUsername().toLowerCase(), String.valueOf(token.getPassword()),
PasswordUtil.getStaticSalt());
return password;

}
}

密码加密工具类

import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.Key;
import java.security.SecureRandom;
public class PasswordUtil {

/**
* JAVA6支持以下任意一种算法 PBEWITHMD5ANDDES PBEWITHMD5ANDTRIPLEDES
* PBEWITHSHAANDDESEDE PBEWITHSHA1ANDRC2_40 PBKDF2WITHHMACSHA1
* */

/**
* 定义使用的算法为:PBEWITHMD5andDES算法
*/
public static final String ALGORITHM = "PBEWithMD5AndDES";//加密算法
public static final String Salt = "63293188";//密钥

/**
* 定义迭代次数为1000次
*/
private static final int ITERATIONCOUNT = 1000;

/**
* 获取加密算法中使用的盐值,解密中使用的盐值必须与加密中使用的相同才能完成操作. 盐长度必须为8字节
*
* @return byte[] 盐值
* */
public static byte[] getSalt() throws Exception {
// 实例化安全随机数
SecureRandom random = new SecureRandom();
// 产出盐
return random.generateSeed(8);
}

public static byte[] getStaticSalt() {
// 产出盐
return Salt.getBytes();
}

/**
* 根据PBE密码生成一把密钥
*
* @param password
* 生成密钥时所使用的密码
* @return Key PBE算法密钥
* */
private static Key getPBEKey(String password) {
// 实例化使用的算法
SecretKeyFactory keyFactory;
SecretKey secretKey = null;
try {
keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
// 设置PBE密钥参数
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
// 生成密钥
secretKey = keyFactory.generateSecret(keySpec);
} catch (Exception e) {
e.printStackTrace();
}

return secretKey;
}

/**
* 加密明文字符串
*
* @param plaintext
* 生成密钥时所使用的密码
* @param password
* 待加密的明文字符串
* @param salt
* 盐值
* @return 加密后的密文字符串
* @throws Exception
*/
public static String encrypt(String plaintext, String password, byte[] salt) {

Key key = getPBEKey(password);
byte[] encipheredData = null;
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATIONCOUNT);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);

cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);

encipheredData = cipher.doFinal(plaintext.getBytes());
} catch (Exception e) {
}
return bytesToHexString(encipheredData);
}

/**
* 解密密文字符串
*
* @param ciphertext
* 待解密的密文字符串
* @param password
* 生成密钥时所使用的密码(如需解密,该参数需要与加密时使用的一致)
* @param salt
* 盐值(如需解密,该参数需要与加密时使用的一致)
* @return 解密后的明文字符串
* @throws Exception
*/
public static String decrypt(String ciphertext, String password, byte[] salt) {

Key key = getPBEKey(password);
byte[] passDec = null;
PBEParameterSpec parameterSpec = new PBEParameterSpec(getStaticSalt(), ITERATIONCOUNT);
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);

cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);

passDec = cipher.doFinal(hexStringToBytes(ciphertext));
}

catch (Exception e) {
e.printStackTrace();
}
return new String(passDec);
}

/**
* 将字节数组转换为十六进制字符串
*
* @param src
* 字节数组
* @return
*/
public static String bytesToHexString(byte[] src) {
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}

/**
* 将十六进制字符串转换为字节数组
*
* @param hexString
* 十六进制字符串
* @return
*/
public static byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}

private static byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}

@Test
public void test() {
int i=10;
for (int j = 0; j < i; j++) {
if((j)%3==0)
{
System.out.print("<br>");
}
else {
System.out.print(j);
}
}
System.out.print(-1%2==0);
String str = "admin";
String password = "123456";

LogUtil.info("明文:" + str);
LogUtil.info("密码:" + password);

try {
byte[] salt = PasswordUtil.getStaticSalt();
String ciphertext = PasswordUtil.encrypt(str, password, salt);
LogUtil.info("密文:" + ciphertext);
String plaintext = PasswordUtil.decrypt(ciphertext, password, salt);
LogUtil.info("明文:" + plaintext);
} catch (Exception e) {
e.printStackTrace();
}
}
}

而且我们在{ SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());} 红色标识的地方 有时候 我们是写对象 有的时候是写 用户名

这块我 强调一下 这里是根据 SecurityUtils.getSubject().getPrincipal() 后面要获取的 也就是 我们在上面 往里放什么 后面取什么

shiro 集群版的 session共享

我只要 把session 管理权力移交出来 交给 缓存管理 达到 一个资源的共享 这里 shiro 给我准备了一个默认的native session manager,DefaultWebSessionManager,所以我们要修改 spring 配置文件,注入 DefaultWebSessionManager。我们继续看DefaultWebSessionManager的源码,发现其父类 DefaultSessionManager 中有sessionDAO 属性,这个属性是真正实现了session储存的类,这个就是我们自己实现的 redis session的储存类。

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.system.utils.RedisManager;
import com.system.utils.SerializerUtil;

public class RedisSessionDao extends AbstractSessionDAO {

private Logger logger = LoggerFactory.getLogger(this.getClass());

private RedisManager redisManager;

/**
* The Redis key prefix for the sessions
*/
private static final String KEY_PREFIX = "shiro_redis_session:";

@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}

@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
redisManager.del(KEY_PREFIX + session.getId());
}

@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisManager.keys(KEY_PREFIX + "*");
if(keys != null && keys.size()>0){
for(byte[] key : keys){
Session s = (Session)SerializerUtil.deserialize(redisManager.get(SerializerUtil.deserialize(key)));
sessions.add(s);
}
}
return sessions;
}

@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}

@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("session id is null");
return null;
}

Session s = (Session)redisManager.get(KEY_PREFIX + sessionId);
return s;
}

private void saveSession(Session session) throws UnknownSessionException{
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
//设置过期时间
long expireTime = 1800000l;
session.setTimeout(expireTime);
redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime);
}

public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}

public RedisManager getRedisManager() {
return redisManager;
}
}

import java.io.Serializable;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisManager {

@Autowired
private RedisTemplate<Serializable, Serializable> redisTemplate;

/**
* 过期时间
*/
// private Long expire;

/**
* 添加缓存数据(给定key已存在,进行覆盖)
* @param key
* @param obj
* @throws DataAccessException
*/
public <T> void set(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(bkey, bvalue);
return null;
}
});
}

/**
* 添加缓存数据(给定key已存在,不进行覆盖,直接返回false)
* @param key
* @param obj
* @return 操作成功返回true,否则返回false
* @throws DataAccessException
*/
public <T> boolean setNX(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(bkey, bvalue);
}
});

return result;
}

/**
* 添加缓存数据,设定缓存失效时间
* @param key
* @param obj
* @param expireSeconds 过期时间,单位 秒
* @throws DataAccessException
*/
public <T> void setEx(String key, T obj, final long expireSeconds) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
connection.setEx(bkey, expireSeconds, bvalue);
return true;
}
});
}

/**
* 获取key对应value
* @param key
* @return
* @throws DataAccessException
*/
public <T> T get(final String key) throws DataAccessException{
byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() {
@Override
public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
return connection.get(key.getBytes());
}
});
if (result == null) {
return null;
}
return SerializerUtil.deserialize(result);
}

/**
* 删除指定key数据
* @param key
* @return 返回操作影响记录数
*/
public Long del(final String key){
if (StringUtils.isEmpty(key)) {
return 0l;
}
Long delNum = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.del(keys);
}
});
return delNum;
}

public Set<byte[]> keys(final String key){
if (StringUtils.isEmpty(key)) {
return null;
}
Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.keys(keys);
}
});

return bytesSet;
}

}

序列化工具类

import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

/**
* 序列化工具类
* @author HandyZcy
*
*/
public class SerializerUtil {

private static final JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();

/**
* 序列化对象
* @param obj
* @return
*/
public static <T> byte[] serialize(T obj){
try {
return jdkSerializationRedisSerializer.serialize(obj);
} catch (Exception e) {
throw new RuntimeException("序列化失败!", e);
}
}

/**
* 反序列化对象
* @param bytes 字节数组
* @param cls cls
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] bytes){
try {
return (T) jdkSerializationRedisSerializer.deserialize(bytes);
} catch (Exception e) {
throw new RuntimeException("反序列化失败!", e);
}
}
}

整体配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
"
>

<description>Shiro安全配置</description>

<!-- 分布式 配置参考:http://blog.csdn.net/lishehe/article/details/45223823 -->

<!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
<bean />

<!--
用户授权信息Cache(本机内存实现)
<bean />
-->

<!-- shiro 的自带 ehcahe 缓存管理器
<bean class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
</bean>
-->

<!-- 自定义cacheManager -->
<bean class="com.system.shiro.RedisCache">
<constructor-arg ref="redisManager"></constructor-arg>
</bean>

<!-- 自定义redisManager-redis -->
<bean class="com.system.shiro.RedisCacheManager">
<property name="redisManager" ref="redisManager" />
</bean>

<!--自定义Realm -->
<bean />

<!-- 凭证匹配器 -->
<bean class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm" />
<property name="sessionMode" value="http" />
<property name="sessionManager" ref="defaultWebSessionManager" />

<!-- redis 缓存 -->
<property name="cacheManager" ref="redisCacheManager" />
</bean>

<bean class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">

<!-- session存储的实现 -->
<property name="sessionDAO" ref="shiroRedisSessionDAO" />

<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="shareSession" />

<!-- 设置全局会话超时时间,默认30分钟(1800000) -->
<property name="globalSessionTimeout" value="1800000" />

<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
<property name="deleteInvalidSessions" value="true" />

<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000" />

<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>

<!--
通过@Component 注解交由 Spring IOC 管理
<bean class="com.system.utils.RedisManager"></bean>
-->

<!-- session会话存储的实现类 -->
<bean class="com.system.shiro.RedisSessionDao">
<property name="redisManager" ref="redisManager"/>
</bean>

<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID" />
<!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
<property name="path" value="/" />
<property name="httpOnly" value="true"/>
</bean>

<!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager" />
<!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/index.jsp" />
<!-- 登录成功后要跳转的连接 -->
<property name="successUrl" value="/loginSuccess.shtml" />
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/error/forbidden.jsp" />
<!-- 自定义权限配置:url 过滤在这里做 -->
<property name="filterChainDefinitions">
<!-- 参考:http://blog.csdn.net/jadyer/article/details/12172839 -->
<!--
Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)故filterChainDefinitions的配置顺序为自上而下,以最上面的为准
-->
<!-- Pattern里用到的是两颗星,这样才能实现任意层次的全匹配 -->
<value>
<!-- 静态资源放行 -->
/statics/** = anon
/common/** = anon
/error/** = anon

<!-- 登录资源放行 -->
/toLogin/** = anon
/login/** = anon

<!-- shiro 自带登出 -->
/logout = logout

<!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:role:list 权限才可以正常发起'/role'请求-->
/role/** = authc,roles[superman],perms[superman:role:list]
/right/** = authc,roles[superman],perms[superman:right:list]
/manager/preEditPwd = authc
/manager/editUserBase = authc
<!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:manager:list 才可以正常发起'/manager'请求 -->
/manager/** = authc,roles[superman],perms[superman:manager:list]
/** = authc
</value>
</property>
</bean>

<!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->
<!-- 配置以下两个bean即可实现此功能 -->
<!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->
<bean depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>

免责声明:文章转载自《shiro的单机版 和 集群版》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇c#反射机制学习和利用反射获取类型信息Ubuntu12.04下tomcat的安装与配置下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

将 Shiro 作为一个许可为基础的应用程序 五:password加密/解密Spring应用

考虑系统password的安全,眼下大多数系统都不会把password以明文的形式存放到数据库中。 一把会採取下面几种方式对password进行处理 password的存储 “编码”存储 Shiro 提供了 base64和 16 进制字符串编码/解码的 API支持,方便一些编码解码操作。 Shiro内部的一些数据的存储/表示都使用了 base64和 16...

shiro 配置注解异常 java.lang.ClassNotFoundException: org.aspectj.util.PartialOrder$PartialComparable

解决方案: pom 文件添加: <!-- 解决shiro注解(shiro 使用 aop) --> <dependency>   <groupId>aspectj</groupId>   <artifactId>aspectjweaver</artifactId>   <vers...

使用Java反射机制将Bean对象转换成Map(驼峰命名方式 — 下划线命名方式)

packagecom.yunping.asap.core.util; importjava.beans.PropertyDescriptor; importjava.lang.reflect.Field; importjava.lang.reflect.Method; importjava.util.ArrayList; importjava.util....

Spring batch学习 (1)

          Spring Batch 批处理框架 埃森哲和Spring Source研发                          主要解决批处理数据的问题,包含并行处理,事务处理机制等。具有健壮性 可扩展,和自带的监控功能,并且支持断点和重发。让程序员更加注重于业务实现。           Spring Batch 结构如下      ...

理解 Spring(二):AOP 的概念与实现原理

什么是 AOP AOP 的基本术语 Spring AOP 的简单应用 Spring AOP 与动态代理 Spring AOP 的实现原理(源码分析) 扩展:为什么 JDK 动态代理要求目标类必须实现接口 什么是 AOP AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它是对 OOP(Object O...

SSD固态盘应用于Ceph集群的四种典型使用场景

在虚拟化及云计算技术大规模应用于企业数据中心的科技潮流中,存储性能无疑是企业核心应用是否虚拟化、云化的关键指标之一。传统的做法是升级存储设备,但这没解决根本问题,性能和容量不能兼顾,并且解决不好设备利旧问题。因此,企业迫切需要一种大规模分布式存储管理软件,能充分利用已有硬件资源,在可控成本范围内提供最佳的存储性能,并能根据业务需求变化,从容量和性能两方面同...