论坛首页 入门技术论坛

Spring3.0 + Security3.1.3 配置

浏览 3749 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2013-11-11  

近期对Spring+Security的框架进行了一些简单的研究,将研究的结果贴出来,供大家学习参考。

废话少说,还是代码来的直接明了。

首先需要一些相关的源代码:

MyAccessDecisionManager.java:

 

/**
 * 
 */
package com.simonsw.security;

import java.util.Collection;
import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

/**
 * @author Simon Lv
 * @since Oct 31, 2013
 */
public class MyAccessDecisionManager implements AccessDecisionManager {
	private Logger logger = LoggerFactory.getLogger(getClass());
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.AccessDecisionManager#decide(org.
	 * springframework.security.core.Authentication, java.lang.Object,
	 * java.util.Collection)
	 */
	@Override
	public void decide(Authentication authentication, Object obj,
			Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException {
		if (configAttributes == null) {
			return;
		}
		// 所请求的资源拥有的权限(一个资源对多个权限)
		Iterator<ConfigAttribute> iterator = configAttributes.iterator();
		while (iterator.hasNext()) {
			ConfigAttribute configAttribute = iterator.next();
			// 访问所请求资源所需要的权限
			String needPermission = configAttribute.getAttribute();
			logger.debug("[MyAccessDecisionManager] needPermission is " + needPermission);
			// 用户所拥有的权限authentication
			for (GrantedAuthority ga : authentication.getAuthorities()) {
				logger.debug("[MyAccessDecisionManager] ga.getAuthority() is " + ga.getAuthority());
				if (needPermission.contains((ga.getAuthority()))) {
					return;
				}
			}
		}
		// 没有权限让我们去捕捉
		throw new AccessDeniedException(" 没有权限访问!");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.AccessDecisionManager#supports(org
	 * .springframework.security.access.ConfigAttribute)
	 */
	@Override
	public boolean supports(ConfigAttribute arg0) {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.AccessDecisionManager#supports(java
	 * .lang.Class)
	 */
	@Override
	public boolean supports(Class<?> arg0) {
		return true;
	}

}

 MyAuthenticationManager.java:

 

 

/**
 * 
 */
package com.simonsw.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.simonsw.base.entity.Resource;
import com.simonsw.base.entity.Role;
import com.simonsw.base.entity.RoleResource;
import com.simonsw.base.entity.UserRole;
import com.simonsw.base.entity.Users;
import com.simonsw.base.service.RoleResourceService;
import com.simonsw.base.service.UserRoleService;
import com.simonsw.base.service.UserService;
import com.simonsw.common.util.StringUtils;

/**
 * @author Simon Lv
 * @since Oct 31, 2013
 */
public class MyAuthenticationManager implements UserDetailsService {
	@Autowired
	private UserService userService;
	@Autowired
	private UserRoleService userRoleService;
	@Autowired
	private RoleResourceService roleResourceService;
	private Logger logger = LoggerFactory.getLogger(getClass());

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.core.userdetails.UserDetailsService#
	 * loadUserByUsername(java.lang.String)
	 */
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		logger.debug("[MyAuthenticationManager] username ==> " + username);
		if (!StringUtils.isEmpty(username)) {
			throw new UsernameNotFoundException("用户名不能为空!");
		}
		Users user = userService.getUserByName(username);
		Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(user);

		boolean enables = true;
		boolean accountNonExpired = true;
		boolean credentialsNonExpired = true;
		boolean accountNonLocked = true;
		// 封装成spring security的user
		User userdetail = new User(user.getUsername(), user.getPassword(),
				enables, accountNonExpired, credentialsNonExpired,
				accountNonLocked, grantedAuths);
		return userdetail;
	}

	// 取得用户的权限
	private Set<GrantedAuthority> obtionGrantedAuthorities(Users user) {
		Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
		List<Resource> resources = new ArrayList<Resource>();
		Role role;
		List<RoleResource> roleResources;
		List<UserRole> userRoles = userRoleService.getUserRoleByUserId(user);
		for (UserRole userRole : userRoles) {
			role = userRole.getRole();
			roleResources = roleResourceService.getUserRoleByRoleId(role);
			for (RoleResource roleResource : roleResources) {
				resources.add(roleResource.getResource());
			}
		}

		for (Resource res : resources) {
			authSet.add(new SimpleGrantedAuthority(res.getModelname()));
		}
		return authSet;
	}
}

 MyLogoutSuccessHandler.java:

 

 

/**
 * 
 */
package com.simonsw.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * @author Simon Lv
 * @since Nov 5, 2013
 */
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.web.authentication.logout.LogoutSuccessHandler
	 * #onLogoutSuccess(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.Authentication)
	 */
	@Override
	public void onLogoutSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {
		if (authentication != null) {
			logger.debug(authentication.getName() + "Logout==>");
		}
		response.sendRedirect(request.getContextPath());

	}

}

 MySecurityFilter.java:

 

 

/**
 * 
 */
package com.simonsw.security;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

/**
 * @author Simon Lv
 * @since Oct 31, 2013
 */
public class MySecurityFilter extends AbstractSecurityInterceptor implements
		Filter {
	private Logger logger = LoggerFactory.getLogger(getClass());

	// 与applicationContext-security.xml里的myFilter的属性securityMetadataSource对应,
	// 其他的两个组件,已经在AbstractSecurityInterceptor定义
	private FilterInvocationSecurityMetadataSource securityMetadataSource;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.intercept.AbstractSecurityInterceptor
	 * #obtainSecurityMetadataSource()
	 */
	@Override
	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
	 * javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

	private void invoke(FilterInvocation fi) throws IOException,
			ServletException {
		logger.debug("[MySecurityFilter] 用户发送请求!");
		InterceptorStatusToken token = null;
		token = super.beforeInvocation(fi);
		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		} finally {
			super.afterInvocation(token, null);
		}
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return securityMetadataSource;
	}

	public void setSecurityMetadataSource(
			FilterInvocationSecurityMetadataSource securityMetadataSource) {
		this.securityMetadataSource = securityMetadataSource;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(FilterConfig arg0) throws ServletException {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.intercept.AbstractSecurityInterceptor
	 * #getSecureObjectClass()
	 */
	@Override
	public Class<?> getSecureObjectClass() {
		// 下面的MyAccessDecisionManager的supports方面必须放回true,否则会提醒类型错误
		return FilterInvocation.class;
	}

}

 MySecurityMetadataSource.java:

 

 

/**
 * 
 */
package com.simonsw.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

import com.simonsw.base.entity.Role;
import com.simonsw.base.entity.RoleResource;
import com.simonsw.base.entity.UserRole;
import com.simonsw.base.entity.Users;
import com.simonsw.base.service.RoleResourceService;
import com.simonsw.base.service.UserRoleService;
import com.simonsw.base.service.UserService;

/**
 * @author Simon Lv
 * @since Oct 31, 2013
 */
public class MySecurityMetadataSource implements
		FilterInvocationSecurityMetadataSource {
	@Autowired
	protected UserService userService;
	@Autowired
	protected UserRoleService userRoleService;
	@Autowired
	protected RoleResourceService roleResourceService;

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

	private static LinkedHashMap<String, Collection<ConfigAttribute>> resourceMap = null;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.SecurityMetadataSource#getAttributes
	 * (java.lang.Object)
	 */
	@Override
	public Collection<ConfigAttribute> getAttributes(Object object)
			throws IllegalArgumentException {
		String requestURL = ((FilterInvocation) object).getRequestUrl();

		logger.debug("[MySecurityMetadataSource] 请求地址 ===> "
				+ ((FilterInvocation) object).getRequestUrl());
		if (null == resourceMap) {
			loadResourceDefine();
			logger.debug("[MySecurityMetadataSource] 我需要的认证 ==> "
					+ resourceMap.toString());
		}
		for (Map.Entry<String, Collection<ConfigAttribute>> entry : resourceMap
				.entrySet()) {
			logger.debug("[MySecurityMetadataSource] entry.getKey() ===> "
					+ entry.getKey());
			logger.debug("[MySecurityMetadataSource] request ===> " + requestURL);
			if (entry.getKey().equals(requestURL)) {
				return entry.getValue();
			}
		}
		return null;
	}

	/**
	 * Load all resource
	 */
	private void loadResourceDefine() {
		resourceMap = new LinkedHashMap<String, Collection<ConfigAttribute>>();
		Map<String, String> resource = getResource();
		for (Map.Entry<String, String> entry : resource.entrySet()) {
			Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
			configAttributes.add(new SecurityConfig(entry.getValue()));
			resourceMap.put(entry.getKey(),
					configAttributes);
		}
	}

	/**
	 * 加载所有资源与权限的关系
	 * 
	 * @return
	 */
	private Map<String, String> getResource() {
		Map<String, String> resourceMap = new HashMap<String, String>();
		List<Users> users = userService.findAll();
		List<UserRole> userRoles;
		Role role;
		List<RoleResource> roleResources;
		userfor: for (Users user : users) {
			logger.debug("userId => " + user.getUserid());
			userRoles = userRoleService.getUserRoleByUserId(user);
			if (userRoles == null) {
				logger.debug("userfor => break!!!!");
				break userfor;
			}
			rolefor: for (UserRole userRole : userRoles) {
				role = userRole.getRole();
				roleResources = roleResourceService.getUserRoleByRoleId(role);
				if (roleResources == null) {
					break rolefor;
				}
				for (RoleResource roleResource : roleResources) {
					String url = roleResource.getResource().getValue();
					if (!resourceMap.containsKey(url)) {
						resourceMap.put(url, role.getName());
					} else {
						String roleName = resourceMap.get(url);
						resourceMap.put(url, roleName + "," + role.getName());
					}
				}
			}
		}
		return resourceMap;

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.access.SecurityMetadataSource#
	 * getAllConfigAttributes()
	 */
	@Override
	public Collection<ConfigAttribute> getAllConfigAttributes() {
		// TODO Auto-generated method stub
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.access.SecurityMetadataSource#supports(java
	 * .lang.Class)
	 */
	@Override
	public boolean supports(Class<?> arg0) {
		// TODO Auto-generated method stub
		return true;
	}

}

 MyUsernamePasswordAuthenticationFilter.java:

 

 

/**
 * 
 */
package com.simonsw.security;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.simonsw.base.entity.Users;
import com.simonsw.base.service.UserService;
import com.simonsw.common.util.CipherUtil;
import com.simonsw.common.util.StringUtils;

/**
 * @author Simon Lv
 * @since Oct 31, 2013
 */
public class MyUsernamePasswordAuthenticationFilter extends
		UsernamePasswordAuthenticationFilter {
	@Autowired
	protected UserService userService;
	
	private Logger logger = LoggerFactory.getLogger(getClass());

	public static final String VALIDATE_CODE = "validateCode";
	public static final String USERNAME = "username";
	public static final String PASSWORD = "password";

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		if (!request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: "
							+ request.getMethod());
		}

		// checkValidateCode(request);

		String username = obtainUsername(request);
		logger.debug("[MyUsernamePasswordAuthenticationFilter] username ==> "+ username);
		String password = CipherUtil.generatePassword(obtainPassword(request));
		logger.debug("[MyUsernamePasswordAuthenticationFilter] password ==> "+ password);
		
		// 验证用户账号与密码是否对应
		username = username.trim();

		Users users = userService.getUserByName(username);

		if (users == null || !users.getPassword().equals(password)) {
			throw new AuthenticationServiceException(
					"password or username is notEquals");
		}

		// UsernamePasswordAuthenticationToken实现 Authentication
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		// Place the last username attempted into HttpSession for views

		// 允许子类设置详细属性
		setDetails(request, authRequest);

		// 运行UserDetailsService的loadUserByUsername 再次封装Authentication
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	protected void checkValidateCode(HttpServletRequest request) {
		HttpSession session = request.getSession();

		String sessionValidateCode = obtainSessionValidateCode(session);
		// 让上一次的验证码失效
		session.setAttribute(VALIDATE_CODE, null);
		String validateCodeParameter = obtainValidateCodeParameter(request);
		if (StringUtils.isEmpty(validateCodeParameter)
				|| !sessionValidateCode.equalsIgnoreCase(validateCodeParameter)) {
			throw new AuthenticationServiceException("validateCode.notEquals");
		}
	}

	private String obtainValidateCodeParameter(HttpServletRequest request) {
		Object obj = request.getParameter(VALIDATE_CODE);
		return null == obj ? "" : obj.toString();
	}

	protected String obtainSessionValidateCode(HttpSession session) {
		Object obj = session.getAttribute(VALIDATE_CODE);
		return null == obj ? "" : obj.toString();
	}

	@Override
	protected String obtainUsername(HttpServletRequest request) {
		Object obj = request.getParameter(USERNAME);
		return null == obj ? "" : obj.toString();
	}

	@Override
	protected String obtainPassword(HttpServletRequest request) {
		Object obj = request.getParameter(PASSWORD);
		return null == obj ? "" : obj.toString();
	}
}

具体的security.xml配置信息:

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

	<global-method-security pre-post-annotations="enabled" />

	<!-- 此目录下不需要过滤 -->
	<http pattern="/js/**" security="none" />
	<http pattern="/css/**" security="none" />
	<http pattern="/images/**" security="none" />
	<http pattern="/login" security="none" />

	<http use-expressions="true" entry-point-ref="authenticationProcessingFilterEntryPoint">
		<!-- if no limit, go to 403 page -->
		<access-denied-handler error-page="/403" />
		<!-- Logout -->
		<logout invalidate-session="true" logout-url="/logout"
			success-handler-ref="myLogoutSuccessHandler" />
		<!-- 实现免登陆验证 -->
		<remember-me />
		<session-management invalid-session-url="/">
			<concurrency-control max-sessions="10"
				error-if-maximum-exceeded="true" />
		</session-management>

		<custom-filter ref="loginFilter" position="FORM_LOGIN_FILTER" />
		<custom-filter ref="securityFilter" before="FILTER_SECURITY_INTERCEPTOR" />
	</http>

	<!-- 登录验证器 -->
	<beans:bean id="loginFilter"
		class="com.simonsw.security.MyUsernamePasswordAuthenticationFilter">
		<!-- 处理登录 -->
		<beans:property name="filterProcessesUrl" value="/user/login"></beans:property>
		<beans:property name="authenticationSuccessHandler"
			ref="loginLogAuthenticationSuccessHandler"></beans:property>
		<beans:property name="authenticationFailureHandler"
			ref="simpleUrlAuthenticationFailureHandler"></beans:property>
		<beans:property name="authenticationManager" ref="myAuthenticationManager" />
	</beans:bean>

	<beans:bean id="loginLogAuthenticationSuccessHandler"
		class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
		<beans:property name="defaultTargetUrl" value="/"></beans:property>
	</beans:bean>
	<beans:bean id="simpleUrlAuthenticationFailureHandler"
		class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<beans:property name="defaultFailureUrl" value="/"></beans:property>
	</beans:bean>

	<!-- 认证过滤器 -->
	<beans:bean id="securityFilter" class="com.simonsw.security.MySecurityFilter">
		<!-- 用户拥有的权限 -->
		<beans:property name="authenticationManager" ref="myAuthenticationManager" />
		<!-- 用户是否拥有所请求资源的权限 -->
		<beans:property name="accessDecisionManager" ref="myAccessDecisionManager" />
		<!-- 资源与权限对应关系 -->
		<beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" />
	</beans:bean>

	<!-- 实现了UserDetailsService的Bean -->
	<authentication-manager alias="myAuthenticationManager">
		<authentication-provider user-service-ref="myAuthenticationManagers" />
	</authentication-manager>

	<beans:bean id="myAccessDecisionManager"
		class="com.simonsw.security.MyAccessDecisionManager"></beans:bean>

	<beans:bean id="mySecurityMetadataSource"
		class="com.simonsw.security.MySecurityMetadataSource">
	</beans:bean>
	<beans:bean id="myAuthenticationManagers"
		class="com.simonsw.security.MyAuthenticationManager">
	</beans:bean>

	<beans:bean id="myLogoutSuccessHandler" class="com.simonsw.security.MyLogoutSuccessHandler" />

	<!-- 未登录的切入点 -->
	<beans:bean id="authenticationProcessingFilterEntryPoint"
		class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<beans:property name="loginFormUrl" value="/login"></beans:property>
	</beans:bean>

</beans:beans>

 最后一定要在web.xml中配置加上:

<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring-security.xml</param-value>
	</context-param>

 

    <!-- Spring Security Session -->
    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
     
	<!-- Spring security Filter -->
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

 启动Tomcat,登录正常工作。

 

具体想看源代码,请访问GitHub网站上的源代码:

https://github.com/simon5408/DummyProj

 

顺便说一下,得到源代码以后,需要数据库支持,具体的数据库结构及数据请看附件。

 

   发表时间:2013-11-24   最后修改:2013-11-24
发现现在spring官方文档组织得挺混乱的!我想找下关于spring ldap的文档,循着网络找了个 http://projects.spring.io/spring-ldap/,一看!没想到是spring ldap 2.0的。现在实际上spring ldap 3.x了!
搂住你知道怎么找到spring ldap 3.0的源码和软件包吗?
0 请登录后投票
   发表时间:2013-11-26  
有哪些jar包啊
0 请登录后投票
论坛首页 入门技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics