原地址: http://www.smth.org/pc/pccon.php?id=3559&nid=74518&s=all
小按:
第一次写心得笔记,手都有点抖,班门弄斧啊,呵呵~~~欢迎各位大侠扔砖!
本文是一篇学习笔记,概要介绍了Java登录与授权机制及其应用。
在一些用到的API关键字上做了链接,可以在线查相应的文档。
=======================================================================================
安全性是Java鼓吹得最多的特性之一,的确,Java的安全特性涵盖了从应用级别到语言级别乃至JVM本身。以前大家都知道有个Sandbox,但仅有Sandbox尚不能满足,或者说不能很方便地做到我们所需要的全部安全需求,譬如现在一个系统首先起码需要一个登录功能,更进一步的话,还需要对用户访问资源的行为进行约束,下面我想大致讲一下Java是怎样做这些事情的,基本上是一个总结或者说是“读后感”的性质,同时给出一个简单的实现例子,这个例子其实还是模仿人家的,呵呵……
谈到访问控制,或者说“授权”,这里有两层含义,一是从资源的角度,这个socket端口是否被允许操作?这个文件是可读的?可写的?还是可执行的?还是以上都行?这就是我们在UNIX下用“ls -l”命令列出当前目录下文件时,那些“-rwx-”之类的含义;二是从访问者的角度,我想通过80端口看Web上新浪欧洲杯的新闻,在这个系统中有没有这个资格?我想播放D盘上一个名为“friends.rm”的视频文件,我得到了访问这个文件的权限了吗?我有运行播放器的权限吗?
Java在访问控制策略上同时考虑了这两方面内容,你说“不对呀,我用FileOutputStream写文件,用Socket类连接远程主机都用得好好的,没什么限制呀”,这我们得先谈谈什么叫做“安全管理器”(SecurityManger)。安全管理器从JDK 1.0就开始有了,多古老啊!Java从设计的那一天开始就考虑了安全因素,安全管理器是Sandbox的最重要的一个部分,也是访问控制的总协调者,我们能够在通常情况下正常使用网络和文件,那是因为当启动application的时候(注意是application,不是applet!),如果你不加“-Djava.security.manager”选项,JVM是不会启动Sandbox的,这时你可以“为所欲为”,而不会碰到SecurityException之类的异常;一旦加入了“-Djava.security.manager”选项,你就会发现有一连串的异常出现喽!
Exception in thread "main" java.security.AccessControlException: access denied (……)
……
Java内置了一个默认的安全策略,这种情况下安全管理器首先装载的是这个默认的策略,不信啊,不信你检查一下你的“%JAVA_HOME%\jre\lib\security\”目录,是不是有个叫“java.policy”的文件?用notepad打开看看:
// Standard extensions get all permissions by default
grant codeBase "file:${java.home}/lib/ext/*" {
permission java.security.AllPermission;
};
// default permissions granted to all domains
grant {
// Allows any thread to stop itself using the java.lang.Thread.stop()
// method that takes no argument.
// Note that this permission is granted by default only to remain
// backwards compatible.
// It is strongly recommended that you either remove this permission
// from this policy file or further restrict it to code sources
// that you specify, because Thread.stop() is potentially unsafe.
// See "http://java.sun.com/notes" for more information.
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on un-privileged ports
permission java.net.SocketPermission "localhost:1024-", "listen";
// "standard" properies that can be read by anyone
permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";
permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";
permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";
};
可以看到,JVM给沙箱内的application分配的权限仅限于中止线程,监听1024以上的TCP端口,以及对一些系统属性的读取权限,像一般的socket操作和文件操作的权限都没有。
了解了安全管理器的概念以后我们回到授权问题上来。对用户来说,最担心的莫过于机器中病毒,病毒本质上是一种恶意的程序,所以访问控制首先是要对代码的权限进行控制,上面我一直都在谈Sandbox,也就是所谓的“沙箱”,熟悉Java安全性发展历史的朋友大概对它不会陌生,初期的Java是采用这样一种安全策略,即:本地代码是可信的,而远程代码是不可信的,譬如applet是一种从网络上下载到本地并在浏览器上运行的一段远程代码,因而是不可信的,所以早期的applet被完全置于Sandbox当中,得到的权限是非常有限的;在1.0以后,直至Java 2出现之前,安全策略作了一些灵活的改变,applet不再是完全被歧视的“二等公民”了,因为有了签名applet,用户可以选择信任这种经过签名的applet,从而applet也可以做一些以前被认为是“出格”的事情;到了Java 2,情况又变了,以前一向被信任的本地代码似乎也变得不是那么可靠了,这还真说不准,难保谁不会在你出去跟女朋友逛街的时候,偷偷溜进来在你机器上拷个病毒什么的 ^_^ ,这样本地代码就落到了和远程代码相等同的地位了,这是比较符合现实世界场景的,在Java 2中的安全策略被称之为“可配置的安全策略”,任何代码,只要是通过安全管理器访问,就必须为它预先设定好访问权限,在这个之外的资源还是别的什么东东,对不起,java.security.AccessControlException: access denied…… 此路不通!
简单总结一下Java安全模型的发展史,大概就是下面的几幅图了:
JAAS的API基本上位于javax.security.auth包及其下属子包中,很容易找到的。
由于现在普遍是多用户的系统,所以在实现代码级访问控制之外,我们还希望能够对用户的行为进行约束,因为对系统造成破坏的因素不仅仅是恶意代码,人自身的有意或无意的不当操作也会危及系统,譬如向上面说的你不在的时候别人可以在你机器上拷病毒,如果系统能在你不在的时候也能拒绝这个家伙的登录企图,那样麻烦岂不是少很多?于是在Java安全核心之外,提供了一个名为“Java认证与授权服务”(Java Authentication and Authorization Services,JAAS)东东,专门用来处理对用户的认证和授权,这也就是所谓的“以用户为中心的授权模型”,说白了就是在“以代码为中心的授权模型”上再加一层,首先用户要获得访问权限,然后用户去操纵代码,代码来实行真正的访问操作。下面我主要是讲讲JAAS是如何工作的。
我们使用指定的用户名“user”和口令“letmepass”,确定以后分别传给当前的NameCallback和PasswordCallback,然后回到MyLoginModule的login过程,该过程从回调对象处得到NameCallback和PasswordCallback,进行认证(这里仅仅是简单的用户名和口令的对比),
MyLoginModule: Authentication pass!
并决定是否commit,由于在login.conf中定义该登录模块是required,所以是一个必须通过才能整体认证成功的模块。
MyLoginModule: Add a new principal to current subject.
如果整体得到认证通过,那么Subject就可以授权允许MyAction中的代码了,如语句Subject.doAs(…)所示,该代码的动作是读取当前目录下的myfile.txt文件,并将其内容打印到控制台,注意到在策略文件jaas.policy中赋予MyPrincipal身份对myfile.txt的读取权限,所以我们成功看到控制台下出现
Access successfully! Reading file:
==================================
Why?
Because they care!
Because they want to know the truth!
Because they want their country back!
Because it still belongs to us as long as the people have the guts to fight for what they believe in!
==================================
这是我喜欢的一部经典影片“JFK”中检察官Garrison激情的最后陈词中的一段,呵呵!
以上过程我们可以用个图表来表示:
以上流程中使用到的Java源代码和配置文件如下:
// MyPrincipal.java
package com.jungleford.auth;
import java.security.Principal;
public class MyPrincipal implements Principal { // 一个Principal的例子
private String name; // Principal的名字
public MyPrincipal(String name) {
this.name = name;
}
public String getName() { //取得Principal的名字
return this.name;
}
public boolean equals(Object principal) { // 判断两个Pincipal相同的依据
if (principal instanceof MyPrincipal)
return this.name.equals(((MyPrincipal)principal).getName());
else
return false;
}
public String toString() { // Principal的表示
return "MyPrincipal: " + this.name;
}
public int hashCode() { // 确定本对象的散列值
// 用于有基于散列容器的场合,判断在散列容器中是否是同一个对象。
// 如果对hashCode感兴趣,请参见:
// http://www-900.ibm.com/developerWorks/cn/java/j-jtp05273/
return this.name.hashCode();
}
}
下载源代码
// MyLoginModule.java
package com.jungleford.auth;
import java.util.*;
import java.io.IOException;
import java.security.Principal;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
import javax.security.auth.spi.*;
public class MyLoginModule implements LoginModule { // 一个登录模块的例子
private Subject subject; // 登录主体的表征
private CallbackHandler cbHandler; // 回调对象,提供终端下获取用户名、口令的界面
private Map sharedState; // 用于缓存中间结果的共享区
private Map options; // 用于保存某些登录模块所需要用到的一些配置选项
private boolean succeeded = false; // 一次login成功的标志
private boolean cmtSucceeded = false; // 整体登录成功的提交标志
private String username; // 取得用户名
private char[] password; // 取得口令
private Principal principal; // 取得登录后的身份标志
public void initialize(Subject subject,CallbackHandler cbHandler, Map sharedState,Map options) { // 初始化过程
System.out.println("Login module initializing ...");
System.out.println();
this.subject = subject;
this.cbHandler = cbHandler;
this.sharedState = sharedState;
this.options = options;
}
public boolean login() throws LoginException { // 一次登录过程
if (cbHandler == null) // 尚未配置回调对象
throw new LoginException("Error: No CallbackHandler available " +
"to garner authentication information from the user");
Callback[] cbs = new Callback[2]; // 仅使用用户名回调和口令回调
cbs[0] = new NameCallback("Login: ");
cbs[1] = new PasswordCallback("Password: ", false);
try {
cbHandler.handle(cbs);
username = ((NameCallback)cbs[0]).getName();
char[] temp = ((PasswordCallback)cbs[1]).getPassword();
if (temp == null) { // 口令为空
temp = new char[0];
}
password = new char[temp.length];
System.arraycopy(temp, 0, password, 0, temp.length);
((PasswordCallback)cbs[1]).clearPassword(); // 清除内存中的口令痕迹
}
catch (IOException ioe) {
throw new LoginException(ioe.toString());
}
catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() +
" not available to garner authentication information " +
"from the user");
}
boolean usrCorrect = false; // 用户名正确否?
boolean pwdCorrect = false; // 口令正确否?
if (username.equals("user")) // 目前仅允许用户名为user的登录
usrCorrect = true;
if (usrCorrect &&
password.length == 9 &&
password[0] == 'l' &&
password[1] == 'e' &&
password[2] == 't' &&
password[3] == 'm' &&
password[4] == 'e' &&
password[5] == 'p' &&
password[6] == 'a' &&
password[7] == 's' &&
password[8] == 's') {// user的口令指定为letmepass
System.out.println("MyLoginModule: Authentication pass!");
System.out.println();
pwdCorrect = true;
succeeded = true;
return true; // 一次登录成功
}
else {
System.out.println("MyLoginModule: Authentication failed!");
System.out.println();
succeeded = false;
username = null;
for (int i = 0; i < password.length; i++) // 清除内存中的口令痕迹
password[i] = ' ';
password = null;
if (!usrCorrect) {
throw new FailedLoginException("Username incorrect!");
}
else {
throw new FailedLoginException("Password incorrect!");
}
}
}
public boolean commit() throws LoginException { // 根据登录配置策略判断是否整体登录成功
if (succeeded == false) {
return false;
}
else {
principal = new MyPrincipal(username);
if (!subject.getPrincipals().contains(principal))
subject.getPrincipals().add(principal); // 把新的身份添加到subject中
System.out.println("MyLoginModule: Add a new principal to current subject.");
System.out.println();
username = null;
for (int i = 0; i < password.length; i++) // 清除内存中的口令痕迹
password[i] = ' ';
password = null;
cmtSucceeded = true;
return true;
}
}
public boolean abort() throws LoginException { // 放弃登录,将状态复位至登录前
if (succeeded == false) {
return false;
}
else if (succeeded == true && cmtSucceeded == false) {
succeeded = false;
username = null;
if (password != null) {
for (int i = 0; i < password.length; i++) // 清除内存中的口令痕迹
password[i] = ' ';
password = null;
}
principal = null;
}
else {
logout();
}
return true;
}
public boolean logout() throws LoginException { // 注销,并将状态复位至登录前
subject.getPrincipals().remove(principal);
succeeded = false;
succeeded = cmtSucceeded;
username = null;
if (password != null) {
for (int i = 0; i < password.length; i++) // 清除内存中的口令痕迹
password[i] = ' ';
password = null;
}
principal = null;
return true;
}
}
下载源代码
// MyCallbackHandler.java
package com.jungleford.auth;
import java.io.IOException;
import javax.security.auth.callback.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class MyCallbackHandler implements CallbackHandler {
public void handle(Callback[] cbs)
throws IOException, UnsupportedCallbackException {
String username =
JOptionPane.showInputDialog(null, "Available name: " + "user", "Enter your name",
JOptionPane.QUESTION_MESSAGE);
String password =
JOptionPane.showInputDialog(null, "Available password: " + "letmepass", "Enter your password",
JOptionPane.QUESTION_MESSAGE);
for (int i = 0; i < cbs.length; i++) {
if (cbs[i] instanceof TextOutputCallback) {
TextOutputCallback toc = (TextOutputCallback)cbs[i];
switch (toc.getMessageType()) {
case TextOutputCallback.INFORMATION:
System.out.println(toc.getMessage());
break;
case TextOutputCallback.ERROR:
System.out.println("Error: " + toc.getMessage());
break;
case TextOutputCallback.WARNING:
System.out.println("Warning: " + toc.getMessage());
break;
default:
throw new IOException("Unsupported message type: " +
toc.getMessageType());
}
}
else if (cbs[i] instanceof NameCallback) {
// prompt the user for a username
NameCallback nc = (NameCallback)cbs[i];
//System.err.print(nc.getPrompt());
//System.err.flush();
nc.setName(username);
}
else if (cbs[i] instanceof PasswordCallback) {
// prompt the user for sensitive information
PasswordCallback pc = (PasswordCallback)cbs[i];
//System.err.print(pc.getPrompt());
//System.err.flush();
pc.setPassword(password.toCharArray());
}
else {
throw new UnsupportedCallbackException(cbs[i], "Unrecognized Callback");
}
}
}
}
下载源代码
//MyAction.java
package com.jungleford.auth;
import java.io.*;
import java.security.*;
public class MyAction implements PrivilegedAction { // 对资源的授权访问动作
public Object run() { // run方法是必须overriding的
// 这里我们假设访问动作是读取当前目录下myfile.txt文件的内容
File file = new File("myfile.txt");
String content = "";
try {
BufferedReader reader =
new BufferedReader(
new FileReader(file));
String line = reader.readLine();
while (line != null) {
content += line + "\n";
line = reader.readLine();
}
}
catch (Exception e) {
System.err.println("Error: Reading file failed!");
System.err.println();
e.printStackTrace();
}
return content;
}
}
下载源代码
//JAASTest.java
package com.jungleford.auth;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
public class JAASTest { // 测试我们JAAS登录和授权的shell
public static void main(String[] args) {
LoginContext lc = null;
try {// 创建context,使用自定义的回调对象,策略名为JAASTest
// 简单起见,仅使