這篇隨筆談一談如何在Java環境下利用Unix/Linux的用戶名和密碼對用戶的權限作出過濾。為方便大家學習交流,本文中給出了源代碼,借此拋磚引玉,歡迎大家對這個簡單的登錄模型做出改進或者設計出自己的技術方案。
由標題我們不難看出,與本文相關的知識點主要有3個:
1 JAAS這個解耦設計的多層驗證方法(1.4後已歸入Java核心庫中)
2 應用JNI訪問底層代碼,及JNI中簡單的類型匹配
3 在shadow模式下,Unix/Linux系統的用戶驗證
首先聊聊JAAS,顧名思義,JAAS由認證和授權兩個主要組件組成。JAAS的交互點在LoginContext這個類裡面,在構造LoginContext時,常常需要指定兩個內容(還有其他默認的構造子重載形式),這兩個內容是LoginModule的名字和Subject。
為使描述直觀,先給出代碼如下:
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
public class MyLogin {
public MyLogin(){
Subject subject = new Subject();
subject.getPrincipals().add(new Principal(){
public String getName() {
return "yiyang";
}
});
subject.getPrivateCredentials().add("sh0w00f");
try {
LoginContext lc = new LoginContext("mylogin",subject);
lc.login();
} catch (LoginException e) {
e.printStackTrace();
}
}
public static void main(String[] args){
new MyLogin();
}
}
先說LoginModule的名字,在系統屬性java.security.auth.login.config中(或者在jre/lib/security/java.security)指定了LoginModule的配置文件,LoginModule在Java中被定義成一個接口,這個地方應用了面向對象的依賴倒置原則,使用了類似JDBC這樣的SPI的機制來定制認證策略。根據用戶在構造LoginContext時指定的LoginModule的名字Java在系統環境中找到對應的LoginModule配置文件,這個配置文件的最簡單形式如下:
mylogin {
UnixLoginModule required
};
這時當我們應用mylogin這個名字實例化LoginContext的時候,系統就會自動的找到UnixLoginModule這個LoginModule去處理。後面的required是一個flag標志,表示此次驗證的關鍵性,有4個值可以選擇,當選定required時則表示必須成功,由此我們就可以定義一系列的驗證,形成一個過濾層,並根據不同的flag得出最後的結論,比如:我們可能希望我們的web用戶只要通過數據庫的驗證,而不必通過操作系統的驗證。
此外我們還可以設置一些其他的參數(以key=value的形式),而且實際上驗證是兩階段提交的,並且可以通過回調函數的形式在具體的認證平台上做一些個性化Context設置。對這些JAAS細節感興趣的朋友可以讀相應的JAAS文檔規范。
再說第二個參數Subject,這個主題封裝了用戶需要驗證的信息,主要包括principal和公鑰私鑰兩部分,詳細的設置方法可以參考上面的代碼。
lc.login返回了一個true或false表示了這次的驗證是否成功。
當一個Subject成功login後,就可以通過這個Subject做一些特許的動作Subject.doAs,這些動作根據Subject中principal的不同在com.sun.security.auth.PolicyFile指定的配置文件做了定義,這部分是屬於JAAS授權的內容,因為在我們的程序中暫時用不到,所以不做詳細討論了,我們僅僅根據login返回的true或false來決定用戶是否可以登錄我們的系統即可。
OK,說到這裡,我們給出UnixLoginModule的實現代碼:
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
public class UnixLoginModule implements LoginModule {
private String usr, passwd;
public boolean abort() throws LoginException {
return false;
}
public boolean commit() throws LoginException {
System.out.println("Passing final confirmation\ndone");
return true;
}
public boolean login() throws LoginException {
;
if (PasswdCheck.check(this.usr, this.passwd) == 0) {
System.out.println("Your Login Succeed");
return true;
}
System.out.println("Your Login failed");
return false;
}
public boolean logout() throws LoginException {
return false;
}
public void initialize(Subject subject, CallbackHandler callbackHandler,
Map<String, ?> sharedState, Map<String, ?> options) {
this.passwd = (String) subject.getPrivateCredentials().iterator()
.next();
this.usr = (String) subject.getPrincipals().iterator().next().getName();
}
}
代碼中的各個方法是LoginModule所定義的必須實現的方法。注意到代碼中,我們應用了PasswdCheck.check(this.usr, this.passwd)來做最後的驗證,這是因為對Unix系統用戶的驗證必須調用系統API才可以,而系統API是以C的形式提供的,因此我們需要借助JNI。現在我們看看PasswdCheck這個類:
public class PasswdCheck {
static{
System.out.println(System.getProperty("java.library.path"));
Runtime.getRuntime().load("/home/yiyang/eclipse/workspace/JAAS/libpasswd.so");
}
public native static int check(String usr, String passwd);
}
在這裡用到了JNI來調用底層的用戶名密碼驗證方案,為此我們需要構造出libpasswd.so這個庫。
一步一步來:
1 用javah生成JNI的頭文件:
javah PasswdCheck
得到如下代碼:
/**//* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/**//* Header for class PasswdCheck */
#ifndef _Included_PasswdCheck
#define _Included_PasswdCheck
#ifdef __cplusplus
extern "C" ...{
#endif
/**//*
* Class: PasswdCheck
* Method: check
* Signature: (Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_PasswdCheck_check
(JNIEnv *, jclass, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
現在把頭文件中定義的函數實現:
在jni.h這個頭文件中定義了jni和c之間的類型關系,通過分析,用戶名密碼的字符串可以通過如下函數獲取:
char * username =(*env)->GetStringUTFChars(env, usr, NULL);
char * password =(*env)->GetStringUTFChars(env, psw, NULL);
其他的簡單型別很多被直接typedef了。
我們真對生成的頭文件,實現如下:
#include "PasswdCheck.h"//生成的頭文件
#include "pwd.h"//getspnam
#include "stdio.h"
#include "unistd.h"//crypt必需
#include "shadow.h"//getspnam
#define _XOPEN_SOURCE//crypt必需
JNIEXPORT jint JNICALL Java_PasswdCheck_check
(JNIEnv * env, jclass jc, jstring usr, jstring psw){
char * username =(*env)->GetStringUTFChars(env, usr, NULL);
char * password =(*env)->GetStringUTFChars(env, psw, NULL);
struct spwd * sp = getspnam(username);
char* p;
p = crypt(password, sp->sp_pwdp);
return strcmp(sp->sp_pwdp,p);
}
上面的實現中的結構體spwd定義如下:
struct spwd {
char *sp_namp; /* user login name */
char *sp_pwdp; /* encrypted password */
long int sp_lstchg; /* last password change */
long int sp_min; /* days until change allowed. */
long int sp_max; /* days before change required */
long int sp_warn; /* days warning for expiration */
long int sp_inact; /* days before account inactive */
long int sp_expire; /* date when account expires */
unsigned long int sp_flag; /* reserved for future use */
}
getspnam函數可以獲取一個被單向加密後的密碼(有4種可選加密形式)
crypt函數把我們的原始密碼按相同密鑰和算法加密後,即可通過比較加密後字符串的形式獲取是否密碼正確的信息。需要主義的是只有在使用shadow機制的系統中才應用getspnam,如果/etc/passwd直接描述了密碼,則可以通過函數getpwnam來獲取(或者直接解析文本),這時一般采用的是13位的DES加密,問題變得簡單。
在編寫完實現後通過命令
gcc -lcrypt PasswdCheck.c -shared -o libpasswd.so
進行編譯,把這個庫cp到/usr/lib(或其他ld_library_path)下就可以用平台相關的方式System.loadLibrary加載,否則就要用系統絕對路徑名了(利用System.load)
因為只有root能獲取到getspnam,所以我們只能這樣來執行我們的java進行驗證,sudo java MyLogin (yiyang is in group wheel defined in /etc/sudoers)
否則將得到如下出錯信息:
#
# An unexpected error has been detected by Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0xb7ef95ad, pid=9726, tid=3084450720
#
# Java VM: Java HotSpot(TM) Client VM (1.6.0_03-b05 mixed mode)
# Problematic frame:
# C [libpasswd.so+0x5ad] Java_PasswdCheck_check+0x61
#
# An error report file with more information is saved as hs_err_pid9726.log
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#
當然,如果我們不用JNI,而采用Web Services(具體方法見筆者上一篇blog: http://yangyi.blogjava.net)那麼可以通過set suid的形式定制一個進程了(不過這已經是另一個話題),畢竟用root啟動tomcat不是很讓人放心:
chown root XXX
chmod +s XXX
Anyway, 至此通過JAAS認證Unix用戶的基本思路就描述完了,讀者可以填補其中的漏洞並把JAAS用到自己的工作場景中。