作者: 常明,Java架构师
[请尊重原创,盗版必究,转载请指明出处]
一个系列结束了,也到了快过年了,提前半个月祝大家新年快乐,万事大吉,年后回来年前程序无BUG,跳槽薪资翻倍。早日找到女朋友。。。。。。
前面系列文章讨论的CAS,确切地说是Web SSO解决方案,应用系统是Web系统,即B/S模式。
在移动互联网应用大趋势下,传统互联网应用都在向移动化方向延伸。
移动应用不仅包括移动Web应用(触屏版、H5应用),更多的是Native APP原生应用(安卓、苹果等APP),即软件架构是C/S模式。
对于CAS认证中心管控的Web应用群,如何将这些原生APP应用纳入其中?
由于没有Web应用浏览器天然所具有的处理Cookie、处理HTTP重定向能力,原生APP的登录会话管理一般采用自主开发。
在服务器端创建并保持会话,将会话句柄返给APP客户端持有,后续需要登录后访问的API均需带上这个会话句柄作为请求的一个参数。这个会话句柄和我们Java Web应用的jsessionid很类似。
上述是Native APP登录管理的实现方式,那如何接入CAS认证中心呢?可以有两种方式:一种是APP直接访问CAS认证中心,先得到TGT,再得到ST。APP拿到ST后,就可以访问配置成CAS Client的移动服务端应用,服务端和认证中心验证过ST后,即可按上述方式建立起本地会话。
另一种方式可采用服务端代理模式,即APP先向移动服务端应用提交登录请求,服务端再向CAS认证中心登录认证。这种方式将CAS认证中心的非浏览器登录接口只暴露给移动服务端应用,起到很好的安全防护功能。本文将采用第二种方式给大家示范。
CAS提供了一个支持RESTful风格API的插件,4.1.1新版是cas-server-support-rest,老版是cas-server-integration-restlet 可以获得TGT和ST。
这里我们使用另外一种方式,不用CAS插件,思路和《支持Web应用跨域登录CAS》文章介绍类似,通过修改login-webflow流程返回JSON格式View。由于是服务端代理模式,不必返回ST,认证成功即可建立本地会话了。
下面,我们就一步步加以实现:
1.改造login-webflow.xml,增加Native APP登录处理流程分支(在基于前面文章增加rlogin流程基础上修改)
在流程初始化处理完成后,增加一新节点mode,它首先来检查登录请求中是否包含一个变量mode,并且mode的值为app。如果没有,就继续走原常规流程。如果有,说明是Native APP登录处理情况。<on-start>
后加入如下分支流程定义:
<action-state id="mode">
<evaluate expression="modeCheckAction.check(flowRequestContext)"/>
<transition on="rlogin" to="serviceAuthorizationCheckR" />
<transition on="app" to="serviceAuthorizationCheckR" />
<transition on="normal" to="ticketGrantingTicketCheck" />
</action-state>
产生lt后,我们要做个判断,看是app情况还是rlogin情况,app走app处理流程。
<action-state id="generateLoginTicketR">
<evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
<transition on="generated" to="modeCheckForLt" />
</action-state>
<decision-state id="modeCheckForLt">
<if test="flowScope.mode != null && flowScope.mode == 'rlogin'" then="rLoginTicket" else="appLoginTicket" />
</decision-state>
增加appLoginTicket,注意它的输出视图是appLoginTicket。这和rlogin情况的输出视图不同。
<view-state id="appLoginTicket" view="appLoginTicket" model="credential">
<binder>
<binding property="username" required="true" />
<binding property="password" required="true"/>
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credential'" />
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmitWithRLogin">
<evaluate expression="authenticationViaRFormAction.doBind(flowRequestContext, flowScope.credential)" />
</transition>
</view-state>
登录认证信息提交后,需要根据mode返回不同的VIEW,app模式返回appRes,rlogin模式返回rLoginRes,故修改节点如下:
<action-state id="sendTicketGrantingTicketR">
<evaluate expression="sendTicketGrantingTicketAction" />
<transition on="success" to="modeCheck" />
</action-state>
<decision-state id="modeCheck">
<if test="flowScope.mode != null && flowScope.mode == 'rlogin'" then="rLoginRes" else="appRes" />
</decision-state>
<end-state id="rLoginRes" view="rLoginRes" />
<end-state id="appRes" view="appRes" />
2.增加appLoginTicket和appRes新视图
在nebula_views.properties中添加(原始是default_views.properties):
appLoginTicket.(class)=org.springframework.web.servlet.view.JstlView
appLoginTicket.url=/WEB-INF/view/jsp/nebula/ui/appLoginTicket.jsp
appRes.(class)=org.springframework.web.servlet.view.JstlView
appRes.url=/WEB-INF/view/jsp/nebula/ui/appRes.jsp
同时在相应目录下创建这两个文件,文件内容如下:
appLoginTicket.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%out.print("{\"lt\":\"");%>${loginTicket}<%out.print("\",\"execution\":\"");%>${flowExecutionKey}<%out.print("\"}");%>
appLoginRes.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%out.print("{\"ret\":\"");%>${ret}<%out.print("\",\"msg\":\"");%>${msg}<%out.print("\"}");%>
3.修改modeCheckAction内容,增加处理app情况,核心代码如下:
public class ModeCheckAction{
public static final String NORMAL = "normal";
public static final String APP = "app";
public static final String RLOGIN = "rlogin";
public ModeCheckAction() {
}
public Event check(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
//根据mode判断请求模式,如mode=rlogin,是AJAX远程登录模式,
//app是app登录模式,不存在是原模式,认证中心本地登录
String mode = request.getParameter("mode");
if(mode!=null&&mode.equals("rlogin")){
context.getFlowScope().put("mode", mode);
return new Event(this, RLOGIN);
}
if(mode!=null&&mode.equals("app")){
context.getFlowScope().put("mode", mode);
return new Event(this, APP);
}
return new Event(this, NORMAL);
}
}
至此,CAS认证中心改造完成!
4.开发支持APP登录的移动服务端接口。接收APP登录请求,采用HttpClient转发至CAS认证中心登录,返回json数据解析并最终返回给客户端。本地会话采用redis维护,登录成功,返回access_token。
接口定义:url: /login.json
入参: username string
password string
出参: ret string
msg string
access_token string
核心代码如下:
@RequestMapping("/login.json")
public @ResponseBody ResultBean login(HttpServletRequest request,
HttpServletResponse response) {
ResultBean resultBean = new ResultBean();
String username = request.getParameter("username");
String password = request.getParameter("password");
HttpClient httpClient = new DefaultHttpClient();
String url = SSO_SERVER_URL + "?mode=app&service=" + SSO_CLIENT_SERVICE;
HttpGet httpGet = new HttpGet(url);
try{
HttpResponse httpClientResponse = httpClient.execute(httpGet);
int statusCode = httpClientResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK){
String result = EntityUtils.toString(httpClientResponse.getEntity(),
"utf-8").replace('\r', ' ').replace('\n', ' ').trim();
//解析json数据
ObjectMapper objectMapper = new ObjectMapper();
LtBean ltBean = objectMapper.readValue(result, LtBean.class);
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("username", username));
formparams.add(new BasicNameValuePair("password", password));
formparams.add(new BasicNameValuePair("lt", ltBean.getLt()));
formparams.add(new BasicNameValuePair("execution", ltBean.getExecution()));
formparams.add(new BasicNameValuePair("_eventId", "submit"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8");
HttpPost httpPost = new HttpPost(SSO_SERVER_URL);
httpPost.setEntity(entity);
httpClientResponse = httpClient.execute(httpPost);
statusCode = httpClientResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK){
result = EntityUtils.toString(httpClientResponse.getEntity(), "utf-8")
.replace('\r', ' ').replace('\n', ' ').trim();
objectMapper = new ObjectMapper();
resultBean = objectMapper.readValue(result, ResultBean.class);
if(resultBean.getRet().equals("")){
String access_token = UUID.randomUUID().toString(); //会话句柄
TokenUtil.setAccess_token(access_token, username); //放入redis
resultBean.setRet("0");
resultBean.setMsg("登录成功");
resultBean.setAccess_token(access_token);
}
}
}
}catch(Exception e){
e.printStackTrace();
resultBean.setRet("-2");
resultBean.setMsg("系统服务错误,请稍后再试!");
return resultBean;
}finally{
httpClient.getConnectionManager().shutdown();
}
return resultBean;
}
- 开发app客户端登录
APP开发不是本文重点,这里略。
热门评论
我看到这句:
String access_token = UUID.randomUUID().toString(); //会话句柄
TokenUtil.setAccess_token(access_token, username); //放入redis
感觉和CAS没多大关系啊,这样实现是不是有点太牵强
前后端分离的html项目,可以使用你的文章解决认证问题。但别的业务系统跳转过来,如何解决不需要登录问题呢?
移动端原生请求也有cookie啊