前言:我在上一篇文章(PS:)中介绍了shiro框架的一些基本概念以及自定义Realm可以实现的自定义授权和认证的目的。在这一篇文章中我将介绍如何将Shiro与Spring、SpringMVC等框架非***式的整合到一起,从而接管Web项目的权限控制

注:为了使文章篇幅不至于过长,我在下面将只会粘贴关键代码,如需完整源代码请自行下载:

一 Shiro框架与Spring框架整合的配置

(1)测试项目使用MySQL数据库,其测试SQL语句是:

-- ------------------------------ Table structure for usr_func-- ----------------------------DROP TABLE IF EXISTS `usr_func`;CREATE TABLE `usr_func` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `name` varchar(100) DEFAULT NULL,  `description` varchar(100) DEFAULT NULL,  `code` varchar(100) DEFAULT NULL,  `url` varchar(200) DEFAULT NULL,  `status` varchar(20) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;-- ------------------------------ Records of usr_func-- ----------------------------INSERT INTO `usr_func` VALUES ('1', '用户管理-查询', null, 'YHGL:CX', null, 'enable');INSERT INTO `usr_func` VALUES ('2', '用户管理-新增', null, 'YHGL:XZ', null, 'enable');INSERT INTO `usr_func` VALUES ('3', '用户管理-编辑', null, 'YHGL:BJ', null, 'enable');INSERT INTO `usr_func` VALUES ('4', '用户管理-停用', null, 'YHGL:TY', null, 'enable');INSERT INTO `usr_func` VALUES ('5', '用户管理-启用', null, 'YHGL:QY', null, 'enable');INSERT INTO `usr_func` VALUES ('6', '用户管理-删除', null, 'YHGL:SC', null, 'enable');INSERT INTO `usr_func` VALUES ('7', '文章管理-查询', null, 'WZGL:CX', null, 'enable');INSERT INTO `usr_func` VALUES ('8', '文章管理-新增', null, 'WZGL:XZ', null, 'enable');INSERT INTO `usr_func` VALUES ('9', '文章管理-编辑', null, 'WZGL:BJ', null, 'enable');INSERT INTO `usr_func` VALUES ('10', '文章管理-删除', null, 'WZGL:SC', null, 'enable');-- ------------------------------ Table structure for usr_role-- ----------------------------DROP TABLE IF EXISTS `usr_role`;CREATE TABLE `usr_role` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `roleName` varchar(100) DEFAULT NULL,  `description` varchar(100) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;-- ------------------------------ Records of usr_role-- ----------------------------INSERT INTO `usr_role` VALUES ('1', 'manager', '管理员');INSERT INTO `usr_role` VALUES ('2', 'editor', '编辑');INSERT INTO `usr_role` VALUES ('3', 'author', '作者');INSERT INTO `usr_role` VALUES ('4', 'subscriber', '订阅者');INSERT INTO `usr_role` VALUES ('5', 'contributor', '投稿者');-- ------------------------------ Table structure for usr_role_func-- ----------------------------DROP TABLE IF EXISTS `usr_role_func`;CREATE TABLE `usr_role_func` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `roleId` int(11) DEFAULT NULL,  `funcId` int(11) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `roleId` (`roleId`),  CONSTRAINT `roleId` FOREIGN KEY (`roleId`) REFERENCES `usr_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;-- ------------------------------ Records of usr_role_func-- ----------------------------INSERT INTO `usr_role_func` VALUES ('1', '1', '1');INSERT INTO `usr_role_func` VALUES ('2', '1', '2');INSERT INTO `usr_role_func` VALUES ('3', '1', '3');INSERT INTO `usr_role_func` VALUES ('4', '1', '4');INSERT INTO `usr_role_func` VALUES ('5', '1', '5');INSERT INTO `usr_role_func` VALUES ('6', '1', '6');INSERT INTO `usr_role_func` VALUES ('7', '1', '7');INSERT INTO `usr_role_func` VALUES ('8', '1', '8');INSERT INTO `usr_role_func` VALUES ('9', '1', '9');INSERT INTO `usr_role_func` VALUES ('10', '1', '10');INSERT INTO `usr_role_func` VALUES ('11', '2', '7');INSERT INTO `usr_role_func` VALUES ('12', '2', '8');INSERT INTO `usr_role_func` VALUES ('13', '2', '9');INSERT INTO `usr_role_func` VALUES ('14', '2', '10');INSERT INTO `usr_role_func` VALUES ('15', '3', '7');INSERT INTO `usr_role_func` VALUES ('16', '3', '8');INSERT INTO `usr_role_func` VALUES ('17', '3', '9');INSERT INTO `usr_role_func` VALUES ('18', '4', '7');INSERT INTO `usr_role_func` VALUES ('19', '5', '8');-- ------------------------------ Table structure for usr_user-- ----------------------------DROP TABLE IF EXISTS `usr_user`;CREATE TABLE `usr_user` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `username` varchar(100) DEFAULT NULL,  `password` varchar(256) DEFAULT NULL,  `mobile` varchar(30) DEFAULT NULL,  `email` varchar(100) DEFAULT NULL,  `createTime` datetime DEFAULT NULL,  `updateTime` datetime DEFAULT NULL,  `channelId` int(11) DEFAULT NULL,  `status` varchar(20) DEFAULT '1',  PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;-- ------------------------------ Records of usr_user-- ----------------------------INSERT INTO `usr_user` VALUES ('1', 'admin', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918', '110', 'admin@zifangsky.cn', '2016-10-04 10:33:23', '2016-10-06 10:38:40', '1', 'enable');INSERT INTO `usr_user` VALUES ('2', 'test', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', '3456789', 'test@110.com', '2016-10-18 18:25:12', '2016-10-19 18:25:17', '2', 'enable');INSERT INTO `usr_user` VALUES ('5', 'zifangsky', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', '911', 'admin@zifangsky.cn', '2016-10-20 11:46:45', '2016-10-20 11:46:57', '1', 'enable');INSERT INTO `usr_user` VALUES ('6', 'sub', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', null, null, null, null, null, 'disable');INSERT INTO `usr_user` VALUES ('7', 'contributor', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', null, null, null, null, null, 'disable');-- ------------------------------ Table structure for usr_user_role-- ----------------------------DROP TABLE IF EXISTS `usr_user_role`;CREATE TABLE `usr_user_role` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `userId` int(11) DEFAULT NULL,  `roleId` int(11) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `userId` (`userId`),  CONSTRAINT `userId` FOREIGN KEY (`userId`) REFERENCES `usr_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- ------------------------------ Records of usr_user_role-- ----------------------------INSERT INTO `usr_user_role` VALUES ('1', '1', '1');INSERT INTO `usr_user_role` VALUES ('2', '5', '3');INSERT INTO `usr_user_role` VALUES ('3', '5', '5');INSERT INTO `usr_user_role` VALUES ('4', '2', '4');INSERT INTO `usr_user_role` VALUES ('5', '6', '2');

上面的5张表合起来正好是一个简单的RBAC模型,当然上篇文章的末尾地方也提到过方面的内容。需要注意的是,密码没有使用明文密码,而是经过了简单的SHA256加密,其密码原文分别是admin和123456

(2)在web.xml文件中添加shiro的拦截范围:

shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
*.html
REQUEST
FORWARD
shiroFilter
*.json
REQUEST
FORWARD

因为我设置的spring mvc的拦截范围是.html 和 .json格式的url,同时项目中凡是涉及到业务逻辑的地方都经过了SpringMVC而不是直接使用jsp来访问,因此我这里设置shiro管理权限的部分也就是这两个url格式

(3)缓存采用Ehcache:

在Spring的配置文件context.xml中是这样配置缓存的:

(4)shiro相关配置文件context_shiro.xml:

Shiro 配置
        
        
        
        
        
           
            
                /error/*= anon                /user/index.html = login                /user/user/logout.html = logout                /user/user/admin.html = roles[manager]                /article/index* = perms[WZGL:CX]                /article/add* = perms[WZGL:XZ]                /article/edit* = perms[WZGL:BJ]                /**/*.htm* = auth /**/*.json* = auth            
            
/user/user/login.html
/user/user/check.json
/user/user/verify.html
/user/user/checkVerifyCode.json

关于这里的shiro的一些配置,我觉得以下几点需要简单说明下:

i)缓存管理:

可以看出,我这里是引用了前面定义好的Ehcache工厂——cacheManagerFactory

ii)自定义Realm:

在配置securityManager这里,为了能够在后面更灵活地控制权限认证和授权过程,因此这里定义了一个自定义的Realm。其具体代码是:

package cn.zifangsky.security;import java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set;import javax.servlet.http.HttpServletRequest;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.SimpleAuthenticationInfo;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import cn.zifangsky.model.UsrFunc;import cn.zifangsky.model.bo.UsrRoleBO;import cn.zifangsky.model.bo.UsrUserBO;/** * 自定义Realm,用于自定义登录认证以及自定义授权管理 * @author zifangsky * */public class CustomRealm extends AuthorizingRealm {	/**	 * 返回一个唯一的Realm名称	 * */	@Override	public String getName() {		return "CustomRealm";	}		/**	 * 授权	 */	@Override	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {		SimpleAuthorizationInfo info = null;		UsrUserBO userBO = (UsrUserBO) principalCollection.fromRealm(getName()).iterator().next();		if(userBO != null){			info = new SimpleAuthorizationInfo();			List
 roleBOs = userBO.getUsrRoleBOs();  //所有角色 if(roleBOs != null && roleBOs.size() > 0){ List
 roleNames = new ArrayList<>();  //所有角色名 Set
 funcCodes = new HashSet<>();  //该用户拥有的所有角色的所有权限的code集合 for(UsrRoleBO roleBO : roleBOs){ roleNames.add(roleBO.getRolename()); List
 funcs = roleBO.getFuncs();  //一个角色的所有权限的code集合 if(funcs != null && funcs.size() > 0){ for(UsrFunc f : funcs){ funcCodes.add(f.getCode()); } } } //添加所有的角色和权限 info.addRoles(roleNames); info.addStringPermissions(funcCodes); } } return info; } /**  * 认证  * 在这里由于登录之后在session中已经存在用户信息了,同时这里的token里的认证信息也是在  * cn.zifangsky.security.LoginFilter的createToken方法中从session获取的用户信息然后再添加的。  * 因此这里就省略了对token里的信息的校验步骤,直接从session中获取userBO并返回认证之后的凭证  */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = attributes.getRequest(); UsrUserBO userBO = (UsrUserBO) request.getSession().getAttribute("userBO"); SimpleAuthenticationInfo info = null; if(userBO != null){ info = new SimpleAuthenticationInfo(userBO, userBO.getPassword(), getName()); } return info; }}

这里的代码逻辑并不复杂,如果动手试验过我在上篇文章中介绍的基础知识的话,那么这里理解起来将会更加容易了。在授权部分能够获取一个用户的所有角色和权限信息,那是因为在常规的登录成功之后就将这些信息放到session中了

iii)自定义过滤器:

从上面的配置文件可以看出,我这里定义了3个过滤器,分别是:auth、login、clean。auth这个过滤器就是在shiro原来的认证过滤器的基础上添加了不需要shiro权限认证的url列表;而login则是在常规的登录成功之后跳转到主页时通过从session中取出用户实体生成shiro认证时需要的信息。至于为什么这样做而不是在常规登录过程中将shiro的认证注入进去,我的答案是:为了将shiro的权限管理与我们的业务逻辑尽可能地解耦合,这样以后换另一种权限管理框架时也可以不用修改业务代码

LoginFilter这个类的具体代码如下:

package cn.zifangsky.security;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.web.filter.authc.AuthenticatingFilter;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import cn.zifangsky.model.bo.UsrUserBO;public class LoginFilter extends AuthenticatingFilter {	/**	 * 生成需要认证的信息	 */	@Override	protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {		UsernamePasswordToken token = null;				ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();		HttpServletRequest r = attributes.getRequest();				//从session中取出用户		UsrUserBO userBO = (UsrUserBO) r.getSession().getAttribute("userBO");		if(userBO != null){			token = new UsernamePasswordToken(userBO.getUsername(), userBO.getPassword(), false, getHost(request));		}		return token;	}	@Override	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {		return this.executeLogin(request, response);	}}

关于这个地方的代码执行流程是这样的:

a)登录成功访问主页时,因为此时在shiro中并没有任何凭证信息,因此认证失败,转向执行上面的onAccessDenied方法。当然,我这里设置的是继续拦截,执行从父类AuthenticatingFilter中继承来的executeLogin方法

b)关于AuthenticatingFilter这个类中的executeLogin方法,其源码是这样的:

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {        AuthenticationToken token = createToken(request, response);        if (token == null) {            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +                    "must be created in order to execute a login attempt.";            throw new IllegalStateException(msg);        }        try {            Subject subject = getSubject(request, response);            subject.login(token);            return onLoginSuccess(token, subject, request, response);        } catch (AuthenticationException e) {            return onLoginFailure(token, e, request, response);        }    }

可以看出,这里先是执行createToken方法获取了认证信息,最后再是返回onLoginSuccess方法的执行结果(PS:这个方法默认是true)

而createToken这个方法我这里是复写了的,其目的是从session中获取待认证的信息:

/**	 * 生成需要认证的信息	 */	@Override	protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {		UsernamePasswordToken token = null;				ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();		HttpServletRequest r = attributes.getRequest();				//从session中取出用户		UsrUserBO userBO = (UsrUserBO) r.getSession().getAttribute("userBO");		if(userBO != null){			token = new UsernamePasswordToken(userBO.getUsername(), userBO.getPassword(), false, getHost(request));		}		return token;	}

当然,最后还会执行我自定义的CustomRealm类中的doGetAuthenticationInfo用于校验认证信息,实现shiro的“登录”。在这里,因为都是从session中获取登录成功的用户的详细信息,因此就省略了校验这一步骤,直接从session中取出数据并返回认证成功之后的凭证信息,到此,整个认证流程就结束了

我觉得以上的流程还是比较合理的,因为上面这一执行流程并没有和常规的登录业务进行耦合。相反,我发现一些网友直接将上面executeLogin方法中执行的一些代码插入到常规的的登录流程中,我个人认为那样做是不太优雅的。当然,那样做理解起来稍微简单一些,看大家个人的看法吧

iv)shiro默认拦截器:

在上面的shiro的配置文件中,我除了使用了几个自定义的过滤器之外,还使用了几个shiro默认拦截器。shiro常用的几个默认拦截器分别是:

  • authc    基于表单的拦截器,如“/**=authc”,如果没有登录会跳到相应的登录页面登录。当然,我这里使用时经过了包装,也就是“auth”

  • anon    匿名拦截器,即不需要登录即可访问,一般用于过滤静态资源

  • logout    退出拦截器

  • roles    用于验证用户拥有指定角色才可以访问

  • perms    用于验证用户拥有指定权限才可以访问

到此,shiro相关的配置就已经结束了

二 常规的登录流程

(1)controller层的登录、注销方法:

/**	 * 登录校验	 * 	 * @param UsrUser	 *            登录时的User对象,包括form表单中的用户名和密码	 * @param request	 * @return 是否登录成功标志	 */	@RequestMapping(value = "/user/user/check.json",method={RequestMethod.POST})	@ResponseBody	public Map
 loginCheck(@RequestBody UsrUser user,HttpServletRequest request) { HttpSession session = request.getSession(); Map
 result = new HashMap<>(); Boolean codeCheck = (Boolean) session.getAttribute("codeCheck");  //验证码是否校验成功标志 session.removeAttribute("codeCheck"); if (codeCheck != null && codeCheck) { UsrUserBO userBO = userManager.login(user.getUsername(), user.getPassword()); if (userBO != null) { session.setAttribute("userBO", userBO); // 登录成功之后加入session中 result.put("result", "success"); } else { result.put("result", "error"); } }else{ result.put("result", "error"); } return result; } /**  * 注销登录  *   * @param request  * @return 重定向回登录页面  */ @RequestMapping("/user/user/logout.html") public ModelAndView logout(HttpServletRequest request) { userManager.logout(request.getSession()); return new ModelAndView("redirect:/user/user/login.html"); }

注:我这里的登录采用的是异步登录,并且并没有任何与Shiro相关的内容

(2)login方法对应的业务逻辑层的方法:

@Override	public UsrUserBO login(String username, String password) {		if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {			UsrUserBO result = new UsrUserBO();			// 密码采用sha256加密			UsrUser user = userMapper.selectByNamePasswd(username, EncryptionUtil.sha256Hex(password));			logger.info("登录,用户: " + username);						if(user != null){				//根据用户id查询所有角色				List
 roles = userRoleMapper.selectRolesByUserId(user.getId()); List
 roleBOs = new ArrayList<>(); if(roles != null && roles.size() > 0){ for(UsrRole role : roles){ //根据角色id查询所有权限 List
 funcs = roleFuncMapper.selectFuncsByRoleId(role.getId()); UsrRoleBO roleBO = new UsrRoleBO(); roleBO.setId(role.getId()); roleBO.setRolename(role.getRolename()); roleBO.setDescription(role.getDescription()); roleBO.setFuncs(funcs);  //角色对应的所有权限 roleBOs.add(roleBO); } } result.setId(user.getId()); result.setUsername(user.getUsername()); result.setPassword(user.getPassword()); result.setMobile(user.getMobile()); result.setEmail(user.getEmail()); result.setChannelid(user.getChannelid()); result.setCreatetime(user.getCreatetime()); result.setUpdatetime(user.getUpdatetime()); result.setStatus(user.getStatus()); result.setUsrRoleBOs(roleBOs);  //用户对应的所有角色 return result; } return null; } return null; }

注:i)UsrUserBO实体类:

package cn.zifangsky.model.bo;import java.util.List;import cn.zifangsky.model.UsrUser;public class UsrUserBO extends UsrUser {	private List
 usrRoleBOs; public List
 getUsrRoleBOs() { return usrRoleBOs; } public void setUsrRoleBOs(List
 usrRoleBOs) { this.usrRoleBOs = usrRoleBOs; }}

ii)UsrRoleBO实体类:

package cn.zifangsky.model.bo;import java.util.List;import cn.zifangsky.model.UsrFunc;import cn.zifangsky.model.UsrRole;public class UsrRoleBO extends UsrRole {	private List
 funcs; public List
 getFuncs() { return funcs; } public void setFuncs(List
 funcs) { this.funcs = funcs; }}

三 测试

(1)运行这个测试项目后使用test/123456登录,并访问:/user/user/admin.html

可以发现,最后会跳转到/error/403.jsp页面,因为我们在shiro的配置文件中定义了访问“/user/user/admin.html”需要有“manager”角色,显然 test 用户并没有这个角色

(2)注销之后使用 admin/admin登录,并再次访问:/user/user/admin.html

可以发现,现在就有足够的权限访问这个页面了

特别注意:在本篇文章中我粘贴的代码并不完整(PS:其他配置文件、视图页面等)。因此,如果想要运行这个测试项目的话还请在github下载完整源代码。同时,我在shiro的配置文件中还定义了几个其他的权限验证的配置(如:/article/index* = perms[WZGL:CX]),感兴趣的同学也可以在下载源代码之后测试下

四 基于注解的Shiro权限控制

在上面的代码中,我都是将权限控制相关的代码配置在了shiro的配置文件中的“filterChainDefinitions”这一项。但是,这种方式的权限控制明显是有点呆板的,因为如果在将权限设计得非常分散的话,如果都像上面这样配置,那么shiro的配置文件将变得非常臃肿。这时,我们就可以考虑通过在controller层的处理方法上添加shiro的注解来控制权限了。具体的配置如下:

(1)在SpringMVC的配置文件中添加以下配置:

    
        
        
        
        
        
            
                
                
                    redirect:redirect:/user/user/login.html                                
                
                    redirect:/error/403.jsp                            
                
    

(2)在controller层的一个方法上面添加@RequiresPermissions注解:

package cn.zifangsky.controller;import org.apache.shiro.authz.annotation.RequiresPermissions;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controllerpublic class AirticleController {		@RequestMapping("/article/delete.html")	@RequiresPermissions({ "WZGL:SC" })	public ModelAndView test() {		System.out.println("拥有'文章管理-删除'的权限");		return new ModelAndView("/article/delete");	}}

这个注解很简单,顾名思义就是需要拥有某个权限才能访问这个方法。很显然,代表权限的这些自定义code在登录完成访问主页的时候通过CustomRealm类中的doGetAuthorizationInfo方法就被托管给Shiro了,因此这里就可以直接校验用户是否拥有某个权限了

(3)测试:

使用sub/123456 登录,并访问:/article/delete.html

很明显,能够正常访问

(2)接着注销后使用 zifangsky /123456 登录,同样访问:/article/delete.html

可以发现,并没有权限访问,页面自动跳转到403界面去了

到此,关于Shiro入门的一些常见用法的介绍就到此结束了。感谢大家的浏览,如有疑问请在下面评论中留言,我将会认真回复,谢谢!

PS:上面图片中的水印是我个人博客的域名,因此还请管理员手下留情不要给我标为“转载文章”,谢谢!!!