由标题我们不难看出,与本文相关的知识点主要有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();
}
}
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();
}
}
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);
}
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
#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);
}
#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 */
}
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
#
# 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用到自己的工作场景中。