【springboot集成shiro demo】

shiro architecture

摘自apache官网:https://shiro.apache.org/architecture.html
在这里插入图片描述

  • Subject (org.apache.shiro.subject.Subject) A security-specific ‘view’ of the entity (user, 3rd-party service, cron job, etc) currently interacting with the software.

  • SecurityManager (org.apache.shiro.mgt.SecurityManager) As mentioned above, the SecurityManager is the heart of Shiro’s architecture. It is mostly an ‘umbrella’ object that coordinates its managed components to ensure they work smoothly together. It also manages Shiro’s view of every application user, so it knows how to perform security operations per user.

  • Authenticator (org.apache.shiro.authc.Authenticator) The Authenticator is the component that is responsible for executing and reacting to authentication (log-in) attempts by users. When a user tries to log-in, that logic is executed by the Authenticator. The Authenticator knows how to coordinate with one or more Realms that store relevant user/account information. The data obtained from these Realms is used to verify the user’s identity to guarantee the user really is who they say they are.

  • Authentication Strategy (org.apache.shiro.authc.pam.AuthenticationStrategy) If more than one Realm is configured, the AuthenticationStrategy will coordinate the Realms to determine the conditions under which an authentication attempt succeeds or fails (for example, if one realm succeeds but others fail, is the attempt successful? Must all realms succeed? Only the first?).

  • Authorizer (org.apache.shiro.authz.Authorizer) The Authorizer is the component responsible determining users’ access control in the application. It is the mechanism that ultimately says if a user is allowed to do something or not. Like the Authenticator, the Authorizer also knows how to coordinate with multiple back-end data sources to access role and permission information. The Authorizer uses this information to determine exactly if a user is allowed to perform a given action.

  • SessionManager (org.apache.shiro.session.mgt.SessionManager) The SessionManager knows how to create and manage user Session lifecycles to provide a robust Session experience for users in all environments. This is a unique feature in the world of security frameworks - Shiro has the ability to natively manage user Sessions in any environment, even if there is no Web/Servlet or EJB container available. By default, Shiro will use an existing session mechanism if available, (e.g. Servlet Container), but if there isn’t one, such as in a standalone application or non-web environment, it will use its built-in enterprise session management to offer the same programming experience. The SessionDAO exists to allow any datasource to be used to persist sessions.

  • SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) The SessionDAO performs Session persistence (CRUD) operations on behalf of the SessionManager. This allows any data store to be plugged in to the Session Management infrastructure.

  • CacheManager (org.apache.shiro.cache.CacheManager) The CacheManager creates and manages Cache instance lifecycles used by other Shiro components. Because Shiro can access many back-end data sources for authentication, authorization and session management, caching has always been a first-class architectural feature in the framework to improve performance while using these data sources. Any of the modern open-source and/or enterprise caching products can be plugged in to Shiro to provide a fast and efficient user-experience.

  • Cryptography (org.apache.shiro.crypto.*) Cryptography is a natural addition to an enterprise security framework. Shiro’s crypto package contains easy-to-use and understand representations of crytographic Ciphers, Hashes (aka digests) and different codec implementations. All of the classes in this package are carefully designed to be very easy to use and easy to understand. Anyone who has used Java’s native cryptography support knows it can be a challenging animal to tame. Shiro’s crypto APIs simplify the complicated Java mechanisms and make cryptography easy to use for normal mortal human beings.

  • Realms (org.apache.shiro.realm.Realm) As mentioned above, Realms act as the ‘bridge’ or ‘connector’ between Shiro and your application’s security data. When it comes time to actually interact with security-related data like user accounts to perform authentication (login) and authorization (access control), Shiro looks up many of these things from one or more Realms configured for an application. You can configure as many Realms as you need (usually one per data source) and Shiro will coordinate with them as necessary for both authentication and authorization.

以上说明:

1、SecurityManager 是shiro架构的核心,使用示例之一:在shiro的配置类中

@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		
		// 配置自定义的认证(用户账号\密码认证)
		CustomisedAuthenticator customisedAuthenticator =  new CustomisedAuthenticator();
		// 配置认证策略,只要有一个Realm认证成功即可,并且返回所有认证成功信息
		customisedAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
		securityManager.setAuthenticator(customisedAuthenticator);
		// 配置自定义的授权(角色或者权限等)
		CustomisedAuthorizer customisedAuthorizer = new CustomisedAuthorizer();
		securityManager.setAuthorizer(customisedAuthorizer);

		// 单个realm	 
		//securityManager.setRealm(getUserRealm());
		List<Realm> realms = new ArrayList<Realm>();
		realms.add(getUserRealm());
		realms.add(getAnotherRealm());
		// 多realm
		securityManager.setRealms(realms);
		return securityManager;
	}

2、Authenticator 用于账号/密码的认证

3、Authorizer 用于授权角色roles/权限privileges

4、Realms :As mentioned above, Realms act as the ‘bridge’ or ‘connector’ between Shiro and your application’s security data.
realm是连接shiro和应用数据安全的桥梁。开发者可在realm中完成Authenticator 和Authorizer 的业务实现。
You can configure as many Realms as you need (usually one per data source) and Shiro will coordinate with them as necessary for both authentication and authorization.
只要需要可以配置一个或多个Realm,shiro在必要时会为认证和授权提供协调处理。

多Realm的情况需要注意一下,见后文
realm示例

package com.xl.practice.springbootshiropractice.shiro;

import java.util.Set;

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.springframework.beans.factory.annotation.Autowired;

import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;

public class UserRealm extends AuthorizingRealm {

	@Autowired
	private UserService userService;

	/**
	 * 用户通过验证后,Shiro可通过该方法对登录用户进行授权 角色 或 权限, 只有先通过用户验证后才会走到用户授权这一步
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
	System.out.println("=================================== UserRealm的授权方法  ===================================");
		// 获取登录用户名
//		String name = (String) principalCollection.getPrimaryPrincipal();
		UserLogin userLogin = (UserLogin) principalCollection.getPrimaryPrincipal();
		String name = userLogin.getUsername();
		/**
		 * 获取当前登录用户的权限和角色
		 */
		Set<String> privileges = userService.listPrivilege(name);
		Set<String> roles = userService.listRole(name);
		// Shiro配置授权 信息
		SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
		/**
		 * 把通过service获取到的角色和权限放进去
		 */
		s.setStringPermissions(privileges);
		s.setRoles(roles);
		return s;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		String name = token.getPrincipal().toString();
		// 获取数据库中的密码
		User user = userService.selectUserByName(name);
		String passwordInDB = user.getPassword();
//		SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(name, passwordInDB, null, getName());
		SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(token, passwordInDB, null, getName());
		return a;
	}

}

shiro官方手册:https://shiro.apache.org/reference.html
截图:
在这里插入图片描述

demo

springboot与shiro基础

流程介绍

以下为shiro认证过程的简要类图关系

在这里插入图片描述

说明

由上图可知,如需完成shiro认证功能的集成,只需设计3个类(不包括shiro的配置类见下文),其中只有一个是必须的,其余两个根据业务需求而定。

1、Subject的login方法的参数类:AuthenticationToken (非必须)
如果业务上需要自定义字段/方法,则可以设计一个自定义的Token继承Shiro的UsernamePasswordToken,如上图中的UserLogin。如果不需要则直接将UsernamePasswordToken作为Subject.login的参数即可:将用户名/密码封装到UsernamePasswordToken。

2、自定义的RealmAuthenticator(非必须)
如果系统中有多个Realm,并且在认证的时候需要确定用具体的那一个或哪几个Realm,则需要自定义一个RealmAuthenticator,如上图中的CustomisedAuthenticator,继承自ModularRealmAuthenticator,并且,这类需要在shiro的配置类中进行显示的配置。如果不需要则shiro会使用默认的ModularRealmAuthenticator,根据源码可知默认的实现会使用所有的Realm。

3、自定义Realm(必须)
自定义的Realm是必须要的,因为需要在这个类里面实现具体的业务认证(授权也是在这个类里面实现的),从shiro的设计中也可看出:
自定义的Realm需要继承AuthenticatingRealm而AuthenticatingRealm中的认证方法是抽象方法,也就是说必须要有用户自定义的类来继承并实现之。

以下为shiro授权过程的简要类图关系

在这里插入图片描述

说明

由上图可知,要完成shiro授权的集成只需要设计2个类即可,一个必须,一个非必须,但是注意授权的前提是完成认证。

1、自定义的RealmAuthorizer类(非必须)
如果系统中有多个Realm,并且在授权的时候需要确定用具体的那一个或哪几个Realm,则需要自定义一个RealmAuthorizer,如上图中的CustomisedAuthorizer,继承自ModularRealmAuthorizer,并且,本类需要在shiro的配置类中进行显示的配置。如果不需要则shiro会使用默认的ModularRealmAuthorizer,根据源码可知默认的实现会使用所有的Realm。

2、自定义Realm(必须)
自定义的Realm是必须要的,因为需要在这个类里面实现具体的业务授权(认证也是在这个类里面实现的),从shiro的设计中也可看出:
自定义的Realm需要继承AuthorizingRealm而AuthorizingRealm中的认证方法是抽象方法,也就是说必须要有用户自定义的类来继承并实现之。

引入依赖

	<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-web</artifactId>
		<version>1.6.0</version>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
	<dependency>
		<groupId>org.apache.shiro</groupId>
		<artifactId>shiro-spring</artifactId>
		<version>1.6.0</version>
	</dependency>

编写shiro配置类

package com.xl.practice.springbootshiropractice.shiro;

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

import javax.servlet.Filter;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfiguration {

	@Bean
	public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}

	@Bean
	public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
		autoProxyCreator.setProxyTargetClass(true);
		return autoProxyCreator;
	}

	@Bean
	public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
		System.out.println("ShiroConfiguration.shirFilter()");
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

		// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面。
		shiroFilterFactoryBean.setLoginUrl("/notLogin");
		//未授权访问,跳转的地址setUnauthorizedUrl方法只针对部分过滤器有效,有些过滤器是无效的
//		shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
		// 必须设置 SecurityManager
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 拦截器
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		/**
		 *  自定义拦截器
		 */
		Map<String, Filter> customisedFilter = new HashMap<>();
		customisedFilter.put("url", new CustomisedURLPathMatchingFilter());
		
		// 配置映射关系
		filterChainDefinitionMap.put("/login", "anon");
		filterChainDefinitionMap.put("/**", "authc");
		filterChainDefinitionMap.put("/**", "url");
		shiroFilterFactoryBean.setFilters(customisedFilter);
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return shiroFilterFactoryBean;
	}

	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		
		// 配置自定义的认证(用户账号\密码认证)
		CustomisedAuthenticator customisedAuthenticator =  new CustomisedAuthenticator();
		// 配置认证策略,只要有一个Realm认证成功即可,并且返回所有认证成功信息
		customisedAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
		securityManager.setAuthenticator(customisedAuthenticator);
		// 配置自定义的授权(角色或者权限等)
		CustomisedAuthorizer customisedAuthorizer = new CustomisedAuthorizer();
		securityManager.setAuthorizer(customisedAuthorizer);

		// 单个realm	 
		//securityManager.setRealm(getUserRealm());
		List<Realm> realms = new ArrayList<Realm>();
		realms.add(getUserRealm());
		realms.add(getAnotherRealm());
		// 多realm
		securityManager.setRealms(realms);
		return securityManager;
	}

	@Bean
	public UserRealm getUserRealm() {
		UserRealm myShiroRealm = new UserRealm();
		// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false;
		myShiroRealm.setAuthenticationCachingEnabled(true);
		myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return myShiroRealm;
	}
	
	@Bean
	public AnotherRealm getAnotherRealm() {
		AnotherRealm anotherRealm = new AnotherRealm();
		// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false;
		anotherRealm.setAuthenticationCachingEnabled(true);
		anotherRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return anotherRealm;
	}

	/**
	 * 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
	 * 所以我们需要修改下doGetAuthenticationInfo中的代码; )
	 * 
	 * @return
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

		hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
//        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));

		return hashedCredentialsMatcher;
	}

	/**
	 * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
}

自定义Token类

package com.xl.practice.springbootshiropractice.shiro;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * 封装登录的信息,用户名、密码、登录类型
 * @author Administrator
 *
 */
public class UserLogin extends UsernamePasswordToken {
	/**
	 * 登录类型
	 */
	private String type;
	
	public UserLogin() {
		super();
	}

	public UserLogin(String type,String username,String password) {
		super(username,password);
		this.type = type;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}
}

自定义Realm

第一个realm

package com.xl.practice.springbootshiropractice.shiro;

import java.util.Set;

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.springframework.beans.factory.annotation.Autowired;

import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;

public class UserRealm extends AuthorizingRealm {

	@Autowired
	private UserService userService;

	/**
	 * 用户通过验证后,Shiro可通过该方法对登录用户进行授权 角色 或 权限, 只有先通过用户验证后才会走到用户授权这一步
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
	System.out.println("=================================== UserRealm的授权方法  ===================================");
		// 获取登录用户名
//		String name = (String) principalCollection.getPrimaryPrincipal();
		UserLogin userLogin = (UserLogin) principalCollection.getPrimaryPrincipal();
		String name = userLogin.getUsername();
		/**
		 * 获取当前登录用户的权限和角色
		 */
		Set<String> privileges = userService.listPrivilege(name);
		Set<String> roles = userService.listRole(name);
		// Shiro配置授权 信息
		SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
		/**
		 * 把通过service获取到的角色和权限放进去
		 */
		s.setStringPermissions(privileges);
		s.setRoles(roles);
		return s;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		String name = token.getPrincipal().toString();
		// 获取数据库中的密码
		User user = userService.selectUserByName(name);
		String passwordInDB = user.getPassword();
//		SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(name, passwordInDB, null, getName());
		SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(token, passwordInDB, null, getName());
		return a;
	}

}

第二个Realm

package com.xl.practice.springbootshiropractice.shiro;

import java.util.HashSet;
import java.util.Set;

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.springframework.beans.factory.annotation.Autowired;

import com.xl.practice.springbootshiropractice.entity.User;
import com.xl.practice.springbootshiropractice.service.UserService;

public class AnotherRealm extends AuthorizingRealm {
	
	@Autowired
	private UserService userService;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("+++++++++++++++++++++++++++++ AnotherRealm的授权方法  +++++++++++++++++++++++++++++");
		// Shiro配置授权 信息
		SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
		Set<String> privileges = new HashSet<String>();
		Set<String> roles = new HashSet<String>();
		
		UserLogin userLogin = (UserLogin) principals.getPrimaryPrincipal();
		String type = userLogin.getType();
		
		privileges.add("AAA");
		
		s.setRoles(roles);
		s.setStringPermissions(privileges);
		return s;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		String name = token.getPrincipal().toString();
		// 获取数据库中的密码
		User user = userService.selectUserByName(name);
		String passwordInDB = user.getPassword();
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, passwordInDB, null, getName());
		return simpleAuthenticationInfo;
	}

}

自定义认证的RealmAuthenticator

package com.xl.practice.springbootshiropractice.shiro;

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

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

public class CustomisedAuthenticator extends ModularRealmAuthenticator {
	@Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {
        System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        // 强制转换回自定义的CustomizedToken
        UserLogin userLogin = (UserLogin) authenticationToken;
        // 登录类型
        String type = userLogin.getType();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
        	System.out.println(realm.getName());
            if (realm.getName().contains(type)) {
                typeRealms.add(realm);
            }
        }
//        return doMultiRealmAuthentication(typeRealms, userToken);
        // 判断是单Realm还是多Realm
        if (typeRealms.size() == 1){
            System.out.println("doSingleRealmAuthentication() execute ");
            return doSingleRealmAuthentication(typeRealms.get(0), userLogin);
        }
        else{
            System.out.println("doMultiRealmAuthentication() execute ");
            return doMultiRealmAuthentication(typeRealms, userLogin);
        }
    }
}

自定义授权的RealmAuthorizer

package com.xl.practice.springbootshiropractice.shiro;

import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomisedAuthorizer extends ModularRealmAuthorizer  {

	@Override
	public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
		 assertRealmsConfigured();
		 UserLogin userLogin = (UserLogin) principals.getPrimaryPrincipal();
		 String type = userLogin.getType();
		 
	        for (Realm realm : getRealms()) {
	            if (!(realm instanceof Authorizer)) continue;
	            if (realm.getName().contains(type)) { // 有多个realm的情况,过滤掉其他不需要的realm,业务逻辑可根据实际情况改变
	            	if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
	            		return true;
	            	}
	            }
	        }
	        return false;
	}
	
}

问题

在这里插入图片描述

相关文章

Springboot整合shiro:第一篇 用户验证

源文件

shiro授权类图关系简化版
shiro认证 类图关系简化版
demo项目源码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值