mysql Found invalid password for user
最近接到用户反馈,一直使用的mysql帐号却连接不上了。具体错误如下:
1 E:\mysql-5.6.16\client\Debug>mysql.exe -h127.0.0.1 -P3306 -uedgeyang -p 2 Enter password: ******** 3 ERROR 1045 (28000): Access denied for user 'edgeyang'@'127.0.0.1' (using password 4 : NO)
首先,登录上去查看用户权限。发现show grants确实没有用户,但mysql.user表内是有该用户的权限记录的。初步有两个猜测:
- 权限表没有加载到内存,但flush privileges刷新权限后同样不能登录;
- 表损坏,但check table显示表正常。
mysql> show grants for edgeyang@'127.0.0.1'; ERROR 1141 (42000): There is no such grant defined for user 'edgeyang' on host ' 127.0.0.1'
然后,发现mysql.user表内的权限密码是明文的。密码一般都是加密存储的,明文存储mysql不能解析?
mysql> select host,user,password from user where user='edgeyang' \G *************************** 1. row *************************** host: 127.0.0.1 user: edgeyang password: edgeyang 1 row in set (0.00 sec)
查看mysql error log,发现如下信息。看样子是密码非法,导致mysql不能识别该用户。
[Warning] Found invalid password for user: 'edgeyang@127.0.0.1'; Ignoring user
那mysql用户密码是用什么加密的,并且加密后密码有怎样的格式(方便以后判断是否非法密码)?
查看mysql 5.6.16源码,追踪flush privileges命令的执行情况,看看mysql是如何加载权限表的。
第一步,调用函数reload_acl_and_cache重载权限表,并根据需要刷新日志文件(例如错误日志,慢查询日志)。其中权限表分三个部分:
- acl_reload函数加载的权限表,mysql.user,mysql.db,mysql.proxies_priv;
- grant_reload函数加载的权限表,mysql.tables_priv,mysql.columns_priv;
- servers_reload函数加载的权限表,mysql.servers。
1 bool reload_acl_and_cache(THD *thd, unsigned long options, 2 TABLE_LIST *tables, int *write_to_binlog) 3 { 4 if (thd) 5 { 6 bool reload_acl_failed= acl_reload(thd); 7 bool reload_grants_failed= grant_reload(thd); 8 bool reload_servers_failed= servers_reload(thd); 9 } 10 11 if (options & REFRESH_ERROR_LOG) 12 if (flush_error_log()) 13 { 14 /* 15 When flush_error_log() failed, my_error() has not been called. 16 So, we have to do it here to keep the protocol. 17 */ 18 my_error(ER_UNKNOWN_ERROR, MYF(0)); 19 result= 1; 20 } 21 22 if ((options & REFRESH_SLOW_LOG) && opt_slow_log) 23 logger.flush_slow_log(); 24 }
第二步,查看acl_reload函数。函数的功能如下:
- 打开权限表,清空权限的缓存;
- 调用函数acl_load加载权限;
- 关闭权限表。
1 /* 2 Forget current user/db-level privileges and read new privileges 3 from the privilege tables. 4 */ 5 my_bool acl_reload(THD *thd) 6 { 7 TABLE_LIST tables[3]; 8 /* 9 To avoid deadlocks we should obtain table locks before 10 obtaining acl_cache->lock mutex. 11 */ 12 tables[0].init_one_table(C_STRING_WITH_LEN("mysql"), 13 C_STRING_WITH_LEN("user"), "user", TL_READ); 14 tables[1].init_one_table(C_STRING_WITH_LEN("mysql"), 15 C_STRING_WITH_LEN("db"), "db", TL_READ); 16 tables[2].init_one_table(C_STRING_WITH_LEN("mysql"), 17 C_STRING_WITH_LEN("proxies_priv"), 18 "proxies_priv", TL_READ); 19 if (open_and_lock_tables(thd, tables, FALSE, MYSQL_LOCK_IGNORE_TIMEOUT)) 20 { 21 goto end; 22 } 23 24 delete_dynamic(&acl_wild_hosts); 25 my_hash_free(&acl_check_hosts); 26 27 if ((return_val= acl_load(thd, tables))) 28 { // Error. Revert to old list 29 init_check_host(); 30 } 31 else 32 { 33 //delete something 34 } 35 if (old_initialized) 36 mysql_mutex_unlock(&acl_cache->lock); 37 end: 38 close_acl_tables(thd); 39 DBUG_RETURN(return_val); 40 }
第三步,查看acl_load函数。函数功能如下:
- 逐行读取mysql.user表记录;
- 逐字段读取,并检查字段值在mysql各版本是否合法;
- 读取其他表,如mysql.db。
其中第二个小步骤里面,函数set_user_salt会检查password字段合法性。
1 /* 2 Initialize structures responsible for user/db-level privilege checking 3 and load information about grants from open privilege tables. 4 */ 5 static my_bool acl_load(THD *thd, TABLE_LIST *tables) 6 { 7 acl_cache->clear(1); // Clear locked hostname cache 8 9 init_sql_alloc(&global_acl_memory, ACL_ALLOC_BLOCK_SIZE, 0); 10 /* 11 Prepare reading from the mysql.user table 12 */ 13 if (init_read_record(&read_record_info, thd, table=tables[0].table, 14 NULL, 1, 1, FALSE)) 15 goto end; 16 while (!(read_record_info.read_record(&read_record_info))) 17 { 18 /* Read legacy password */ 19 password= get_field(&global_acl_memory, 20 table->field[MYSQL_USER_FIELD_PASSWORD]); 21 password_len= password ? strlen(password) : 0; 22 user.auth_string.str= password ? password : const_cast<char*>(""); 23 user.auth_string.length= password_len; 24 25 { 26 /*省略此处代码,为了兼容mysql其他版本的权限信息*/ 27 /* 28 Transform hex to octets and adjust the format. 29 */ 30 if (set_user_salt(&user, password, password_len)) 31 { 32 sql_print_warning("Found invalid password for user: '%s@%s'; " 33 "Ignoring user", user.user ? user.user : "", 34 user.host.get_host() ? user.host.get_host() : ""); 35 continue; 36 } 37 } 38 } // END while reading records from the mysql.user table 39 }
第四步,set_user_salt判断密码是否合法。由此可知,mysq.user存储的加密后l密码的合法格式是:
- 为空;
- 新格式密码为41个字符,并且以字符*开头;
- 旧格式密码为16个字符。
1 #define SCRAMBLE_LENGTH 20 2 #define SCRAMBLE_LENGTH_323 8 3 /* length of password stored in the db: new passwords are preceeded with '*' */ 4 #define SCRAMBLED_PASSWORD_CHAR_LENGTH (SCRAMBLE_LENGTH*2+1) 5 #define SCRAMBLED_PASSWORD_CHAR_LENGTH_323 (SCRAMBLE_LENGTH_323*2) 6 /** 7 Convert scrambled password to binary form, according to scramble type, 8 Binary form is stored in user.salt. 9 */ 10 11 static 12 bool 13 set_user_salt(ACL_USER *acl_user, const char *password, uint password_len) 14 { 15 bool result= false; 16 /* Using old password protocol */ 17 if (password_len == SCRAMBLED_PASSWORD_CHAR_LENGTH) 18 { 19 get_salt_from_password(acl_user->salt, password); 20 acl_user->salt_len= SCRAMBLE_LENGTH; 21 } 22 else if (password_len == SCRAMBLED_PASSWORD_CHAR_LENGTH_323) 23 { 24 get_salt_from_password_323((ulong *) acl_user->salt, password); 25 acl_user->salt_len= SCRAMBLE_LENGTH_323; 26 } 27 else if (password_len == 0 || password == NULL) 28 { 29 /* This account doesn't use a password */ 30 acl_user->salt_len= 0; 31 } 32 else if (acl_user->plugin.str == native_password_plugin_name.str || 33 acl_user->plugin.str == old_password_plugin_name.str) 34 { 35 /* Unexpected format of the hash; login will probably be impossible */ 36 result= true; 37 } 38 39 /* 40 Since we're changing the password for the user we need to reset the 41 expiration flag. 42 */ 43 acl_user->password_expired= false; 44 45 return result; 46 }
第五步,通过password()函数加密密码后,重新flush privileges就可以登录了。
mysql> update user set password=password('edgeyang') where user='edgeyang'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> flush privileges; Query OK, 0 rows affected (0.01 sec) mysql> show grants for edgeyang@'127.0.0.1'; +------------------------------------------------------------------------------- -------------------------------------------+ | Grants for edgeyang@127.0.0.1 | +------------------------------------------------------------------------------- -------------------------------------------+ | GRANT ALL PRIVILEGES ON *.* TO 'edgeyang'@'127.0.0.1' IDENTIFIED BY PASSWORD ' *3C316168860E5204C4A2AEDDF36D13E99D80A981' | +------------------------------------------------------------------------------- -------------------------------------------+ 1 row in set (0.00 sec)
最后,看看password函数的实现和mysql文档,可以发现password采用了两次sha1来加密密码。
mysql password函数文档描述:http://dev.mysql.com/doc/refman/5.5/en/encryption-functions.html#function_password
1 inline static 2 void compute_two_stage_sha1_hash(const char *password, size_t pass_len, 3 uint8 *hash_stage1, uint8 *hash_stage2) 4 { 5 /* Stage 1: hash password */ 6 compute_sha1_hash(hash_stage1, password, pass_len); 7 8 /* Stage 2 : hash first stage's output. */ 9 compute_sha1_hash(hash_stage2, (const char *) hash_stage1, SHA1_HASH_SIZE); 10 }
浙公网安备 33010602011771号