Spring security搭建与使用

Spring security权限管理


意义:

Spring security是一个为基于Spring的应用系统提供一个声明式的访问控制的安全框架,

提供了一组在Spring应用上下文中配置的Bean,利用IOCDIAOP等功能,

减少了为企业应用系统安全控制编写大量重复代码的工作

 

原理:

Spring security 使用的是servlet 规范中标准的过滤器机制,通过提供多个过滤器,拦截servlet请求,并在应用程序处理该请求之前将这些请求转给认证和访问决策管理器进行安全处理

 

用法:

配置文件实现,只需要在配置文件中指定拦截的url所需要的权限、配置userDetailsService指定用户名、密码、对应权限,就可以实现

使用数据库存储,实现UserDetailsServiceloadUserByUsername(String userName)方法,根据userName来实现自己的业务逻辑返回UserDetails的实现类

通过自定义filter重写Spring security拦截器,实现动态过滤用户权限

修改Spring security的源代码,实现自定义参数检验用户,并且过滤权限


工作流程图:


该图引自(http://blog.csdn.net/code__code/article/details/53885510)



Spring security的使用(基于spring security4.2.3):

首先是web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <display-name>demo-security</display-name>
  <!-- 初始化spring容器 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring/applicationContext-*.xml</param-value>
	</context-param>
	
	<!-- 配置Spring Security -->  
    <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>
    
    <!-- spring容器监听器 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
	<!-- 解决post乱码 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>utf-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<filter>
    	<filter-name>HiddenHttpMethodFilter</filter-name>
    	<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
	</filter>
	<filter-mapping>
    	<filter-name>HiddenHttpMethodFilter</filter-name>
    	<!-- 备注,这边的名称必须和配置'springmvc'的servlet名称一样 -->
    	<servlet-name>demo-security</servlet-name>    
	</filter-mapping>
	
	<!-- springmvc的前端控制器 -->
	<servlet>
		<servlet-name>demo-security</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring/springmvc.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>demo-security</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
</web-app>

Spring security的配置文件applicationContext-security.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"  
        xmlns:b="http://www.springframework.org/schema/beans" xmlns:beans="http://www.springframework.org/schema/beans"  
        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-4.2.xsd  
                            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd">  
      
    <!--登录页面不过滤 -->
    <http pattern="/login.jsp" security="none" />
    <!-- 静态资源不过滤 -->
    <http pattern="/js/**" security="none" />
    <http pattern="/favicon.ico" security="none" />
    
    <http auto-config="true">
    	
	<!-- 页面设置可访问role,比如访问admin.jsp页面需要ROLE_ADMIN权限 -->
    	<intercept-url pattern="/index.jsp" access="hasAnyRole('ADMIN','USER')"/>
    	<intercept-url pattern="/admin.jsp" access="hasRole('ADMIN')"/>
    	<intercept-url pattern="/user.jsp" access="hasAnyRole('ADMIN','USER')"/>
        <form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" 
        authentication-failure-url="/login.jsp?error=true" />
     	
     	<!-- 权限不足页面 -->
     	<access-denied-handler error-page="/accessDenied.jsp"/>

        <!-- 用户退出的跳转页面 -->     
        <logout invalidate-session="true" logout-success-url="/login.jsp"/>
        
        <!-- SESSION超时后跳转到该页面 -->  
        <session-management invalid-session-url="/login.jsp" />  
        
        <!-- session-management防止多用户同时登录一个账号 -->
        <session-management>  
            <concurrency-control max-sessions="1" error-if-maximum-exceeded="false" />  
        </session-management>
        
        <!--自定义一个filter,这点与 Acegi是不一样的,不能修改默认的filter了, 这个filter位于FILTER_SECURITY_INTERCEPTOR之前 -->  
        <custom-filter ref="myFilter" before="FILTER_SECURITY_INTERCEPTOR" />  
        
        <!-- 关闭csrf -->
        <csrf disabled="true"/>
    </http>
    
    <!--一个自定义的filter,必须包含 authenticationManager,accessDecisionManager,securityMetadataSource三个属性,   
           我们的所有控制将在这三个类中实现,解释详见具体配置 -->  
    <b:bean id="myFilter" class="com.security.MyFilterSecurityInterceptor">  
        <b:property name="authenticationManager" ref="authenticationManager" /><!-- 登陆认证 -->  
        <b:property name="accessDecisionManager" ref="myAccessDecisionManagerBean" /><!-- 资源访问决策 --> 
        <b:property name="securityMetadataSource" ref="securityMetadataSource" /><!-- 资源和权限列表 -->  
    </b:bean>
    
   <!--  验证配置,认证管理器,实现用户认证的入口,主要实现UserDetailsService接口即可   -->
    <authentication-manager alias="authenticationManager">  
        <authentication-provider user-service-ref="myUserDetailService">  
           <!-- 1.如果用户的密码采用加密的话 <password-encoder hash="md5" />   -->
           <!-- 2.自定义加密器 -->
           <password-encoder ref="myPasswordEncoder" />
        </authentication-provider>  
    </authentication-manager>
    
    <!-- 加载自定义加密器 -->
    <b:bean id="myPasswordEncoder" class="com.security.MyPasswordEncoder" />
    
    <!-- 在这个类中,你就可以从数据库中读入用户的密码,角色信息,是否锁定,账号是否过期等   -->
    <b:bean id="myUserDetailService" class="com.security.MyUserDetailService" />
    
    <!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源   -->
    <b:bean id="myAccessDecisionManagerBean" class="com.security.MyAccessDecisionManager" />  

    <!-- 资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色访问   -->
    <b:bean id="securityMetadataSource" class="com.security.MyInvocationSecurityMetadataSource"/>
    	
</beans:beans>   

Spring security功能实现:

1.MyFilterSecurityInterceptor:拦截器链控制

2.MyInvocationSecurityMetadataSourceService:权限资源加载以及判断url是否需要进入权限认证

3.MyUserDetailsService:用户相关信息查询及存储

4.MyAccessDecisionManager:权限认证

5.MyPasswordEncoder:密码验证


MyUserDetailsService:

package com.security;

import java.util.ArrayList;
import java.util.List;

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 org.springframework.stereotype.Service;

import com.dao.AdminDao;
import com.pojo.Admin;
import com.pojo.Role;
import com.service.AdminService;

public class MyUserDetailService implements UserDetailsService{
	
	@Autowired
	private AdminDao adminDao;
	
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		System.err.println("进入UserDetailService:"+username);
		Admin admin = adminDao.findAdminByAdminName(username);
		if(admin != null){
			List<GrantedAuthority> grantedAuthorities = new ArrayList <GrantedAuthority>();
			List<Role> rolelist = adminDao.findRoleByAdminId(admin.getAdminId());
			if(rolelist != null){
				for(Role role:rolelist){
					System.out.println("role:"+role.getRoleName());
					GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
					grantedAuthorities.add(grantedAuthority);
				}
			}
			User user = new User(String.valueOf(admin.getAdminId()), admin.getPassword(), grantedAuthorities);
			System.out.println("user:"+user);
			return user;
			
		}
		throw new UsernameNotFoundException("该用户不存在");
	}

}
通过实现UserDetailsService接口,重写loadUserByUsername(String username)这个方法来返回一个UserDetails实现类


MyFilterSecurityInterceptor:

package com.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.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;
import org.springframework.stereotype.Service;

public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter{
	
	
	private FilterInvocationSecurityMetadataSource securityMetadataSource;
	
	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {    
        return this.securityMetadataSource;    
    }
	
	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource){ 
		this.securityMetadataSource = newSource;   
	}
	
	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 {
		//fi里面有一个被拦截的url
		//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
		//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
		InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
        	//执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
	}
	
	
	public void init(FilterConfig filterConfig) throws ServletException {
		
	}

	public void destroy() {
		
	}

	@Override
	public Class<?> getSecureObjectClass() {
		return FilterInvocation.class;
	}

	@Override
	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}


   
	
}

在用户登录之后,每次访问资源都会被这个拦截器拦截,它会调用MyInvocationSecurityMetadataSourceService中的getAttributes方法,来判断当前url是否需要权限认证,若需要,则会调用MyAccessDecisionManager的decide方法判断用户是否拥有此权限。


MyInvocationSecurityMetadataSource:

package com.security;

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

import javax.servlet.http.HttpServletRequest;

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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

import com.dao.AdminDao;
import com.pojo.Power;
import com.pojo.Role;

@Service
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource{
	
	@Autowired
	private AdminDao adminDao;
	
	private HashMap<String,Collection<ConfigAttribute>> map = null;//用于存放所有role与其所对应的权限资源
	
	private void loadResourceDefine(){
		System.err.println("资源加载");
		map = new HashMap<String, Collection<ConfigAttribute>>();
		Collection<ConfigAttribute> array;
		ConfigAttribute cfg;
		
		List<Power> powerlist = adminDao.findAllPower();
		if(powerlist != null){
			for(Power power:powerlist){
				array = new ArrayList<ConfigAttribute>();
				List<Role> rolelist = adminDao.findRoleByPowerId(power.getPowerId());
				if(rolelist != null){
					for(Role role:rolelist){
						cfg = new SecurityConfig(role.getRoleName());
						array.add(cfg);
					}
				}
				map.put(power.getUrl(), array);
			}
		}
	}
	//参数是要访问的url,返回的是这个url所对应的role或权限集合
	public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
		
		if(map == null) loadResourceDefine();
		
		HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
		AntPathRequestMatcher matcher;
		String resUrl;
		
		for(Iterator<String> iter = map.keySet().iterator();iter.hasNext();){
			resUrl = iter.next();
			if(request.getRequestURI().contains(resUrl)){
				System.out.println("需要进行权限匹配:"+map.get(resUrl));
				return map.get(resUrl);
			}
		}
		System.out.println("URL:"+request.getRequestURI());
		return null;
	}

	public Collection<ConfigAttribute> getAllConfigAttributes() {
		// TODO Auto-generated method stub
		return null;
	}

	public boolean supports(Class<?> clazz) {
		// TODO Auto-generated method stub
		return true;
	}
}

实现FilterInvocationSecurityMetadataSource接口,loadResourceDefine这个方法是为了获取所有role以及相对应的资源权限,并缓存起来,避免每次访问资源都需要查询一次数据库(提升性能),然后getAttributes根据参数(被拦截url)返回权限集合,在这个类中,也可以自己添加url的正则表达式用以验证该url的合法性。


MyAccessDecisionManager:

package com.security;

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

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;
import org.springframework.stereotype.Service;

public class MyAccessDecisionManager implements AccessDecisionManager{
	
	public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException {
	if(configAttributes == null || configAttributes.size() <1) {
            return;
        }
	//configAttributes存放的是当前访问url所对应的role或权限集合
	System.out.println("configAttributes:"+configAttributes.toString());
	ConfigAttribute c;
        String needRole;
        for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            System.out.println("needRole:"+needRole);
            for(GrantedAuthority ga : authentication.getAuthorities()) {
            	System.out.println("ga:"+ga.toString());
            	if(needRole.equals(ga.getAuthority())){
            		System.err.println("权限匹配通过");
            		return;
            	}else{
            		System.out.println("权限匹配失败");
            	}
            }
        }
        throw new AccessDeniedException("权限不足");
	}

	public boolean supports(ConfigAttribute attribute) {
		// TODO Auto-generated method stub
		return true;
	}

	public boolean supports(Class<?> clazz) {
		// TODO Auto-generated method stub
		return true;
	}
}

实现AccessDecisionManager接口,decide方法就是权限控制方法,url需要什么样的权限,就需要看需求了。通过就返回,不通过直接抛出异常就可以了。spring security会自动跳转到权限不足页面的


这样,spring security流程就结束了

下面是页面以及数据库的相关代码

login.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>  
<html>  
<head>  
<title>登录</title> 
<script type="text/javascript" src="./js/jquery-1.9.1.js"></script> 
</head>  
<body>  
    <form action ="login" method="POST">  
    <table>  
        <tr>  
            <td>用户:</td>  
            <td><input type ='text' name='username'></td>  
        </tr>  
        <tr>  
            <td>密码:</td>  
            <td><input type ='password' name='password'></td>  
        </tr>  
        <tr>  
            <td><input name ="reset" type="reset"></td>  
            <td><input name ="submit" type="submit"></td>  
        </tr>  
    </table>  
    </form> 
</body>  
</html>
index.jsp:
<%@page language="java" import="java.util.*" pageEncoding="UTF-8"%>   
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>   
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">  
 
<html>  
 <script type="text/javascript" src="/js/jquery-1.9.1.js"></script>
<head>  

<title>My JSP 'index.jsp' starting page</title>   
</head>  
 
<body>  
      <h3>这是首页</h3>欢迎 
      <a href="/logout">安全退出</a>
      <sec:authorize access="hasRole('ROLE_ADMIN')">
 	  	<P>Admin</P>
 	  </sec:authorize>
      <sec:authorize access="hasRole('ROLE_USER')">
 	  	<P>User</P>
 	  </sec:authorize>
 	
    <a href="admin.jsp">进入admin页面</a>   
    <a href="user.jsp">进入user页面</a>
    
    </br>
    
    <div>
    	<input type="button" value="增加" οnclick="add()"  />
    </div>
    <div>
    	<input type="button" value="删除" οnclick="del()" />
    </div>
    <div>
    	<input type="button" value="修改" οnclick="update()" />
    </div>
    <div>
    	<input type="button" value="查看" οnclick="search()" />
    </div>
    
    <script>
    function add(){
    	$.ajax({
    		type:"POST",
    		url:"/add",
    		success:function(data){
    			console.log(data);
    			if(data.status == 200){
    				alert("增加成功");
    			}else if(data.status == 400){
    				alert("增加失败");
    			}else if(data == "fail"){
    				alert("权限不足");
    			}
    			//alert(data.status);
    		}
    	})
    }
    function del(){
    	$.ajax({
    		type:"POST",
    		url:"/delete",
    		success:function(data){
    			if(data.status == 200){
    				alert("增加成功");
    			}else if(data.status == 400){
    				alert("增加失败");
    			}else if(data == "fail"){
    				alert("权限不足");
    			}
    			//alert(data.status);
    		}
    	})
    }
    function update(){
    	$.ajax({
    		type:"POST",
    		url:"/update",
    		success:function(data){
    			alert(data.status);
    		}
    	})
    }
    function search(){
    	$.ajax({
    		type:"POST",
    		url:"/search",
    		success:function(data){
    			alert(data.status);
    		}
    	})
    }
    </script>
    
</body>  
 
</html>
admin.jsp:
<%@page language="java" import="java.util.*" pageEncoding="utf-8"%>  
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">  
<html>  
<head>  
<title>My JSP 'admin.jsp' starting page</title>  
</head>  
<body>  
    欢迎来到Admin页面.  
       
</body>  
</html>
user.jsp:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>  
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">  
<html>  
  <head>  
 
    <title>My JSP 'user.jsp' starting page</title>  
 
  </head>  
 
  <body>  
    <h3>这里是User页面</h3>  
  </body>  
</html>
accessDenied.jsp:
<%@page language="java" import="java.util.*" pageEncoding="utf-8"%>  
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">  
<html>  
<head>  
<title>My JSP 'accessDenied.jsp' starting page</title>  
</head>  
<body>  
    <h1>权限不足</h1>
       
</body>  
</html>

表admin:

CREATE TABLE `admin` (
  `adminId` int(11) NOT NULL auto_increment,
  `adminName` varchar(255) default NULL,
  `password` varchar(255) default NULL,
  PRIMARY KEY  (`adminId`)
)
INSERT INTO `admin` VALUES ('1', 'admin', 'admin');
INSERT INTO `admin` VALUES ('2', 'user', 'user');
表role:
CREATE TABLE `role` (
  `roleId` int(11) NOT NULL auto_increment,
  `roleName` varchar(255) default NULL,
  PRIMARY KEY  (`roleId`)
)
INSERT INTO `role` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `role` VALUES ('2', 'ROLE_USER');
表power:
CREATE TABLE `power` (
  `powerId` int(11) NOT NULL auto_increment,
  `url` varchar(255) default NULL,
  PRIMARY KEY  (`powerId`)
)
INSERT INTO `power` VALUES ('1', '/search');
INSERT INTO `power` VALUES ('2', '/add');
INSERT INTO `power` VALUES ('3', '/update');
INSERT INTO `power` VALUES ('4', '/delete');
关联表admin-role:
CREATE TABLE `admin-role` (
  `Id` int(11) NOT NULL auto_increment,
  `adminId` int(11) default NULL,
  `roleId` int(11) default NULL,
  PRIMARY KEY  (`Id`)
)
INSERT INTO `admin-role` VALUES ('1', '1', '1');
INSERT INTO `admin-role` VALUES ('2', '2', '2');
关联表role-power:
CREATE TABLE `role-power` (
  `id` int(11) NOT NULL auto_increment,
  `roleId` int(11) default NULL,
  `powerId` int(11) default NULL,
  PRIMARY KEY  (`id`)
)
INSERT INTO `role-power` VALUES ('1', '1', '1');
INSERT INTO `role-power` VALUES ('2', '1', '2');
INSERT INTO `role-power` VALUES ('3', '1', '3');
INSERT INTO `role-power` VALUES ('4', '1', '4');
INSERT INTO `role-power` VALUES ('5', '2', '1');



下载链接:http://download.csdn.net/download/zpgrzy/10130290,如无积分,可以Q513816084





  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值