基于容器的用户安全管理系统
作者:板桥banq
上页
3.4 Session Bean实现
本项目需要一个Facade类统一实现用户资料的操作,建立无状态Session Bean 为UserManager的EJB。在这个类中,主要实现有关用户的新增、删除或修改。代码如下:
public class UserManagerBean implements SessionBean {
private final static Logger logger = Logger.getLogger(UserManagerBean.class);
SessionContext sessionContext;
UserHome userHome;
UsersRolesHome usersRolesHome;
UsersGroupsHome usersGroupsHome;
RoleManagerLocalHome roleManagerLocalHome;
SequenceGeneratorLocalHome sequenceHome;
// ejbCreate()一般只执行一次,以后再调用时不再执行,通过ejbCreate()可以
//实现一些类属性的缓冲
public void ejbCreate() throws CreateException {
try {
ServiceLocator serviceLocator = new ServiceLocator();
userHome = (UserHome) serviceLocator.getLocalHome(
JNDINames.USER_HOME);
sequenceHome = (SequenceGeneratorLocalHome)
serviceLocator.getLocalHome(JNDINames.SEQUENCEGENERATOR_HOME);
usersRolesHome = (UsersRolesHome) serviceLocator.getLocalHome(
JNDINames.USERSROLES_HOME);
usersGroupsHome = (UsersGroupsHome) serviceLocator.getLocalHome(
JNDINames.USERSGROUPS_HOME);
roleManagerLocalHome = (RoleManagerLocalHome)
serviceLocator.getLocalHome( JNDINames.ROLEMANAGER_HOME);
} catch (Exception ex) {
logger.error("Service Locate error:" + ex);
throw new CreateException();
}
}
//从Sequence EJB组件获得自增的序列号
public int getNewId(String name) {
try {
SequenceGeneratorLocal seq = sequenceHome.create();
return seq.nextSequenceNumber(name);
} catch (Exception ex) {
throw new EJBException("Error generating id for : " + name + ex);
}
}
//创建一个用户
public void createUser(UserEvent userEvent) {
User userDTO = userEvent.getUser();
if (nameIsExisted(userDTO)){
logger.debug("name :" + userDTO.getName() + " has been exsited");
userEvent.setErrors(Constants.NAME_EXISTED);
return;
}
if (emailIsExisted(userDTO)){
logger.debug("eamil :" + userDTO.getEmail() + " has been exsited");
userEvent.setErrors(Constants.EMAIL_EXISTED);
return;
}
UserLocal userLocal = null;
try {
String id = Integer.toString(getNewId(Constants.SEQUENCE_USER_NAME));
userLocal = userHome.create(id);
userDTO.setUserId(id);
updateUser(userEvent);
//创建该用户的默认角色
createUsersRoles(userDTO);
} catch (Exception ex) {
logger.error(ex);
throw new EJBException("create user error : " + ex);
}
}
//检查是否有重复用户名
private boolean nameIsExisted(User userDTO) {
boolean found = false;
User user = getUserByName(userDTO.getName());
if (user != null)
found = true;
return found;
}
//检查是否有重复E-mail
private boolean emailIsExisted(User userDTO) {
boolean found = false;
User user = getUserByEmail(userDTO.getEmail());
if (user != null)
found = true;
return found;
}
//创建用户默认角色,也就是建立用户和角色的对应关系
public void createUsersRoles(User userDTO) throws Exception {
UsersRoles usersRoles = null;
try {
RoleManagerLocal roleManagerLocal = roleManagerLocalHome.create();
String roleId = roleManagerLocal.getRoleId(Role.USER);
usersRoles = usersRolesHome.create(userDTO.getUserId(), roleId);
} catch (Exception ex) {
logger.error(ex);
throw new EJBException("createUsersRoles error : " + ex);
}
}
//修改用户资料
public void updateUser(UserEvent userEvent) {
User userDTO = userEvent.getUser();
UserLocal userLocal = null;
try {
userLocal = userHome.findByPrimaryKey(userDTO.getUserId());
userLocal.setName(userDTO.getName());
userLocal.setEmail(userDTO.getEmail());
userLocal.setPassword(userDTO.getPassword());
} catch (Exception ex) {
logger.error(ex);
}
}
//以E-mail获得用户
public User getUserByEmail(String emailAddress) {
emailAddress = emailAddress.trim().toLowerCase();
UserLocal userLocal = null;
try {
userLocal = userHome.findByEmail(emailAddress);
} catch (Exception ex) {
logger.warn(ex);
}
return getUser(userLocal);
}
//以Id查询用户
public User getUserById(String userId) {
userId = userId.trim().toLowerCase();
logger.debug(" userId =" + userId);
UserLocal userLocal = null;
try {
userLocal = userHome.findByPrimaryKey(userId);
} catch (Exception ex) {
logger.warn(ex);
}
return getUser(userLocal);
}
//获得用户User实例 DTO模式
private User getUser(UserLocal userLocal) {
if (userLocal == null)
return null;
logger.debug(" userId =" + userLocal.getUserId());
User user = new UserModel(userLocal.getUserId());
user.setEmail(userLocal.getEmail());
user.setName(userLocal.getName());
user.setPassword(userLocal.getPassword());
user.setUserId(userLocal.getUserId());
return user;
}
//获得用户的Principal
public User getUserByPrincipal() throws Exception, PrincipalException {
Principal principal = null;
try {
principal = sessionContext.getCallerPrincipal();
} catch (Exception e) {
logger.error(e);
throw new PrincipalException();
}
if (principal == null) {
throw new PrincipalException();
}
String name = principal.getName();
return getUserByName(name);
}
…
}
在UserManager中基本实现了用户资料的相关操作。在本项目中,还有用户组以及角色的相关操作。为避免UserManager中包含过多逻辑,需要再建立一个无状态Session Bean,如RoleManager。
用户注册登录后,其Session生存周期的活动将一直维持到其离开系统。因此可以建立一个有状态Session Bean保存用户的资料如User实例,这样不必经常到数据库读取。有状态Session Bean还可以作为一个总的Facade类,分别包含其他Facade群,这样,系统会显得条理分明。创建有状态Session Bean代码如下:
public class SecurityFacadeBean extends EJBControllerBean {
private final static Logger logger = Logger.getLogger(SecurityFacadeBean.class);
SessionContext sessionContext;
RoleManagerLocalHome roleManagerLocalHome;
UserManagerLocalHome userManagerLocalHome;
AsyncSenderLocalHome asyncSenderLocalHome;
private User user = null;
…
//获得Facade类 UsermanagerLocal
public UserManagerLocal getUserManager() {
UserManagerLocal userManagerLocal = null;
try {
userManagerLocal = userManagerLocalHome.create();
} catch (Exception ex) {
logger.error("getUserManager() error:" + ex);
}
return userManagerLocal;
}
//获得Facade类RoleManagerLocal
public RoleManagerLocal getRoleManager() {
RoleManagerLocal roleManagerLocal = null;
try {
roleManagerLocal = roleManagerLocalHome.create();
} catch (Exception ex) {
logger.error("getRoleManager() error:" + ex);
}
return roleManagerLocal;
}
//获得当前session的User实例
public User getUser() {
if (this.user != null)
return this.user;
logger.debug("user is null, get it from principal ...");
setUser();
return this.user;
}
//密码丢失查询
public boolean getPassword(String email) {
logger.debug("--> enter getpassword");
boolean success = false;
try {
User user = getUserManager().getUserByEmail(email);
if (user == null)
return success;
String subject = " 用户名和密码";
StringBuffer buffer = new StringBuffer();
buffer.append(" 用户:").append(user.getName());
buffer.append(" 密码:").append(user.getPassword());
if (sendMail(user.getEmail(), subject, buffer.toString()))
success = true;
} catch (Exception ex) {
logger.error(" getPassword: " + ex);
}
return success;
}
//调用E-mail发送组件,通过JMS发出E-mail
public boolean sendMail(String toAddress, String subject, String content) {
try {
logger.debug(" -->enter send mail");
Mail mail = new Mail();
mail.setToAddress(toAddress);
mail.setFromAddress("banq@jdon.com");
mail.setSubject(subject);
mail.setContent(content);
String msg = MailUtil.getMsgFromMail(mail);
AsyncSenderLocal asyncSenderLocal = asyncSenderLocalHome.create();
asyncSenderLocal.sendAMessage(msg);
logger.debug(" -->send mail to: " + toAddress + " successfully!");
return true;
} catch (Exception ex) {
logger.error(" sendMail() error : " + ex);
return false;
}
}
//判断当前用户是否是管理员
public boolean isAdministrator() {
return sessionContext.isCallerInRole(Role.ADMINISTRATOR);
}
//判断当前用户是否是普通用户
public boolean isUser() {
return sessionContext.isCallerInRole(Role.USER);
}
…
}
可以看到,SecurityFacadeBean是继承接口框架系统中的EJBController,SecurityFacadeBean作为一个总的Facade类,通过接口框架负责和Web实现联系,如图6-6所示。
图6-6 EJB Facade群
3.5 EJB容器安全配置
J2EE容器的安全管理框架以角色为联系纽带,分两个方向。一个是用户资料系统;另外一个是访问权限系统。后者是通过web.xml或ejb-jar.xml配置实现的。
在本项目中,为了限制角色对某些类或方法的访问权限,可以在ejb-jar.xml中设置。
<assembly-descriptor>
<security-role>
<description>the role is super user </description>
<role-name>Admin</role-name>
</security-role>
<security-role>
<description>register user</description>
<role-name>User</role-name>
</security-role>
<method-permission>
<role-name>User</role-name>
<method>
<ejb-name>RoleManager</ejb-name>
<method-name>*</method-name>
</method>
</method-permission>
…
</assembly-descriptor>
在本EJB中定义了两个角色:Admin和User(当然可以根据实际情况定义角色)。具体的权限是在method-permission中设置,可以指定某个类的具体方法,“*”表示所有的方法。
既然定义了角色,那么在具体类的配置中也需要声明具体角色的指向,如SecurityFacadeBean的ejb-jar配置如下:
<session>
<display-name>SecurityFacade</display-name>
<ejb-name>SecurityFacade</ejb-name>
<local-home>
com.jdon.security.auth.ejb.SecurityFacadeLocalHome
</local-home>
<local>com.jdon.security.auth.ejb.SecurityFacadeLocal</local>
<ejb-class>com.jdon.security.auth.ejb.SecurityFacadeBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
<ejb-local-ref>
<description />
<ejb-ref-name>ejb/UserManager</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>
com.jdon.security.auth.ejb.UserManagerLocalHome
</local-home>
<local>com.jdon.security.auth.ejb.UserManagerLocal</local>
<ejb-link>UserManager</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
<description />
<ejb-ref-name>ejb/RoleManager</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>
com.jdon.security.auth.ejb.RoleManagerLocalHome
</local-home>
<local>com.jdon.security.auth.ejb.RoleManagerLocal</local>
<ejb-link>RoleManager</ejb-link>
</ejb-local-ref>
<ejb-local-ref>
<description />
<ejb-ref-name>ejb/AsyncSender</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local-home>
com.jdon.asyncsender.ejb.AsyncSenderLocalHome
</local-home>
<local>com.jdon.asyncsender.ejb.AsyncSenderLocal</local>
<ejb-link>AsyncSender</ejb-link>
</ejb-local-ref>
<security-role-ref>
<description />
<role-name>User</role-name>
<role-link>User</role-link>
</security-role-ref>
</session>
在最后几行,指定角色名User指向了security-role中的User,两者名称可以不一样,但role-link必须和security-role是一致的。role-name可以是RegisterUser,那么在SecurityFacadeBean调用就是isCallerInRole("RegisterUse"),检查当前访问SecurityFacadeBean的角色是否是容器中已经定义的、允许访问的角色RegisterUse。
通过ejb-jar.xml的配置,使得角色的访问权限的设置和管理变得非常方便。在系统运行时,有可能有新的角色加入或原有角色权限的修改。这些都可以通过ejb-jar.xml进行修改,修改完毕,运行中的J2EE系统立即生效。
具体ejb-jar.xml的权限配置可参考Sur公司EJB 标准手册。
为了使该EJB的权限机制激活,还需要在相应的容器配置文件中进行适当配置。如在JBoss中部署该EJB时,需要在jboss.xml加入:
<security-domain>java:/jaas/SecurityRealm</security-domain>
这表示该EJB的安全域将使用JAAS中的SecurityRealm配置。关于如何设置JBoss的SecurityRealm将在部署配置相关章节中讨论。
4 JMS 邮件发送组件
在J2EE中,JMS(Java Message System)提供了一种异步处理机制的实现。JMS通过异步的、非阻塞的消息传递,将消息的生产者和使用者松散的联系在一起。对于使用者,它无所谓是谁产生了消息或者是在何时产生的。这就能够建立一种动态的、灵活的可靠的系统。所谓可靠,因为JMS将每个消息都保存起来,只有确保本消息处理后才会完全放弃。否则,将会反复提交处理。这种可靠的机制使得JMS能够成功的在证券、银行系统中得到广泛应用。
JMS中的消息类型有两种:Topic和Queue。Topic的操作是使用发布/订阅(publish/subscribe)的方式;Queue的操作是点对点(ponit to point)的方式。
publish/subscribe:发布者(publisher)发布消息到Topic,订阅者(subsribe)从Topic订阅消息,订阅者的数量是不受限制的。
ponit to point:点对点传输消息,建立在消息队列Queue的基础上,每个消息使用者只对应一个消息队列,不像publish/subscribe那样可以有很多消息使用者。
本项目中,可以由单个独立的程序来实现邮件发送,这个程序作为一个消息使用者,只需一个就可以了,因此使用Queue来实现。
JMS在消息到达消息使用者,有两种——同步和异步。
同步是指消息使用者明确地主动地调用从Queue或Topic中得到消息,一直进行循环直至一个消息到达,或者也可以设定超时时间。很明显这个方法是比较耗费CPU资源的。
异步接受是指消息到达时,主动通知消息使用者,消息使用者可以实现message listener接口。这样每当消息到达时,JMS provider 会通过调用这个listener的onMessage方法来传输这个消息。该方法相对要有效率,邮件发送组件将采取这种方式。
下面描述这个邮件发送组件是如何具体实现的。
在这个组件中,消息生产者应该是具体应用程序。但是组件为了达到通用目的,需要建立一个专门的消息生产者,可以称之为AsyncSender,(异步发送器)。具体应用程序通过调用这个异步发送器,将邮件内容发给JMS的Queue,负责邮件发送的类作为这个Queue另外一端的消息使用者,在Queue中有需要发送的邮件出现时就立即将它从Queue中取出,实现邮件发送。
4.1 消息发送器
AsyncSender属于消息生产者,它是具体应用系统直接调用的,可以使用无状态Session Bean来实现。
在AsyncSender中,首先要通过JNDI获得一个ConnectionFactory,创建一个QueueConnection实例,这样就获得与JMS Provider的一个连接。再由这个QueueConnection创建一个QueueSession,这样就启动了一个和JMS Provider连接相关的线程,这个线程可以是发送或接收消息。
AsyncSender还需要通过JNDI获得一个已经存在的Queue。这样,通过将发送消息的线程和这个Queue联系起来,表示向这个Queue中发送消息。
AsyncSender的bean代码如下:
/**
* JMS客户端 消息生产者
* <p>Copyright: Jdon.com Copyright (c) 2003</p>
* <p>Company: 上海汲道计算机技术有限公司</p>
* @author banq
* @version 1.0
*/
public class AsyncSenderBean implements SessionBean {
private final static Logger logger = Logger.getLogger(AsyncSenderBean.class);
SessionContext sessionContext;
private SessionContext sc;
private Queue queue;
private QueueConnectionFactory qFactory;
public void ejbCreate() throws CreateException {
try {
ServiceLocator serviceLocator = new ServiceLocator();
//查询JNDI获得QueueConnectionFactory
qFactory =
serviceLocator.getQueueConnectionFactory(
JNDINames.QUEUE_CONNECTION_FACTORY);
//查询JNDI 获得已经存在的Queue
queue = serviceLocator.getQueue(JNDINames.ASYNC_SENDER_QUEUE);
} catch (ServiceLocatorException sle) {
throw new EJBException("AsyncSenderEJB.ejbCreate failed", sle);
}
}
//发送邮件的方法
public void sendAMessage(String msg) {
QueueSession session = null;
QueueConnection qConnect = null;
QueueSender qSender = null;
try {
//创建一个QueueConnection
qConnect = qFactory.createQueueConnection();
//创建一个QueueSession
session = qConnect.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
logger.debug("-->>create sender");
//创建一个发送者
qSender = session.createSender(queue);
TextMessage jmsMsg = session.createTextMessage();
//设置发送内容
jmsMsg.setText(msg);
//向目标queue发送
qSender.send(jmsMsg);
logger.debug("-->>send ok msg:"+msg);
} catch (Exception e) {
logger.error("sendAMessage error " + e);
throw new EJBException("askMDBToSendAMessage: Error!", e);
} finally {
try {
if (qConnect != null) { qConnect.close(); }
} catch (Exception e) {}
}
}
…
}
其中,QueueConnectionFactory的JNDI名称是java:comp/env/jms/QueueConnection Factory,而Queue的JNDI名称是java:comp/env/jms/MailQueue,分别需要在ejb-jar.xml中落实这两个环境变量。
在ejb-jar.xml中,有:
<session>
<display-name>AsyncSender</display-name>
<ejb-name>AsyncSender</ejb-name>
<local-home>
com.jdon.asyncsender.ejb.AsyncSenderLocalHome
</local-home>
<local>com.jdon.asyncsender.ejb.AsyncSenderLocal</local>
<ejb-class>com.jdon.asyncsender.ejb.AsyncSenderBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<!-- resource-ref一般用来设置数据库资源或JMS Provider资源 -->
<resource-ref>
<description />
<res-ref-name>jms/QueueConnectionFactory</res-ref-name>
<res-type>javax.jms.QueueConnectionFactory</res-type>
<res-auth>Container</res-auth>
</resource-ref>
<!-- 和resource-ref指定的资源相关的环境配置 -->
<resource-env-ref>
<description />
<resource-env-ref-name>jms/MailQueue</resource-env-ref-name>
<resource-env-ref-type>javax.jms.Queue</resource-env-ref-type>
</resource-env-ref>
</session>
QueueConnectionFactory和Queue与具体JMS容器有关。在JBoss 3.0中,提供了现成的QueueConnectionFactory和Queue,当然也可以参考JBoss手册自己设置建立这两个配置。
这里使用JBoss的ConnectionFactory和已经存在的/testQueue,其JNDI名称写法分别是java:/ConnectionFactory和queue/testQueue,这些都是JBoss的规定写法。如果是Topic,那么就需要写成topic/testTopic,testTopic/testQueue是JBoss容器在启动时建立好的一个Topic/Queue。在jboss.xml中,写入下列代码:
<session>
<ejb-name>AsyncSender</ejb-name>
<local-jndi-name>AsyncSenderLocal</local-jndi-name>
<resource-ref>
<res-ref-name>jms/QueueConnectionFactory</res-ref-name>
<jndi-name>java:/ConnectionFactory</jndi-name>
</resource-ref>
<resource-env-ref>
<resource-env-ref-name>jms/MailQueue</resource-env-ref-name>
<jndi-name>queue/testQueue</jndi-name>
</resource-env-ref>
</session>
通过上面开发,消息的生产者功能基本完成,通过调用EJB AsyncSender的sendAMessage(String msg)方法,可以将msg发送到JMS的Queue中。
4.2 MDB
在Queue的另外一端是消息的使用者。MDB(Message-Driven Beans)专门处理JMS异步消息,Session Bean和Entity Bean只允许同步地去发送消息和接收消息,不支持异步。MDB是一个message listener,它能够从一个Queue或一个durable subscription中可靠地接收消息。
MDB与一个普通的消息使用者客户端的区别是,EJB容器将自动做下面的事情,无需应用者再自己编程实现:
创建一个消息接受者(QueueReceiver/TopicSubscriber)接收消息。在部署时,将destination和ConnectionFactory与MDB联合起来。在JBoss中通过指定destination-jndi-name来实现。
自动实现message listener接口(无需调用setMessageListener方法)。
容器自动指定了消息签收模式。
因此,使用MDB作为消息的使用者就非常简单,而且没有home和remote接口,只有一个bean类。建立MDB MailerBean代码如下:
/**
* JMS消息使用者 EJB消息Bean
* <p>Copyright: Jdon.com Copyright (c) 2003</p>
* <p>Company: 上海汲道计算机技术有限公司</p>
* @author banq
* @version 1.0
*/
public class MailerBean implements MessageDrivenBean, MessageListener {
private final static Logger logger = Logger.getLogger(MailerBean.class);
MessageDrivenContext messageDrivenContext;
public void ejbCreate(){
}
public void ejbRemove() {}
//当消息来时,将自动激活这个方法
public void onMessage(Message msg){
logger.debug(" --> enter onMessage ..");
TextMessage textMessage = null;
String xmlMailMessage = null;
Mail recMail = null;
try {
textMessage = (TextMessage) msg;
xmlMailMessage = textMessage.getText();
//将XML文本转换成Mail对象实例
recMail = MailUtil.getMailFromMsg(xmlMailMessage);
logger.debug(" --> begin connect the server ....");
sendMail(recMail);
logger.debug(" --> send mail ok ");
} catch (JMSException je) {
logger.error("MailerMDB.onMessag error" + je);
throw new EJBException("MailerMDB.onMessage" + je);
} catch (Exception me) {
logger.error("MailerMDB.onMessag error" + me);
}
}
//发送邮件
private void sendMail(Mail mail) throws Exception{
getMailHelper().createAndSendMail(mail);
}
private MailHelper getMailHelper(){
return (new MailHelper());
}
public void setMessageDrivenContext(MessageDrivenContext messageDrivenContext) {
this.messageDrivenContext = messageDrivenContext;
}
}
MailHelper是一个专门使用J2EE容器的mail service发送邮件的类,代码如下:
public class MailHelper {
private final static Logger logger = Logger.getLogger(MailHelper.class);
//创建一个email message 并且使用J2EE mail services发送它
public void createAndSendMail(Mail mail) throws MailerAppException {
try {
logger.debug(" --> lookup mail session");
InitialContext ic = new InitialContext();
Session session = (Session) ic.lookup(JNDINames.MAIL_SESSION);
Message msg = new MimeMessage(session);
logger.debug(" --> beigin to set mail ");
msg.setFrom(new InternetAddress(mail.getFromAddress()));
msg.setRecipients(Message.RecipientType.TO,
InternetAddress.parse(mail.getToAddress(), false));
msg.setSubject(mail.getSubject());
msg.setText(mail.getContent());
msg.setSentDate(new Date());
logger.debug(" --> beigin to send now ....");
Transport.send(msg);
} catch (Exception e) {
logger.error("createAndSendMail exception : " + e);
throw new MailerAppException("Failure while sending mail");
}
}
}
由上可见,MDB MailerBean是在Queue中有消息到达时,取出后委托MailHelper使用J2EE容器的Mail Service发送邮件。
这两个类都比较依赖容器,所以要进行容器的配置。
在ejb-jar.xml中配置MailerBean如下:
<message-driven>
<display-name>Mailer</display-name>
<ejb-name>Mailer</ejb-name>
<ejb-class>com.jdon.mailer.ejb.MailerBean</ejb-class>
<transaction-type>Container</transaction-type>
<message-driven-destination>
<destination-type>javax.jms.Queue</destination-type>
</message-driven-destination>
<!-- 使用容器的邮件操作资源 -->
<resource-ref>
<description />
<res-ref-name>mail/DefaultMail</res-ref-name>
<res-type>javax.mail.Session</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</message-driven>
由于使用了容器的邮件发送服务,那么需要指定这个服务的JNDI,在jboss.xml当中加入:
<message-driven>
<ejb-name>Mailer</ejb-name>
<destination-jndi-name>queue/testQueue</destination-jndi-name>
<resource-ref>
<res-ref-name>mail/DefaultMail</res-ref-name>
<jndi-name>java:/Mail</jndi-name>
</resource-ref>
</message-driven>
这表示使用JNDI为java:/Mail的邮件服务资源,那么再部署发布本组件时,就需要在JBoss服务器中配置这个邮件服务资源,具体如何配置参见部署发布的章节。
下页