spring boot整合spring security和jwt实现分布式认证和权限处理

spring security是spring官方比较推荐的用于认证和权限的解决方案,市面上做认证和权限的开发框架还有shiro,spring boot也提供了shiro的解决方案,本人在之前的开发中大多使用了shiro,但是spring security也是值得一学的,除了官方推荐的原因像spring oauth2.0也会用到它,所以学习一下还是很有必要的,本次将围绕spring security的认证授权、jwt进行学习分享

1 依赖的引入

<!-- springsecurity的依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--令牌-->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

2 编写认证授权的处理类,主要用于未登录、没有权限的信息的处理逻辑

package com.debug.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 处理没有权限的类
 */
@Component
public class RestAuthAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        Map<String, String> map = new HashMap<>(2);
        map.put("code", "403");
        map.put("msg", "你没有操作权限");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));
    }
}
package com.debug.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定401返回值
 */
@Component
public class RestAuthUnauthorizedHandler implements AuthenticationEntryPoint {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        Map<String, String> map = new HashMap<>(2);
        map.put("code", "401");
        map.put("msg", "请先进行认证");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));
    }

}

3  编写springsecurity的主要配置类

该配置类主要编写security的相关配置如哪些资源需要经过security处理、认证和授权的逻辑、认证或授权失败的处理、认证密码的加密配置等

需要注意的是要开启bean的重写,原因是有的spring boot版本bean的重写默认是不允许的(如本人使用的2.1.3版本),该配置写在spring节点下面

  main:
    allow-bean-definition-overriding: true

下面是该配置类的全部代码,先上代码后面再解释与之关联的类

package com.debug.security;

import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityUserService securityUserService;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private RestAuthUnauthorizedHandler restAuthUnauthorizedHandler;

    @Autowired
    private RestAuthAccessDeniedHandler restAuthAccessDeniedHandler;

  

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(this.securityUserService).passwordEncoder(new BCryptPasswordEncoder());
    }

    /***注入自定义的CustomPermissionEvaluator*/
    @Bean
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new CustomPermissionEvaluator());

        return handler;
    }

    /*@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //这里可启用我们自己的登陆验证逻辑
        auth.authenticationProvider(authenticationProvider);
    }*/

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                .antMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(restAuthUnauthorizedHandler)
                .and()
                //配置没有权限的自定义处理类
                .exceptionHandling().accessDeniedHandler(restAuthAccessDeniedHandler)
                .and().headers().cacheControl();
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);

    }
}

其中SecurityUserServiceImpl主要用于处理认证、权限分配、刷新token等,代码如下:

package com.debug.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.debug.entity.TSystemPermission;
import com.debug.entity.TSystemRole;
import com.debug.entity.TSystemUser;
import com.debug.jwt.JwtTokenUtil;
import com.debug.security.MyAuthenticationProvider;
import com.debug.service.SecurityUserService;
import com.debug.service.TSystemPermissionService;
import com.debug.service.TSystemRoleService;
import com.debug.service.TSystemUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SecurityUserServiceImpl implements SecurityUserService {

    @Autowired
    private TSystemUserService tSystemUserService;


    @Autowired
    private TSystemRoleService tSystemRoleService;

    @Autowired
    private TSystemPermissionService tSystemPermissionService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;


    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<TSystemUser> qw = new QueryWrapper<TSystemUser>();
        qw.eq("login_name", username);
        TSystemUser user = tSystemUserService.getOne(qw);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<TSystemPermission> permissionList = tSystemPermissionService.getRolePermission(user.getId(), "1");
        List<TSystemRole> roleList = tSystemRoleService.getUserRole(user.getId());

        String roleName = roleList.get(0).getName();

        StringBuffer buf = new StringBuffer();
        for (TSystemPermission permission : permissionList) {
            String per = permission.getPermission();
            buf.append(per + ",");

        }

        String sp = buf.toString().substring(0, buf.toString().lastIndexOf(","));
        List<GrantedAuthority> authList = AuthorityUtils.commaSeparatedStringToAuthorityList(roleName + "," + sp);

        UserDetails u = new User(user.getLoginName(), user.getPassword(), authList);

        return u;
    }

    public String login(String username, String password) {

        QueryWrapper<TSystemUser> qw = new QueryWrapper<TSystemUser>();
        qw.eq("login_name", username);
        TSystemUser user = tSystemUserService.getOne(qw);

        // 这里我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
        if (!new BCryptPasswordEncoder().matches(password, user.getPassword())) {
            throw new BadCredentialsException("密码不正确");
        }

        return jwtTokenUtil.generateToken(username);
    }

    public String refreshToken(String oldToken) {
        String token = oldToken;
        if (!jwtTokenUtil.isTokenExpired(token)) {
            return jwtTokenUtil.refreshToken(token);
        }
        return "error";
    }
}

考虑到分布式代码环境,请求大多来源于其他项目或者微服务,所以我们需要通过过滤器来处理请求,下面是过滤器的代码

package com.debug.security;

import com.debug.jwt.JwtTokenUtil;
import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Token过滤器.
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private SecurityUserService securityUserService;

    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    public JwtAuthenticationTokenFilter(SecurityUserService securityUserService, JwtTokenUtil jwtTokenUtil) {
        this.securityUserService = securityUserService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
       
        if (authHeader != null) {
            String authToken = authHeader;
            String username = jwtTokenUtil.getUsernameFromToken(authToken);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.securityUserService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {

                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                }
            }
        }
        chain.doFilter(request, response);
    }

}

过滤器对token进行校验只有token正确才能通过过滤器

到此处为止用户虽然可以认证了,但是程序仍然不知道权限的验证逻辑,所以我们需要编写权限的验证逻辑类,如下所示

package com.debug.security;

import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Collection;

/**
 * 我们需要自定义对hasPermission()方法的处理,
 * 就需要自定义PermissionEvaluator,创建类CustomPermissionEvaluator,实现PermissionEvaluator接口。
 */
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {


    @Autowired
    private SecurityUserService securityUserService;

    /**
     * 自定义验证方法
     *
     * @param authentication     登录的时候存储的用户信息
     * @param targetDomainObject @PreAuthorize("hasPermission('/hello/**','r')") 中hasPermission的第一个参数
     * @param permission         @PreAuthorize("hasPermission('/hello/**','r')") 中hasPermission的第二个参数
     * @return
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {

        System.out.println("=======" + permission);
        // 获得loadUserByUsername()方法的结果
        //TSystemUser user = (TSystemUser) authentication.getPrincipal();

        // 获得loadUserByUsername()中注入的权限
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        // 遍历用户权限进行判定
        for (GrantedAuthority authority : authorities) {

            // 如果访问的Url和权限用户符合的话,返回true
            String permissionUrl = authority.getAuthority();

            if (permission.equals(permissionUrl)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

到此大部分功能就完成了但是登录认证并获取token的逻辑还未实现,所以我们需要一个controller来处理相关业务逻辑

package com.debug.controller;

import com.debug.service.SecurityUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private SecurityUserService securityUserService;

    /**
     * 用户登录
     *
     * @param username 用户名
     * @param password 密码
     * @return 操作结果
     * @throws AuthenticationException 错误信息
     */
    @PostMapping(value = "/login", params = {"username", "password"})
    public String getToken(String username, String password) throws AuthenticationException {
        return securityUserService.login(username, password);
    }
}

4  jwt工具类和RSA工具类

我们知道jwt由三部分组成分别是头信息、载荷、签名,最重要的部分则是签名,我们使用jwt生成token就需要这个签名,签名的方式则使用RSA非对称加密,下面先来看看RSA的加解密原理:

私钥加密:持有公钥或私钥可以解密

公钥加密:持有私钥才可以解密

从上我们可以总结出私钥可以对上述两种方式进行解密,考虑到安全性如token被篡改的可能,服务端需要使用私钥,请求端只能持有公钥;服务端用私钥加密,请求端则使用公钥解密

因此我们需要一个RSA工具类,一个jwt工具类

package com.debug.jwt;

import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@Component
public class RsaUtils {
    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }

    /**
     * 测试RSAUtils工具类的使用
     *
     * @param args
     */
    /*public static void main(String[] args) throws Exception {

        String pubKeyPath = "D:\\rsa\\rsa_public_key.pem";
        String priKeyPath = "D:\\rsa\\rsa_private_key.pem";

        String secret = "sc@Login(Auth}*^31)&czxy%";

        RsaUtils util=new RsaUtils();
        util.generateKey(pubKeyPath, priKeyPath, secret);


        System.out.println("ok");



        PublicKey publicKey = util.getPublicKey(pubKeyPath);
        System.out.println("公钥:" + publicKey);


        PrivateKey privateKey = util.getPrivateKey(priKeyPath);
        System.out.println("私钥:" + privateKey);

    }*/
}

这个工具类可以生成公钥和私钥的pem文件,后面的jwt工具类需要这两个文件进行加解密

package com.debug.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 *
 */
@Component
public class JwtTokenUtil implements Serializable {

    @Autowired
    private RsaUtils rsaUtils;

    @Value("${myjwt.pubKeyPath}")
    private String pubKeyPath;
    @Value("${myjwt.priKeyPath}")
    private String priKeyPath;

    //过期时间-单位为秒(默认30分钟)
    private Long overTime = 1800L;

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) throws Exception {
        Date expirationDate = new Date(System.currentTimeMillis() + overTime * 1000);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.RS256, rsaUtils.getPrivateKey(priKeyPath)).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(rsaUtils.getPublicKey(pubKeyPath)).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userName 用户名
     * @return 令牌
     */
    public String generateToken(String userName) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userName);
        claims.put("created", new Date());
        try {
            return generateToken(claims);
        } catch (Exception e) {
            return null;
        }

    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }

}

5 写一个测试用controller,加上权限注解可以是jsr的也可以使用spring security提供的

package com.debug.controller;

import com.debug.entity.TEducationLevel;
import com.debug.service.TEducationLevelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TEducationLevelService tEducationLevelService;


    //@PreAuthorize("hasPermission('/test/list/**','r')")
    //@PreAuthorize("hasPermission('/test/list/**','ROLE_ADMIN')")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @RequestMapping("/list")
    @ResponseBody
    public List<TEducationLevel> list() {

        return tEducationLevelService.list();
    }
}

先不登录直接访问看看:

请求头加上token则可以正常访问

如果使用其他普通角色登录并带上token,访问则提示没有操作权限:

到此spring boot就完成了和spring security和jwt的整合,但如果要做成更切合实际的单点登录,还需要借助redis,例如把token持久化到redis并设置过期时间,关于单点登录大家可以自行查阅资料

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值