利用spring+springMvc对单点登录(SSO)的简单实现(含源码

一、简介

       和oauth2.0的原理有些相似。都是客户端登录的时候需要去服务端认证一下。认证通过才能进行登录。不同的是,单点登录需要自己去维持一个认证服务器与用户浏览器的全局会话、客户端端与用户浏览器的局部会话,通过判断确认用户是否登录。

详细原理不介绍了,如果不知道原理不建议直接实现过程,需要先去补一补原理。推荐一篇我学习的时候看到大神写的文章,通俗易懂 :  单点登录原理与简单实现 

当然自己写过一遍,也遇到一些问题和疑惑的地方,下面来一一列举。

为了方便,需要从上面文章扣一个图和简易说明过来,单点登录的流程图。

二、步骤说明

简要说明(标红地方为我当初困惑的地方以及实现起来遇到问题的地方)

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

解释:

问题1:sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌中,为什么要建立全局会话?作用是什么?怎么创建?

答:认证中心需要根据是否建立全局会话来判断用户是不是在某个客户端系统中登录过。用上图解释就是,在系统1登录过后,认证中心建立起全局会话,当系统2再来认证中心进行登录请求的时候,我们发现存在全局会话,就可以看做用户已经在系统1中登录过,无需在继续跳转登录页面,而是直接携带信息返回给系统2。我这边实现的全局会话为了简便是利用tomcat的session建立的,在登录成功后执行以下代码 request.getSession().setAttribute("isLogin", userName);

问题2:sso认证中心校验令牌,返回有效,注册系统1 中为什么要在认证服务器中注册系统1?

答:注册系统1到认证中心是为了方便实现用户退出时候的单点退出。单点退出和登录一个道理,一个客户端系统退出,所有系统全部退出。当系统集群中,在某一个系统中用户退出后,认证中心收到退出请求,将会逐个发消息到注册在认证中心的子系统中去,确保所有子系统全部退出。

问题3:系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源 中局部会话的作用是什么?怎么创建?

答:局部会话是子系统和用户之间(浏览器)建立的会话,用来验证用户是否已经登录,就如同传统的web应用登录后创建的会话一样。如果检测到局部会话存在则表示该系统已经登录过,无需去认证中心进行验证,反过来也是一样,局部会话不存在就需要去认证中心验证。同样为了简便,我也是利用tomcat的session建立的,和建立全局会话相同。

问题4:sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌 中,怎么验证用户已经登录?

答:这个问题是我当初最疑惑的地方,后来发现是自己没有执行看文章,多仔细看几遍就发现是通过检查是否存在全局会话来判断用户是否登录的,这同样是为什么要建立全局会话的原因。

三、代码

        正常来说,客户端和认证中心都需要配置拦截器或者过滤器,我这为了偷懒,直接在controller中进行判断。但是并不是所有的子系统或者认证中心都利用了springMvc,甚至有系统直接连开发语言都不一样。所以要根据实际情况来选择。怎么方便、怎么舒服就怎么选。还有我选这个的原因就是。。。。。。我对拦截器这些不太熟,工作中没有用到过,当初学习的时候用过但是都忘了,都忘了,忘了。代码狗就是无尽的学习以及复习。

废话不多说,直接上代码。

客户端部分

contoller部分,为方便。很多的跳转路径都是写死的。

[java]  view plain  copy
  1. package com.yzz.ssoclient1.controller;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.Enumeration;  
  5. import java.util.HashMap;  
  6. import java.util.Map;  
  7. import java.util.Set;  
  8.   
  9. import javax.servlet.http.HttpServletRequest;  
  10. import javax.servlet.http.HttpSession;  
  11.   
  12. import org.apache.commons.httpclient.HttpClient;  
  13. import org.apache.commons.httpclient.HttpException;  
  14. import org.apache.commons.httpclient.methods.PostMethod;  
  15. import org.springframework.stereotype.Controller;  
  16. import org.springframework.ui.ModelMap;  
  17. import org.springframework.web.bind.annotation.RequestMapping;  
  18. import org.springframework.web.servlet.ModelAndView;  
  19.   
  20. import com.alibaba.fastjson.JSONObject;  
  21. import com.yzz.ssoclient1.util.SessionUtil;  
  22.   
  23. /** 
  24.  *  
  25.  * @author yzz 
  26.  *客户端部分,本想只用一个session存储局部会话,收到服务端的退出请求后直接调用request.getSession().removeAttribute("token")清空局部会话; 
  27.  *结果发现,服务端利用httpClient通知客户端的时候是新建立的一个会话,此时的session和我们局部建立的session并不是同一个。 
  28.  *解决办法: 
  29.  *自己维护一个session管理类,利用map将局部会话的session对象和id存储起来。收到请求后再销毁该session 
  30.  */  
  31.   
  32. @Controller  
  33. public class SSOClientController {  
  34.   
  35.       
  36.     //拦截所有获取资源请求  
  37.     @RequestMapping("")  
  38.     public String ssoClient(HttpServletRequest request,ModelMap map){  
  39.           
  40.         //判断请求的链接中是否有token参数  
  41.         String token=request.getParameter("token");  
  42.         String url=request.getParameter("url");  
  43.           
  44.           
  45.         if(token!=null){  
  46.             //如果有表示是认证服务器返回的  
  47.             String allSessionId=request.getParameter("allSessionId");  
  48.             return "redirect:http://localhost:8088/SSOClient1/checkToken?token="+token+"&allSessionId="+allSessionId;  
  49.         }else if(url!=null){  
  50.               
  51.             return "redirect:http://localhost:8088/SSOClient1/login?url="+url;  
  52.         }else{  
  53.             //其他请求,继续判断是否创建了和用户之间的局部会话  
  54.             JSONObject j=(JSONObject) request.getSession().getAttribute("token");  
  55.             if(j!=null){  
  56.                 System.out.println("客户端1已经登录,存在局部会话:"+j);  
  57.                 System.out.println("本次局部会话的localSessionId:"+request.getSession().getId());  
  58.                 map.addAttribute("userName", j.getString("userName"));  
  59.                 map.addAttribute("allSessionId", j.getString("allSessionId"));  
  60.                 return "index";  
  61.             }else{  
  62.                 //未登录  
  63.                   
  64.                 return "redirect:http://localhost:8088/SSOServer?clientUrl=http://localhost:8088/SSOClient1";  
  65.             }  
  66.         }     
  67.     }  
  68.       
  69.     //客户端接收token并且进行验证  
  70.     @RequestMapping(value="/checkToken")  
  71.     public String checkToken(HttpServletRequest request,ModelMap map){  
  72.           
  73.         String token=request.getParameter("token");  
  74.         String allSessionId=request.getParameter("allSessionId");  
  75.           
  76.         //利用httpClient进行验证  
  77.         String basePath = request.getScheme() + "://" + request.getServerName() + ":"  
  78.                 + request.getServerPort() + request.getContextPath();  
  79.         HttpClient httpClient = new HttpClient();  
  80.         PostMethod postMethod = new PostMethod("http://localhost:8088/SSOServer/tokenCheck");  
  81.         postMethod.addParameter("token", token);  
  82.         postMethod.addParameter("allSessionId", allSessionId);  
  83.         postMethod.addParameter("clientUrl",basePath);  
  84.           
  85.         try {  
  86.             httpClient.executeMethod(postMethod);  
  87.             String resultJson = postMethod.getResponseBodyAsString();  
  88.               
  89.             postMethod.releaseConnection();  
  90.             //用httpClient得到的json数据默认被转义了两次变成了"{\\"header\\":\\"认证成功!\\",\\"userName\\":\\"admin\\",\\"erroeCode\\":0}"  
  91.             //需要数据还原 \\" 变成  " 同时去掉前后的双引号   
  92.               
  93.             resultJson=resultJson.replaceAll("\\\\\"""\"");  
  94.             resultJson=resultJson.substring(1, resultJson.length()-1);  
  95.             JSONObject j=JSONObject.parseObject(resultJson);  
  96.             j.put("allSessionId", allSessionId);  
  97.             int errorCode=j.getIntValue("erroeCode");  
  98.             if(errorCode==0){  
  99.                 //创建客户端和用户的局部会话  
  100.                 request.getSession().setAttribute("token", j);  
  101.                 String localSessionId=request.getSession().getId();  
  102.                 HttpSession localSession=request.getSession();  
  103.                 System.out.println("创建局部会话,localSessionId是:"+request.getSession().getId());  
  104.                 map.addAttribute("userName", j.getString("userName"));  
  105.                 map.addAttribute("allSessionId", j.getString("allSessionId"));  
  106.                 //存储局部会话  
  107.                   
  108.                 SessionUtil.setSession(localSessionId, localSession);  
  109.                 //存储对应关系  
  110.                 SessionUtil.setLink(allSessionId, localSessionId);  
  111.                   
  112.             }else{  
  113.                   
  114.             }  
  115.         } catch (HttpException e) {  
  116.             // TODO Auto-generated catch block  
  117.             e.printStackTrace();  
  118.         } catch (IOException e) {  
  119.             // TODO Auto-generated catch block  
  120.             e.printStackTrace();  
  121.         }  
  122.         return "index";  
  123.     }  
  124.       
  125.     //客户端登录  
  126.     @RequestMapping(value="/login")  
  127.     public ModelAndView login(HttpServletRequest request){  
  128.         String url=request.getParameter("url");  
  129.         ModelAndView model=new ModelAndView();  
  130.         model.setViewName("login");  
  131.         model.addObject("url", url);  
  132.         return model;  
  133.     }  
  134.       
  135.     //退出  
  136.     @RequestMapping(value="/logout")  
  137.     public void logout(String allSessionId){  
  138.           
  139.         System.out.println("客户端1收到退出请求");  
  140.         String localSessionId=SessionUtil.getLocalSessionId(allSessionId);  
  141.           
  142.         HttpSession localSession=SessionUtil.getSession(localSessionId);  
  143.           
  144.         localSession.removeAttribute("token");  
  145.           
  146.         //localSession.invalidate();  
  147.           
  148.     }  
  149.       
  150.       
  151.       
  152.       
  153. }  

客户端session管理类 SessionUtil.java

[java]  view plain  copy
  1. package com.yzz.ssoclient1.util;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5.   
  6. import javax.servlet.http.HttpSession;  
  7. public class SessionUtil {  
  8.   
  9.     private static Map <String, HttpSession> SESSIONMAP=new HashMap<String, HttpSession>();  
  10.     private static Map <String,String> sessionLink=new HashMap<String, String>();  
  11.     public static HttpSession getSession(String localSessionId){  
  12.         return SESSIONMAP.get(localSessionId);  
  13.     }  
  14.       
  15.     public static void setSession(String localSessionId,HttpSession localSession){  
  16.          SESSIONMAP.put(localSessionId, localSession);  
  17.     }  
  18.       
  19.     public static void remove(String localSessionId){  
  20.         SESSIONMAP.remove(localSessionId);  
  21.     }  
  22.       
  23.     public static String getLocalSessionId(String allSessionId){  
  24.         return sessionLink.get(allSessionId);  
  25.     }  
  26.     public static void setLink(String allSessionId,String localSessionId){  
  27.         sessionLink.put(allSessionId, localSessionId);  
  28.     }  
  29.     public static void removeL(String allSessionId,String localSessionId){  
  30.         sessionLink.remove(allSessionId);  
  31.     }  
  32. }  

mavn配置的pom.xml

[html]  view plain  copy
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  2.   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">  
  3.   <modelVersion>4.0.0</modelVersion>  
  4.   <groupId>com.yzz.ssoclient1</groupId>  
  5.   <artifactId>SSOClient1</artifactId>  
  6.   <packaging>war</packaging>  
  7.   <version>0.0.1-SNAPSHOT</version>  
  8.   <name>SSOClient1 Maven Webapp</name>  
  9.   <url>http://maven.apache.org</url>  
  10.   <properties>  
  11.     <!-- spring版本号 -->  
  12.     <spring.version>4.0.2.RELEASE</spring.version>  
  13.   </properties>  
  14.     
  15.   <dependencies>  
  16.     <dependency>  
  17.       <groupId>junit</groupId>  
  18.       <artifactId>junit</artifactId>  
  19.       <version>3.8.1</version>  
  20.       <scope>test</scope>  
  21.      </dependency>  
  22.         <dependency>    
  23.             <groupId>org.codehaus.jackson</groupId>    
  24.             <artifactId>jackson-mapper-asl</artifactId>    
  25.             <version>1.9.13</version>    
  26.         </dependency>  
  27.       <!-- spring核心包 -->    
  28.         <dependency>    
  29.             <groupId>org.springframework</groupId>    
  30.             <artifactId>spring-core</artifactId>    
  31.             <version>${spring.version}</version>    
  32.         </dependency>  
  33.             
  34.         <dependency>    
  35.             <groupId>org.springframework</groupId>    
  36.             <artifactId>spring-web</artifactId>    
  37.             <version>${spring.version}</version>    
  38.         </dependency>   
  39.            
  40.         <dependency>    
  41.             <groupId>org.springframework</groupId>    
  42.             <artifactId>spring-oxm</artifactId>    
  43.             <version>${spring.version}</version>    
  44.         </dependency>    
  45.         <dependency>    
  46.             <groupId>org.springframework</groupId>    
  47.             <artifactId>spring-tx</artifactId>    
  48.             <version>${spring.version}</version>    
  49.         </dependency>    
  50.     
  51.         <dependency>    
  52.             <groupId>org.springframework</groupId>    
  53.             <artifactId>spring-jdbc</artifactId>    
  54.             <version>${spring.version}</version>    
  55.         </dependency>    
  56.     
  57.         <dependency>    
  58.             <groupId>org.springframework</groupId>    
  59.             <artifactId>spring-webmvc</artifactId>    
  60.             <version>${spring.version}</version>    
  61.         </dependency>    
  62.         <dependency>    
  63.             <groupId>org.springframework</groupId>    
  64.             <artifactId>spring-aop</artifactId>    
  65.             <version>${spring.version}</version>    
  66.         </dependency>    
  67.     
  68.         <dependency>    
  69.             <groupId>org.springframework</groupId>    
  70.             <artifactId>spring-context-support</artifactId>    
  71.             <version>${spring.version}</version>    
  72.         </dependency>    
  73.     
  74.         <dependency>    
  75.             <groupId>org.springframework</groupId>    
  76.             <artifactId>spring-test</artifactId>    
  77.             <version>${spring.version}</version>    
  78.         </dependency>   
  79.           
  80.         <dependency>  
  81.             <groupId>commons-httpclient</groupId>  
  82.             <artifactId>commons-httpclient</artifactId>  
  83.             <version>3.1</version>  
  84.         </dependency>   
  85.         <dependency>    
  86.             <groupId>commons-io</groupId>    
  87.             <artifactId>commons-io</artifactId>    
  88.             <version>2.4</version>    
  89.         </dependency>    
  90.         <dependency>    
  91.             <groupId>commons-codec</groupId>    
  92.             <artifactId>commons-codec</artifactId>    
  93.             <version>1.9</version>    
  94.         </dependency>  
  95.         <dependency>  
  96.             <groupId>commons-dbcp</groupId>  
  97.             <artifactId>commons-dbcp</artifactId>  
  98.             <version>1.4</version>  
  99.         </dependency>   
  100.         <dependency>    
  101.             <groupId>mysql</groupId>    
  102.             <artifactId>mysql-connector-java</artifactId>    
  103.             <version>5.1.30</version>    
  104.         </dependency>  
  105.         <dependency>    
  106.             <groupId>javax</groupId>    
  107.             <artifactId>javaee-api</artifactId>    
  108.             <version>7.0</version>    
  109.         </dependency>  
  110.        <dependency>    
  111.             <groupId>jstl</groupId>    
  112.             <artifactId>jstl</artifactId>    
  113.             <version>1.2</version>    
  114.         </dependency>    
  115.         <!-- 阿里的json包 -->  
  116.         <dependency>  
  117.             <groupId>com.alibaba</groupId>  
  118.             <artifactId>fastjson</artifactId>  
  119.             <version>1.2.24</version>  
  120.         </dependency>  
  121.   </dependencies>  
  122.   <build>  
  123.     <finalName>SSOClient1</finalName>  
  124.   </build>  
  125. </project>  

还有两个jsp页面,一个登陆的,一个显示的。比较简单不贴出来了。web.xml和spring-mvc.xml都是用的以前第三方登录项目的,稍微改了下,不重复列了。第三方登录项目链接

客户端的工程结构图如下


服务端部分

篇幅问题,我就只列出关键部分代码

验证部分 SSOServerContoller

[java]  view plain  copy
  1. package com.yzz.ssoserver.controller;  
  2.   
  3. import java.util.UUID;  
  4.   
  5. import javax.json.JsonObject;  
  6. import javax.servlet.http.HttpServletRequest;  
  7.   
  8. import org.springframework.http.HttpEntity;  
  9. import org.springframework.stereotype.Controller;  
  10. import org.springframework.web.bind.annotation.RequestMapping;  
  11. import org.springframework.web.bind.annotation.RequestMethod;  
  12. import org.springframework.web.bind.annotation.ResponseBody;  
  13.   
  14. import com.alibaba.fastjson.JSON;  
  15. import com.alibaba.fastjson.JSONArray;  
  16. import com.alibaba.fastjson.JSONObject;  
  17. import com.yzz.ssoserver.util.TokenUtil;  
  18. import com.yzz.ssoserver.util.UrlUtil;  
  19.   
  20. @Controller  
  21. public class SSOServerController {  
  22.   
  23.     //判断用户是否登录,偷懒,利用controller代替拦截器  
  24.     @RequestMapping("")  
  25.     public String loginCheck(String clientUrl,HttpServletRequest request){  
  26.         String userName=(String)request.getSession().getAttribute("isLogin");  
  27.         //未登录跳转到客户端登录页面(也可以是服务器自身拥有登录界面)  
  28.         if(userName==null){  
  29.               
  30.             System.out.println("路径:"+clientUrl+" 未登录,跳转登录页面");  
  31.             return "redirect:"+clientUrl+"?url=http://localhost:8088/SSOServer/user/login";  
  32.         }else{  
  33.             //以登录携带令牌原路返回  
  34.             String token = UUID.randomUUID().toString();  
  35.             System.out.println("已经登录,登录账号:"+userName+"服务端产生的token:"+token);  
  36.             //存储  
  37.             TokenUtil.put(token, userName);  
  38.             return "redirect:"+clientUrl+"?token="+token+"&allSessionId="+request.getSession().getId();  
  39.         }     
  40.     }  
  41.       
  42.     //令牌验证  
  43.     @ResponseBody  
  44.     @RequestMapping(value="/tokenCheck",method=RequestMethod.POST)  
  45.     public String tokenCheck(String token,String clientUrl,String allSessionId){  
  46.           
  47.         JSONObject j=new JSONObject();  
  48.         String userName=TokenUtil.get(token);  
  49.           
  50.         //token一次性的,用完即毁  
  51.         TokenUtil.remove(token);  
  52.         if(userName!=null){  
  53.             //设置返回消息  
  54.             j.put("erroeCode"0);  
  55.             j.put("header""认证成功!");  
  56.             j.put("userName", userName);  
  57.               
  58.             //存储地址信息,用于退出时销毁  
  59.               
  60.             String url=UrlUtil.get(allSessionId);  
  61.             if(url==null){  
  62.                 url=clientUrl;  
  63.             }else{  
  64.                 url+=","+clientUrl;  
  65.             }  
  66.               
  67.             UrlUtil.put(allSessionId, url);  
  68.               
  69.         }  
  70.         return j.toJSONString();  
  71.     }  
  72. }  

用户管理部分 UserController

[java]  view plain  copy
  1. package com.yzz.ssoserver.controller;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStream;  
  6. import java.io.InputStreamReader;  
  7. import java.io.OutputStream;  
  8. import java.io.OutputStreamWriter;  
  9. import java.net.HttpURLConnection;  
  10. import java.net.MalformedURLException;  
  11. import java.net.URL;  
  12. import java.util.UUID;  
  13.   
  14. import javax.servlet.http.Cookie;  
  15. import javax.servlet.http.HttpServletRequest;  
  16. import javax.servlet.http.HttpServletResponse;  
  17.   
  18. import org.apache.commons.httpclient.HttpClient;  
  19. import org.apache.commons.httpclient.HttpException;  
  20. import org.apache.commons.httpclient.methods.PostMethod;  
  21. import org.springframework.beans.factory.annotation.Autowired;  
  22. import org.springframework.stereotype.Controller;  
  23. import org.springframework.web.bind.annotation.RequestMapping;  
  24. import org.springframework.web.bind.annotation.RequestMethod;  
  25. import org.springframework.web.servlet.ModelAndView;  
  26.   
  27. import com.yzz.ssoserver.bean.User;  
  28. import com.yzz.ssoserver.dao.UserDao;  
  29. import com.yzz.ssoserver.util.TokenUtil;  
  30. import com.yzz.ssoserver.util.UrlUtil;  
  31.   
  32. /** 
  33.  *  
  34.  * @author Administrator 
  35.  * 
  36.  */  
  37. @RequestMapping("/user")  
  38. @Controller  
  39. public class UserController {  
  40.     @Autowired  
  41.     private UserDao baseDao;  
  42.     @RequestMapping("/getName")  
  43.     public ModelAndView getName(){  
  44.         ModelAndView model=new ModelAndView("index");  
  45.           
  46.         String userName=baseDao.getName();  
  47.         model.addObject("userName",userName);  
  48.         return model;  
  49.     }  
  50.       
  51.     //登录验证  
  52.     @RequestMapping(value="/login",method=RequestMethod.POST)  
  53.     public String login(HttpServletRequest request,HttpServletResponse response){  
  54.         ModelAndView model=new ModelAndView();  
  55.         String userName=request.getParameter("userName");  
  56.         String userPassword=request.getParameter("userPassword");  
  57.         String redirectUrl=request.getParameter("redirectUrl");  
  58.         User user=baseDao.login(userName,userPassword);  
  59.         if(user!=null){  
  60.             //设置状态(通过session判断该浏览器与认证中心的全局会话是否已经建立),生成令牌  
  61.             request.getSession().setAttribute("isLogin", userName);  
  62.             String token = UUID.randomUUID().toString();  
  63.               
  64.             //存储  
  65.             TokenUtil.put(token, userName);  
  66.             /*设置cookie到浏览器 
  67.             Cookie cookie=new Cookie("sso", userName); 
  68.             cookie.setMaxAge(60); 
  69.             response.addCookie(cookie); 
  70.             */  
  71.             //将token发送给客户端,附带本次全局会话的sessionId  
  72.             String allSessionId=request.getSession().getId();  
  73.             System.out.println("全局会话allSessionId:"+allSessionId);  
  74.             return "redirect:"+redirectUrl+"?token="+token+"&allSessionId="+allSessionId;     
  75.         }  
  76.         return "redirect:http://localhost:8088/SSOServer/redirectUrl?msg=loginError";  
  77.     }  
  78.       
  79.     @RequestMapping(value="/redirectUrl",method=RequestMethod.POST)  
  80.     public ModelAndView redirectUrl(HttpServletRequest request){  
  81.         ModelAndView model=new ModelAndView();  
  82.         String msg=request.getParameter("msg");  
  83.         if(msg.equals("loginError")){  
  84.             msg="账号密码错误";  
  85.             model.setViewName("error");  
  86.             model.addObject("msg",msg);  
  87.         }  
  88.         return model;  
  89.     }  
  90.       
  91.     //登出  
  92.     @RequestMapping(value="/logout")  
  93.     public String logOut(String allSessionId,String redirectUrl,HttpServletRequest request){  
  94.         String url=UrlUtil.get(allSessionId);  
  95.         UrlUtil.remove(allSessionId);  
  96.         //删除全局会话  
  97.         request.getSession().removeAttribute("isLogin");  
  98.           
  99.         //通知各个客户端删除局部会话  
  100.         String [] urls=url.split(",");  
  101.         //使用httpClient通知客户端的时候发现是新建立了一个服务器与客户端的会话,导致sessionId和客户建立的局部会话id不相同,无法做到删除局部会话  
  102.         HttpClient httpClient=new HttpClient();  
  103.         PostMethod postMethod=new PostMethod();  
  104.           
  105.         for (String u : urls) {  
  106.               
  107.             postMethod.setPath(u+"/logout");  
  108.             postMethod.addParameter("allSessionId", allSessionId);  
  109.               
  110.             try {  
  111.                 httpClient.executeMethod(postMethod);  
  112.                 postMethod.releaseConnection();  
  113.               
  114.             } catch (HttpException e) {  
  115.                 // TODO Auto-generated catch block  
  116.                 e.printStackTrace();  
  117.             } catch (IOException e) {  
  118.                 // TODO Auto-generated catch block  
  119.                 e.printStackTrace();  
  120.             }  
  121.         }  
  122.           
  123.         return "redirect:"+redirectUrl;  
  124.     }  
  125.       
  126. }  

数据库部分以及dao部分,图省事,直接使用的是sping 的jdbcTemplate。实现简单的验证的话也可以不用数据库,直接定死用户名和密码。

dao

[java]  view plain  copy
  1. package com.yzz.ssoserver.dao;  
  2.   
  3. import java.sql.ResultSet;  
  4. import java.util.ArrayList;  
  5. import java.util.List;  
  6.   
  7. import javax.swing.text.html.HTMLDocument.HTMLReader.ParagraphAction;  
  8. import javax.swing.tree.RowMapper;  
  9. import javax.swing.tree.TreePath;  
  10.   
  11. import org.springframework.beans.factory.annotation.Autowired;  
  12. import org.springframework.jdbc.core.JdbcTemplate;  
  13. import org.springframework.stereotype.Repository;  
  14.   
  15. import com.yzz.ssoserver.bean.User;  
  16. import com.yzz.ssoserver.mapping.UserMapping;  
  17. @Repository  
  18. public class UserDao {  
  19.   
  20.     @Autowired  
  21.     private  JdbcTemplate jdbcTemplate;  
  22.   
  23.   
  24.     public String getName(){  
  25.         return jdbcTemplate.queryForObject("select user_name from user_info where user_id=1", String.class);  
  26.     }  
  27.       
  28.     public User login(String userName,String userPassword){  
  29.         User u=new User();  
  30.         String sql=" select * from user_info where user_name=? and user_password=? ";  
  31.           
  32.         Object[] param= new Object[]{userName,userPassword};  
  33.           
  34.         u=jdbcTemplate.queryForObject(sql, new UserMapping(), param);  
  35.           
  36.         return u;  
  37.     }  
  38.   
  39. }  

spring.xml,在客户端的xml基础中中加入如下配置

[html]  view plain  copy
  1. <!-- 数据库设置 -->  
  2.     <bean id="propertyConfigurer"    
  3.         class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">    
  4.         <property name="location" value="classpath:db.properties" />    
  5.     </bean>   
  6.       
  7.     <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"    
  8.         destroy-method="close">    
  9.         <property name="driverClassName" value="${driver}" />    
  10.         <property name="url" value="${url}" />    
  11.         <property name="username" value="${username}" />    
  12.         <property name="password" value="${password}" />    
  13.         <!-- 初始化连接大小 -->    
  14.         <property name="initialSize" value="${initialSize}"></property>    
  15.         <!-- 连接池最大数量 -->    
  16.         <property name="maxActive" value="${maxActive}"></property>    
  17.         <!-- 连接池最大空闲 -->    
  18.         <property name="maxIdle" value="${maxIdle}"></property>    
  19.         <!-- 连接池最小空闲 -->    
  20.         <property name="minIdle" value="${minIdle}"></property>    
  21.         <!-- 获取连接最大等待时间 -->    
  22.         <property name="maxWait" value="${maxWait}"></property>    
  23.     </bean>   
  24.       
  25.     <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">    
  26.         <property name="dataSource" ref="dataSource"></property>    
  27.     </bean>    
  28.       

还有一些工具类,都是简单的map操作和客户端的sessionUtil差不多。bean以及RowMapper的实现就不列了。db.properties和sql我直接用的是以前第三方登录的项目,可以再以前文章找,上面再客户端代码介绍部分有链接。

pom.xml和web.xml和客户端的差不多,也不列了。

其中需要用多个客户端进行测试,客户端的代码全相同,自己重新建项目复制代码就好。

四、展示

打开浏览器分别访问两个子系统 localhost:8088/SSOClient1 和localhost:8088/SSOClient2,如下将全部提示需要登录。


随便在一个中登录,在重新访问另外一个,将出现如下效果,这是在客户端1中登录,在去访问客户端2,此时显示已登录。

退出也是同样的道理,随便在一个里面退出。在去访问另外一个地址。你会发现需要登录。

总结

       现在已经出现很多的sso单点登录的集成框架,用的最多的是CAS框架:CAS(Central Authentication Service)。也有很多公司提供了直接的SSO模块。但是原理还是需要自己掌握。这个demo还有许多地方要完善,例如,没有拦截器。没有完善的授权控制,这儿默认一次性,用完即删。没有控制完善的会话检测功能,每次登录还需要直接从新输入地址,没有考虑跨域之类的(如果需要实现跨域的话可以考虑在ajax中利用jsonp来请求数据,或者是在过滤器中进行处理)。

源码下载

      别想着把源码下载了就能直接运行。我可以肯定的告诉那些想着下了源码就用的人,100%是报错的。这个的主要作用是参考,每人的机器环境都不一样的。我把这个项目从公司电脑换的自己的电脑照样报错。mavn配置不同、tomcat配置不同、jdk不同都会报错。重要的代码和架构全在这儿贴出来了。

如果实在是想把源码拿来就用的话,那就去看看我的另外一篇解决移植错误的博客吧:MAVEN项目移植错误的解决方法。最后自己改一下对应的tomcat端口已经路径。

重新复习了下过滤器和拦截器部分的知识,对这个简单的demo进行了部分修改,修改如下。(无法更改以删除资源,改的部分贴着直接贴出来算了)

1、客户端1部分使用拦截器interceptor实现登录验证以及部分请求验证。

2、客户端2部分使用过滤器OncePerRequestFilter 实现登录验证以及部分请求验证。

3、服务器部分少部分返回路径进行修改。

修改后的效果:只要其中一个客户端登录状态发生变化,另外一个客户端只需要刷新页面或者进行任何操作就能对应的更改状态。无需浏览器手动重新输入地址,更加人性化。

例如:客户端1登录后,客户端2在现有的任意界面里刷新一下直接跳转到显示界面。无需重新手动输入项目路径。退出也是相同的道理。

客户端1使用拦截器

[java]  view plain  copy
  1. package com.yzz.ssoclient1.interceptor;  
  2.   
  3. import java.io.PrintWriter;  
  4.   
  5. import javax.servlet.http.HttpServletRequest;  
  6. import javax.servlet.http.HttpServletResponse;  
  7.   
  8. import org.springframework.web.servlet.HandlerInterceptor;  
  9. import org.springframework.web.servlet.ModelAndView;  
  10.   
  11. import com.alibaba.fastjson.JSONObject;  
  12. import com.yzz.ssoclient1.controller.SSOClientController;  
  13. import com.yzz.ssoclient1.util.SessionUtil;  
  14.   
  15. /** 
  16.  * 拦截器模块,用于对资源请求的过滤 
  17.  * @author yzz 
  18.  * 
  19.  */  
  20. public class SSOInterceptor implements HandlerInterceptor{  
  21.     private static boolean type=false;  
  22.     //请求完全处理完之后调用,一般用于清理资源  
  23.     public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)  
  24.             throws Exception {  
  25.         // TODO Auto-generated method stub  
  26.           
  27.     }  
  28.       
  29.     //业务请求完成后,视图生成之前调用  
  30.     public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)  
  31.             throws Exception {  
  32.         // TODO Auto-generated method stub  
  33.           
  34.     }  
  35.     //业务请求之前调用  
  36.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
  37.         // TODO Auto-generated method stub  
  38.         //任何请求路径,先判断是否登录,可在xml中进行配置,设置拦截哪一些,不拦截哪一些  
  39.           
  40.         JSONObject j=(JSONObject) request.getSession().getAttribute("token");  
  41.         String token=request.getParameter("token");  
  42.         String url=request.getParameter("url");  
  43.         String allSessionId=request.getParameter("allSessionId");  
  44.         SSOClientController c=new SSOClientController();  
  45.         response.setContentType("text/html;charset=UTF-8");  
  46.         if(j==null){//未登录,继续判断。未登录包括几种情况  
  47.               
  48.             if(token!=null){ //带有参数token,表示是服务端返回到客户端的token验证部分  
  49.                 return true;  
  50.             }else if(url!=null){ //带有url参数,表示服务器判断未登录后跳转到客户端对应的登界面  
  51.                   
  52.                 //对特殊情况进行验证,即客户端1还未登录,卡在输入账号密码的页面,但是客户端2登录了,此时只要刷新客户端1的页面应改为登录状态  
  53.                 if(SessionUtil.getNum()==0){  
  54.                     PrintWriter out = response.getWriter();   
  55.                     StringBuilder builder = new StringBuilder();   
  56.                     builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");  
  57.                     builder.append("window.top.location.href=\"");   
  58.                     builder.append("http://localhost:8088/SSOServer");  //这里是http://ip:port/项目名  
  59.                     builder.append("?clientUrl=http://localhost:8088/SSOClient1\";</script>");  //这里是重新登录的页面url  
  60.                     out.print(builder.toString());    
  61.                     out.close();   
  62.                     SessionUtil.addNum();  
  63.                     return false;  
  64.                 }else{    
  65.                     SessionUtil.removeNum();  
  66.                     return true;  
  67.                 }  
  68.                   
  69.             }else if(allSessionId!=null && token==null){//只有allSessionId的时候,表示退出登录  
  70.                 return true;  
  71.             }else{  
  72.                 //跳转到服务端登录引导页面  
  73.                 PrintWriter out = response.getWriter();   
  74.                 StringBuilder builder = new StringBuilder();   
  75.                 builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");   
  76.                  
  77.                 builder.append("window.top.location.href=\"");   
  78.                 builder.append("http://localhost:8088/SSOServer");  //这里是http://ip:port/项目名  
  79.                 builder.append("?clientUrl=http://localhost:8088/SSOClient1\";</script>");  //这里是重新登录的页面url  
  80.                 out.print(builder.toString());   
  81.                 out.close();    
  82.                 return false;  
  83.             }  
  84.               
  85.         }else{  
  86.             //已经登录,执行请求  
  87.             return true;  
  88.         }  
  89.     }  
  90.   
  91.       
  92.   
  93. }  

客户端1拦截器xml配置

[html]  view plain  copy
  1. <!-- 配置拦截器-->  
  2.      <mvc:interceptors>  
  3.      <!-- 使用 bean 定义一个 Interceptor,直接定义在 mvc:interceptors 下面的 Interceptor 将拦截所有的请求 -->    
  4.         <bean class="com.yzz.ssoclient1.interceptor.SSOInterceptor"/>    
  5.           
  6.         <!-- 另外一种 ,在次一级的mvc:interceptors中进行配置可设置指定拦截路径和指定不拦截路径。  
  7.         <mvc:interceptor>    
  8.                  用<mvc:mapping>标签指定要拦截的路径   
  9.             <mvc:mapping path="/*"/>   
  10.               
  11.                 用<mvc:exclude-mapping>标签指定不要要拦截的路径,切记不知能只设置指定不拦截的路径,否则不生效。必须要设置指定拦截路径之后才设置不拦截的      
  12.             <mvc:exclude-mapping path="/user/check"/>    
  13.                  指定使用哪个拦截器进行拦截   
  14.             <bean class="com.yzz.ssoclient1.interceptor.SSOInterceptor"></bean>    
  15.         </mvc:interceptor>    
  16.         -->  
  17.      </mvc:interceptors>  

客户端2,使用过滤器

[java]  view plain  copy
  1. package com.yzz.ssoclient2.filter;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. import javax.servlet.Filter;  
  6. import javax.servlet.FilterChain;  
  7. import javax.servlet.FilterConfig;  
  8. import javax.servlet.ServletException;  
  9. import javax.servlet.ServletRequest;  
  10. import javax.servlet.ServletResponse;  
  11. import javax.servlet.annotation.WebFilter;  
  12. import javax.servlet.http.HttpServletRequest;  
  13. import javax.servlet.http.HttpServletResponse;  
  14. import javax.servlet.http.HttpSession;  
  15.   
  16. import org.springframework.web.filter.OncePerRequestFilter;  
  17.   
  18. import com.alibaba.fastjson.JSONObject;  
  19. import com.yzz.ssoclient2.util.SessionUtil;  
  20.   
  21. public class SSOClientFilter extends OncePerRequestFilter{  
  22.   
  23.   
  24.     @Override  
  25.     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)  
  26.             throws ServletException, IOException {  
  27.         // TODO Auto-generated method stub  
  28.           
  29.         request.setCharacterEncoding("UTF-8");    
  30.         response.setCharacterEncoding("UTF-8");  
  31.         //判断请求的链接中是否有token参数  
  32.         HttpServletRequest servletRequest = (HttpServletRequest) request;  
  33.         HttpServletResponse servletResponse = (HttpServletResponse) response;  
  34.         HttpSession session = servletRequest.getSession();  
  35.         JSONObject j=(JSONObject) session.getAttribute("token");  
  36.         String token=request.getParameter("token");  
  37.         String url=request.getParameter("url");  
  38.         String allSessionId=request.getParameter("allSessionId");  
  39.           
  40.         if(j==null){//未登录,继续判断。未登录包括几种情况  
  41.               
  42.             if(token!=null){ //带有参数token,表示是服务端返回到客户端的token验证部分  
  43.                 filterChain.doFilter(request, response);  
  44.             }else if(url!=null){ //带有url参数,表示服务器判断未登录后跳转到客户端对应的登界面  
  45.                 //对特殊情况进行验证,即客户端2还未登录,卡在输入账号密码的页面,但是客户端1登录了,此时只要刷新客户端2的页面应改为登录状态  
  46.                 if(SessionUtil.getNum()==0){  
  47. //                  PrintWriter out = response.getWriter();   
  48. //                  StringBuilder builder = new StringBuilder();   
  49. //                  builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");  
  50. //                  builder.append("window.top.location.href=\"");   
  51. //                  builder.append("http://localhost:8088/SSOServer");  //这里是http://ip:port/项目名  
  52. //                  builder.append("?clientUrl=http://localhost:8088/SSOClient1\";</script>");  //这里是重新登录的页面url  
  53. //                  out.print(builder.toString());    
  54. //                  out.close();   
  55.                     SessionUtil.addNum();  
  56.                       
  57.                     servletResponse.sendRedirect("http://localhost:8088/SSOServer?clientUrl=http://localhost:8088/SSOClient2");  
  58.                    // return false;  
  59.                 }else{    
  60.                     SessionUtil.removeNum();  
  61.                     filterChain.doFilter(request, response);  
  62.                 }  
  63.                   
  64.             }else if(allSessionId!=null && token==null){//只有allSessionId的时候,表示退出登录  
  65.                 filterChain.doFilter(request, response);  
  66.             }else{  
  67.                 //跳转到服务端登录引导页面  
  68. //              PrintWriter out = response.getWriter();   
  69. //                StringBuilder builder = new StringBuilder();   
  70. //                builder.append("<script type=\"text/javascript\" charset=\"UTF-8\">");   
  71. //                 
  72. //                builder.append("window.top.location.href=\"");   
  73. //                builder.append("http://localhost:8088/SSOServer");  //这里是http://ip:port/项目名  
  74. //                builder.append("?clientUrl=http://localhost:8088/SSOClient1\";</script>");  //这里是重新登录的页面url  
  75. //                out.print(builder.toString());   
  76. //                out.close();   
  77.                 servletResponse.sendRedirect("http://localhost:8088/SSOServer?clientUrl=http://localhost:8088/SSOClient2");  
  78.   
  79.                 //return false;  
  80.             }  
  81.               
  82.         }else{  
  83.             //已经登录,执行请求  
  84.             filterChain.doFilter(request, response);  
  85.         }  
  86.     }  
  87.   
  88.       
  89.   
  90. }  

客户端2过滤器xml配置

[html]  view plain  copy
  1. <filter>    
  2.             <filter-name>ssoClientFilter</filter-name>    
  3.             <filter-class>com.yzz.ssoclient2.filter.SSOClientFilter</filter-class>    
  4.         </filter>    
  5.         <filter-mapping>    
  6.             <filter-name>ssoClientFilter</filter-name>    
  7.             <url-pattern>/*</url-pattern>    
  8.         </filter-mapping>  

两个客户端的controlller部分去掉原来拦截所有的部分,也就是注解@RequestMapping("")的部分,增加以下代码

[java]  view plain  copy
  1. //显示页面  
  2.     @RequestMapping(value="/show")  
  3.     public String show(String allSessionId){  
  4.                   
  5.         return "index";  
  6.     }  

同时两个客户端工具类增加一个计数器,比较简单就不列了。服务端部分只改了几个返回路径,自己过一边就能发现。


猜你喜欢

转载自blog.csdn.net/fjz_lihuapiaoxiang/article/details/80314603
今日推荐