热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ssm+freemark集成shiro

1.导入的jar包net.mingsoftshiro-freemarker-tags0.1

技术分享技术分享

1.导入的jar包

		
		
		
		    net.mingsoft
		    shiro-freemarker-tags
		    0.1
		
		
		  
	        org.apache.shiro  
	        shiro-core  
	      
	      
	        org.apache.shiro  
	        shiro-web  
	      
	      
	        org.apache.shiro  
	        shiro-ehcache  
	      
	      
	        org.apache.shiro  
	        shiro-spring  
	    
	    
2.在web.xml中加入shiro filter

    
    
        shiroFilter
        org.springframework.web.filter.DelegatingFilterProxy
        
            targetFilterLifecycle
            true
        
    
    
        shiroFilter
        /*
        REQUEST
        FORWARD
    
此过滤器要放在第一个,且名称要与spring-shiro,xml中shiro filter一致

3.在freemarker中加入shiro标签

3.1新建一个FreeMarkerConfigExtend类继承FreeMarkerConfigurer,

package com.business.util;

import java.io.IOException;

import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import com.jagregory.shiro.freemarker.ShiroTags;

import freemarker.template.Configuration;
import freemarker.template.TemplateException;

public class FreeMarkerConfigExtend extends FreeMarkerConfigurer {
	
	@Override  
	public void afterPropertiesSet() throws IOException, TemplateException {  
        super.afterPropertiesSet();
        Configuration cfg = this.getConfiguration();
        cfg.setSharedVariable("shiro", new ShiroTags());//shiro标签
        cfg.setNumberFormat("#");//防止页面输出数字,变成2,000
        //可以添加很多自己的要传输到页面的[方法、对象、值]
        
        
        /*
         * 在controller层使用注解再加一层判断
         * 注解 @RequiresPermissions("/delete")
         */
        
        /*shiro标签*/
        /**
        1.游客
        <@shiro.guest>  
        	您当前是游客,登录
         
		
		2.user(已经登录,或者记住我登录)
		<@shiro.user>  
			欢迎[<@shiro.principal/>]登录,退出  
		   

		3.authenticated(已经认证,排除记住我登录的)
		<@shiro.authenticated>  
			用户[<@shiro.principal/>]已身份验证通过  
		   		
		
		4.notAuthenticated(和authenticated相反)
		<@shiro.notAuthenticated>
		          当前身份未认证(包括记住我登录的)
		 
		
		5.principal标签(能够取到你在realm中保存的信息比如我存的是ShiroUser对象,取出其中urlSet属性)
		
		<@shiro.principal property="urlSet"/>
		
		6.hasRole标签(判断是否拥有这个角色)
		<@shiro.hasRole name="admin">  
			用户[<@shiro.principal/>]拥有角色admin
7.hasAnyRoles标签(判断是否拥有这些角色的其中一个) <@shiro.hasAnyRoles name="admin,user,member"> 用户[<@shiro.principal/>]拥有角色admin或user或member
8.lacksRole标签(判断是否不拥有这个角色) <@shiro.lacksRole name="admin"> 用户[<@shiro.principal/>]不拥有admin角色 9.hasPermission标签(判断是否有拥有这个权限) <@shiro.hasPermission name="user:add"> 用户[<@shiro.principal/>]拥有user:add权限 10.lacksPermission标签(判断是否没有这个权限) <@shiro.lacksPermission name="user:add"> 用户[<@shiro.principal/>]不拥有user:add权限 **/ } }
3.2修改spring-mvc-servlet.xml中的freemarker配置

技术分享
4.新建CustomCredentialsMatcher类继承shiro的SimpleCredentialsMatcher类,这个类作用是自定义密码验证

package com.business.shiro;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

import com.business.util.MD5Util;

/** 
 * Description: 告诉shiro如何验证加密密码,通过SimpleCredentialsMatcher或HashedCredentialsMatcher 
 * @Author: zh 
 * @Create Date: 2017-5-9 
 */  
  
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {  
      
    @Override   
    public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {    
             
	    UsernamePasswordToken token = (UsernamePasswordToken) authcToken; 
	    
	    
	    Object tokenCredentials = MD5Util.hmac_md5(String.valueOf(token.getPassword()));
        Object accountCredentials = getCredentials(info);
        //将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
        return equals(tokenCredentials, accountCredentials);
    }  
    
}
5.新建ShiroDbRealm类
package com.business.shiro;

import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;

import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import com.business.dao.UserDao;
import com.business.entity.Menu;
import com.business.entity.Role;
import com.business.entity.User;
import com.business.entity.UserRole;
import com.business.service.sysService.MenuService;
import com.business.service.sysService.RoleService;
import com.business.service.sysService.UserRoleService;
import com.business.service.sysService.UserService;
import com.business.util.SessionUtil;
import com.common.util.BizUtil;
import com.google.common.collect.Sets;

/**
 * @description:shiro权限认证
 * @author:zhanghao
 * @date:2017/5/8 14:51
 */
public class ShiroDbRealm extends AuthorizingRealm {
    private static final Logger LOGGER = Logger.getLogger(ShiroDbRealm.class);

    @Autowired private UserService userService;
    @Autowired private UserDao userDao;
    @Autowired private RoleService roleService;
    @Autowired private UserRoleService userRoleService;
    @Autowired private MenuService menuService;
    
    public ShiroDbRealm(CacheManager cacheManager, CredentialsMatcher matcher) {
        super(cacheManager, matcher);
    }
    
    /**
     * Shiro登录认证(原理:用户提交 用户名和密码  --- shiro 封装令牌 ---- realm 通过用户名将密码查询返回 ---- shiro 自动去比较查询出密码和用户输入密码是否一致---- 进行登陆控制 )
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authcToken) throws AuthenticationException {
        LOGGER.info("Shiro开始登录认证");
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        User user = userDao.getByName(token.getUsername());
        // 账号不存在
        if (user == null) {
            return null;
        }
        // 账号未启用
        if (user.getStatus() == 1) {
            throw new DisabledAccountException();
        }
        //将用户信息保存在session中
        SessionUtil.addSession(user);
        UserRole userRole = userRoleService.getByUserId(user.getId());
        Role role = roleService.getById(userRole.getRoleId());
        // 读取用户的url和角色
        Set roles = Sets.newHashSet(role.getName());
        List menuIds = BizUtil.stringToLongList(role.getMenu(), ",");
        List menuList = menuService.getListByIds(menuIds);
        List menuStr = BizUtil.extractToList(menuList, "url");
        
        Set urls = Sets.newHashSet(menuStr);
        urls.remove("");
        urls.remove(null);
        
        ShiroUser shiroUser = new ShiroUser(user.getId(), user.getLoginName(), user.getUsername(), urls);
        shiroUser.setRoles(roles);
        // 认证缓存信息
        return new SimpleAuthenticationInfo(shiroUser, user.getPassword().toCharArray(),getName());
    }

    /**
     * Shiro权限认证
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(shiroUser.getRoles());
        info.addStringPermissions(shiroUser.getUrlSet());
        
        return info;
    }
    
    @Override
    public void onLogout(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        removeUserCache(shiroUser);
    }

    /**
     * 清除用户缓存
     * @param shiroUser
     */
    public void removeUserCache(ShiroUser shiroUser){
        removeUserCache(shiroUser.getLoginName());
    }

    /**
     * 清除用户缓存
     * @param loginName
     */
    public void removeUserCache(String loginName){
        SimplePrincipalCollection principals = new SimplePrincipalCollection();
        principals.add(loginName, super.getName());
        super.clearCachedAuthenticationInfo(principals);
    }
    
	@PostConstruct
	public void initCredentialsMatcher() {
		//该句作用是重写shiro的密码验证,让shiro用我自己的验证-->指向重写的CustomCredentialsMatcher
		setCredentialsMatcher(new CustomCredentialsMatcher());

	}
}
6.自定义shiroUser

package com.business.shiro;

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

/**
 * @description:自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息
 * @author:zhanghao
 * @date:2017/5/9
 */
public class ShiroUser implements Serializable {
    private static final long serialVersiOnUID= -1373760761780840081L;
    
    private Long id;
    private final String loginName;
    private String name;
    private Set urlSet;
    private Set roles;

    public ShiroUser(String loginName) {
        this.loginName = loginName;
    }

    public ShiroUser(Long id, String loginName, String name, Set urlSet) {
        this.id = id;
        this.loginName = loginName;
        this.name = name;
        this.urlSet = urlSet;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set getUrlSet() {
        return urlSet;
    }

    public void setUrlSet(Set urlSet) {
        this.urlSet = urlSet;
    }

    public Set getRoles() {
        return roles;
    }

    public void setRoles(Set roles) {
        this.roles = roles;
    }

    public String getLoginName() {
        return loginName;
    }

    /**
     * 本函数输出将作为默认的输出.
     */
    @Override
    public String toString() {
        return loginName;
    }
}
7.两个缓存类

package com.business.shiro.cache;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;

import org.apache.log4j.Logger;
import org.apache.shiro.cache.CacheException;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;

/**
 * 使用spring-cache作为shiro缓存
 * @author L.cm
 *
 */
@SuppressWarnings("unchecked")
public class ShiroSpringCache implements org.apache.shiro.cache.Cache {
	private static final Logger logger = Logger.getLogger(ShiroSpringCache.class);
	
	private final org.springframework.cache.Cache cache;
	
	public ShiroSpringCache(Cache cache) {
		if (cache == null) {
			throw new IllegalArgumentException("Cache argument cannot be null.");
		}
		this.cache = cache;
	}

	@Override
	public V get(K key) throws CacheException {
		if (logger.isTraceEnabled()) {
			logger.trace("Getting object from cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass());
		}
		ValueWrapper valueWrapper = cache.get(key);
		if (valueWrapper == null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Element for [" + key + "] is null.");
			}
			return null;
		}
		return (V) valueWrapper.get();
	}

	@Override
	public V put(K key, V value) throws CacheException {
		if (logger.isTraceEnabled()) {
			logger.trace("Putting object in cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass());
		}
		V previous = get(key);
		cache.put(key, value);
		return previous;
	}

	@Override
	public V remove(K key) throws CacheException {
		if (logger.isTraceEnabled()) {
			logger.trace("Removing object from cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass());
		}
		V previous = get(key);
		cache.evict(key);
		return previous;
	}

	@Override
	public void clear() throws CacheException {
		if (logger.isTraceEnabled()) {
			logger.trace("Clearing all objects from cache [" + this.cache.getName() + "]");
		}
		cache.clear();
	}

	@Override
	public int size() {
		return 0;
	}

	@Override
	public Set keys() {
		return Collections.emptySet();
	}

	@Override
	public Collection values() {
		return Collections.emptySet();
	}

	@Override
	public String toString() {
		return "ShiroSpringCache [" + this.cache.getName() + "]";
	}
}
package com.business.shiro.cache;

import org.apache.log4j.Logger;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.util.Destroyable;

/**
 * 使用spring-cache作为shiro缓存
 * 缓存管理器
 * @author L.cm
 *
 */
public class ShiroSpringCacheManager implements CacheManager, Destroyable {
	private static final Logger logger = Logger.getLogger(ShiroSpringCacheManager.class);
	private org.springframework.cache.CacheManager cacheManager;
	
	public org.springframework.cache.CacheManager getCacheManager() {
		return cacheManager;
	}

	public void setCacheManager(org.springframework.cache.CacheManager cacheManager) {
		this.cacheManager = cacheManager;
	}

	@Override
	public  Cache getCache(String name) throws CacheException {
		if (logger.isTraceEnabled()) {
			logger.trace("Acquiring ShiroSpringCache instance named [" + name + "]");
		}
		org.springframework.cache.Cache cache = cacheManager.getCache(name);
		return new ShiroSpringCache(cache);
	}

	@Override
	public void destroy() throws Exception {
		cacheManager = null;
	}

}
8.spring-shiro.xml




    Shiro安全配置


    
    
        
        
        
        
        
        
        
    

    
    
        
        
        
        
        
        
        
        
    

    
      
        
        
        
        
    

    
    
          
    

    
    
        
        
        
        
        
        
        
        
        
            
                
                /login = anon
                /favicon.ico = anon
                /static/** = anon
                /** = user
            
        
    

    
    
        
    
    
    
    
        
        
        

        
    
    
    
    
        
        
        
    
    
    
    
        
        
    
    
    
    
    
9.如果要再controller层中加入注解判断,还需在spring-mvc-servlet.xml中加入

    
    
        
    
    
    
    
    
          
    
10.缓存的两个xml文件ehcache.xml,spring-ehcache.xml



    
    
   
    

    
    
    
    
    
    
    
    
        
    
    
    
    
    
    
    
    
        


    
    
    
    
        
    
    
    
        
        
    
    
    
    
11.applicationContext.xml导入配置文件

	
	
12.LoginController控制层方法

package com.business.controller.system;

import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.business.controller.BaseController;
import com.business.service.sysService.UserService;
import com.business.util.SessionUtil;
import com.google.common.collect.Maps;

@Controller
public class LoginController extends BaseController {

    @Resource
    private UserService userService;
    
    @RequestMapping(value = "/login",method=RequestMethod.GET)
    public String login() {
        return "login";
    }

    @RequestMapping(value = "/login",method=RequestMethod.POST)
    public String login(RedirectAttributes attributes, String username, String password, @RequestParam(value = "online", defaultValue = "0") Integer rememberMe) {
        
        Map map = Maps.newHashMap();
        if (StringUtils.isBlank(username)) {
            map.put("errorInfo", "用户名不能为空");
        }
        if (StringUtils.isBlank(password)) {
            map.put("errorInfo", "密码不能为空");
        }
        Subject user = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, username+password);
        // 设置记住密码
        token.setRememberMe(1 == rememberMe);
        try {
            user.login(token);
            return "redirect:/menu/index";
        } catch (UnknownAccountException e) {
            map.put("errorInfo", "账号不存在!");
            attributes.addFlashAttribute("error", map);
            return "redirect:/login";
        } catch (DisabledAccountException e) {
            map.put("errorInfo", "账号未启用!");
            attributes.addFlashAttribute("error", map);
            return "redirect:/login";
        } catch (IncorrectCredentialsException e) {
            map.put("errorInfo", "密码错误!");
            attributes.addFlashAttribute("error", map);
            return "redirect:/login";
        } catch (Throwable e) {
            map.put("errorInfo", "登录异常!");
            attributes.addFlashAttribute("error", map);
            return "redirect:/login";
        }
    }
    
    @RequestMapping(value = "/logout")
    public String logout() {
        SessionUtil.removeSession();
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "redirect:/login";
    }
}
12.如何在控制层使用注解,可以在baseController中加入总的异常处理

	@ExceptionHandler  
    public String exception(HttpServletRequest request, Exception e) {  
        //对异常进行判断做相应的处理  
        if(e instanceof AuthorizationException){  
            return "redirect:/logout";  
        }
        return null;
	}















ssm+freemark集成shiro


推荐阅读
  • 深入解析 Spring MVC 的核心原理与应用实践
    本文将详细探讨Spring MVC的核心原理及其实际应用,首先从配置web.xml文件入手,解析其在初始化过程中的关键作用,接着深入分析请求处理流程,包括控制器、视图解析器等组件的工作机制,并结合具体案例,展示如何高效利用Spring MVC进行开发,为读者提供全面的技术指导。 ... [详细]
  • 本文深入解析了 Apache 配置文件 `httpd.conf` 和 `.htaccess` 的优化方法,探讨了如何通过合理配置提升服务器性能和安全性。文章详细介绍了这两个文件的关键参数及其作用,并提供了实际应用中的最佳实践,帮助读者更好地理解和运用 Apache 配置。 ... [详细]
  • 在一系列的学习与实践后,Jsoup学习笔记系列即将进入尾声。本文详细介绍了如何使用Jsoup实现从Saz文件到Csv格式的数据解析功能。未来,计划将此功能进一步封装,开发成具有用户界面的独立应用程序,以增强其实用性和便捷性。对于希望深入掌握Jsoup技术的开发者,本文提供了宝贵的参考和实践案例。 ... [详细]
  • 本文深入探讨了ASP.NET中ViewState、Cookie和Session三种状态管理技术的区别与应用场景。ViewState主要用于保存页面控件的状态信息,确保在多次往返服务器过程中数据的一致性;Cookie则存储在客户端,适用于保存少量用户偏好设置等非敏感信息;而Session则在服务器端存储数据,适合处理需要跨页面保持的数据。文章详细分析了这三种技术的工作原理及其优缺点,并提供了实际应用中的最佳实践建议。 ... [详细]
  • Git基础操作指南:掌握必备技能
    掌握 Git 基础操作是每个开发者必备的技能。本文详细介绍了 Git 的基本命令和使用方法,包括初始化仓库、配置用户信息、添加文件、提交更改以及查看版本历史等关键步骤。通过这些操作,读者可以快速上手并高效管理代码版本。例如,使用 `git config --global user.name` 和 `git config --global user.email` 来设置全局用户名和邮箱,确保每次提交时都能正确标识提交者信息。 ... [详细]
  • 作为140字符的开创者,Twitter看似简单却异常复杂。其简洁之处在于仅用140个字符就能实现信息的高效传播,甚至在多次全球性事件中超越传统媒体的速度。然而,为了支持2亿用户的高效使用,其背后的技术架构和系统设计则极为复杂,涉及高并发处理、数据存储和实时传输等多个技术挑战。 ... [详细]
  • HBase在金融大数据迁移中的应用与挑战
    随着最后一台设备的下线,标志着超过10PB的HBase数据迁移项目顺利完成。目前,新的集群已在新机房稳定运行超过两个月,监控数据显示,新集群的查询响应时间显著降低,系统稳定性大幅提升。此外,数据消费的波动也变得更加平滑,整体性能得到了显著优化。 ... [详细]
  • 深入解析Tomcat:开发者的实用指南
    深入解析Tomcat:开发者的实用指南 ... [详细]
  • 本课程详细介绍了如何使用Python Flask框架从零开始构建鱼书应用,涵盖高级编程技巧和实战项目。通过视频教学,学员将学习到Flask的高效用法,包括数据库事务处理和书籍交易模型的实现。特别感谢AI资源网提供的课程下载支持。 ... [详细]
  • JVM参数设置与命令行工具详解
    JVM参数配置与命令行工具的深入解析旨在优化系统性能,通过合理设置JVM参数,确保在高吞吐量的前提下,有效减少垃圾回收(GC)的频率,进而降低系统停顿时间,提升服务的稳定性和响应速度。此外,本文还将详细介绍常用的JVM命令行工具,帮助开发者更好地监控和调优JVM运行状态。 ... [详细]
  • 在Spring框架中,基于Schema的异常通知与环绕通知的实现方法具有重要的实践价值。首先,对于异常通知,需要创建一个实现ThrowsAdvice接口的通知类。尽管ThrowsAdvice接口本身不包含任何方法,但开发者需自定义方法来处理异常情况。此外,环绕通知则通过实现MethodInterceptor接口来实现,允许在方法调用前后执行特定逻辑,从而增强功能或进行必要的控制。这两种通知机制的结合使用,能够有效提升应用程序的健壮性和灵活性。 ... [详细]
  • 在 Linux 系统中,`/proc` 目录实现了一种特殊的文件系统,称为 proc 文件系统。与传统的文件系统不同,proc 文件系统主要用于提供内核和进程信息的动态视图,通过文件和目录的形式呈现。这些信息包括系统状态、进程细节以及各种内核参数,为系统管理员和开发者提供了强大的诊断和调试工具。此外,proc 文件系统还支持实时读取和修改某些内核参数,增强了系统的灵活性和可配置性。 ... [详细]
  • 深入解析Wget CVE-2016-4971漏洞的利用方法与安全防范措施
    ### 摘要Wget 是一个广泛使用的命令行工具,用于从 Web 服务器下载文件。CVE-2016-4971 漏洞涉及 Wget 在处理特定 HTTP 响应头时的缺陷,可能导致远程代码执行。本文详细分析了该漏洞的成因、利用方法以及相应的安全防范措施,包括更新 Wget 版本、配置防火墙规则和使用安全的 HTTP 头。通过这些措施,可以有效防止潜在的安全威胁。 ... [详细]
  • 构建顶级PHP博客系统:实践与洞见
    构建顶级PHP博客系统不仅需要扎实的技术基础,还需深入理解实际应用需求。本文以Zend Studio为开发环境,MySQL作为数据存储,Apache服务器为运行平台,结合jQuery脚本语言,详细阐述了从环境搭建到功能实现的全过程,分享了开发PHP博客管理系统的宝贵经验和实用技巧。 ... [详细]
  • Spring Boot 实战(一):基础的CRUD操作详解
    在《Spring Boot 实战(一)》中,详细介绍了基础的CRUD操作,涵盖创建、读取、更新和删除等核心功能,适合初学者快速掌握Spring Boot框架的应用开发技巧。 ... [详细]
author-avatar
mobiledu2502877277
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有