用Filter实现GAE上的User Authentication

编者按:好吧,就是spring security的雏形概念。

Google App Engine 提供了非常详细的User Authentication的方法,详情参见Users Java API 概述。主要包括了两种方法:

在 Java 中使用用户身份验证
如果你只有单个servlet,那么很简单,在逻辑处理前调用Google提供的API(UserService)判断当前用户登录情况就可以了。

import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
public class MyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
UserService userService = UserServiceFactory.getUserService();
String thisURL = request.getRequestURI();
if (request.getUserPrincipal() != null) {
response.getWriter().println(“<p>Hello, ” +
request.getUserPrincipal().getName() +
“! You can <a href=\”" +
userService.createLogoutURL(thisURL) +
“\”>sign out</a>.</p>”);
} else {
response.getWriter().println(“<p>Please <a href=\”" +
userService.createLoginURL(thisURL) +
“\”>sign in</a>.</p>”);
}
}
}

使用 web.xml 强制登录和管理访问
如果你的应用比较复杂,不止一个servlet,Google App Engine支持标准部署描述符web.xml中的安全约束,但是不支持自定义角色,只支持login和admin两种权限。具体示例如下

<security-constraint>
<web-resource-collection>
<url-pattern>/profile/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>

另外也可以通过google自己的部署描述符app.yaml来编写。需要注意的是App在Deploy的时候,如果应用中有app.yaml,则SDK会按照它的内容重新生成web.xml和appengine-web.xml并覆盖原来的文件。所以如果你要启用app.yaml,你需要把web.xml和appengine-web.xml中所有的内容按照YAML文件的格式全部写到app.yaml里面去。具体示例如下:

handlers:
- url: /profile/*
login: required
- url: /admin/*
servlet: com.example.AdminServlet
login: admin

编者按:需要吐槽的是app.yaml并不完全支持web.xml,至少我没有找到如何配置load-on-startup=1怎么写,所以我个人不推荐这种用法。
编者又按:app.yaml的支持功能比web.xml+appengine-web.xml还要多,所以如果你不巧正好遇到,还是得听Google的话用app.yaml.
编者最后按:我的应用比较复杂,所以Java中写code的办法没法适用,但是很努力尝试第二种方法均告失败,不管是web.xml还是app.yaml都不成功。这才总结出了上述的一些经验,以及遇到了下面两种都没有找到解法的问题。

  • 登录之后仍然显示Your client does not have permission to get URL
  • Error: User not in required role

以上两个问题均没有比较明确的解法,所以才有了本文的内容。


用Filter实现User Authentication
主要借鉴了Spring Security的思想(明明是写完才发现的。。。),然后借用了第一种方法中的code。具体的方法说起来很简单,就是在需要做权限判断的URL前添加Filter,如果权限判断失败,则跳转到Google Login的URL。先看一下实现的代码。
UserFilter.java中的实现。

@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filter)
throws IOException, ServletException {
log.info(“———–UserFilter doFilter Begin————”);
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String thisURL = request.getRequestURI();
if (request.getUserPrincipal() == null
|| (!userService.isUserLoggedIn())) {
response.setContentType(“text/html”);
response.sendRedirect(userService.createLoginURL(thisURL));
} else {
log.info(userService.getCurrentUser().getEmail());
filter.doFilter(servletRequest, servletResponse);
}
}

web.xml中相应的配置。

<filter>
<filter-name>userFilter</filter-name>
<filter-class>com.pamirhostel.booking.filters.UserFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>userFilter</filter-name>
<url-pattern>/admin/*</url-pattern>
</filter-mapping>

说穿了很简单,但是基于GAE的实现略有不同,总结下来的几点经验。

  • URL最好按照功能划分子目录。例如“/cron/*”是用来实现cron job的。由于GAE的backend,cron job, task queue都是通过调用URL来实现的,所以之前喜欢把所有URI都放在Root下的习惯在这里就不适用了。
  • Filter默认不是HTTP的,所以调用request,response前需要下溯造型成HTTPRequest.
  • request.getUserPrincipal只能用来判断是否当前有Google帐号登录,这是有逻辑漏洞的。例如我的应用是For Google Apps的,所以即使当前用户登录了它自己的Google Account也是没用的。所以需要添加进一步的判断userService.isUserLoggedIn。
  • 如果多组功能需要进行用户验证,直接多添加几个filter-mapping就好了。

目前该Filter很好用,还没有什么further question,有什么问题留下评论一起讨论把。

 

同步你的twitter和开心网记录–基于GAE

一直很烦恼想发的牢骚要在twitter.com和kaixin001.com上说两遍。再有气势的话这么一搞也没啥想法了。
Google App Engine支持Task Queue和Cron Job,简直天生就是做Bot的平台阿。挑了一个周末马上动手搞起来。
从Twitter上抓取消息
我用的是twitter4j.没有特别的比较。觉得还挺方便就用了。不负责推荐。

Twitter twitter = new Twitter("xxx", "xxx");
try {
List list = (List) twitter.getUserTimeline();
for (int i = 0; i < list.size(); i++) {
Status status = list.get(i);
Query query = pm.newQuery(Tweet.class, "id == myId");
query.declareParameters("long myId");
List results = (List) query.execute(status
.getId());
if (results == null || results.size() == 0) {
Tweet tweet = new Tweet(status.getId(), status.getUser()
.getScreenName(), status.getText());
pm.makePersistent(tweet);
if (tweet.getText().indexOf("#kaixin") > -1) {
Queue queue = QueueFactory.getQueue("sendKaixin");
queue.add(TaskOptions.Builder.url("/sendKaixin")
.method(Method.GET).param("tweetid",
String.valueOf(tweet.getId())));
}
}
}
} catch (TwitterException e) {
log.warning(e.toString());
}

这段Code所在的servlet被配置成Cron Job,定义为每30分钟run一次。
大概的逻辑是说下载自己最新的tweets,如果内容包含”#kaixin”(只是为了方便),就调用TaskQueue的”/sendKaixin”发送到kaixin001上。

向kaixin001.com写消息
kaixin001的网站比较复杂,包含很多ajax,js的东西。看了几个外挂,主要都是对wap.kaixin001.com进行抓取和分析。
wap.kaixin001.com页面非常简单,没有session之类的概念。主要是通过登陆之后html之中带的一个”verifyValue”的值对各个组件进行关联。

URL url = new URL(URL_LOGIN);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Referer", URL_REFERER);
StringBuilder postData = new StringBuilder().append("email=")
.append(email.replaceAll("@", "%40")).append("&password=")
.append(password).append(
"&from=&login=+%E7%99%BB+%E5%BD%95+");
OutputStreamWriter writer = new OutputStreamWriter(connection
.getOutputStream());
writer.write(postData.toString());
writer.close();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
System.out.println("Kaixin001 login successfully!");
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(),
"UTF-8"));
StringBuilder resultBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
resultBuilder.append(line);
}
reader.close();
String sResult = resultBuilder.toString();
verifyValue = findText(sResult, "/app/mylist.php?verify=",
"\"", 0);
}

有了verifyValue之后只需要向对应的URL提交form表单就可以了。

URL url = new URL(URL_PREFIX_POST_RECORD + verifyValue);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Referer", URL_PREFIX_WRITE_RECORD
+ verifyValue);
StringBuilder postData = new StringBuilder().append(
"privacy=1&content=").append(URLEncoder.encode(tweet.getText(),"UTF-8"));
OutputStreamWriter writer = new OutputStreamWriter(connection
.getOutputStream());
writer.write(postData.toString());
writer.close();
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
tweet.setStatus(Tweet.STATUS_UPDATED);
}
else{
log.warning("Kaixin001 update record failed" + connection.getResponseCode());
}

由于Google App Engine对URL Fetch有一些限制。所以直接用了它提供的URL来做Http Connection.用这个方法提交的记录会直接显示成手机提交,酷阿。
开发中的问题
要做的事情很明确。
1)抓取twitter.
2) 提交kaixin001
3) 更新tweet状态
但是由于Google App Engine中Cron Job和Task Queue都是基于URL Mapping做的,所以每一个请求都有30s的限制,这就对应用提出了要求。一开始考虑的两个方案都会因为超时而失败。
痛定思痛,考虑这两个功能的本质,Cron Job是定时执行,不论成败。Task Queue是定时循环执行,直到成功。
所以最后我的方案是
1)Cron Job定制抓取Twitter,遇到关键字丢给Task Queue.
2) Task Queue循环向kaixin001.com提交,直到成功。

后话
因为涉及twitter.com和kaixin001.com两个网站的账号和密码,所以个人觉得作在线应用会有一些障碍。可能考虑开源让大家自己搭着玩吧。后话了。大家有意见可以给我提。

 

在Google App Engine for Java上使用JSTL

想要在Google App Engine上部署一个Google Apps的Sample Code — Event Publisher,结果问题连连.
一方面是各种dependencies的问题,这个只能怪自己粗心. 另外一个主要的问题就是Event Publisher使用了JSTL而Google App Engine For Java对JSTL的支持有些特别.特地记录一下提醒自己.
主要问题
服务器报错500.

java.lang.AbstractMethodError: javax.servlet.jsp.PageContext.getELContext()Ljavax/el/ELContext;

原因分析
这个错误信息还是非常有名的.Google一下发现大致的原因是getELConext()这个方法是JSTL1.2之后才支持的方法.需要Servlet API 2.5以及JSP API 2.1的支持.具体产生错误的地方包括下面这三个地方.

注意事项一:注意使用的servlet标准版本
web.xml中会标注目前支持的servlet API版本.

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
...
</web-app>

如果你使用了2.4的话,那可能会抱错NoSuchMethodError.本文的错误和这个无关,只是总结一个可能影响JSTL使用的一个因素.

注意事项二:添加jsp header
GAE for Java需要用到下面的jsp header.如果你的jstl在GAE for Java中不工作,那么可以尝试添加下面的语句.

<%@page isELIgnored="false" %>

注意事项三:jstl.jar和standard.jar
Event Publisher需要使用第三方的jstl.jar和standard.jar,所以我在GAE for Java项目中也引入了这两个类库.
但是其实GAE for Java已经提供了对JSTL的支持.而且它的实现和标准的jstl略有差异(标准的jstl.jar不认注意事项二中的tag).所以千万不要画蛇添足自己引入这两个jar文件.

OK,还有什么问题.请留言给我吧.

 

使用Google App Engine的XMPP服务

XMPP作为IM通讯标准协议已经得到了广泛的应用。Sametime, Gtalk 以及Cisco Webex Connect都是基于XMPP协议的IM产品。GAE前一段时间推出了XMPP服务.也许是希望能有更多支持gtalk的服务出现,从而推进gtalk的发展吧.
最近终于有空试用了一下,感觉还不错.想想普通的Web hosting由于缺少独立IP,没有办法实现端口映射,所以基本没有办法用来部署IM机器人。如今有了GAE的XMPP服务,做一个相关的机器人就不只是梦了。如果有同学想在Gtalk,Webex Connect以及其他基于XMPP的IM平台上开发机器人的话,GAE绝对是一个不错的选择.

XMPP消息发送
GAE提供了完整的XMPP类库。你可以在你的web应用中对用户发送XMPP应用或者邀请.示例代码如下.

import com.google.appengine.api.xmpp.JID;
import com.google.appengine.api.xmpp.Message;
import com.google.appengine.api.xmpp.MessageBuilder;
import com.google.appengine.api.xmpp.SendResponse;
import com.google.appengine.api.xmpp.XMPPService;
import com.google.appengine.api.xmpp.XMPPServiceFactory;
...
String Content = "Hello World";
JID jid = new JID("abc@gmail.com");
Message msg = new MessageBuilder()
.withRecipientJids(jid).withBody(content).build();
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
String result = null;
if (xmpp.getPresence(jid).isAvailable()) {
SendResponse status = xmpp.sendMessage(msg);
if ( status.getStatusMap().get(jid) == SendResponse.Status.SUCCESS){
result = "Success";
}
else{
result = "Failed";
log.info(status.getStatusMap().get(jid).toString());
}
}
else{
xmpp.sendInvitation(jid);
}

XMPP消息接收
XMPP服务支持app-id@appspot.com,anything@app-id.appspotchat.com以及anything@version.latest.app-id.appspotchat.com三种ID,发送到这三种ID的XMPP消息都会自动mapping到下面的URL.该URL默认只能admin才能访问,无须再在web.xml中进行安全配置.

/_ah/xmpp/message/chat/

在web.xml中将此URL映射到你的servlet之后,就可以对发送的XMPP消息进行处理了.

@SuppressWarnings("serial")
public class XMPPReceiverServlet extends HttpServlet {
private static final Logger log = Logger
.getLogger(XMPPReceiverServlet.class.getName());
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException {
XMPPService xmpp = XMPPServiceFactory.getXMPPService();
Message message = xmpp.parseMessage(req);
JID fromJid = message.getFromJid();
String body = message.getBody();
log.info("Receive Message: " + body + " from " + fromJid);
String response = "Echo:"+fromJid.getId();
message = new MessageBuilder().withRecipientJids(fromJid).withBody(
response).build();
xmpp.sendMessage(message);
}
}
}

如上所示,GAE的XMPP提供方便的辅助工具帮你从HTTP参数中获得Message信息.

Message message = xmpp.parseMessage(req);

注意:对GAE XMPP服务ID的添加邀请都会被自动接收,但是邀请无法通知到应用中.

目前XMPP的不足
目前GAE XMPP服务的不足主要包括下面四个方面.

  • 开发与调试不便
  • 无法使用自定义域名
  • 无法判断用户状态
  • 性能限制

开发与调试不便
目前Google App Engine SDK不支持XMPP 服务.只能提供最基本的编译功能.所以如果你想要开发发送或者接收XMPP消息的功能.唯一的办法只有实现编译无误以后上传到Google App Engine上进行测试调试.麻烦可见一斑.

无法使用自定义域名
目前Google App Engine支持的XMPP地址只有@appspot.com和@app-id.appspotchat.com两种.不过考虑到App Engine的Web 服务已经支持了自定义域名.XMPP服务对自定义域名帐号的支持,也是可以期待的一件事情.

无法判断用户状态
目前GAE XMPP服务只能识别对方用户”在线”和”离线”两种状态.作为XMPP特色之一,自定义状态已经被各大IM工具广泛的推广了.尤其是Cisco Webex Connect集成了UC之后添加的”In a Meeting”,”In a Webex Meeting”,”On phone”等状态,让人用的非常舒服.希望GAE XMPP服务的未来版本能对它有所支持,阿门.

性能限制
和普通Web服务一样,GAE的XMPP服务也要受到网络带宽,CPU,内存等各方面的限制.除此之外,对发送的XMPP消息长度也有一定的限制.具体的数据可以参考资料来源中的介绍.皑皑,免费的,还能说啥呢.

OK了,还等什么,做一个属于你自己的XMPP bot吧.
资料来源
Google App Engine, The XMPP Java API

 

无觅相关文章插件,快速提升流量