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 password=“xx"或String password=new String("xx")的时候发生了什么?
二、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内存就会暴露。
三、隐式的String.intern()
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
String password="xx"和String password=new String("xx");的时候,字符串字面量"xx"会被JVM内建一个String对象放在字符串常量池中然后再被引用,那字符串常量池又怎么理解?
整个String类的源码中和pool有关的,就只有intern()方法,作为字符串常量池的出入口,“A pool of strings, initially empty, is maintained privately by the class String”,字符串常量池,初始化为空,String类私有。进一步看注释的内容,当intern()方法被调用时候,会先去字符串常量池中通过equals(String已经改写了,主要逻辑是比较字符对象中char[i]的数据是否一致)查找是否有相同对象,如果有则返回,如果没有则将该字符串对象添加到常量池然后将常量池中该对象的引用返回。
更有意思的一句注释,“ All literal strings and string-valued constant expressions are interned.”,所有字面量字符串和常量字符串表达式都会被interned,这里interned的直接翻译是拘留,可以形象的理解成intern()方法的语义,将该字符串对象放入常量池然后返回其引用。
此时再回顾String password="xx"和String password=new String("xx")。
如下图所示,String str="xx",在编译期的时候str会指向字节码常量池中的一个符号引用,等到运行时该符号引用分配内存的时候才会替换成内存地址,然后返回给栈内存中的str;而String str2=new String("xx");则会将常量池中已经创建好的对象str返回给String的构造方法 String(String original),其中的original参数引用的就是常量池中str保存的地址,为了更好理解,重新贴一下String类的改构造方法。两者都存在字符串字面量"xx",因此都会被interned。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
所以,当你第一次写下String password="xx"的时候,会先到常量池通过equals查找,找不到则建立对象然后返回引用;而String password=new String("xx").intern();,会先执行new指令,给password在堆内存中分配内存,然后根据字符串字面量"xx"会导致其到常量池中查找,找到返回给构造方法调用,找不到则创建对象然后赶回给构造方法调用,紧接着的intern()方法还会再一次进入到常量池查找,“刚才堆内存中创建的对象.equals(常量池中对象)”,找到则返回其引用。
过程有些曲折,但是这正如“ All literal strings and string-valued constant expressions are interned.”和intern()方法表达的语义一样。
java语言规范中关于字符串字面量的定义:The Java™ Language Specification(3.10.5. String Literals,在第三章第十节第五小段)
java API中英文对照:
四、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();
}
}