SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
实现单点登录的实质就是要解决如何产生和存储信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下几个:
只要解决了以上的问题,达到了开头讲得效果就可以说是SSO。最简单实现SSO的方法就是用Cookie,实现流程如下所示:
但是目前Cookie的实现存在两个问题:
第一个问题可以通过对Cookie来处理,第二个问题却是硬伤了。
SSO的实现除了Cookie之外还有许多实现方式,这里暂且分析一下一个基于Cookie的实现源码。
首先给出本次分析的结论,具体源码贴在结论之后。
以上就是这个SSO系统的具体实现逻辑。分析出来实现逻辑比较简单。可适用于一般的小型单域名的网站。
以下为具体实现代码:
web.xml
<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>com.jc.sso.client.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
SSOInfo info = SSOCookieUtil.vistSsoCookie((HttpServletRequest)request);
SsoManager.setSSOInfo(info);
info.setRequestObj((HttpServletRequest)request);
info.setResponseObj((HttpServletResponse)response);
try {
// pass the request along the filter chain
chain.doFilter(request, response);
} finally {
SsoManager.clearSSOInfo();
}
}
记录SSO的ticket的bean,为了额外信息的获取,同时记录了HttpServletRequest和HttpServletResponse(这里只是为了额外信息的记录,比如访问Ip地址等等)。
private User user;
private String ticket;
private String uid;//标识pc主机的id
private String app;
/**
* 是否已经进行过ticket的校验
*/
private boolean isValidated = false;
private HttpServletRequest requestObj;
private HttpServletResponse responseObj;
public boolean isLogin() {
if (ticket == null) {
return false;
}
if (!isValidated) {
throw new NotValidateException();
}
return user != null;
}
一个Cookie的操作类
public static SSOInfo vistSsoCookie(HttpServletRequest request) {
Cookie[] cookies = getAllCookies(request);
if(cookies == null || cookies.length == 0){
return new SSOInfo(null);
}
String ticket = null;
String uid = "none";
for(Cookie cookie : cookies){
if(TICKET_GRANT_TICKET_COOKIE.equals(cookie.getName())){
ticket = cookie.getValue();
} else if(UID_COOKIE.equals(cookie.getName())){
ticket = cookie.getValue();
}
}
SSOInfo si = new SSOInfo(ticket);
si.setUid(uid);
String app = SsoManager.config.getValue(SsoManager.CONFIG_APP_ID);
si.setApp(app);
return si;
}
初次访问会返回一个ticket为null的SSOInfo。
存储线程级别的SSOInfo。(注意,上面是在filter中进行的初始化,此时请求继续分发)
SsoManager.setSSOInfo(info);
private static ThreadLocal<SSOInfo> tempStore = new ThreadLocal<SSOInfo>();
public static SSOInfo getSSOInfo() {
return tempStore.get();
}
public static void setSSOInfo(SSOInfo info) {
tempStore.set(info);
}
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/*.html" />
<bean class="com.jc.site.common.interceptor.UserAccountInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
UserAccountInterceptor.preHandle
if (SsoManager.validateWebTicket() ) {//登陆状态
String userId = SsoManager.getSSOInfo().getUser().getUserId();
public static boolean validateWebTicket() {
SSOInfo si = tempStore.get();
if (si == null) {
logger.warn("The ssoinfo object is missed, check whether some unexpected operation on ThreadLocal is executed!");
return false;
}
validate2Server(si);
return si.isLogin();
}
validate2Server
private static void validate2Server(SSOInfo si) {
if (si == null) {
return;
}
if (si.isValidated()) {
return ;
}
if (si.getTicket() == null || si.getTicket().length() == 0) {
si.setValidated(true);
return;
}
…………(后台是远程调用验证系统传入ticket)
}
验证系统
@RequestMapping(value = "validate.html")
@ResponseBody
public String validateLogin(@RequestParam(value="t", required=false)String ticket, String app,
@RequestParam(value="did", required=false)String deviceId) {
if (StringUtils.isEmpty(ticket)) {
return setErrorView("ticket值为空");
}else if(StringUtils.isEmpty(app)) {
return setErrorView("app类型不能为空");
} else if(StringUtils.isEmpty(deviceId)) {
return setErrorView("设备ID为空");
}
try {
String user = authManager.checkTGT(ticket, app);
if (user != null) {
return buildSuccessResponse(user);
} else {
return buildErrorResponse(user);
}
} catch (Exception e) {
logger.error("登录异常(Unexpected)", e);
return setErrorView("服务异常,请稍后再试");
}
}
这里以ticket作为key来从redis中获取userId的信息
public String checkTGT(String tgt, String app) {
String user = null;
try{
user = redisTemplate.get(tgt);
int tmpApp = StringUtil.getIntValue(app, SSOConstant.APP_SITE);
if(!StringUtils.isEmpty(user)){
prolongTicket(tgt,app, user, getValiateTime(tmpApp));
}
}catch(Exception e){
logger.error("检查TGT异常:",e);
}
return user;
}
以上就是认证的整个流程,下面是登陆流程
try {
userInDb = loginServiceImpl.login(user);
} catch (PasswordNotMatchException e) {
if (logger.isInfoEnabled()) {
logger.info("登录失败,密码错误");
}
}
//4. 登陆成功情况下,生成ticket
user.setType(SSOConstant.APP_SITE);
String ticket = authManager.generateSiteTGT(request, response, "" + user.getType(), userInDb);
public String generateSiteTGT(HttpServletRequest request,HttpServletResponse response, String app, User user) {
String tgt = null;
try{
UUID uuid = UUID.randomUUID();
tgt = app + "-" + SSOConstant.TICKET_GRANT_TICKET + "-" + uuid.toString().replaceAll("-", "");
int tmpApp = StringUtil.getIntValue(app, SSOConstant.APP_SITE);
int longLogin = getValiateTime(tmpApp);
setupTicket(tgt, app, "", user, longLogin);
Cookie ticket = new Cookie(SSOConstant.TICKET_GRANT_TICKET_COOKIE, tgt);
String domain = PropertiesUtil.getString(SSOConstant.PROPERY_DOMAIN);
ticket.setDomain(domain);
ticket.setPath("/");
ticket.setMaxAge(longLogin);
response.addCookie(ticket);
}catch(Exception e){
logger.error("生成TGT异常:",e);
}
return tgt;
}
private void setupTicket(String ticket, String app, String deviceId, User user, int longLogin) {
if (ticket == null) {
return ;
}
if(longLogin < 1){
//保存30分钟
longLogin = SSOConstant.TICKET_GRANT_TICKET_TIME_OUT_DEFAULT;
}
String oldTicket = redisTemplate.get(app + "_" + user.getUserId());
if (oldTicket != null) {
redisTemplate.delKey(oldTicket);
}
redisTemplate.setex(ticket, longLogin, user.getId() + ":" + user.getUserId());
redisTemplate.setex(app + "_" + user.getUserId(), longLogin, ticket);
}
原文:http://my.oschina.net/kanlianhui/blog/393276