本文部分内容节选自Enterprise JavaBeans 3.0 by Bill Burke & Richard Monson-Haefel
5 Security
EJB规范虽然规定了如何将安全信息从客户端传递到服务器,但是EJB规范并没有规定客户端如何取得安全信息,也没有规定如何进行验证。对于大多数应用服务器而言,JNDI验证是最为常见的一种方式。客户端在与JNDI InitialContext建立连接时进行验证,之后在调用远程EJB时,安全信息会传递到服务器并在服务器中进行传播。验证的执行过程是将一个或者多个角色与给定的用户进行关联。用户一旦通过了验证,应用服务器会对该用户进行授权。与验证不同,EJB规范对授权进行了明确的定义。
5.1 JAAS
OpenEJB可以使用标准的JAAS机制来实现验证和授权。通常情况下,你需要通过设置java.security.auth.login.config系统属性来配置login modules的配置文件。如果没有指定这个系统属性,那么缺省会使用login.config作为文件名。以下是conf目录下一个login.config文件的例子:
PropertiesLogin {
org.apache.openejb.core.security.jaas.PropertiesLoginModule required
Debug=true
UsersFile="users.properties"
GroupsFile="groups.properties";
};
以上文件中指定了org.apache.openejb.core.security.jaas.PropertiesLoginModule作为login module。同时指定了users.properties和groups.properties,其内容分别如下:
conf/users.properties
Kevin=password
WhiteSock=password
conf/groups.propertie
Manager=Kevin
Employee=Kevin,WhiteSock
以下是修改后的MovieDaoJpaImpl.java:
@Stateful
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class MovieDaoJpaImpl implements MovieDaoJpaLocal, MovieDaoJpaRemote {
//
@PersistenceContext(unitName = "movie", type = PersistenceContextType.TRANSACTION)
private EntityManager entityManager;
@SuppressWarnings("unchecked")
@PermitAll
public List<Movie> getAllMovies() {
Query query = entityManager.createQuery("SELECT m from Movie as m");
return query.getResultList();
}
@RolesAllowed({"Employee", "Manager"})
public void addMovies(List<Movie> movies) {
for(Movie m : movies) {
entityManager.persist(m);
}
}
@RolesAllowed({"Manager"})
public void deleteMovie(Movie movie) {
Movie m = entityManager.merge(movie);
entityManager.remove(m);
}
}
public static void main(String args[]) throws Exception {
//
Properties properties = new Properties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.RemoteInitialContextFactory");
properties.setProperty(Context.PROVIDER_URL, "ejbd://localhost:4201");
properties.setProperty(Context.SECURITY_PRINCIPAL, "Kevin");
properties.setProperty(Context.SECURITY_CREDENTIALS, "password");
InitialContext ctx = new InitialContext(properties);
//
List<Movie> movies = new ArrayList<Movie>();
movies.add(new Movie("Dances with Wolves", "Kevin Costner", 1990));
movies.add(new Movie("Legends of the Fall", "Edward Zwich", 1994));
movies.add(new Movie("A Very Long Engagement", "Jean-Pierre Jeunet", 2004));
//
Object ref = ctx.lookup("MovieDaoJpaImplRemote");
MovieDaoJpaRemote dao = (MovieDaoJpaRemote) PortableRemoteObject.narrow(ref, MovieDaoJpaRemote.class);
dao.addMovies(movies);
for(Movie m : dao.getAllMovies()) {
System.out.println("dao.getAllMovies(): " + m);
dao.deleteMovie(m);
}
}
以上代码中,如果尝试以"WhiteSock"作为Context.SECURITY_PRINCIPAL进行登陆,那么在执行到dao.deleteMovie(m)时,会抛出javax.ejb.EJBAccessException异常。
5.2 Plug Points
OpenEJB的安全机制有多种扩展点,其中最复杂的方式是实现SecurityService接口。OpenEJB的缺省实现是SecurityServiceImpl,它继承自AbstractSecurityService。在conf/openejb.xml中通过以下方式进行配置:
<SecurityService id="My Default Security Service">
className org.apache.openejb.core.security.SecurityServiceImpl
realmName PropertiesLogin
</SecurityService>
如果采用JAAS进行验证授权,那么有一种比较简单的扩展方式是定制LoginModule。以下是个简单的例子:
import java.io.IOException;
import java.security.Principal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.InitialContext;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.sql.DataSource;
import org.apache.openejb.core.security.jaas.GroupPrincipal;
import org.apache.openejb.core.security.jaas.UserPrincipal;
public class JdbcLoginModule implements LoginModule {
//
private boolean debug;
//
private User user;
private Subject subject;
private CallbackHandler callbackHandler;
private Set<Principal> principals = new HashSet<Principal>();
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
//
this.subject = subject;
this.callbackHandler = callbackHandler;
debug = "true".equalsIgnoreCase((String)options.get("Debug"));
//
if(debug) {
System.out.println("# in JdbcLoginModule.initialize()");
}
}
public boolean login() throws LoginException {
//
if(debug) {
System.out.println("# in JdbcLoginModule.login()");
}
//
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username: ");
callbacks[1] = new PasswordCallback("Password: ", false);
try {
callbackHandler.handle(callbacks);
} catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
}
//
String name = ((NameCallback) callbacks[0]).getName();
char[] password = ((PasswordCallback) callbacks[1]).getPassword();
if (password == null) password = new char[0];
//
List<User> users;
try {
users = getUser(name, new String(password));
} catch (Exception e) {
throw new RuntimeException("failed to get user by name: " + name, e);
}
if(users != null && users.size() == 1) {
user = users.get(0);
} else {
throw new LoginException("invalid user name: " + name);
}
//
return true;
}
public boolean commit() throws LoginException {
//
if(debug) {
System.out.println("# in JdbcLoginModule.commit()");
}
//
principals.add(new UserPrincipal(user.getName()));
for(Role r : user.getRoles()) {
principals.add(new GroupPrincipal(r.getName()));
}
subject.getPrincipals().addAll(principals);
return true;
}
public boolean logout() throws LoginException {
//
if(debug) {
System.out.println("# in JdbcLoginModule.logout()");
}
//
subject.getPrincipals().removeAll(principals);
principals.clear();
return true;
}
public boolean abort() throws LoginException {
//
if(debug) {
System.out.println("# in JdbcLoginModule.abort()");
}
//
subject.getPrincipals().removeAll(principals);
principals.clear();
return true;
}
private List<User> getUser(String name, String password) throws Exception {
//
InitialContext ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:openejb/Resource/mysqlDataSource");
//
boolean succeed = false;
Connection con = null;
Map<Integer, User> users = new HashMap<Integer, User>();
try {
//
con = ds.getConnection();
PreparedStatement pstmt = con.prepareStatement("select " +
"u.id as user_id, u.name as user_name, " +
"r.id as role_id, r.name as role_name " +
"from user u, role r, user_role ur " +
"where u.id = ur.user_id and r.id = ur.roles_id " +
"and u.name = ? and u.password = ?");
pstmt.setString(1, name);
pstmt.setString(2, password);
//
ResultSet rs = pstmt.executeQuery();
while ( rs.next() ) {
//
Integer userId = rs.getInt("user_id");
String userName = rs.getString("user_name");
User user = users.get(userId);
if(user == null) {
user = new User();
user.setId(userId.intValue());
user.setName(userName);
users.put(userId, user);
}
if(user.getRoles() == null) {
user.setRoles(new ArrayList<Role>());
}
//
Role role = new Role();
role.setId(rs.getInt("role_id"));
role.setName(rs.getString("role_name"));
user.getRoles().add(role);
}
rs.close();
pstmt.close();
//
succeed = true;
} finally {
if(con != null) {
try {
con.close();
} catch(Exception e) {
if(succeed) {
throw e;
}
}
}
}
//
List<User> r = new ArrayList<User>();
r.addAll(users.values());
return r;
}
}
在login.config中配置JdbcLogin realm,例如:
JdbcLogin {
com.yourpackage.JdbcLoginModule required
Debug=true;
};
conf/openejb.xml中的相关配置如下:
<Resource id="mysqlDataSource" type="DataSource"> JdbcDriver com.mysql.jdbc.Driver JdbcUrl jdbc:mysql://localhost:3306/ejb UserName root Password password JtaManaged true </Resource> <SecurityService id="My Default Security Service"> className org.apache.openejb.core.security.SecurityServiceImpl realmName JdbcLogin </SecurityService>
数据库中的相关表如下:
mysql> select * from user;
+----+-----------+----------+
| id | name | password |
+----+-----------+----------+
| 1 | Kevin | password |
| 2 | WhiteSock | password |
+----+-----------+----------+
mysql> select * from role;
+----+----------+
| id | name |
+----+----------+
| 1 | Manager |
| 2 | Employee |
+----+----------+
mysql> select * from user_role;
+---------+----------+
| User_id | roles_id |
+---------+----------+
| 1 | 1 |
| 2 | 2 |
| 2 | 1 |
+---------+----------+