springboot集成shiro

一.简介

Shiro作为优秀的安全框架,包括了身份验证、授权、密码和会话管理,等功能。本文将介绍springboot集成Shiro的各个功能的具体操作。
Shiro使用的核心api有:

  1. Subject :与Shiro交互的主要类,相当于一个操作Shiro的用户
  2. SecurityManager :Shiro的核心,管理所有用户的操作
  3. Realm :身份验证和授权的数据获取层
  4. CredentialsMatcher :加密方式配置类
  5. AuthenticationToken :用于认证使用
  6. ShiroFilterFactoryBean :处理url授权和过滤

二.springboot集成Shiro

  1. 前期准备
    使用Shiro前必须先建立user(用户),role(角色),permission(权限)3张主表和user_role(用户角色),role_permission(角色权限)2张关系表:
  • 用户表
CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
  `salt` varchar(100) DEFAULT NULL COMMENT '盐',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态  0:禁用   1:正常',
  `create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `delete_flag` int(2) NOT NULL DEFAULT '0' COMMENT '删除标识 -1=删除 0=正常',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';
  • 角色表
CREATE TABLE `role` (
  `role_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) DEFAULT NULL COMMENT '角色名称',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  `create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色';
  • 权限表
CREATE TABLE `permission` (
  `permission_id` bigint(11) NOT NULL AUTO_INCREMENT,
  `perms` varchar(20) DEFAULT NULL COMMENT '权限',
  `name` varchar(20) DEFAULT NULL COMMENT '权限名',
  `created_by` bigint(11) DEFAULT NULL COMMENT '创建人',
  `created_date` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  • 用户角色表
CREATE TABLE `user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户与角色对应关系';
  • 角色权限表
CREATE TABLE `sys_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
  `permission_id` bigint(20) DEFAULT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色与权限对应关系';
  1. 在pom.xml中添加shiro-spring依赖:
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.1</version>
</dependency>

最新依赖可以自行到maven仓库中找:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring
3. 创建Realm数据层
如果不想创建自定义的Realm也可以用JdbcRealm的,我这里用一个自定义,灵活很多,也方便。

package com.zcx.demo.shiroweb.shiro.realm;

import com.zcx.demo.shiroweb.mapper.UserMapper;
import com.zcx.demo.shiroweb.mapper.RoleMapper;
import com.zcx.demo.shiroweb.mapper.PermissionMapper;
import com.zcx.demo.shiroweb.vo.User;
import org.apache.commons.collections.map.HashedMap;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

public class CustomRealm extends AuthorizingRealm {

    @Resource
    UserMapper userMapper;
    
    @Resource
    RoleMapper roleMapper;
    
    @Resource
    PermissionMapper permissionMapper;

	/**
     *授权的方法
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {


        String username = (String) principalCollection.getPrimaryPrincipal();

        List<String> roles = getRoleByName(username);
        List<String> permissions = getPermissionsByRoles(roles);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }

	/**
     * 身份验证方法
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        User user = getUserByName(username);
        if (user == null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, user.getPassword(), "customRealm");
        return simpleAuthenticationInfo;
    }

    private User getUserByName(String username) {
        return userMapper.getUserByName(username);

    }
    
    private List<String> getPermissionsByRoles(List<String> roles) {
        return permissionMapper.selectPermsByRoles(roles);
    }

    private List<String> getRoleByName(String username) {
        List<String> roles = soleMapper.selectRoleByName(username);
        return roles;

    }
}

  1. 配置ShiroConfig
package com.zcx.demo.shiroweb.core.config;

import com.zcx.demo.shiroweb.shiro.filter.RoleRoFilter;
import com.zcx.demo.shiroweb.shiro.manager.CustomSessionManager;
import com.zcx.demo.shiroweb.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;

@Configuration
public class ShiroConfig {

    @Bean
    public CustomRealm customRealm(){
        return new CustomRealm();
    }

    @Bean
    public SecurityManager securityManager(CustomRealm customRealm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setUnauthorizedUrl("/500");
        shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        HashMap<String, String> map = new HashMap<>();
        map.put("/user/login","anon");
        map.put("/user/testRole1","roles[admin]");
        map.put("/user/testRole2","roles[admin,admin1]");
        map.put("/user/testPerm1","perms[user:update]");
        map.put("/user/testPerm2","perms[user:update,user:delete]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

}

常用过滤器有:

authc:所有已登陆用户可访问
roles:有指定角色的用户可访问,通过[ ]指定具体角色,这里的角色名称与数据库中配置一致
perms:有指定权限的用户可访问,通过[ ]指定具体权限,这里的权限名称与数据库中配置一致
anon:所有用户可访问
  1. 在Controller中的使用
package com.zcx.demo.shiroweb.controller;

import com.zcx.demo.shiroweb.vo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @ResponseBody
    @RequestMapping(value = "login",method = RequestMethod.POST)
    public String login(User user) {
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        token.setRememberMe(user.isRememberMe());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
        } catch (Exception e) {
            return e.getMessage();
        }
        return "登录成功";
    }

    @RequiresRoles("admin")//除了在配置文件的过滤器中配置权限外还可以直接用注解设置权限
    @RequiresPermissions({"user:update","user:delete"})
    @RequestMapping("testRole1")
    public String testRole1(){
        return "admin test1 success";
    }
    
    @RequestMapping("/testRole2")
    public String testRole2(){
        return "admin test2 success";
    }
    @RequestMapping("/testPerm1")
    public String testPerm1(){
        return "Perm test1 success";
    }
    @RequestMapping("/testPerm2")
    public String testPerm2(){
        return "perm test2 success";
    }
}

到这里Shiro简单的集成就完成了。

三.Shiro加密使用

上面只是Shiro的简单集成,没有配置Shiro的加密,但是有哪个公司的的密码没加密呢,加密后又怎么校验呢,现在就来讲加密校验这块,为了便于其他地方使用,建议抽出一个方法来做。

package com.zcx.demo.shiroweb.utils;

import com.zcx.demo.shiroweb.vo.User;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class EncryptHelp {
    private RandomNumberGenerator numberGenerator = new SecureRandomNumberGenerator();
    public static final String PASSWORD_ALGORITHM_NAME = "md5"; 
    public static final int PASSWORD_HASH_ITERATIONS = 2;
	
    public void encryptPassword(User user) {
        user.setSalt(numberGenerator.nextBytes().toHex());
        String newPassword = new SimpleHash(PASSWORD_ALGORITHM_NAME, user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()), PASSWORD_HASH_ITERATIONS).toHex();
        user.setPassword(newPassword);
    }
}

在ShiroConfig配置文件中添加加密,便于身份验证时调用:

@Configuration
public class ShiroConfig {

    @Bean
    public CredentialsMatcher credentialsMatcher(){
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(EncryptHelp.PASSWORD_ALGORITHM_NAME);
        matcher.setHashIterations(EncryptHelp.PASSWORD_HASH_ITERATIONS);
        return matcher;
    }
    
    @Bean
    public CustomRealm customRealm(CredentialsMatcher credentialsMatcher){
        CustomRealm customRealm = new CustomRealm();
        customRealm.setCredentialsMatcher(credentialsMatcher);//设置加密方式
        return customRealm;
    }

然后再Realm的身份验证中将盐添加进去就好了:

public class CustomRealm extends AuthorizingRealm {
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        User user = getPasswordByName(username);
        if (user == null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, user.getPassword(), "customRealm");
        simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));//将盐设置到校验对象中
        return simpleAuthenticationInfo;
    }
}

这里加密就设置完了。

四.设置session共享

正常情况使用session是没啥问题的,但是当涉及到多台服务器时session就存在共享问题,现在普遍做法是通过redis来共享session。

  1. 编写RedisSessionDAO
import com.zcx.demo.shiroweb.utils.RedisUtil;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

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

@Component
public class RedisSessionDAO extends AbstractSessionDAO {
    private String SESSION_PREFIX = "session:";

    @Autowired
    private RedisUtil redisUtil;


    private String getKey(String key) {
        return SESSION_PREFIX + key;
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session,sessionId);
        saveSession(session);
        return sessionId;
    }

    private void saveSession(Session session) {
        if (session == null || session.getId() == null){
            return;
        }
        Serializable id = session.getId();
        String key = getKey(id.toString());
        redisUtil.set(key, session,600);
    }

    @Override
    protected Session doReadSession(Serializable serializable) {
        System.out.println("read session");
        String key = getKey(serializable.toString());
        return (Session) redisUtil.get(key);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        saveSession(session);
    }

    @Override
    public void delete(Session session) {
        Serializable id = session.getId();
        String key = getKey(id.toString());
        redisUtil.delete(key);
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = redisUtil.keys(SESSION_PREFIX);
        Set<Session> sessionSet = new HashSet<>(keys.size());
        if (CollectionUtils.isEmpty(keys)){
            return null;
        }
        for (String key : keys) {
            Session value = (Session) redisUtil.get(key);
            if (value == null) {
                continue;
            }
            sessionSet.add(value);
        }
        return sessionSet;
    }
}

  1. 在ShiroConfig中添加SessionManager
@Configuration
public class ShiroConfig {
    @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDao){
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager ();
        defaultWebSessionManager.setSessionDAO(sessionDao);
        return defaultWebSessionManager;
    }
    @Bean
    public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setSessionManager(sessionManager);
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }
}

这样session共享就设置好了,但是这样做每次获取session时都会去redis拿数据,对redis的开销还是蛮大的,所以一般还会做个缓存,那就是自定义SessionManager。

package com.zcx.demo.shiroweb.shiro.manager;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;

import javax.servlet.ServletRequest;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            return null;
        }
        ServletRequest request = null;
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }

        if (request != null) {
            Object session = request.getAttribute(sessionId.toString());
            if (session != null) {
                return (Session) session;
            }
        }

        Session session = super.retrieveSession(sessionKey);
        if (request != null) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }
}

将ShiroConfig中的DefaultWebSessionManager改为CustomSessionManager 就可以好了。

五.设置缓存

这里使用redis做为缓存。

  1. 实现Cache接口
package com.zcx.demo.shiroweb.shiro.cache;

import com.zcx.demo.shiroweb.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Set;

@Component
public class RedisCache<K,V> implements Cache<K,V> {
    private static final String CACHE_PREFIX = "cache:";

    @Autowired
    private RedisUtil redisUtil;

    private String getKey(String key) {
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        return CACHE_PREFIX + key;
    }

    @Override
    public V get(K o) throws CacheException {
        if (o == null){
            return null;
        }
        String key = getKey(o.toString());
        return (V) redisUtil.get(key);
    }

    @Override
    public V put(K k, V v) throws CacheException {
        if (k == null || v == null){
            return null;
        }
        String key = getKey(k.toString());
        redisUtil.set(key,v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        if (k == null){
            return null;
        }
        String key = getKey(k.toString());
        V bytes = (V) redisUtil.get(key);
        redisUtil.delete(key);
        return bytes;
    }

    @Override
    public void clear() throws CacheException {

    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }

}
  1. 实现CacheManager接口
package com.zcx.demo.shiroweb.shiro.manager;

import com.zcx.demo.shiroweb.shiro.cache.RedisCache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RedisCacheManager implements CacheManager {

    @Autowired
    private RedisCache redisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
}
  1. 配置ShiroConfig
@Configuration
public class ShiroConfig {
    @Bean
    public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager, CacheManager cacheManager){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setSessionManager(sessionManager);
        defaultWebSecurityManager.setCacheManager(cacheManager);
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }
}

六.设置保存cookie

  1. 配置ShiroConfig
@Configuration
public class ShiroConfig {
    @Bean
    public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager, CacheManager cacheManager
            ,CookieRememberMeManager rememberMeManager){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setSessionManager(sessionManager);
        defaultWebSecurityManager.setCacheManager(cacheManager);
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager);
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }

    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        Cookie cookie = new SimpleCookie("rememberMe");
        cookie.setMaxAge(1000000);
        rememberMeManager.setCookie(cookie);
        return rememberMeManager;
    }
}
  1. Controller中使用
@RequestMapping("/user")
public class UserController {


    @ResponseBody
    @RequestMapping(value = "login",method = RequestMethod.POST)
    public String login(User user) {
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        token.setRememberMe(user.isRememberMe());//设置记住登录
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
        } catch (Exception e) {
            return e.getMessage();
        }
        return "登录成功";
    }
}

七.自定义过滤器

1.编写过滤器

package com.zcx.demo.shiroweb.shiro.filter;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;


public class RoleRoFilter extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest, servletResponse);
        String[] roles = (String[]) o;
        if (roles == null || roles.length == 0){
            return true;
        }
        for (String role : roles) {
            if (subject.hasRole(role)) {
                return true;
            }
        }
        return false;
    }
}

  1. 添加到配置
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        shiroFilterFactoryBean.setUnauthorizedUrl("/500.html");
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        HashMap<String, String> map = new HashMap<>();
        map.put("/user/login","anon");
        map.put("/user/testRole1","roles[admin]");
        map.put("/user/testRole2","roleOr[admin,admin1]");//使用过滤器
        map.put("/user/testPerm1","perms[user:update]");
        map.put("/user/testPerm2","perms[user:update,user:deletee]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("roleOr",new RoleRoFilter());//添加过滤器
        shiroFilterFactoryBean.setFilters(filterMap);
        return shiroFilterFactoryBean;
    }
}

最后附上demo地址:https://github.com/zcxshare/springboot-shiro-demo

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值