Jwt入门教程:实战( 三) | 使用Springboot基于拦截器和redis的Jwt判断用户登录以及安全校验


使用Springboot基于拦截器和redis的Jwt判断用户登录以及安全校验


这里简单介绍用户登录解析Jwt token,从请求的session、以及redis中获取我们想要信息,再做具体业务操作。


不熟悉Jwt概念和用法的可以参考前面两篇:

看了前两篇,下面就不多说了,直接贴代码:(代码可能较长,因为附带了redis的配置,继续往下看。)

首先,准备工作:配置redis(下方redis的代码较长,大家斟酌,需要的可以copy,放在最后)

下面是正文的开始:


1.MVC配置拦截器
/**
 * MVC配置
 */
@Configuration
public class WebMvcConfig implements  WebMvcConfigurer  {
    @Autowired
    private TokenInterceptor tokenInterceptor;//自定义Token拦截器

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");//token拦截
    }
     
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("*")
                .allowedHeaders("Content-Type", "Authorization")
                .allowCredentials(false)
                .maxAge(3600);
    }
}


2.拦截器方法:(这里使用了redis)

import com.huangtu.common.util.user.JwtUtilsHelper;
import com.huangtu.config.RedisUtilsTwo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.impl.Base64Codec;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

/**
 * @author 慌途L
 */
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {

	//注入redis工具类
    @Autowired
    private RedisUtilsTwo redisUtilsTwo;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            String cookie = request.getHeader("cookie");
            System.out.println(cookie + "\n");

            /**
             * accessToken=eyJhbGciOiJIUzUxMiIsImlhdCI6MTU0MzE5OTg4MywiZXhwIjoxNTQzMjE0MjgzfQ.eyJ1c2VyX2lkIjoxMDAxLCJ5c2VyX25hbWUiOiJhZG1pbiIsImlwIjoiMTkyLjE2OC4xMC4yMDMifQ.-qXmphVPtcmO3PLmuQIiFL7khhKiDjwPZvGyxLbyBOkIMdtFJ7whLY0hSabbJwYH9Dcxmhf1tdPKN2jGSP5YBQ; JSESSIONID=6A769332782C0F436510214757C16063
             */
            //token最后还跟了sessionid,去除
            String[] split1 = cookie.split(";");

            /**
             * accessToken=eyJhbGciOiJIUzUxMiIsImlhdCI6MTU0MzE5OTg4MywiZXhwIjoxNTQzMjE0MjgzfQ.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VyX25hbWU5OiJhZG1pbiIsImlwIjoiMTkyLjE2OC4xMC4yMDMifQ.-qXmphVPtcmO3PLmuQIiFL7khhKiDjwPZvGyxLbyBOkIMdtFJ7whLY0hSabbJwYH9Dcxmhf1tdPKN2jGSP5YBQ
             */
            //得到accessToken
            String[] split = split1[0].split("=");
            System.out.println("accessToken"+split[1]);

            //验证token
            String login = verificationToken(split[1]);

            return true;
        } catch (Exception e){
            e.printStackTrace();
            return false;
        }

    }
    /**
     * 验证token
     *
     * @return
     */
    public String verificationToken(String jwt) {
        String[] splitJwt = jwt.split("\\.");
        String params = "";
        if (splitJwt.length == 3) {
            //从redis里面拿到当前用户的accessToken进行对比
            String accessToken = redisUtilsTwo.get("accessToken");
            if(StringUtils.isNotBlank(accessToken)){
                if(jwt.equals(accessToken)){
                    System.out.println("token验证通过");

                    String header = splitJwt[0];
                    String payload = splitJwt[1];
                    String signature = splitJwt[2];//带过来的签名
                    System.out.println(Base64Codec.BASE64URL.decodeToString(header));
                    System.out.println(Base64Codec.BASE64URL.decodeToString(payload));

                    //得到user数据--jwt的payload
                    Claims claims = JwtUtilsHelper.accessTonkenDecodePart(jwt);
                    if (!claims.isEmpty() && claims.size()>0) {
                        Jwt jwt1 = JwtUtilsHelper.accessTonkenDecodeAll(jwt);
                        JSONObject jsonObject = JSONObject.fromObject(jwt1.getHeader());
                        long iat = Integer.parseInt(jsonObject.get("iat").toString());
                        long exp = Integer.parseInt(jsonObject.get("exp").toString());

                        //登录时间和过期时间
                        System.out.println(iat+"||||"+exp);
                        if (!JwtUtilsHelper.isTokenExpired(exp)) {//判断token有效性
                            String userId = claims.get("user_id").toString();
                            String userName = claims.get("user_name").toString();
                            String ip = claims.get("ip").toString();
                            System.out.println("userId="+userId);
                            System.out.println("userName="+userName);
                            System.out.println("ip="+ip);
                            if ("169061".equals(userId) && "admin".equals(userName) && "192.168.110.555".equals(ip)) {
                                params = "token有效";//校验成功,数据完全一致
                            } else {
                                params = "用户id或用户名或ip地址有误";
                            }
                        } else {
                            params = "token过期";
                        }
                    } else {
                        params = "token的payload无数据";
                    }
                } else {
                    params = "token被篡改";
                }
            } else{
                params = "用户为登录";
            }
        } else {
            params = "Jwt Token格式错误";
        }
        return params;
    }

}


3.使用Postman请求:(用的是我项目的请求格式,大家随意),这里我放在请求头里面:使用名字cookie

在这里插入图片描述


![在这里插入图片描述](https://img-blog.csdnimg.cn/20181126204008148.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10


4.到这里,基本上就结束了,大家可以在拦截器里面debugger一步步走。

配置文件:

spring:
    redis:
        database: 0
        host: localhost
        port: 6379
        password: 123456     # 密码()
        timeout: 6000  # 连接超时时长(毫秒)
        pool:
            max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
            max-wait: -1      # 连接池最大阻塞等待时间(使用负值表示没有限制)
            max-idle: 10      # 连接池中的最大空闲连接
            min-idle: 5       # 连接池中的最小空闲连接
    cache:
        type: none

JedisPool:

/**
 * @author 慌途L
 */
@Configuration
@EnableCaching
public class RedisConfigTwo extends CachingConfigurerSupport {
    private Logger logger = Logger.getLogger(getClass());
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.pool.max-wait}")
    private long maxWaitMillis;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.database}")
    private int dataBase;

    @Bean
    public JedisPool redisPoolFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);

        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password,dataBase);
        return jedisPool;
    }
}

redis工具类:

import org.springframework.stereotype.Component;
import redis.clients.jedis.BinaryClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author 慌途L
 */
@Component
public class RedisUtilsTwo{
    @Resource
    private JedisPool jedisPool;

    /**
     * 获取数据
     * @param key
     * @return
     */
    public  String get(String key){
        String value=null;
        Jedis jedis=null;
        try{
            jedis=jedisPool.getResource();
            value=jedis.get(key);
        }catch (Exception e){
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        }finally {
            close(jedis);
        }
        return value;
    }


    public  void del(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.del(key);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }

    public  void set(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.set(key, value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }

    public  void set(String key, String value, int time) {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.set(key, value);
            jedis.expire(key, time);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }

    public  void hset(String key, String field, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.hset(key, field, value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }

    /**
     * 获取数据
     *
     * @param key
     * @return
     */
    public  String hget(String key, String field) {

        String value = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            value = jedis.hget(key, field);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }

        return value;
    }

    public  void hdel(String key, String field) {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.hdel(key, field);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }
    /**
     * 存储REDIS队列 顺序存储
     * @param  key reids键名
     * @param  value 键值
     */
    public  void lpush(String key, String value) {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.lpush(key, value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
    }

    /**
     * 移除队列中出现第一个数据
     * @param key
     * @param value
     * @return
     */
    public long lrem(String key, String value){
        Jedis jedis = null;
        long l =0 ;
        try{
            jedis = jedisPool.getResource();
            l = jedis.lrem(key,1,value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return l;
    }

    /**
     * 存储REDIS队列 反向存储
     * @param  key reids键名
     * @param  value 键值
     */
    public  long rpush(String key, String value) {
        Jedis jedis = null;
        long flage=0;
        try {
            jedis = jedisPool.getResource();
            flage = jedis.rpush(key, value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return flage;
    }

    /**
     * 存储REDIS队列 反向存储
     * @param  key reids键名
     * @param  value 键值
     */
    public  long rpush(String key, String value,int seconds) {
        Jedis jedis = null;
        long flage=0;
        try {
            jedis = jedisPool.getResource();
            flage = jedis.rpush(key, value);
            jedis.expire(key, seconds);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return flage;
    }

    /**
     * 在列表的元素前或者后插入元素,返回List的长度
     * @param key
     * @param where LIST_POSITION    LIST_POSITION.BEFORE之前添加;LIST_POSITION.AFTER之后添加
     * @param pivot 以该元素作为参照物,是在它之前,还是之后
     * @param value
     * @return Long
     */
    public long linsert(String key, BinaryClient.LIST_POSITION where, String pivot, String value){
        Jedis jedis = null;
        long flage=0;
        try {
            jedis = jedisPool.getResource();
            flage = jedis.linsert(key, where, pivot, value);
        } catch (Exception e) {
            flage = -1;
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return flage;
    }

    /**
     * 将一个或多个值插入到已存在的列表头部,当成功时,返回List的长度;当不成功(即key不存在时,返回0)
     * @param key
     * @param value String
     * @return Long
     */
    public long lpushx(String key, String value){
        Jedis jedis = null;
        long flage=0;
        try {
            jedis = jedisPool.getResource();
            flage = jedis.lpushx(key, value);
        } catch (Exception e) {
            flage = -1;
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return flage;
    }

    /**
     * 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端
     * @param  key reids键名
     * @param  destination 键值
     */
    public  void rpoplpush(String key, String destination) {

        Jedis jedis = null;
        try {

            jedis = jedisPool.getResource();
            jedis.rpoplpush(key, destination);

        } catch (Exception e) {

            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {

            //返还到连接池
            close(jedis);

        }
    }

    /**
     * 获取队列数据
     * @param  key 键名
     * @return
     */
    public  List lpopList(String key) {

        List list = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            list = jedis.lrange(key, 0, -1);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return list;
    }
    /**
     * 获取队列数据,返回并删除列表的第一个(LPOP)或最后一个(RPOP)元素。
     * @param  key 键名
     * @return
     */
    public String rpop(String key) {
        String bytes = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            bytes = jedis.rpop(key);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return bytes;
    }

    public  void hmset(Object key, Map hash) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.hmset(key.toString(), hash);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);

        }
    }
    public  void hmset(Object key, Map hash, int time) {
        Jedis jedis = null;
        try {

            jedis = jedisPool.getResource();
            jedis.hmset(key.toString(), hash);
            jedis.expire(key.toString(), time);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);

        }
    }
    public  List hmget(Object key, String... fields) {
        List result = null;
        Jedis jedis = null;
        try {

            jedis = jedisPool.getResource();
            result = jedis.hmget(key.toString(), fields);

        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);

        }
        return result;
    }

    public  Set hkeys(String key) {
        Set result = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.hkeys(key);

        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);

        }
        return result;
    }
    public  List lrange(String key, int from, int to) {
        List result = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.lrange(key, from, to);

        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);

        }
        return result;
    }
    public  Map hgetAll(String key) {
        Map result = null;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.hgetAll(key);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);
        }
        return result;
    }

    public  long llen(String key) {

        long len = 0;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.llen(key);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();
        } finally {
            //返还到连接池
            close(jedis);
        }
        return len;
    }

    /**
     * 修改list中指定位置的值
     * @param key
     * @param index
     * @param value
     * @return  正确返回ok
     */
    public String lset(String key,long index,String value){
        Jedis jedis = null;
        String s = null;
        try {
            jedis = jedisPool.getResource();
            s = jedis.lset(key,index,value);
        } catch (Exception e) {
            //释放redis对象
            jedisPool.returnBrokenResource(jedis);
            e.printStackTrace();

        } finally {
            //返还到连接池
            close(jedis);
        }
        return s;
    }

    private  void close(Jedis jedis) {
        try{
            jedisPool.returnResource(jedis);
        }catch (Exception e){
            if(jedis.isConnected()){
                jedis.quit();
                jedis.disconnect();
            }
        }
    }
}


结语:目前使用的就是这些啦,希望对大家有所帮助。后续会有在shiro中的使用,敬请期待!


欢迎关注公众号:慌途L
后面会慢慢将文章迁移至公众号,也是方便在没有电脑的情况下可以进行翻阅,更新的话会两边同时更新,大家不用担心!
在这里插入图片描述


  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实现用户手机号验证码登录可以分为以下几个步骤: 1. 用户输入手机号和验证码,点击登录按钮。 2. 后端接收到手机号和验证码后,先验证验证码是否正确。 3. 如果验证码正确,后端生成JWT token并将token存储到Redis中,同时将token返回给前端。 4. 前端将token存储到本地,以便后续请求时使用。 5. 后续请求时,前端需要在请求头中加入token,后端通过解析token来判断用户是否已登录。 下面是具体实现过程: 1. 在阿里云短信控制台创建短信模板,获取accessKeyId和accessKeySecret。 2. 在Spring Boot项目中添加依赖: ``` <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.0.3</version> </dependency> ``` 3. 实现发送短信验证码的接口: ``` @PostMapping("/sendSms") public Result sendSms(@RequestParam("phone") String phone) { // 生成随机验证码 String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000)); // 发送短信验证码 DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile); CommonRequest request = new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("RegionId", "cn-hangzhou"); request.putQueryParameter("PhoneNumbers", phone); request.putQueryParameter("SignName", "短信签名"); request.putQueryParameter("TemplateCode", "短信模板编号"); request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}"); try { CommonResponse response = client.getCommonResponse(request); // 将验证码存储到Redis中,有效期为5分钟 redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES); return Result.success("短信验证码发送成功"); } catch (Exception e) { return Result.error("短信验证码发送失败"); } } ``` 4. 实现用户手机号验证码登录的接口: ``` @PostMapping("/login") public Result login(@RequestParam("phone") String phone, @RequestParam("code") String code) { // 验证验证码是否正确 String redisCode = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisCode)) { return Result.error("验证码已过期,请重新发送"); } if (!redisCode.equals(code)) { return Result.error("验证码不正确"); } // 生成JWT token,并存储到Redis中 String token = JwtUtils.generateToken(phone); redisTemplate.opsForValue().set(phone, token, 1, TimeUnit.DAYS); // 将token返回给前端 return Result.success(token); } ``` 5. 实现JWT token的生成和解析: ``` public class JwtUtils { private static final String SECRET_KEY = "jwt_secret_key"; // JWT密钥 private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; // JWT过期时间(7天) public static String generateToken(String phone) { Date now = new Date(); Date expiration = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(phone) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getPhoneFromToken(String token) { try { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); return claims.getSubject(); } catch (Exception e) { return null; } } } ``` 6. 在拦截中验证token并获取用户信息: ``` public class JwtInterceptor implements HandlerInterceptor { private static final String AUTH_HEADER = "Authorization"; // token在请求头中的名称 @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(AUTH_HEADER); if (StringUtils.isBlank(token)) { throw new BusinessException("未登录登录已过期"); } String phone = JwtUtils.getPhoneFromToken(token); if (StringUtils.isBlank(phone)) { throw new BusinessException("无效的token"); } String redisToken = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisToken) || !redisToken.equals(token)) { throw new BusinessException("未登录登录已过期"); } return true; } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值