Spring Security

Spring Security介绍 文章主要分三部分 1、Spring Security的架构及核心组件:(1)认证;(2)权限拦截;(3)数据库管理;(4)权限缓存;(5)自定义决策; 2、环境搭建与使用,使用当前热门的Spring Boot来搭建环境,结合项目中实际的例子来做几个Case; 3、Spring Security的优缺点总结,结合第二部分中几个Case的实现来总结Spring Security的优点和缺点。 1、Spring Security介绍 ​ 整体介绍,Spring Security为基于J2EE开发的企业应用软件 阅读详情

安全及权限控制是每个应用难以绕开的部分,这部分业务也不是特别复杂,但是我想这块业务具有通用性的特点,既然有开源的框架,完全可以拿来主义,没有必要重复造轮子。虽然学习新的框架前期需要投入一些精力,但开源框架的架构设计会比还是菜鸟的我设计出来的好点,而且会用之后,后期的收益还是不错的。在Java Web开发中,安全框架Spring Security和Apache Shiro比较主流,虽然Shiro入门简单些,但是由于个人非常喜欢Spring就选择Spring Security。

理论上,Spring Security和SpringMVC结合使用,会简单的多,而且很多资料的也是根据SpringMVC的介绍的,但是部门使用的项目的Struts2+Spring4.2+Hibernate4.3.。那我只好在此基础上集成Spring Security,这会稍微麻烦一点,像我这样的小白独立研究起来,真是跌跌撞撞哎。

废话有些多,项目框架上面说完了。我采用的Spring Security的版本为4.0.3.集成步骤如下:

1.通过Java编程的方式创建配置WebSecurityConfigurerAdapter【功能类似applicationContext-security.xml】:官方对其作用阐述为

The configuration creates a Servlet Filter known as the springSecurityFilterChain which is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the log in form, etc) within your application. 

package com.cqs.security.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.encoding.ShaPasswordEncoder;
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.core.userdetails.UserDetailsService;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true,securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	protected Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

	@Resource(name = "customUserDetailsService")
	private UserDetailsService userDetailsService;
	@Resource(name = "authProvider")
	private AuthenticationProvider authenticationProvider;


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//		//加密用户密码
//        ShaPasswordEncoder encoder = new ShaPasswordEncoder();
//		//加载用户信息
//        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
        auth.userDetailsService(userDetailsService);
		//加载授权信息
		auth.authenticationProvider(authenticationProvider);
    }

	@Autowired
	public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
		//内存中直接用户和授权信息,测试的时候很好用
		auth.inMemoryAuthentication().withUser("test").password("test").roles("Test");
	}


	//权限配置
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
				.authorizeRequests()
				//不拦截的资源
				.antMatchers("/", "/demo2/login.jsp","/images/**","/scripts/**","/styles/**","/user/login.action").permitAll()
				//只有ANONYMOUS角色的用户可访问,Spring Security内置的,未登录之前的用户均是ANONYMOUS
				.antMatchers("/demo2/user/demo3.jsp").hasRole("ANONYMOUS")
				//配置角色:注意Spring Security4之后,这里不必添加ROLE_前缀,Spring Security4会自动补上的。 注意:角色大小写敏感
				.antMatchers("/user/**.action").access("hasRole('ADMIN')  or hasRole('DBA')  or hasRole('TEST') ")
				.antMatchers("/demo2/**.jsp").access("hasRole('ADMIN') or hasRole('TEST')")
				.anyRequest().authenticated()//拦截所有请求
			  .and()
			  	.formLogin().loginPage("/demo2/login.jsp").permitAll()//登陆页面
				//j_username和j_password对用的是登陆页面的对应用户名和密码的name属性而不是ID属性
	  			.usernameParameter("j_username").passwordParameter("j_password")
			  .and()
				.exceptionHandling().accessDeniedPage("/Access_Denied")
			  .and().
				csrf().disable();//关闭csrf
	}

    //struts2必须配置
    @Bean(name="authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
//
//	@Bean
//	public BCryptPasswordEncoder bCryptPasswordEncoder(){
//		return new BCryptPasswordEncoder();
//	}
}


package com.cqs.security.config;

import com.cqs.demo2.models.user.model.User;
import com.cqs.demo2.models.user.service.UserService;
import com.cqs.security.role.model.Role;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService{

    private Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class);

    @Resource(name = "userService")
    private UserService userService;

    @Override  @Transactional(readOnly=true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<User> users = userService.getUserByName(username);
        int num = users.size();
        User user = null;
       if(num == 1) {
           user = users.get(0);
       }
       if(num == 0){
           logger.warn("不存在该用户{}",username);
           user = null;
       }else if (num > 1){
           throw new RuntimeException("存在多个同名的用户");
       }
        logger.debug(ReflectionToStringBuilder.toString(user));
        return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(),
                user.getStatus().equalsIgnoreCase("active"), true, true, true, getGrantedAuthorities(user));
    }


     
    protected List<GrantedAuthority> getGrantedAuthorities(User user){
        List<GrantedAuthority> authorities = new ArrayList();
        for (Role role :user.getRoles()) {
            logger.debug(ReflectionToStringBuilder.toString(role));
            //注意:这里要ROLE_加上前缀,否则在创建角色而的时候统一加上
            authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
//            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
     
}

package com.cqs.security.config;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component("authProvider")
public class AuthProvider implements AuthenticationProvider {
    private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {
        String username = auth.getName();
        String password = auth.getCredentials().toString();
        logger.debug("username:{} ,\tpassword:{}",username,password);

        UserDetails user = customUserDetailsService.loadUserByUsername(username);
        logger.debug(ReflectionToStringBuilder.toString(user));
        if (user != null) {
            Authentication token = new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
            return token;
        }
        return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return false;
    }
}
这里逻辑控制比较简单。以后再慢慢完善吧

2.配置拦截器:既可以在xml中配置,可以使通过Java  Configuration的方式配置,先说XML配置

在web.xml中添加

    <!--Spring Security配置-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
有个别资料说springSecurityFilterChain必须配置在struts2Filter的前面,我正好配置在 struts2Filter之前,没试放在struts2Filter之后的场景,反正springSecurityFilterChain在前面是可用的。

3.编写登录方法

 @Resource
 private AuthenticationManager authenticationManager;

public String login() {
        String username = request.getParameter("j_username");
        String password = request.getParameter("j_password");
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        HttpServletRequest request = ServletActionContext.getRequest();
        // generate session if one doesn't exist
        request.getSession();

        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authenticatedUser = null;
        try {
            authenticatedUser = authenticationManager.authenticate(token);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        SecurityContextHolder.getContext().setAuthentication(authenticatedUser);
        return SUCCESS;
    }
4.数据:编写User,Role类,为了方便,使用Groovy进行编写

package com.cqs.demo2.models.user.model

import com.cqs.security.role.model.Role
import org.hibernate.annotations.Cascade
import org.hibernate.annotations.CascadeType
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.hibernate.annotations.GenericGenerator
import org.hibernate.validator.constraints.Email

import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.JoinTable
import javax.persistence.ManyToMany
import javax.persistence.Table
import javax.persistence.Temporal
import javax.persistence.TemporalType
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size

/**
 * Created by li on 2016/1/12.
 */
@Entity
@Table
class User {
    @Id
    @GenericGenerator(name="idGenerator",strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(length = 32)
    String id;

    @Column(length = 60)
    @Size(max = 60,message = "用户名不能超过60")
    @NotNull
    String name;

    @Column(length = 16)
    @NotNull
    @Size(max = 16)
    String password;



    @Column(length = 50)
    @Email
    String email;

    @Column(length = 10)
    String status;

    public String toString(){
        return "用户名: " + getName() +" 主键: " + getId();
    }

    @Temporal(TemporalType.TIMESTAMP)
    Date createDate;


    @Temporal(TemporalType.TIMESTAMP)
    Date updateDate;

    @Fetch(FetchMode.SUBSELECT)
    @ManyToMany(fetch = FetchType.EAGER)
    @Cascade(value = CascadeType.SAVE_UPDATE)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    List<Role> roles ;
}

package com.cqs.security.role.model

import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.hibernate.annotations.GenericGenerator

import javax.persistence.Column
import javax.persistence.ElementCollection
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.JoinColumn
import javax.persistence.Table

/**
 * Created by li on 2016/1/8.
 */
@Entity
@Table
class Role {
    @Id
    @GenericGenerator(name="idGenerator",strategy = "uuid")
    @GeneratedValue(generator = "idGenerator")
    @Column(length = 32,nullable = false)
    String id;
    @Column(length = 30,nullable = false)
    String name;

}

Role和User编写比较粗糙。 通过Junit4插入几条模拟数据。

package com.cqs.demo2.models.user.service;

import com.cqs.demo2.models.user.model.User;
import com.cqs.security.role.model.Role;
import config.BaseTestConfig;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.Resource;
import java.util.*;

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.*;

public class UserServiceTest extends BaseTestConfig {

    @Resource
    private UserService userService;
    private User user = new User();
    private List<Role> roles = new ArrayList<>();

    String[] usersName = {
        "ADMIN","CUSTOMER","DBA"
    };


   void initRoles(int i){
        Role role = new Role();
        role.setName(usersName[i]);
        roles.add(role);

    }

    public void initUser(int num){
        roles.clear();
        user.setName(usersName[num].toLowerCase());
        user.setEmail("fhdjkasj@163.com");
        user.setCreateDate(new Date());
        user.setUpdateDate(new Date());
        user.setPassword("1");
        user.setStatus(Math.random() > 0.5 ? "active" : "inactive");
        initRoles(num);
        user.setRoles(roles);
    }


    @Test
    public void testCreate() throws Exception {
        for (int i = 0; i < usersName.length; i++) {
            initUser(i);
            userService.create(user);
        }
    }
}
数据库为


5调试:为了调试方便,需将Spring Security的详细信息打印出来。在log4j.properties添加

log4j.logger.org.springframework.security=DEBUG, STDOUT
6.编写登录等页面
<%@ page contentType="text/html;charset=UTF-8" language="java"  isELIgnored="false" %>
<%@include file="./commons/taglib.jsp"%>

<!DOCTYPE  html>
<html lang="zh-CN">
<head>
    <script type="text/ecmascript" src="${ctx}/scripts/jquery-1.11.0.min.js"></script>
    <script type="text/ecmascript" src="${ctx}/scripts/plugins/boostrap/bootstrap.min.js"></script>
    <script type="text/ecmascript" src="${ctx}/scripts/plugins/boostrap/bootstrap-datetimepicker.js"></script>
    <script type="text/javascript" src="${ctx}/scripts/commoms/math.js" ></script>
    <script type="text/javascript">
        $().ready(function(){
            console.log("Hello User123");
        });
    </script>
    <link rel="stylesheet" type="text/css" media="screen" href="${ctx}/styles/plugins/boostrap/bootstrap.min.css" />
</head>

<body>
<div class="container">
    <h1>
        欢迎来到登陆界面
    </h1>


    <form  class="form-horizontal" action="${ctx}/user/login.action" method = "post" style="width: 35%;" >
        <input type="hidden"  name="${_csrf.parameterName}"   value="${_csrf.token}"/>
        <div class="form-group">
            <label for="name" >
                用户名:
            </label>
            <%--<input name="j_username" id="name" value="admin" placeholder="userName" class="form-control"/>--%>
            <s:textfield name="j_username" id="name" value="admin" placeholder="userName" class="form-control" autofocus="autofocus" />
        </div>
        <div class="form-group">
            <label for="password" >
                密码:
            </label>
            <s:password name="j_password" id="password" value="admin" class="form-control"  />
            <%--<input name="j_password"  id="password" value="admin"  placeholder="password" class="form-control" />--%>
        </div>

        <div class="form-group">
            <%--<input type="submit" value="登录" class="form-control btn-default"/>--%>
                <s:submit value="登录" class="form-control btn-default"/>
        </div>
    </form>
</div>
</body>
</html>
其他页面就不粘贴了,运行程序,进入登录页面,并使用admin账号登陆


成功登陆


 查看后台日志


可知是通过ROLE_ADMIN角色进行访问的。现在通过没激活的DBA账号访问


查看后台


现在使用customer的账号登录,之前所有的/user/**.action并没有对ROLE_CUSTOMER角色授权,所以登陆后


没有数据,也就是action方法没有执行,查看后台日志


符合预期。

现在只是完成了先对简单的功能,还有如用户密码加密,对方法权限控制,csrf允许等等均还没弄,以后有时间再慢慢研究吧。

参考:Spring Security Reference

Spring Security 4 Method security using @PreAuthorize,@PostAuthorize, @Secured, EL

Spring Security 4 Hibernate Integration Annotation+XML Example


Spring Security 登陆成功后的处理        Spring Security 完成登陆后一般需要一些自定义的处理,例如,记录登陆日志,初始化用户菜单等等 这里就需要自定义的 登陆成功处理了,Spring  提供了 AuthenticationSuccessHandler 接口,完成这个接口就可以了。 但是,修改到这里处理后,原先系统的自动转向处理就没有了,接口中应该怎么写,才可以保持原来的功能呢?   代码如下:   ... 阅读详情

相关推荐

spring security详解

1.概要 Spring是非常流行和成功的Java应用开发框架,SpringSecurity正是Spring家族中的成员。SpringSecurity基于Spring框架,提供了一套Web应用安全性的完整解决方案。正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是SpringSecurity重要核心功能。 (1)用户认证指的是:验证某个用户是否为系统

wumingdu1234的博客 7710

Spring Security拦截配置

除了基于角色和URL模式的拦截配置,Spring Security还支持自定义过滤器链。这允许我们添加自定义过滤器来处理特定的安全需求。创建一个实现了Filter接口的自定义过滤器类,实现doFilter方法来自定义处理逻辑。配置,将自定义过滤器注册到Spring Security中的过滤器链中。// ...省略其他配置...@Bean// ...省略其他配置...在上述示例代码中,我们创建了一个名为CustomFilter的自定义过滤器,并将其注册到URL模式为。

2301_79108888的博客 2568

Spring Boot中集成Spring Security 专题

当然,Spring Security明确的设计可以处理这种常见需求,但是作为替代你应该使用项目的域对象的安全能力来实现这个目标。例如在默认的安全配置中authorizeRequests(),formLogin()、httpBasic()这三个方法返回的分别是ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer,他们都是SecurityConfigurer接口的实现类,分别代表的是不同类型的安全配置器。

2401_86940371的博客 2220

Security源码接口PasswordEncoder不同版本的用法

PasswordEncoder是一个加密接口,新老版本放的包路径不一样,新版本方法名简化,对老版本已经弃用。老版本类路径:org.springframework.security.authentication.encoding.PasswordEncoder接口定义了两个方法,encodePassword()方法是对原始密码进行加密,采用hash+salt方式,在方法中应用系统得提供盐值(salt...

heirenheiren的专栏 8009

Spring Security 自定义拦截器Filter实现登录认证

Spring Security 自定义拦截器Filter 实现登录认证

qq_34898847的博客 7625

MD5加密解密代码

package test; import java.security.NoSuchAlgorithmException; import org.springframework.security.authentication.encoding.Md5PasswordEncoder; import org.springframework.security.authenticati

梦醒时分 3478

SpringBoot security 安全认证(二)——登录拦截器

本节内容:实现登录拦截器,除了登录接口之外所有接口访问都要携带Token,并且对Token合法性进行验证,实现登录状态的保持。核心内容:1、要实现登录拦截器,从Request请求中获取token,从缓存中获取Token并验证登录是否过期,若验证通过则放行;2、实现对拦截器配置,SpringBoot 安全模块使用HttpSecurity 来完成请求安全管理。

朗朗的博客 3232

Spring Security 详解

Spring SecuritySpring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。​ 一般Web应用的需要进行认证和授权。​ 而认证和授权也是SpringSecurity作为安全框架的核心功能。视频地址:​ 我们先要搭建一个简单的SpringBoot工程① 设置父工

qq_22075913的博客 12万+

Spring Security配置详细

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,专为基于Spring的应用程序设计。本教程将指导你如何在一个简单的Spring Boot应用程序中集成Spring Security,以实现基本的用户认证和授权功能。

2401_83447580的博客 2497

Spring Security 4 原理详解

场景: 用户通过提交username和password请求登陆 服务器验证身份信息是否正确 获取用户信息(包括角色集合) 利用3中的信息构建Security Context 在此之后,该用户的所有请求,Spring Security的访问控制机制将根据Security Context中的信息判断用户是否具有权限 以上,前3点即为认证,构建Security Context,为之后5的授权(访

lbqssss的博客 8298

Spring Security快速入门

目录1 Spring Security介绍2 创建工程2.1 创建maven工程2.2 Spring容器配置2.3 Servlet Context配置2.4 加载 Spring容器3 认证3.1 认证页面3.2.安全配置3.2.Spring Security初始化2.3.默认根路径请求2.4.认证成功页面2.5 测试4 授权5 小结 1 Spring Security介绍 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Sprin

赵广陆 1万+

Spring Security团队正式宣布Spring Security OAuth终止维护

Spring Security团队正式宣布Spring Security OAuth终止维护。spring 官宣终止维护spring security oauth目前官网的主页已经高亮提醒大家彻底停止维护。官网已经通知EOL旧的Spring Security OAuth项目终止到2.5.2.RELEASE版本,该项目将不会再进行任何的迭代,包括Bug修复,之前胖哥已经提醒该项目即将停止维护,有心的同学已经进行了迁移。2020年就已经宣布了EOL时间表项目文档和代码仓库被移除目前该项目的官方文档已经正式从sp

资料免费分享,点击名片 1441

Spring Security开发手册(从零搭建,超详细教程)

一篇文章快速上手Spring Security框架

laiminwei的博客 3061

学习笔记七-AuthenticationProvider

AuthenticationProvider 认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。当我们使用 authentication-provid

云智禅师的专栏 3万+

SpringBoot + Spring Security多种登录方式:账号+微信网页授权登录

大家好,我是宝哥!一、概述实现账号用户名+微信网页授权登录集成在Spring Security的思路,最重要的一点是要实现微信登录通过Spring Security安全框架时,不需要验证账号、密码。二、准备工作要实现该功能,首先需要掌握Spring Security框架和微信扫码登录接口相关技术,如果对这两块还不太熟悉,可以参考:1、Springboot + Spring Security实现前后...

程序员闪充宝 4192

spring security2【与数据库权限关联】

设计数据库 数据库权限概念 角色用户组权限关联 数据库实体图: 2、将用户权限表与springsecurity相关联 使用自定义的provider ...

gdfdfg的专栏 991

基于springSecurity整合jwt拦截器部署

springboot整合springsecurity和jwt的拦截器部署

weixin_61919057的博客 1321
上一篇: Spring4 javaConfig配置方式并集成JUnit
下一篇: 使用Nexus构建私有服务器
lixwcqs
博客等级 码龄15年 23粉丝 72原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值