手记

《SSO CAS单点系列》之 APP原生应用如何访问CAS认证中心(系列结束)

作者: 常明,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;  
}
  1. 开发app客户端登录

APP开发不是本文重点,这里略。

17人推荐
随时随地看视频
慕课网APP

热门评论

我看到这句:

String access_token = UUID.randomUUID().toString(); //会话句柄 

TokenUtil.setAccess_token(access_token, username); //放入redis


感觉和CAS没多大关系啊,这样实现是不是有点太牵强

前后端分离的html项目,可以使用你的文章解决认证问题。但别的业务系统跳转过来,如何解决不需要登录问题呢?

移动端原生请求也有cookie啊

查看全部评论