【shiro】UsernamePasswordToken中char[]替代String的安全性

shiro中UsernamePasswordToken类的源码中有一段注释很有意思。

* <p>Note that this class stores a password as a char[] instead of a String
 * (which may seem more logical).  This is because Strings are immutable and their
 * internal value cannot be overwritten - meaning even a nulled String instance might be accessible in memory at a later
 * time (e.g. memory dump).  This is not good for sensitive information such as passwords. For more information, see the
 * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx">
 * Java Cryptography Extension Reference Guide</a>.</p>

这个类的password字段用一个字符数组来存储,而不是一个似乎更符合逻辑的字符串。原因是java中字符串是不可变的,并且它里面的value(java.lang.String中的value数组)不能被归零化,这意味着内存中即便是一个为null的字符串实例在稍后一段时间中都是可见的(譬如dump内存),所以对于诸如password这种敏感信息而言不安全。

这句话不太好看,但这句话透露了两个方向,一个是为什么要用char[]来代替String,另一个则是如果用String带来的问题是什么。

一、为什么用char[]代替String

对于password而言,之所以用char[]代替String,源码中的clear()方法足以证明,因为char[]的password可以被归零化,当作完必要的逻辑之后,password就没有存在的必要,归零化后即便是dump当前的JVM内存,password也不会暴露。

 /**
     * Clears out (nulls) the username, password, rememberMe, and inetAddress.  The password bytes are explicitly set to
     * <tt>0x00</tt> before nulling to eliminate the possibility of memory access at a later time.
     */
    public void clear() {
        if (this.password != null) {
            for (int i = 0; i < password.length; i++) {
                this.password[i] = 0x00;
            }
            this.password = null;
        }

    }

上面的代码逻辑,如果换成String改写,逻辑是这样的。

public void clear(){
		//password is string
		if(password != null)
			password = null;
	}	

这两者有什么区别呢?password[i]=0x00的时候,操作的是同一片内存,可以这么粗略的理解,如果password[i]=0x01,那么归零后password[i]=0x00,相当于password的字符数组中的数据被抹除了,dump内存下来只能看到归零后的值;而对于String password="xx"或者String password=new String("xx");而言,password=null,此时这个"xx"是否会像password[i]一样归零化呢?答案是不会。为什么不会,这就是使用String带来的问题。

还有一个点,String类中的value是一个final的char[],毫无疑问的是final修饰的char[],一样可以将char[i]归零化,但是纵观String类的源码,除非反射,否则不能修改这个value。

因此使用String带来的问题是password=null之后,原始的密码还留在内存中,那么原始密码存在哪里?

二、String带来的问题是什么

String类在字节码中有一个私有化的字节码常量池String pool,是存储String对象的定长HashTable(参见:点击打开链接)。String password="xx",这句代码在编译的时候会创建一个String对象并存放在字符串常量池中;String password=new
String("xx"),这句代码则会先在常量池中查找,看是否能找到"xx".equals(池对象)(String类是重写了equals方法的,包含了比较String对象中的value[i]是否相等的逻辑),如果找到则传入String类的构造方法,如果找不到则会创建一个String对象然后再进入String构造方法。这也就意味着,无论是哪一种方式,都会在常量池中存在一个String对象"xx",因此无论最终password=null或任何其他值,对于"xx"对象而言,依旧还在内存中的,因此对于password这种安全敏感的数据来说,dump内存是可以看到的。

进一步的验证信息。

1. String password="xx"

package cn.wxy.str;

public class StringDemo {
	public static void main(String[] args) {
		String password = "xx";
	}
}

会首先在字节码常量池中找一个叫做xx的String对象,然后赋值给password,至于这个xx的String对象,结合java.lang.String的源码不难想象。

2. String password=new String("xx")

在看字节码之前,先回顾一下java.lang.String的构造方法。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

先不着急讨论这个构造方法,结合字节码来看。

package cn.wxy.str;

public class StringDemo {
	public static void main(String[] args) {
		String password = new String("xx");
	}
}

会先分配内存给password,然后从常量池中查找xx的String对象,赋值然后调用构造方法。

也就是说,String password="xx"会在String的字节码常量池中创建一个对象,然后password来引用;而String password=new String("xx");会创建两个对象,先在字节码常量池中创建一个对象,然后将该对象的value和hash赋值给password,此时的password在堆内存中。如果"xx"是输入的密码,那么此时dump内存就会暴露。

三、UsernamePasswordToken源码

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shiro.authc;

/**
 * <p>A simple username/password authentication token to support the most widely-used authentication mechanism.  This
 * class also implements the {@link RememberMeAuthenticationToken RememberMeAuthenticationToken} interface to support
 * "Remember Me" services across user sessions as well as the
 * {@link org.apache.shiro.authc.HostAuthenticationToken HostAuthenticationToken} interface to retain the host name
 * or IP address location from where the authentication attempt is occuring.</p>
 * <p/>
 * <p>"Remember Me" authentications are disabled by default, but if the application developer wishes to allow
 * it for a login attempt, all that is necessary is to call {@link #setRememberMe setRememberMe(true)}.  If the underlying
 * <tt>SecurityManager</tt> implementation also supports <tt>RememberMe</tt> services, the user's identity will be
 * remembered across sessions.
 * <p/>
 * <p>Note that this class stores a password as a char[] instead of a String
 * (which may seem more logical).  This is because Strings are immutable and their
 * internal value cannot be overwritten - meaning even a nulled String instance might be accessible in memory at a later
 * time (e.g. memory dump).  This is not good for sensitive information such as passwords. For more information, see the
 * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx">
 * Java Cryptography Extension Reference Guide</a>.</p>
 * <p/>
 * <p>To avoid this possibility of later memory access, the application developer should always call
 * {@link #clear() clear()} after using the token to perform a login attempt.</p>
 *
 * @since 0.1
 */
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {

    /*--------------------------------------------
    |             C O N S T A N T S             |
    ============================================*/

    /*--------------------------------------------
    |    I N S T A N C E   V A R I A B L E S    |
    ============================================*/
    /**
     * The username
     */
    private String username;

    /**
     * The password, in char[] format
     */
    private char[] password;

    /**
     * Whether or not 'rememberMe' should be enabled for the corresponding login attempt;
     * default is <code>false</code>
     */
    private boolean rememberMe = false;

    /**
     * The location from where the login attempt occurs, or <code>null</code> if not known or explicitly
     * omitted.
     */
    private String host;

    /*--------------------------------------------
    |         C O N S T R U C T O R S           |
    ============================================*/

    /**
     * JavaBeans compatible no-arg constructor.
     */
    public UsernamePasswordToken() {
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted
     * during an authentication attempt, with a <tt>null</tt> {@link #getHost() host} and a
     * <tt>rememberMe</tt> default of <tt>false</tt>.
     *
     * @param username the username submitted for authentication
     * @param password the password character array submitted for authentication
     */
    public UsernamePasswordToken(final String username, final char[] password) {
        this(username, password, false, null);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted
     * during an authentication attempt, with a <tt>null</tt> {@link #getHost() host} and
     * a <tt>rememberMe</tt> default of <tt>false</tt>
     * <p/>
     * <p>This is a convience constructor and maintains the password internally via a character
     * array, i.e. <tt>password.toCharArray();</tt>.  Note that storing a password as a String
     * in your code could have possible security implications as noted in the class JavaDoc.</p>
     *
     * @param username the username submitted for authentication
     * @param password the password string submitted for authentication
     */
    public UsernamePasswordToken(final String username, final String password) {
        this(username, password != null ? password.toCharArray() : null, false, null);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, the
     * inetAddress from where the attempt is occurring, and a default <tt>rememberMe</tt> value of <tt>false</tt>
     *
     * @param username the username submitted for authentication
     * @param password the password string submitted for authentication
     * @param host     the host name or IP string from where the attempt is occuring
     * @since 0.2
     */
    public UsernamePasswordToken(final String username, final char[] password, final String host) {
        this(username, password, false, host);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, the
     * inetAddress from where the attempt is occurring, and a default <tt>rememberMe</tt> value of <tt>false</tt>
     * <p/>
     * <p>This is a convience constructor and maintains the password internally via a character
     * array, i.e. <tt>password.toCharArray();</tt>.  Note that storing a password as a String
     * in your code could have possible security implications as noted in the class JavaDoc.</p>
     *
     * @param username the username submitted for authentication
     * @param password the password string submitted for authentication
     * @param host     the host name or IP string from where the attempt is occuring
     * @since 1.0
     */
    public UsernamePasswordToken(final String username, final String password, final String host) {
        this(username, password != null ? password.toCharArray() : null, false, host);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, as well as if the user
     * wishes their identity to be remembered across sessions.
     *
     * @param username   the username submitted for authentication
     * @param password   the password string submitted for authentication
     * @param rememberMe if the user wishes their identity to be remembered across sessions
     * @since 0.9
     */
    public UsernamePasswordToken(final String username, final char[] password, final boolean rememberMe) {
        this(username, password, rememberMe, null);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, as well as if the user
     * wishes their identity to be remembered across sessions.
     * <p/>
     * <p>This is a convience constructor and maintains the password internally via a character
     * array, i.e. <tt>password.toCharArray();</tt>.  Note that storing a password as a String
     * in your code could have possible security implications as noted in the class JavaDoc.</p>
     *
     * @param username   the username submitted for authentication
     * @param password   the password string submitted for authentication
     * @param rememberMe if the user wishes their identity to be remembered across sessions
     * @since 0.9
     */
    public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
        this(username, password != null ? password.toCharArray() : null, rememberMe, null);
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, if the user
     * wishes their identity to be remembered across sessions, and the inetAddress from where the attempt is ocurring.
     *
     * @param username   the username submitted for authentication
     * @param password   the password character array submitted for authentication
     * @param rememberMe if the user wishes their identity to be remembered across sessions
     * @param host       the host name or IP string from where the attempt is occuring
     * @since 1.0
     */
    public UsernamePasswordToken(final String username, final char[] password,
                                 final boolean rememberMe, final String host) {

        this.username = username;
        this.password = password;
        this.rememberMe = rememberMe;
        this.host = host;
    }

    /**
     * Constructs a new UsernamePasswordToken encapsulating the username and password submitted, if the user
     * wishes their identity to be remembered across sessions, and the inetAddress from where the attempt is ocurring.
     * <p/>
     * <p>This is a convience constructor and maintains the password internally via a character
     * array, i.e. <tt>password.toCharArray();</tt>.  Note that storing a password as a String
     * in your code could have possible security implications as noted in the class JavaDoc.</p>
     *
     * @param username   the username submitted for authentication
     * @param password   the password string submitted for authentication
     * @param rememberMe if the user wishes their identity to be remembered across sessions
     * @param host       the host name or IP string from where the attempt is occuring
     * @since 1.0
     */
    public UsernamePasswordToken(final String username, final String password,
                                 final boolean rememberMe, final String host) {
        this(username, password != null ? password.toCharArray() : null, rememberMe, host);
    }

    /*--------------------------------------------
    |  A C C E S S O R S / M O D I F I E R S    |
    ============================================*/

    /**
     * Returns the username submitted during an authentication attempt.
     *
     * @return the username submitted during an authentication attempt.
     */
    public String getUsername() {
        return username;
    }

    /**
     * Sets the username for submission during an authentication attempt.
     *
     * @param username the username to be used for submission during an authentication attempt.
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Returns the password submitted during an authentication attempt as a character array.
     *
     * @return the password submitted during an authentication attempt as a character array.
     */
    public char[] getPassword() {
        return password;
    }

    /**
     * Sets the password for submission during an authentication attempt.
     *
     * @param password the password to be used for submission during an authentication attemp.
     */
    public void setPassword(char[] password) {
        this.password = password;
    }

    /**
     * Simply returns {@link #getUsername() getUsername()}.
     *
     * @return the {@link #getUsername() username}.
     * @see org.apache.shiro.authc.AuthenticationToken#getPrincipal()
     */
    public Object getPrincipal() {
        return getUsername();
    }

    /**
     * Returns the {@link #getPassword() password} char array.
     *
     * @return the {@link #getPassword() password} char array.
     * @see org.apache.shiro.authc.AuthenticationToken#getCredentials()
     */
    public Object getCredentials() {
        return getPassword();
    }

    /**
     * Returns the host name or IP string from where the authentication attempt occurs.  May be <tt>null</tt> if the
     * host name/IP is unknown or explicitly omitted.  It is up to the Authenticator implementation processing this
     * token if an authentication attempt without a host is valid or not.
     * <p/>
     * <p>(Shiro's default Authenticator allows <tt>null</tt> hosts to support localhost and proxy server environments).</p>
     *
     * @return the host from where the authentication attempt occurs, or <tt>null</tt> if it is unknown or
     *         explicitly omitted.
     * @since 1.0
     */
    public String getHost() {
        return host;
    }

    /**
     * Sets the host name or IP string from where the authentication attempt occurs.  It is up to the Authenticator
     * implementation processing this token if an authentication attempt without a host is valid or not.
     * <p/>
     * <p>(Shiro's default Authenticator
     * allows <tt>null</tt> hosts to allow localhost and proxy server environments).</p>
     *
     * @param host the host name or IP string from where the attempt is occuring
     * @since 1.0
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * Returns <tt>true</tt> if the submitting user wishes their identity (principal(s)) to be remembered
     * across sessions, <tt>false</tt> otherwise.  Unless overridden, this value is <tt>false</tt> by default.
     *
     * @return <tt>true</tt> if the submitting user wishes their identity (principal(s)) to be remembered
     *         across sessions, <tt>false</tt> otherwise (<tt>false</tt> by default).
     * @since 0.9
     */
    public boolean isRememberMe() {
        return rememberMe;
    }

    /**
     * Sets if the submitting user wishes their identity (pricipal(s)) to be remembered across sessions.  Unless
     * overridden, the default value is <tt>false</tt>, indicating <em>not</em> to be remembered across sessions.
     *
     * @param rememberMe value inidicating if the user wishes their identity (principal(s)) to be remembered across
     *                   sessions.
     * @since 0.9
     */
    public void setRememberMe(boolean rememberMe) {
        this.rememberMe = rememberMe;
    }

    /*--------------------------------------------
    |               M E T H O D S               |
    ============================================*/

    /**
     * Clears out (nulls) the username, password, rememberMe, and inetAddress.  The password bytes are explicitly set to
     * <tt>0x00</tt> before nulling to eliminate the possibility of memory access at a later time.
     */
    public void clear() {
        this.username = null;
        this.host = null;
        this.rememberMe = false;

        if (this.password != null) {
            for (int i = 0; i < password.length; i++) {
                this.password[i] = 0x00;
            }
            this.password = null;
        }

    }

    /**
     * Returns the String representation.  It does not include the password in the resulting
     * string for security reasons to prevent accidentially printing out a password
     * that might be widely viewable).
     *
     * @return the String representation of the <tt>UsernamePasswordToken</tt>, omitting
     *         the password.
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getName());
        sb.append(" - ");
        sb.append(username);
        sb.append(", rememberMe=").append(rememberMe);
        if (host != null) {
            sb.append(" (").append(host).append(")");
        }
        return sb.toString();
    }

}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-11 07:13:58

【shiro】UsernamePasswordToken中char[]替代String的安全性的相关文章

Java中char和String的相互转换

转自:http://blog.csdn.net/yaokai_assultmaster/article/details/52082763 Java中char是一个基本类型,而String是一个引用类型.有时候我们需要在它们之间互相转换. String转换为char 在Java中将String转换为char是非常简单的. 1. 使用String.charAt(index)(返回值为char)可以得到String中某一指定位置的char. 2. 使用String.toCharArray()(返回值为

c++中char*\wchar_t*\string\wstring之间的相互转换

最近在编程中经常遇到需要多字节字符与宽字节字符相互转换的问题,一直自己贴那几句代码.觉得麻烦,于是就自己写了一个类来封装wchar_t与char类型间的转换, 其他的,诸如:CString\ LPWSTR\TCHAR   CHAR\LPSTR之间也是一样用 头文件: #ifndef USE_H_ #define USE_H_ #include <iostream> #include <windows.h> #include <string> using namespac

C#中char[]与string之间的转换

string 转换成 Char[] string ss = "abcdefg"; char[] cc = ss.ToCharArray(); Char[] 转换成string string s = new string(cc); 此外,byte[] 与 string 之间的装换 byte[] bb = Encoding.UTF8.GetBytes(ss); string s = Encoding.UTF8.GetString(bb); 我们也可以利用 StringBuilder 来进行

C++ 中 int,char*,string,CString之间相互转换-整理

#include <string> //使用C++标准库的string类时 using namespace std; //同上 #include <sstream> #include <iostream> #include <stdlib.h> //要将string类和int类型直接转换最好有这些包含, //因为自己写一个转换函数比较方便,函数定义参考如下 string getstring ( const int n ) { std::stringstrea

delphi中pchar、char、string的定义与区别

Char是单个字符:PChar 就是指向一个以Null 结尾的每个字符为一个字节的字符串的指针. (可以看成是一个字符串)String是字符数组,以字符#0结尾: Char只占一字节的空间,而Pchar保存的是一个指针,在现在32位的系统中占有1个指针字节的大小.type PChar=^Char; PChar类型是一个指向Char的指针,并使用Delphi的扩展语法.它也能够作为一个String或者是一个Char数组指针来对待. Pchar的用途 1.PChar最经常的使用方式就是作为用C或C+

深入理解c++中char*与wchar_t*与string以及wstring之间的相互转换 [转]

本篇文章是对c++中的char*与wchar_t*与string以及wstring之间的相互转换进行了详细的分析介绍,需要的朋友参考下. 1 #ifndef USE_H_ 2 #define USE_H_ 3 4 #include <iostream> 5 #include <windows.h> 6 #include <string> 7 using namespace std; 8 class CUser 9 { 10 public: 11 CUser(); 12

c++ 中double与string之间的转换,char *

/* * main.cpp * * Created on: Apr 7, 2016 * Author: lizhen */ #include <iostream> //#include "MySqrt.h" #include <math.h> #include <vector> #include <typeinfo> #include <exception> #include <stdexcept> #includ

C++ 中int,char,string,CString类型转换

1. c++中string到int的转换 1) 在C标准库里面,使用atoi: #include <cstdlib> #include <string> std::string text = "152"; int number = std::atoi( text.c_str() ); if (errno == ERANGE) //可能是std::errno { //number可能由于过大或过小而不能完全存储 } else if (errno == ????)

c#中的char byte string 类型之间的转换

byte 是字节型,字节,就是储存数据的一种单位而已,一般用于二进制文件的读写.char 是字符型,字符,就是单个的字母.数字.符号等等.string 是字符串型,字符串,就是若干个字符. 而且 byte[] 和 char[] 都是数组类型,string 是变量类型,三者没法直接比较.要么是 byte[].char[].string[],要么 byte.char.string. 如果要定义一个字符串变量,那么应该用 string:如果是一个字符串数组,那么应该用 string[]. 1. byt