JBoss SX
JBoss使用JBoss SX框架来确保应用程序安全。它建立在Java身份验证和授权服务的顶层(JAAS,Java Authentication and Authorization Service)。
当JBoss接收到请求时,目标应用程序不需要知道基本安全数据库的位置或访问方式;
请求被传递到名为“安全域”的JBoss SX组件中,这是一种用来保护所有对组件的球球的抽象;
安全域执行所有必要的安全检查并告知组件用户可否继续进行访问,安全域知道如何使用一个或多个登录模块从数据源加载安全数据;
安全域
可以在server/x/conf/login-config.xml中添加或更改现有安全域定义。
<application-policy>定义一个安全域,JBoss SX根据name生成JNDI上下文,并使用该上下文将安全域绑定到JNDI中;
<login-module>定义登录模块,登录模块可以从属性文件、或数据库中加载安全信息(密码、角色)。
<!--application-policy:定义安全域-->
<application-policy name="authnservice">
<authentication>
<!--login-module:定义登录模块-->
<login-module code="com.alpha.security.authentication.TokenLoginModule" flag="required"/>
<login-module code="com.alpha.security.authentication.LockedAccountLoginModule" flag="required">
<module-option name="dsJndiName">java:/OracleDS</module-option>
<module-option name="lockingQuery">
SELECT 。。。
</module-option>
<module-option name="updateLocked">
UPDATE USER 。。。
</module-option>
<module-option name="updateFailLoginCount">
UPDATE USER 。。。
</module-option>
</login-module>
<login-module code="com.alpha.security.authentication.TokenServiceLoginModule" flag="required">
<module-option name="unauthenticatedIdentity">guest</module-option>
<module-option name="dsJndiName">java:/OracleDS</module-option>
<module-option name="hashAlgorithm">SHA-1</module-option>
<module-option name="hashEncoding">hex</module-option>
<module-option name="principalsQuery">
select PASSWORD
from 。。。
</module-option>
<module-option name="rolesQuery">
SELECT Roles FROM ROLE r,。。。
</module-option>
<module-option name="password-stacking">useFirstPass</module-option>
<module-option name="rolePrefix">alpha/</module-option>
<module-option name="hashCharset">UTF-8</module-option>
</login-module>
<login-module code="com.alpha.security.authentication.PasswordExpirationLoginModule" flag="required">
<module-option name="dsJndiName">java:/OracleDS</module-option>
<module-option name="passwordQuery">
select date_of_creation, date_of_expiration
from (select pw.*
from sec_password pw, sec_user u
where u.login_name = UPPER(?)
and pw.user_id = u.id
order by pw.date_of_creation desc)
where rownum=1
</module-option>
<module-option name="checkEnabledQuery">
select v.boolean_value
from config_key k, config_value v
where k.key = 'Options.system.settings.pwExpireEnabled'
and v.key = k.id
</module-option>
<module-option name="checkFirstLoginQuery">
select v.boolean_value
from config_key k, config_value v
where k.key = 'Options.system.settings.enableFirstLogin'
and v.key = k.id
</module-option>
<module-option name="passAgeQuery">
select v.integer_value
from config_key k, config_value v
where k.key = 'Options.system.settings.pwExpireAge'
and v.key = k.id
</module-option>
<module-option name="notifyPeriodQuery">
select v.integer_value
from config_key k, config_value v
where k.key = 'Options.system.settings.pwNotifyPeriod'
and v.key = k.id
</module-option>
</login-module>
<login-module code="com.alpha.security.authentication.RoleMappingLoginModule" flag="required">
<module-option name="mapping">
。。。
</module-option>
</login-module>
<login-module code="com.alpha.security.authentication.TokenCreatingLoginModule" flag="required"/>
</authentication>
</application-policy>
JMX查看安全域
jboss.security - service=XMLLoginConfig
displayAppConfig( "authnservice" )
安全域信息:
登录模块UsersRolesLoginModule
登录模块可以从属性文件、或数据库中加载安全信息(密码、角色)。
前面的例子中,登录模块是自定义的。
JBoss SX提供了多个登录模块,其中UsersRolesLoginModule用于在属性文件中存储用户名和角色信息。
<application-policy name="JBossWS">
<authentication>
<login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required">
<module-option name="usersProperties">props/jbossws-users.properties</module-option>
<module-option name="rolesProperties">props/jbossws-roles.properties</module-option>
<module-option name="unauthenticatedIdentity">anonymous</module-option>
</login-module>
</authentication>
</application-policy>
这种登录模块常用于开发和测试。
登录模块DatabaseServerLoginModule
DatabaseServerLoginModule用于在数据库中存储用户名和角色信息:
<login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule" flag="required">
<module-option name="password-stacking">useFirstPass</module-option>
<module-option name="dsJndiName">java:/pacsDS</module-option>
<module-option name="principalsQuery">select passwd from users where user_id=?</module-option>
<module-option name="rolesQuery">select roles from roles where user_id=?</module-option>
<module-option name="hashEncoding">base64</module-option>
<module-option name="hashCharset">UTF-8</module-option>
<module-option name="hashAlgorithm">SHA-1</module-option>
</login-module>
<dsJndiName>定义了数据源的JNDI名称。
自定义登录模块
继承javax.security.auth.spi.LoginModule:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.log4j.Logger;
import org.jboss.security.SecurityContextAssociation;
public class PasswordExpirationLoginModule implements LoginModule {
private static final Logger log = Logger.getLogger(PasswordExpirationLoginModule.class);
private final class PasswordDao extends LoginModuleDAO {
private final String userName;
Timestamp expireDate;
Timestamp createDate;
private PasswordDao(String userName) {
super(dsJndiName);
this.userName = userName;
}
@Override
public void innerRun(Connection conn, PreparedStatement ps, ResultSet rs) throws SQLException,
LoginException {
ps = conn.prepareStatement(passwordQuery);
ps.setString(1, userName);
rs = ps.executeQuery();
if (rs.next()) {
expireDate = rs.getTimestamp("date_of_expiration");
createDate = rs.getTimestamp("date_of_creation");
}
}
}
private final class SettingDAO<T> extends LoginModuleDAO {
private final String query;
private T data;
private final Class<T> type;
private SettingDAO(String query, Class<T> type) {
super(dsJndiName);
this.query = query;
this.type = type;
}
@Override
public void innerRun(Connection conn, PreparedStatement ps, ResultSet rs) throws SQLException,
LoginException {
ps = conn.prepareStatement(query);
rs = ps.executeQuery();
if (rs.next()) {
if(type==Integer.class) {
data = (T) Integer.valueOf(rs.getInt(1));
}else if(type==Boolean.class) {
data = (T) Boolean.valueOf(rs.getBoolean(1));
}else if(type==Timestamp.class) {
data = (T) rs.getTimestamp(1);
}else {
data = (T) rs.getObject(1);
}
}
}
public T getResult() {
return data;
}
}
/**
* Flag to allow ignore "about to expire" checking
*/
public static final String IGNORE_EXPIRED = "ignore_expired";
//mili seconds in a day
private static final long timeADay = 86400000L;
private String dsJndiName;
//get pw creation date and expiration date
private String passwordQuery;
private String checkEnabledQuery;
private String checkFirstLoginQuery;
private String notifyPeriodQuery;
private String passAgeQuery;
private CallbackHandler callbackHandler;
private CallbackHelper helper = new CallbackHelper();
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState,
Map options) {
this.callbackHandler = callbackHandler;
dsJndiName = (String) options.get("dsJndiName");
passwordQuery = (String) options.get("passwordQuery");
checkEnabledQuery = (String) options.get("checkEnabledQuery");
checkFirstLoginQuery = (String) options.get("checkFirstLoginQuery");
notifyPeriodQuery = (String) options.get("notifyPeriodQuery");
passAgeQuery = (String) options.get("passAgeQuery");
}
@Override
public boolean login() throws LoginException {
if(log.isDebugEnabled()) {
log.debug("do expire check");
}
final String userName = helper.getUsername(callbackHandler);
PasswordDao dao = new PasswordDao(userName);
dao.run();
if(isCheckFirstLoginEnabled()) {
if(dao.expireDate!=null && dao.expireDate.equals(dao.createDate)) {
throw new FirstLoginException();
}
}
if(isCheckEnabled()) {
long passAge = getPassAge() * timeADay;
SettingDAO<Timestamp> timeDao =
new SettingDAO<Timestamp>("select systimestamp from dual", Timestamp.class);
timeDao.run();
Timestamp now = timeDao.getResult();
if(dao.expireDate!=null) {
log.warn("expiration date has been set for current password.");
long time = dao.expireDate.getTime() - dao.createDate.getTime();
if(time>0) {
passAge = Math.min(passAge, time);
}
}
long currentAge = now.getTime() - dao.createDate.getTime();
if(currentAge >= passAge) {
throw new AccountExpiredException();
}
//ignore about to expire
Boolean isIgnore = (Boolean)(SecurityContextAssociation
.getSecurityContext().getData().get(IGNORE_EXPIRED));
if(isIgnore!=null && isIgnore) {
return false;
}
long notifiPeriod = getNotifyPeriod() * timeADay;
if(currentAge >= passAge - notifiPeriod) {
long time = passAge -currentAge;
int day = (int)(time / timeADay + (time % timeADay==0 ? 0 : 1));
throw new AccountToExpireException(day);
}
}
return false;
}
private boolean isCheckFirstLoginEnabled() throws LoginException {
SettingDAO<Boolean> dao = new SettingDAO(checkFirstLoginQuery, Boolean.class);
dao.run();
Boolean result = dao.getResult();
return result;
}
private int getNotifyPeriod() throws LoginException{
SettingDAO<Integer> dao = new SettingDAO(notifyPeriodQuery, Integer.class);
dao.run();
Integer data = dao.getResult();
if(data==null) {
log.warn("setting for notify period is not found, use hardcode value 14");
return 14;
}
return data;
}
private int getPassAge() throws LoginException {
SettingDAO<Integer> dao = new SettingDAO(passAgeQuery, Integer.class);
dao.run();
Integer data = dao.getResult();
if(data==null) {
log.warn("setting for password expired age is not found");
return 42;
}
return data;
}
private boolean isCheckEnabled() throws LoginException{
SettingDAO<Boolean> dao = new SettingDAO(checkEnabledQuery, Boolean.class);
dao.run();
Boolean result = dao.getResult();
return result;
}
@Override
public boolean commit() throws LoginException {
return false;
}
@Override
public boolean abort() throws LoginException {
return false;
}
@Override
public boolean logout() throws LoginException {
return false;
}
}