zoukankan      html  css  js  c++  java
  • Java-Security(三):加密的用法、PasswordEncoder类源码分析

    在第一篇文章,我们展示了一个demo,其中讲到了对用户的密码进行了明文展示的用法,其实那么做是不安全的,在实际项目中往往会采用各种加密方法(比如:bcrypt,md5,sha1,sha2等)来实现对密码的保护。

    本片文章将会主要讲解如何在Spring Security实现对密码加密的各种用法,以及对BCrypt的用法进一步分析。

    概念

    Spring Security 为我们提供了一套加密规则和密码比对规则,org.springframework.security.crypto.password.PasswordEncoder 接口,该接口里面定义了三个方法。

    public interface PasswordEncoder {
        //加密(外面调用一般在注册的时候加密前端传过来的密码保存进数据库)
        String encode(CharSequence rawPassword);
    
        //加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比)
        boolean matches(CharSequence rawPassword, String encodedPassword);
    
        //是否需要再次进行编码, 默认不需要
        default boolean upgradeEncoding(String encodedPassword) {
            return false;
        }
    }

    在Spring Security下 PasswordEncoder 的实现类包含:

     其中常用到的分别有下面这么几个:   

        BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
        MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256...)
        DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
        NoOpPasswordEncoder:明文, 不做加密
        其他

    Spring Security中加密的用法:

    使用bcrypt bean

    applicationContext-shiro.xml中配置:

        <bean id="secureRandom" class="java.security.SecureRandom"/>
        <bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
            <constructor-arg name="version" value="$2A" /> <!-- salt随机生成版本 默认$2A-->
            <constructor-arg name="strength" value="10"/> <!-- 使用salt进行加密迭代次数,默认10-->
            <constructor-arg name="random" ref="secureRandom"/> <!-- 随机算法 -->
        </bean>
    
        <security:authentication-manager>
            <security:authentication-provider>
                <security:user-service>
                    <security:user name="user" password="$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y"
                                       authorities="ROLE_USER"/>
                    <security:user name="admin" password="$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi"
                                   authorities="ROLE_USER, ROLE_ADMIN"/>
                </security:user-service>
                <security:password-encoder ref="bCryptPasswordEncoder"/>
            </security:authentication-provider>
        </security:authentication-manager>

    说明:

    1)需要配置 bCryptPasswordEncoder的bean,在该bean配置时,可以指定其构造函数相关参数:

    version:salt随机生成版本,默认:采用 BCryptVersion.$2A.getVersion();

    strength:使用salt进行加密迭代次数,默认:10;

    random:随机算法,默认:new SecureRandom()

    2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。

    密码加密用法:

            // BCrypt加密与验证,内部默认:
            PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
            System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
            // BCrypt密文解析
    
            //参数解释
            //1)2a:加密算法版本号。
            //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。
            //3)密码加密:前面的内容是盐,后面的内容才是真正的密文。
            //以下方式可以更清晰的看出盐和全文。
            String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom());
            String result = BCrypt.hashpw("123456", salt);//全文
            System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
            System.out.println("result:" + result);

    在对密码加密时,可以采用上边这3种方法:

    1)BCryptPasswordEncoder的实例,直接调用 encode方法,此时version,strlength,random都采用默认值。

    2)也可以使用BCrypt来实现,实际上上边BCypt的操作就是BCryptPasswordEncoder#encode内部的方法实现。

    3)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("bCryptPasswordEncoder") private PasswordEncoder bCryptPasswordEncoder;

    使用md5 bean

    applicationContext-shiro.xml中配置

        <bean id="md5" class="org.springframework.security.crypto.password.MessageDigestPasswordEncoder">
            <constructor-arg name="algorithm" value="MD5"/>
            <property name="iterations" value="10"/>
        </bean>
    
        <security:authentication-manager>
            <security:authentication-provider>
                <security:user-service>
                    <security:user name="user" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                                   authorities="ROLE_USER"/>
                    <security:user name="admin" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                                   authorities="ROLE_USER, ROLE_ADMIN"/>
                </security:user-service>
                <security:password-encoder ref="md5"/>
            </security:authentication-provider>
        </security:authentication-manager>

    说明:

    1)需要配置md5 bean,在配置bean时,必须指定MessageDigestPasswordEncoder的构造函数参数:algorithm:指定算法类型,这里是MD5;

    2)另外,md5#iterations参数:迭代次数如果不指定,默认为1,这里指定为10;

    2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。

    密码加密用法:

            MessageDigestPasswordEncoder md5 = new MessageDigestPasswordEncoder("MD5");
            md5.setIterations(10);
            md5Password = "{MD5}" + md5.encode("password");
            System.out.println("MD5密码:" + md5Password);
            System.out.println("MD5密码对比:" + passwordEncoder.matches("password", md5Password));

    在对密码加密时,可以采用上边方法:

    1)MessageDigestPasswordEncoder的实例,可以设置其迭代次数。

    2)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("md5") private PasswordEncoder md5;

    缺省password-encoder(DelegatingPasswordEncoder)

    当缺省<security:password-encoder ref="xxx"/>时,Spring Security会使用系统内置的DelegatingPasswordEncoder,自动动适配 PasswordEncoder。

    applicationContext-shiro.xml中配置:

        <security:authentication-manager>
            <security:authentication-provider>
                <security:user-service>
                    <!-- noop NoOpPasswordEncoder.getInstance()-->
                    <security:user name="user" password="{noop}userpwd" authorities="ROLE_USER"/>
                    <security:user name="admin" password="{noop}adminpwd" authorities="ROLE_USER, ROLE_ADMIN"/>
                      <!-- bcrypt new BCryptPasswordEncoder() -->
                    <security:user name="user1" password="{bcrypt}$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y"
                                   authorities="ROLE_USER"/>
                    <security:user name="admin1" password="{bcrypt}$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi"
                                   authorities="ROLE_USER, ROLE_ADMIN"/>
                    <!-- MD5 new MessageDigestPasswordEncoder("MD5") -->
                    <security:user name="user2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                                   authorities="ROLE_USER"/>
                    <security:user name="admin2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                                   authorities="ROLE_USER, ROLE_ADMIN"/>
                </security:user-service>
                <security:password-encoder ref="md5"/>
            </security:authentication-provider>
        </security:authentication-manager>

    1)如果在<security:authentication-provider>下指定了<security:password-encoder ref="xxx"/>就不需要在<security:user name="xxx" password="yyy"authorities="zzz"/>中的 password 前边加上加密类型({noop}{bcrypt}{MD5}等),否则会导致密码验证失败;
    2)如果在<security:authentication-provider>下未指定<security:password-encoder ref="xxx"/>就必须要在<security:user name="xxx" password="yyy" authorities="zzz"/>中的 password 前边加上加密类型({noop}、{bcrypt}、{MD5}等),否则会导致密码验证失败。因为此时验证密码是否成功,会调用org.springframework.security.crypto.password.DelegatingPasswordEncoder.java中的#encode方法、#matches方法,而DelegatingPasswordEncoder中查找密码加密对应的PasswordEncoder时,会根据密码前缀的加密类型查找:如果查找失败,会导致查找不到delegate,也就是delegate为null。

    密码加密、解密代码示例:

            PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
    
            // 此时encode内部使用的就是 BCryptPasswordEncoder
            String encode = passwordEncoder.encode("password");
            System.out.println("bcrypt密码对比:" + passwordEncoder.matches("password", encode));
    
            // 不带salt,迭代
            String md5NoSaltPassword = "{MD5}" + DigestUtils.md5DigestAsHex("password".getBytes());
            System.out.println("MD5(不含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5NoSaltPassword));
    
            // 待salt,迭代
            MessageDigestPasswordEncoder md5SaltIterationsPassword = new MessageDigestPasswordEncoder("MD5");
            md5SaltIterationsPassword.setIterations(1);
            String md5Password = "{MD5}" + md5SaltIterationsPassword.encode("password");
            System.out.println("MD5(包含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5Password));
    
            String noopPassword = "{noop}password";
            System.out.println("noop密码对比:" + passwordEncoder.matches("password", noopPassword));

    输出结果:

    bcrypt密码对比:true
    MD5(不含salt、iterations)密码对比:true
    MD5(包含salt、iterations)密码对比:true
    noop密码对比:true

    DelegatingPasswordEncoder类讲解

    构造函数初始化

    DelegatingPasswordEncoder本身就是继承了 PasswordEncoder 类,因此也可以在applicationContext-shiro.xml中定义为bean,在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 为该bean。

    但是,实际上这么做是没有意义,因为在<security:authentication-provider>下不指定<security:password-encoder ref="xxx"/>时,系统会缺省的采用DelegatingPasswordEncoder作为PasswordEncoder的实现。

        public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
            if (idForEncode == null) {
                throw new IllegalArgumentException("idForEncode cannot be null");
            } else if (!idToPasswordEncoder.containsKey(idForEncode)) {
                throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
            } else {
                Iterator var3 = idToPasswordEncoder.keySet().iterator();
    
                while(var3.hasNext()) {
                    String id = (String)var3.next();
                    if (id != null) {
                        if (id.contains("{")) {
                            throw new IllegalArgumentException("id " + id + " cannot contain " + "{");
                        }
    
                        if (id.contains("}")) {
                            throw new IllegalArgumentException("id " + id + " cannot contain " + "}");
                        }
                    }
                }
    
                this.idForEncode = idForEncode;
                this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
                this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
            }
        }

    说明:

    1)调用DelegatingPasswordEncoder#constructor()类是PasswordEncoderFactories.java

    public class PasswordEncoderFactories {
        public static PasswordEncoder createDelegatingPasswordEncoder() {
            String encodingId = "bcrypt";
            Map<String, PasswordEncoder> encoders = new HashMap();
            encoders.put(encodingId, new BCryptPasswordEncoder());
            encoders.put("ldap", new LdapShaPasswordEncoder());
            encoders.put("MD4", new Md4PasswordEncoder());
            encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
            encoders.put("noop", NoOpPasswordEncoder.getInstance());
            encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
            encoders.put("scrypt", new SCryptPasswordEncoder());
            encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
            encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
            encoders.put("sha256", new StandardPasswordEncoder());
            encoders.put("argon2", new Argon2PasswordEncoder());
            return new DelegatingPasswordEncoder(encodingId, encoders);
        }
    
        private PasswordEncoderFactories() {
        }
    }

    上边代码是spring security系统中唯一用来初始化DelegatingPasswordEncoder的地方。

    • 1)在Spring Security系统将AuthenticationProvider的bean初始化到Spring容器时会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法初始化DelegatingPasswordEncoder;
    • 2)这个过程也就是给AutheticationProvider#passwordEncoder赋值的触发点;
    • 3)当然如果在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 不为DelegatingPasswordEncoder时,也将不会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法

    2)idToPasswordEncoder属性:DelegatingPasswordEncoder是一个能适配多种PasswordEncoder的委托类,其内部定义了一个Map<String,PasswordEncoder>集合:

    key为:PasswordEncoder的别名;
    value为:PasswordEncoder的具体实现类。

        private final Map<String, PasswordEncoder> idToPasswordEncoder;

    idToPasswordEncoder用来托管PassswordEncoder的实现,这个类是在DelegatingPasswordEncoder#constructor中被传递初始化的。

    3)idForEncode属性通过PasswordEncoderFactories#createDelegatingPasswordEncoder()中初始化DelegatingPasswordEncoder的代码,可以知道idForEncode的值是“bcrypt”;

    4)passwordEncoderForEncode属性:就是BCryptPasswordEncoder对象。

    encode加密

        public String encode(CharSequence rawPassword) {
            return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
        }

    说明:

    1)通过上边DelegatingPasswordEncoder#constructor()代码可以知道:passwordEncoderForEncode属性就是BCryptPasswordEncoder对象

    2)DelegatingPasswordEncoder#encode()方法:实际上就是"bcrypt"加密算法。这点十分重要,往往也是其特殊之处,需要使用者牢记。

    3)rawPassword参数:待加密密码明文。

    matches匹配密码

        public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
            if (rawPassword == null && prefixEncodedPassword == null) {
                return true;
            } else {
                // 根据密文前缀查找 delegate
                String id = this.extractId(prefixEncodedPassword);
                PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
                if (delegate == null) { // delegate查找失败
                    return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
                } else {
                    String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
                    return delegate.matches(rawPassword, encodedPassword);
                }
            }
        }

    说明:

    1)rawPassword参数:密码明文;

    2)prefixEncodedPassword参数:带有加密类型的密码密文,必须带有使用的PasswordEncoder类型(PasswordEncoderFactories#createDelegatingPasswordEncoder()中map#key);

    格式举例:

    {noop}password
    {bcypt}$2a$10$IK/02aEUVRBaeoQsvN.VluPLqNKZ2ZwwTRmAAWXmlnCU5DAjmjtRC
    {MD5}5f4dcc3b5aa765d61d8327deb882cf99
    {MD5}{L5M7tjEyGdBtyFCyk0pBXOLLFi3AOMEBZqdRDTAwV6c=}c05b48c699659f56462bbed387485cc6

    3)当没有指定密码加密类型({bcypt}等)时,会抛出异常:

    java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
        org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:250)
        org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:198)
        org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:90)
        org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:166)
        org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175)
        。。。

    BCryptPasswordEncoder类讲解

    属性:

        private Pattern BCRYPT_PATTERN;
        private final Log logger;
        private final int strength;
        private final BCryptPasswordEncoder.BCryptVersion version;
        private final SecureRandom random;

    说明:

    1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式;

    2)logger:日志操作类;

    3)strlength:生成salt迭代次数;

    4)version:生成salt采用的版本;

    5)random:随机生成slat实现。

    构造函数:

        public BCryptPasswordEncoder() {
            this(-1);
        }
    
        public BCryptPasswordEncoder(int strength) {
            this(strength, (SecureRandom)null);
        }
    
        public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) {
            this(version, (SecureRandom)null);
        }
    
        public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) {
            this(version, -1, random);
        }
    
        public BCryptPasswordEncoder(int strength, SecureRandom random) {
            this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
        }
    
        public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) {
            this(version, strength, (SecureRandom)null);
        }
    
        public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
            this.BCRYPT_PATTERN = Pattern.compile("\A\$2(a|y|b)?\$(\d\d)\$[./0-9A-Za-z]{53}");
            this.logger = LogFactory.getLog(this.getClass());
            if (strength == -1 || strength >= 4 && strength <= 31) {
                this.version = version;
                this.strength = strength == -1 ? 10 : strength;
                this.random = random;
            } else {
                throw new IllegalArgumentException("Bad strength");
            }
        }

    构造函数重构的比较多,在DelegatingPasswordEncoder中使用的就是第一个构造函数,此时属性会赋值默认值:

    1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式,默认值:Pattern.compile("\A\$2(a|y|b)?\$(\d\d)\$[./0-9A-Za-z]{53}")

    2)strlength:生成salt迭代次数,默认值:10;

    3)version:生成salt采用的版本,默认值:BCryptPasswordEncoder.BCryptVersion.$2A

    4)random:随机生成slat实现,默认值:空。

    encode方法:

        public String encode(CharSequence rawPassword) {
            String salt;
            if (this.random != null) {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
            }
    
            return BCrypt.hashpw(rawPassword.toString(), salt);
        }

    BCryptPasswordEncoder实际内部是使用 BCrypt 实现;

    matches方法:

        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            if (encodedPassword != null && encodedPassword.length() != 0) {
                if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                    this.logger.warn("Encoded password does not look like BCrypt");
                    return false;
                } else {
                    return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
                }
            } else {
                this.logger.warn("Empty encoded password");
                return false;
            }
        }

    从BCRYPT_PATTERN的值"\A\$2(a|y|b)?\$(\d\d)\$[./0-9A-Za-z]{53}",可以发现另外一些密文分为3部分:

    • 第一部分:以$2a、$2y、$2b开头;
    • 第二部分:以$数字
    • 第三部分:以$开头后边附加.、/、数字、大写字母、小写字母组成的,且长度为53的字符串。

    测试代码:

        @Test
        public void testPwdEncoder() {
            // 
            // BCrypt加密与验证
            PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
            System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
            // BCrypt密文解析
    
            //在密文中包含3段内容,$是分隔符。
            //1)2a:加密算法版本号。
            //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。
            //3)第3个$之后:前面的内容是盐,后面的内容才是真正的密文。
            //以下方式可以更清晰的看出盐和全文。
            String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom());
            String result = BCrypt.hashpw("123456", salt);//全文
            System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
            System.out.println("result:" + result);
    
            salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2B.getVersion(), 11, new SecureRandom());
            result = BCrypt.hashpw("123456", salt);//全文
            System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
            System.out.println("result:" + result);
        }

    打印:

    passwordEncoder 123456:$2a$10$XSpVd/lavtejOXHeDGNMOe1zxgblnsXWoTi0DFD/vN4Z6EjH1r97q
    passwordEncoder 123456:$2a$10$I9zV9AbsEdi36s7ovTQ2hOhUczFP5CXybnyJv9aNY6Ae6qky9oouu
    salt:$2a$10$yg5TNGzmyNe0di70exM.vO,salt's length:29
    result:$2a$10$yg5TNGzmyNe0di70exM.vOWLx.lMiniZ/BOCoecIc5tF/Q0CvYUJa
    salt:$2b$11$ncuDpd17nju3d6auOrQAr.,salt's length:29
    result:$2b$11$ncuDpd17nju3d6auOrQAr.BZqNeyyVgqhb3gncQyRUvuKHzA2.FOS

    从上边代码测试会发现,BCryptPasswordEncoder 实际内部是使用 BCrypt 实现,另外从测试可以发现使用SpringSecurity缺省password encoder生成密文有以下规则:

    1)在密文中包含3段内容,
    2)2a:salt生成算法版本号;
    3)10:salt迭代次数,默认为10(取值范围是:[4,31]),数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入;
    4)第3个$之后:前面的内容是盐,后面的内容才是真正的密文;
    5)随机生成salt,且salt的长度为29;

  • 相关阅读:
    注册登录过程点滴(一):初始的想法分享是王道
    根据Cron表达式,通过Spring自带的CronSequenceGenerator类获取下次执行时间
    解决jqGrid中,当前页一直显示为0的问题
    使用JDK自带功能,实现一个简单的Web Service接口发布
    Linux 僵尸进程 ( Zombie or defunct )
    C语言赋值操作符
    面试题 ( ++a )和( a++ )
    关于学习Linux的经典书籍
    C语言中的 sizeof 问题
    条件变量 pthread_cond_wait ()
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/12203823.html
Copyright © 2011-2022 走看看