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

springboot整合springsecurity实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)

SpringSecurity提供的Csrf防御机制至此,我们已经实现了,在用户未登录认证的情况下对请求进行拦截,并且对登录请求”login“放行。接下来要实现登录接口的自定义配置。

SpringSecurity 提供的 Csrf 防御机制

至此,我们已经实现了,在用户未登录认证的情况下对请求进行拦截,并且对登录请求”/login“ 放行。接下来要实现登录接口的自定义配置。在实际环境中一般会将用户信息放在数据库中。我们还是先用默认的用户名密码跑一遍登录接口,看默认情况下,springSecurity 是怎么处理登录请求的。
启动项目。使用PostMan 发送 Post 请求至”/login“
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
发现还是报”用户未登录“。之前明明已经实现了对”/login“请求的放行,唯一不同的是之前发送的是Get请求,而在Post请求下还是被拦截了。看控制台信息:
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
可以看到login请求只经过了4个过滤器就报错了,然后默认跳转到了”/error“,最后被自定义的 entryPoint 拦截。
Invalid CSRF token found for http://localhost:8080/login 大概意是说 CSRF token 找不到。

CSRF(跨站请求伪造),是一种常见的 Web 攻击方式。其主要是借助了浏览器默认发送 COOKIE 的这一机制,在用户登录某一网站时,浏览器会自动记录登录COOKIE信息,然后危险网站会有一个超链接,超链接的地址指向了用户刚刚登陆过的正常网站,当用户被诱导点击该链接时,由于浏览器会自动带上COOKIE信息,能够正常访问,这样就达到了伪造请求的目的,给用户带来风险和损失。

简单了解csrf攻击的机制以后我们来看一下 csrfFilter 的实现。

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}

CsrfFilter 中的doFilter 是从 它的父类 OncePerRequestFilter 继承来的,OncePerRequestFilter 过滤器的作用是避免同一请求被同一过滤器过滤多次。在它的doFilter 方法中调用了 CsrfFilter 的 doFilterInternal 方法 其中requireCsrfProtectionMatcher 属性
默认值是 DefaultRequiresCsrfMatcher 类的实例

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods = new HashSet<>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
/* * (non-Javadoc) * * @see * org.springframework.security.web.util.matcher.RequestMatcher#matches(javax. * servlet.http.HttpServletRequest) */
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}

这个类中的 allowedMethods 是一个set 集合,包含了 “GET”, “HEAD”, “TRACE”, “OPTIONS” 也就是说 如果请求类型是其中之一的话,matches 方法返回 false,不会对 csrftoken 进行校验,也就不会报之前的错,因此 CsrfFilter 只对POST 请求校验 csrfToken,之前的测试结果也证明了这一点。

接着看 doFilterInternal 方法,actualToken 变量的值是从 请求头,或者请求参数中获取,显然之前用 PostMan 发送Post 请求并没有带上这个token ,因此会才会报错。

实际上springSecurity 为了防止跨站请求伪造攻击,会要求post 请求携带一个额外的 csrf token 参数,而参数的值是在渲染登录页面时自动给到前端的,在正常提交请求时,页面需要将该 token 值一同携带,后台通过 csrfFilter 校验该 token 值是否合法,而伪造链接虽然能够获取 COOKIE 但并不知道如何传递 token 参数,因而避免了csrf 攻击。

对于前后端分离的项目来说,如果前端页面是小程序,app之类的移动应用,不涉及浏览器应用的话,是无需考虑 csrf 攻击的。这种情况下就需要考虑如何关闭 csrf token 验证功能。

从 doFilterInternal 方法的程序逻辑来看,只要是Post 请求 就必然会涉及到 token 验证这一环节,因此要想关闭 csrf 功能 只有将 csrfFilter 从过滤器链中剔除这一种方法。
打断点,看程序调用堆栈
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
从调用堆栈来看 最先调用的过滤器是 DelegatingFilterProxy 其中有一个delegate 属性 存放了其代理的 filter 对象,然后在 dofilter 方法中 调用了该代理对象的 dofilter 方法 ,此时这个代理过滤器类是 FilterChainProxy, 它的 dofiler 方法中调用了 doFilterInternal 方法 ,我们重点看这个方法:

private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}

其中 getFilters 方法 返回了 一个Filter 的集合。在这里打断点debug可以发现,getFilters 方法 其实是获取到了过滤器链表中所有的过滤器类。默认情况下共有13个过滤器,其中也包含了 CsrfFilter。 然后把 filters 集合传入 VirtualFilterChain 的构造器中,设置其 additionalFilters 属性值。

private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}

最后调用 VirtualFilterChain 的 dofilter 方法:

@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}

这个方法比较关键,currentPosition 的默认值为0,而 size 的值在构造方法中被设置为 additionalFilters 中filter 的个数,也就是 13 ,因此当 currentPosition 小于 13 时,会从 additionalFilters 中获取下一个 filter,调用其 dofilter 方法,并且把当前 VirtualFilterChain 对象 传入dofilter 方法中,由于 VirtualFilterChain 是 FilterChain 的实现类,因此,additionalFilters 中的每一个filter 的 dofilter 方法执行完成后都会返回到 VirtualFilterChain 的 dofilter 方法中,然后 currentPosition 的值加一,以 currentPosition 为索引 获取下一个filter,并执行其dofilter 方法。直至 13个filter 都执行完成。

到此为止,整个过滤器链的执行过程我们已经基本了解了,接着来看 getFilters 方法是如何获取到默认的13个过滤器的。

private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}

public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}

遍历了 filterChains 只要其中的 SecurityFilterChain实现类 和 request 请求匹配,那么返回一个filter 集合。
因此重点还是在FilterChainProxy 类的 filterChains 属性上。而 filterChains属性 是在构造方法中注入的:

public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}

还是老方法,在构造方法打断点,启动项目,看调用堆栈:

protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;
}

根据调用栈 找到 WebSecurity 类的 performBuild 方法,在该方法中 利用 securityFilterChains 构造了一个 FilterChainProxy 实例,securityFilterChainBuilders 是 SecurityBuilder 实现类的list集合,此时只有一个元素即 HttpSecurity ,重点看 HttpSecurity 的 build() 方法
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
httpSecurity 继承了 AbstractConfiguredSecurityBuilder,它的 build() 方法最终是调用了 AbstractConfiguredSecurityBuilder 的 doBuild() 方法:

@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
init();
buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}

重点看其中的 configure 方法:

@SuppressWarnings("unchecked")
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}

private Collection<SecurityConfigurer<O, B>> getConfigurers() {
List<SecurityConfigurer<O, B>> result = new ArrayList<>();
for (List<SecurityConfigurer<O, B>> configs : this.configurers.values()) {
result.addAll(configs);
}
return result;
}

configure() 方法 调用 getConfigurers() 获取 SecurityConfigurer 的集合,然后遍历其中的 configurer 对象 依次调用其 configurer() 方法。在这边打断点可以看到 一共有 13个 configurer 对象 依次对应于13 个过滤器。
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
以 CsrfConfigurer 为例。它的 configurer() 方法主要就是 构造一个 CsrfFilter,然后对其进行一些处理,最终调用 httpSecurity 的 addFilter() 方法 将 CsrfFilter 加入到 httpSecurity 的 filters 属性中

@SuppressWarnings("unchecked")
@Override
public void configure(H http) {
CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
if (requireCsrfProtectionMatcher != null) {
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
}
AccessDeniedHandler accessDeniedHandler = createAccessDeniedHandler(http);
if (accessDeniedHandler != null) {
filter.setAccessDeniedHandler(accessDeniedHandler);
}
LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
if (logoutConfigurer != null) {
logoutConfigurer
.addLogoutHandler(new CsrfLogoutHandler(this.csrfTokenRepository));
}
SessionManagementConfigurer<H> sessionConfigurer = http
.getConfigurer(SessionManagementConfigurer.class);
if (sessionConfigurer != null) {
sessionConfigurer.addSessionAuthenticationStrategy(
getSessionAuthenticationStrategy());
}
filter = postProcess(filter);
http.addFilter(filter);
}

public HttpSecurity addFilter(Filter filter) {
Class<? extends Filter> filterClass = filter.getClass();
if (!comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException(
"The Filter class "
+ filterClass.getName()
+ " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
}
this.filters.add(filter);
return this;
}

到这里不难发现,其实过滤器链中的每一个过滤器都由其对应的 SecurityConfigurer 对象去创建并放到 httpSecurity 的 filters 属性中。而 这些SecurityConfigurer 对象 是从 httpSecurity 的 configurers 属性中获取的,我们在 httpSecurity 的 构造方法上打断点,查看调用堆栈,发现是在 WebSecurityConfigAdapter 类 中的 getHttp() 方法中 构造了一个 httpSecurity 实例:

protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
.postProcess(new DefaultAuthenticationEventPublisher());
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
authenticationBuilder.authenticationEventPublisher(eventPublisher);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return http;
}

这个类我们就很熟悉了,就是我们自定义配置类的父类。在构造了 httpSecurity 实例后 调用了 csrf() 方法:

public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
ApplicationContext context = getContext();
return getOrApply(new CsrfConfigurer<>(context));
}

又是 getOrApply() 方法 这个方法在上一篇的分析中出现过多次,其实就是在这里创建了 CsrfConfigurer 对象并将其放入 httpSecurity 的 configurers 属性中。其它的 configurer 对象 也是由类似的途径创建的。

接着回去看 httpSecurity 的 构造方法调用堆栈:
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
跟随调用堆栈往上找,找到 WebSecurityConfiguration 类的 springSecurityFilterChain 方法:

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

webSecurity.build() 方法最终调用了 AbstractConfiguredSecurityBuilder 的 doBuild() 方法 这个方法我们之前分析过,是一个通用的方法,通过一系列标准化的流程,来初始化和配置当前的 SecurityBuilder 实现类,WebSecurity 和HttpSecurity 都是 SecurityBuilder 的实现类,它们的build() 方法最终都会调用这个 doBuild() 方法。
在doBuild() 方法中 会调用 init() 方法:

private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this);
}
for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
configurer.init((B) this);
}
}

private Collection<SecurityConfigurer<O, B>> getConfigurers() {
List<SecurityConfigurer<O, B>> result = new ArrayList<>();
for (List<SecurityConfigurer<O, B>> configs : this.configurers.values()) {
result.addAll(configs);
}
return result;
}

在init() 方法中首先获取 webSecurity 的 configurers 属性的values 集合 也就是 SecurityConfigurer 列表 而此时 values 集合中只有一个元素,就是我们自定义的 webSecurityConfig 类的实例:
《springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(2)》
然后遍历 SecurityConfigurer 列表依次调用其 init() 方法 webSecurityConfig 继承了 WebSecurityConfigurerAdapter,这里会调用 WebSecurityConfigurerAdapter 的init() 方法,然后在这个方法中,调用 getHttp() 方法 创建一个HttpSecurity 实例。 getHttp() 方法中创建完13个过滤器对应的 SecurityConfig 对象之后,会调用 configure() 方法配置httpSecurity,此时的 configure() 方法调用的是 webSecurityConfig 类中重写的 configure(HttpSecurity http) 方法,因此我们可以在这个方法中对 CsrfConfigurer 进行配置。

接着我们来分析一下 CsrfConfigurer 类 看有没有相关方法来配置 csrfFilter 失效。
CsrfConfigurer 继承了 AbstractHttpConfigurer 类:

public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, B> {
/** * Disables the {@link AbstractHttpConfigurer} by removing it. After doing so a fresh * version of the configuration can be applied. * * @return the {@link HttpSecurityBuilder} for additional customizations */
@SuppressWarnings("unchecked")
public B disable() {
getBuilder().removeConfigurer(getClass());
return getBuilder();
}
@SuppressWarnings("unchecked")
public T withObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) {
addObjectPostProcessor(objectPostProcessor);
return (T) this;
}
}

其中有一个 disabled() 方法,看方法的注释,意思大概是该方法可以移除当前 configurer 对象,移除了 CsrfConfigurer 对象,Csrffilter 对象就不会被创建,正好满足了我们的需求。

来分析一下它的源码,首先调用 getBuilder() 方法,这个方法在 AbstractHttpConfigurer 的父类 SecurityConfigurerAdapter 中:

protected final B getBuilder() {
if (securityBuilder == null) {
throw new IllegalStateException("securityBuilder cannot be null");
}
return securityBuilder;
}

返回了 CsrfConfigurer 类的 securityBuilder 属性对象。这个属性是在 CsrfConfigurer 初始化的时候进行赋值的,根据上文的分析, CsrfConfigurer 是在 HttpSecurity 的 csrf() 方法中调用 getOrApply() 方法进行初始化的。

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
C configurer) throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
throws Exception {
configurer.addObjectPostProcessor(objectPostProcessor);
configurer.setBuilder((B) this);
add(configurer);
return configurer;
}

看到在 apply() 方法中 调用 setBuilder() 方法设置了 CsrfConfigurer 类的 securityBuilder 属性值为当前 this 对象,也就是 HttpSecurity 对象。
因此 getBuilder() 方法返回值就是 HttpSecurity,然后又调用了 removeConfigurer(getClass()) 方法,参数是当前 class 对象 也就是 CsrfConfigurer,最终调用的是 HttpSecurity 的父类 AbstractConfiguredSecurityBuilder 类中的 removeConfigurer(Class clazz)方法, 看一下它的源码:

public <C extends SecurityConfigurer<O, B>> C removeConfigurer(Class<C> clazz) {
List<SecurityConfigurer<O, B>> configs = this.configurers.remove(clazz);
if (configs == null) {
return null;
}
if (configs.size() != 1) {
throw new IllegalStateException("Only one configurer expected for type "
+ clazz + ", but got " + configs);
}
return (C) configs.get(0);
}

很明显,这个方法就是把 HttpSecurity 的 configurers 属性中的 CsrfConfigurer 对象 移除掉。
通过以上分析,我们找到了让 csrf 校验功能失效的方法,即在 webSecurityConfig 的 configure(HttpSecurity http) 方法中增加如下配置:

@Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable();
}

重启项目,此时再用 PostMan 发送 Post 请求至 “/login” 就不会再提示用户未登录了。

通过一步步分析源码,我们不仅找到了如何关闭 SpringSecurity 的 Csrf 校验功能,也对框架内部过滤器链的初始化过程,以及各个过滤器的配置方法都有了一定的了解,这些知识也有助于我们之后对框架做出更多个性化的改动。


推荐阅读
author-avatar
wuzhi61448
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有