本身动手写SSO单点登陆服务端和客户端

本文为转载 ,发表在: https://www.jianshu.com/p/79cfab236877

1、前言

咱们本身动手写单点登陆的服务端目的是为了加深对单点登陆的理解.若是大家公司想实现单点登陆/单点注销功能,推荐使用开源的单点登陆框架CAS.咱们后面的章节也会带同窗们快速搭建CAS Server和CAS Client的环境.css

2、条件

若是没看前面章节的同窗,请返回去观看这几章内容,否则这代码是不太好理解的.前端

  • SSO单点登陆教程(一)多系统的复杂性
  • SSO单点登陆教程(二)单点登陆流程分析
  • SSO单点登陆教程(三)单点注销流程分析

3、环境要求

  • JDK1.7+
  • Maven3.3
  • Eclipse/IDEA

4、准备工做

由于咱们主要讲的是跨域的单点登陆,因此咱们须要把不一样项目部署到不一样域名下.不可能为了完成这个代码,让同窗们去阿里云买三台主机,映射三个IP.因此咱们的实验就在本机来实现.咱们须要修改host文件,让三个域名映射到本机.
host文件存放的位置:C:\Windows\System32\drivers\etc
打开host文件以后,在最后追加以下配置:

java

127.0.0.1 www.sso.com
127.0.0.1 www.crm.com
127.0.0.1 www.wms.com

这段配置的意思是,咱们在浏览器中输入:
http://www.sso.com
http://www.crm.com
http://www.wms.com
其实访问的都是本机:127.0.0.1



git

PS:有些同窗打开这个文件以后,保存的时候可能被拒绝.缘由多是权限不够.解决方法:把host文件拷贝到桌面(有权限的地方便可),修改好以后再把:C:\Windows\System32\drivers\etc的host文件覆盖.github

5、下载基础项目

基础项目代码下载连接在页面底部.web

我在github上传的是maven结构的项目.若是须要导入到Eclipse/IDEA中须要生成对应的Eclipse/IDEA的配置文件.
cmd命令进入到项目的根目录 $项目存放位置/sso-server-base-project
spring

  • 若是是Eclipse,运行mvn eclipse:eclipse
  • 若是是IDEA,运行mvn idea:idea

处理好以后,把项目导入到工具中,咱们就能够开始开发了.数据库

6、项目结构说明

服务端跨域

sso-server-base-project目录
  src
      main
        java
        resources
           -applicationContext.xml
        webapp
          static
          WEB-INF
              views
                -login.jsp
                -logOut.jsp
              -web.xml
  -pom.xml

服务端项目就只配置了SpringMVC的环境.
pom.xml:项目的pom文件,已经配置的Tomcat插件端口为:8443
applicationContext.xml:spring配置文件
static:静态资源目录,存放css,js
login.jsp:登录页面
logOut.jsp:登出页面
web.xml:web的配置文件,配置前端请求DispatherServlet





浏览器

客户端

sso-client-base-project目录
  src
      main
        java
          -cn.wolfcode.sso.controller.MainServlet.java
          -cn.wolfcode.sso.controller.LogOutServlet.java
        webapp
          WEB-INF
              views
                -main.jsp
              -web.xml

客户端没有使用Spring框架.使用Servlet3.0

@WebServlet(name = "mainServlet", urlPatterns = "/main")

在Servlet类上贴这个注解就能够进行映射.
MainServlet.java:处理主页请求/main的servlet.
LogOutServlet.java:处理登出的请求/logOut的servlet
main.jsp:首页


客户端项目导入以后,运行tomcat7:run命令,在浏览器中输入
http://www.crm.com:8088/main
会看到以下界面:

 

7、执行流程图

咱们代码的开发就参考着单点登陆流程图来实现,因此我在这也把这张图放过来.

8、代码实现

准备阶段:

一:在resources目录建立sso.properties,内容以下:

#统一认证中心的地址
server-url-prefix=http://www.sso.com:8443
#本项目的地址
client-host-url=http://www.crm.com:8088

二:添加工具类.
咱们在后续的开发中须要使用这个工具类,写得比较简单,能够先看看,咱们用到再给同窗们解释啥意思.

SSOClientUtil.java

package cn.wolfcode.sso.util;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SSOClientUtil {
    private static Properties ssoProperties = new Properties();
    public static String SERVER_URL_PREFIX;//统一认证中心地址:http://www.sso.com:8443,在sso.properties配置
    public static String CLIENT_HOST_URL;//当前客户端地址:http://www.crm.com:8088,在sso.properties配置
    static{
        try {
            ssoProperties.load(SSOClientUtil.class.getClassLoader().getResourceAsStream("sso.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        SERVER_URL_PREFIX = ssoProperties.getProperty("server-url-prefix");
        CLIENT_HOST_URL = ssoProperties.getProperty("client-host-url");
    }
    /**
     * 当客户端请求被拦截,跳往统一认证中心,须要带redirectUrl的参数,统一认证中心登陆后回调的地址
     * 经过Request获取此次请求的地址 http://www.crm.com:8088/main
     * 
     * @param request
     * @return
     */
    public static String getRedirectUrl(HttpServletRequest request){
        //获取请求URL
        return CLIENT_HOST_URL+request.getServletPath();
    }
    /**
     * 根据request获取跳转到统一认证中心的地址 http://www.sso.com:8443//checkLogin?redirectUrl=http://www.crm.com:8088/main
     * 经过Response跳转到指定的地址
     * @param request
     * @param response
     * @throws IOException
     */
    public static void redirectToSSOURL(HttpServletRequest request,HttpServletResponse response) throws IOException {
        String redirectUrl = getRedirectUrl(request);
        StringBuilder url = new StringBuilder(50)
                .append(SERVER_URL_PREFIX)
                .append("/checkLogin?redirectUrl=")
                .append(redirectUrl);
        response.sendRedirect(url.toString());
    }    
    /**
     * 获取客户端的完整登出地址 http://www.crm.com:8088/logOut
     * @return
     */
    public static String getClientLogOutUrl(){
        return CLIENT_HOST_URL+"/logOut";
    }
    /**
     * 获取认证中心的登出地址 http://www.sso.com:8443/logOut
     * @return
     */
    public static String getServerLogOutUrl(){
        return SERVER_URL_PREFIX+"/logOut";
    }
}

HttpUtil.java

package cn.wolfcode.sso.util;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.util.StreamUtils;
public class HttpUtil {
    /**
     * 模拟浏览器的请求
     * @param httpURL 发送请求的地址
     * @param params  请求参数
     * @return
     * @throws Exception
     */
    public static String sendHttpRequest(String httpURL,Map<String,String> params) throws Exception{
        //创建URL链接对象
        URL url = new URL(httpURL);
        //建立链接
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        //设置请求的方式(须要是大写的)
        conn.setRequestMethod("POST");
        //设置须要输出
        conn.setDoOutput(true);
        //判断是否有参数.
        if(params!=null&&params.size()>0){
            StringBuilder sb = new StringBuilder();
            for(Entry<String,String> entry:params.entrySet()){
                sb.append("&").append(entry.getKey()).append("=").append(entry.getValue());
            }
            //sb.substring(1)去除最前面的&
            conn.getOutputStream().write(sb.substring(1).toString().getBytes("utf-8"));
        }
        //发送请求到服务器
        conn.connect();
        //获取远程响应的内容.
        String responseContent = StreamUtils.copyToString(conn.getInputStream(),Charset.forName("utf-8"));
        conn.disconnect();
        return responseContent;
    }
    /**
     * 模拟浏览器的请求
     * @param httpURL 发送请求的地址
     * @param jesssionId 会话Id
     * @return
     * @throws Exception
     */
    public static void sendHttpRequest(String httpURL,String jesssionId) throws Exception{
        //创建URL链接对象
        URL url = new URL(httpURL);
        //建立链接
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        //设置请求的方式(须要是大写的)
        conn.setRequestMethod("POST");
        //设置须要输出
        conn.setDoOutput(true);
        conn.addRequestProperty("Cookie","JSESSIONID="+jesssionId);
        //发送请求到服务器
        conn.connect();
        conn.getInputStream();
        conn.disconnect();
    }
}

阶段一:

阶段一代码下载连接在页面底部.
第一阶段咱们先完成,拦截客户端的请求,判断是否有局部会话,没有局部会话就重定向到统一认证中心的登录界面.
需求分析:
咱们要在客户端拦截请求,应该使用啥技术呢?若是使用的是Spring框架,咱们可使用拦截器.但咱们的客户端啥框架都没用.要拦截请求,可使用过滤器Filter.


客户端

建立:SSOClientFilter.java,实现javax.servlet.Filter接口,并贴上Servlet3.0的注解

@WebFilter(filterName="SSOClientFilter",urlPatterns="/*")
public class SSOClientFilter implements Filter {
  ....
}

步骤:
1.判断是否有局部会话
2.若是有局部会话,直接放行
3.若是没有,重定向到统一认证中心的checkLogin方法,检查是否有全局会话.


package cn.wolfcode.sso.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import cn.wolfcode.sso.util.SSOClientUtil;
@WebFilter(filterName="SSOClientFilter",urlPatterns="/*")
public class SSOClientFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {} 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        HttpSession session = req.getSession();
        //1.判断是否有局部的会话
        Boolean isLogin = (Boolean) session.getAttribute("isLogin");
        if(isLogin!=null && isLogin){
            //有局部会话,直接放行.
            chain.doFilter(request, response);
            return;
        }
        //没有局部会话,重定向到统一认证中心,检查是否有其余的系统已经登陆过.
        // http://www.sso.com:8443/checkLogin?redirectUrl=http://www.crm.com:8088
        //这是咱们本身写工具类的方法,同窗们能够本身看一下,很简单能看懂的.
        SSOClientUtil.redirectToSSOURL(req, resp);
    }
    @Override
    public void destroy() {}
}

服务端

步骤:
1.接受重定向过来的checkLogin请求.判断是否有全局的会话
2.若是没有全局会话,获取地址栏的redirectUrl参数,放入到request域中.并转发到登录页面.
3.若是有全局会话,目前还没到这个阶段,这个逻辑咱们先不写.咱们先按执行流程来写代码.


在java目录建立SSOServerController.java,并贴上@Controller注解

@Controller
public class SSOServerController {
}

编写checkLogin方法.

package cn.wolfcode.sso.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
/**
 * Created by wolfcode-lanxw
 */
@Controller
public class SSOServerController {
    /**
     * 检查是否有全局会话.
     * @param redirectUrl 客户端被拦截的请求地址
     * @param session      统一认证中心的会话对象
     * @param model        数据模型
     * @return              视图地址
     */
    @RequestMapping("/checkLogin")
    public String checkLogin(String redirectUrl, HttpSession session, Model model){
        //1.判断是否有全局的会话
        //从会话中获取令牌信息,若是取不到说明没有全局会话,若是能取到说明有全局会话
        String token = (String) session.getAttribute("token");
        if(StringUtils.isEmpty(token)){
            //表示没有全局会话
            model.addAttribute("redirectUrl",redirectUrl);
            //跳转到统一认证中心的登录页面.已经配置视图解析器,
            // 会找/WEB-INF/views/login.jsp视图
            return "login";
        }else{
            //有全局会话
            //目前这段逻辑咱们先不写,按着图解流程编写代码
            return "";
        }
    }
}

测试:

服务端和客户端代码写好以后,两个项目都运行tomcat7:run的命令.
在浏览器地址栏输入:
www.crm.com:8088/main
发现咱们的这个请求被拦截了,跳转到了统一认证中心的登录界面.以下图所示:


 

阶段二:

基础项目代码下载连接在页面底部.

服务端:

步骤:
1.编写登录方法,实现认证功能.
2.认证经过,建立令牌.
3.建立全局会话存储令牌信息
4.把令牌存入到数据库t_token表中.



为了减低学习的难度,咱们这个案例里面就不去链接数据库(固然要链接数据库也不难),咱们的认证就使用静态的认证(帐户名:zhangsan,密码:666).
咱们使用java中的Set集合来模拟t_token表.
建立MockDatabaseUtil.java来模拟数据库

package cn.wolfcode.sso.util;
import java.util.*;
/**
 * Created by wolfcode-lanxw
 */
public class MockDatabaseUtil {
    //模拟数据库中的t_token表
    public static Set<String> T_TOKEN = new HashSet<String>();
}

编写统一认证中心的登录方法,在SSOServerController.java中添加login方法.

/**
     * 登录方法
     * @param username      前台登录的用户名
     * @param password      前台登录的密码
     * @param redirectUrl   客户端被拦截的地址
     * @param session       服务端会话对象
     * @param model         模型数据
     * @return               响应的视图地址
     */
    @RequestMapping("/login")
    public String login(String username,String password,String redirectUrl,HttpSession session,Model model){
        if("zhangsan".equals(username)&&"666".equals(password)){
            //帐号密码匹配
            //1.建立令牌信息,只要保证惟一便可,咱们就使用UUID.
            String token = UUID.randomUUID().toString();
            //2.建立全局的会话,把令牌信息放入会话中.
            session.setAttribute("token",token);
            //3.须要把令牌信息放到数据库中.
            MockDatabaseUtil.T_TOKEN.add(token);
            //4.重定向到redirectUrl,把令牌信息带上.  http://www.crm.com:8088/main?token=
            model.addAttribute("token",token);
            return "redirect:"+redirectUrl;
        }
        //若是帐号密码有误,从新回到登陆页面,还须要把redirectUrl放入request域中.
        model.addAttribute("redirectUrl",redirectUrl);
        return "login";
    }

客户端:

1.统一认证中心登陆成功以后,会重定向到以前客户端被拦截的地址,并会把令牌信息在地址栏中做为参数http://www.crm.com:8088/main?token=VcnVMguCDWJX5zHa
此时访问的是客户端的地址,这个地址会被SSOClientFilter拦截到.
咱们在Filter里面须要判断用户地址栏中是否有携带token信息,若是有,说明拥有令牌信息.可是咱们得校验令牌token的有效性,使用HttpUrlConnection发送请求到统一认证中心进行校验.
2.若是统一认证中心给咱们返回true,表示令牌有效.
3.咱们建立局部会话,并放行请求.



SSOClientFilter.java中添加以下代码

public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        HttpSession session = req.getSession();
        //1.判断是否有局部的会话
        Boolean isLogin = (Boolean) session.getAttribute("isLogin");
        if(isLogin!=null && isLogin){
            //有局部会话,直接放行.
            chain.doFilter(request, response);
            return;
        }
        /**-------------------------阶段二添加的代码start---------------------------------**/
        //判断地址栏中是否有携带token参数.
        String token = req.getParameter("token");
        if(StringUtils.isNoneBlank(token)){
            //token信息不为null,说明地址中包含了token,拥有令牌.
            //判断token信息是否由认证中心产生的.
            //验证地址为:http://www.sso.com:8443/verify
            String httpURL = SSOClientUtil.SERVER_URL_PREFIX+"/verify";
            Map<String,String> params = new HashMap<String,String>();
            //把客户端地址栏添加到的token信息传递给统一认证中心进行校验
            params.put("token", token);
            try {
                String isVerify = HttpUtil.sendHttpRequest(httpURL, params);
                if("true".equals(isVerify)){
                    //若是返回的字符串是true,说明这个token是由统一认证中心产生的.
                    //建立局部的会话.
                    session.setAttribute("isLogin", true);
                    //放行该次的请求
                    chain.doFilter(request, response);
                    return;
                }
            } catch (Exception e) {
                //这里能够完善,好比出现异常在前台显示具体页面
                //咱们这个案例就不作这个哈.
                e.printStackTrace();
            }
        }
        /**-------------------------阶段二添加的代码end---------------------------------**/
        //没有局部会话,重定向到统一认证中心,检查是否有其余的系统已经登陆过.
        // http://www.sso.com:8443/checkLogin?redirectUrl=http://www.crm.com:8088
        SSOClientUtil.redirectToSSOURL(req, resp);
    }

服务端:

1.须要在统一认证中心添加一个认证令牌信息的方法.
SSOServerController.java中添加verifyToken方法,具体代码以下:

/**
     * 校验客户端传过来的令牌信息是否有效
     * @param token 客户端传过来的令牌信息
     * @return
     */
    @RequestMapping("/verify")
    @ResponseBody
    public String verifyToken(String token){
        //在模拟的数据库表t_token中查找是否有这条记录
        if(MockDatabaseUtil.T_TOKEN.contains(token)){
            //说明令牌有效,返回true
            return "true";
        }
        return "false";
    }

测试:

到这里为止,阶段二代码就搞定了.单点登陆功能的95%代码完成.
客户端和服务端都运行tomcat7:run命令
在浏览器中按下Ctrl+Shift+Delete按键清楚cookie和缓存,避免干扰.
在浏览器中输入:http://www.crm.com:8088/main,浏览器跳转到统一认证中心的登录页面.输入zhangsan:666,点击登录.此时就访问到了CRM系统的首页.界面以下.


 

阶段三:

阶段三代码下载连接在页面底部.
在前面的代码咱们完成了单系统的登录,如今咱们先看看若是在多系统的环境下,咱们是否能实现多系统的下一次登录,到处运行的功能.

客户端:

1.拷贝sso-client-base-project项目,命名为sso-client-base-project2
2.修改新项目的pom.xml文件第41行,Tomcat插件的启动端口,修改成:8089
3.修改sso.properties文件,修改以下:

server-url-prefix=http://www.sso.com:8443
client-host-url=http://www.wms.com:8089

4.修改/WEB-INF/views/main.jsp的标题,和内容,主要方便测试的时候看到不一样的效果.(可改可不改)

服务端:

须要完善checkLogin方法,添加若是有全局会话的逻辑.

@RequestMapping("/checkLogin")
    public String checkLogin(String redirectUrl, HttpSession session, Model model){
        //1.判断是否有全局的会话
        //从会话中获取令牌信息,若是取不到说明没有全局会话,若是能取到说明有全局会话
        String token = (String) session.getAttribute("token");
        if(StringUtils.isEmpty(token)){
            //表示没有全局会话
            model.addAttribute("redirectUrl",redirectUrl);
            //跳转到统一认证中心的登录页面.已经配置视图解析器,
            // 会找/WEB-INF/views/login.jsp视图
            return "login";
        }else{
            /**---------------------------阶段三添加的代码start--------------------**/
            //有全局会话
            //取出令牌信息,重定向到redirectUrl,把令牌带上  
            // http://www.wms.com:8089/main?token=
            model.addAttribute("token",token);
            /**---------------------------阶段三添加的代码end-----------------------**/
            return "redirect:"+redirectUrl;
        }
    }

测试:

在服务端和两个客户端运行tomcat7:run命令.
在浏览器中按下Ctrl+Shift+Delete按键清楚cookie和缓存,避免干扰.
在浏览器中输入:http://www.crm.com:8088/main,浏览器跳转到统一认证中心的登录页面.输入zhangsan:666,点击登录.此时就访问到了CRM系统的首页.说明已经登陆成功.
接着浏览器中输入:http://www.wms.com:8089/main,发现此次请求就不须要登录,能够直接访问了.到此为止,咱们就完成单点登陆全部的代码.能够实现一次登录,到处穿梭.


9、单点登陆步骤梳理:

客户端

1.拦截客户端的请求判断是否有局部的session    
    2.1若是有局部的session,放行请求.    
    2.2若是没有局部session        
          2.2.1请求中有携带token参数
                    2.2.1.1若是有,使用HttpURLConnection发送请求校验token是否有效.       
                                  2.2.1.1.1若是token有效,创建局部的session.
                                  2.2.1.1.2若是token无效,重定向到统一认证中心页面进行登录.
                    2.2.1.2若是没有,重定向到统一认证中心页面进行登录.
         2.2.2请求中没有携带token参数,重定向到统一认证中心页面进行登录.

服务端

1.检测客户端在服务端是否已经登陆了.(checkLogin方法)
    1.1获取session中的token.
    1.2若是token不为空,说明服务端已经登陆过了,此时重定向到客户端的地址,并把token带上
    1.3若是token为空,跳转到统一认证中心的的登陆页面,并把redirectUrl放入到request域中.

2.统一认证中心的登陆方法(login方法)
    2.1判断用户提交的帐号密码是否正确.
    2.2若是正确
        2.2.1建立token(可使用UUID,保证惟一就能够)
        2.2.2把token放入到session中,还须要把token放入到数据库表t_token中
        2.2.3这个token要知道有哪些客户端登录了,存入数据库t_client_info表中.);
        2.2.4转发到redirectUrl地址,把token带上.
    2.3若是错误
        转发到login.jsp,还须要把redirectUrl参数放入到request域中.

3.统一认证中心认证token方法(verifyToken方法),返回值为String,贴@ResponseBody
    3.1若是MockDatabaseUtil.T_TOKEN.contains(token)结果为true,说明token是有效的.
        3.1.1返回true字符串.
    3.1若是MockDatabaseUtil.T_TOKEN.contains(token)结果为false,说明token是无效的,返回false字符串.

10、代码下载

0.初始项目Demo

熟悉git命令的同窗:

客户端的基础项目:

git clone git@github.com:javalanxiongwei/sso-client-base-project.git
cd sso-client-base-project/
git reset --hard 8401333ea845eb32e5f6091e7326ada1983d1ea3

服务顿的基础项目:

git clone git@github.com:javalanxiongwei/sso-server-base-project.git
cd sso-server-base-project/
git reset --hard 6334d9afa08b3d5fc886ad212b3ec62376f5ff32

不熟悉git命令的同窗

客户端的基础项目
服务端的基础项目

1.阶段一Demo

熟悉git命令的同窗:

客户端阶段一:

git reset --hard b53e0234895b2044ed3042f8f856676c69160281

服务顿阶段一:

git reset --hard 0ee718f408ef82d230fbc61c63b07b29b1277e45

不熟悉git命令的同窗

客户端阶段一
服务顿阶段一

2.阶段二Demo

熟悉git命令的同窗:

客户端阶段二:

git reset --hard b53e0234895b2044ed3042f8f856676c69160281

服务顿阶段二:

git reset --hard 0ee718f408ef82d230fbc61c63b07b29b1277e45

不熟悉git命令的同窗

客户端阶段二
服务端阶段二

3.阶段三Demo

熟悉git命令的同窗:

客户端2阶段三下载:

git clone git@github.com:javalanxiongwei/sso-client-base-project2.git
cd sso-client-base-project2/
git reset --hard 01db6af390ff9f765121d3f9e9b1895b0e671bd5

服务顿阶段三:

git reset --hard 80e7ad5a1d67b5d63d00e3532fed9ef58fe74fd9

不熟悉git命令的同窗

客户端2阶段三
服务端阶段三

相关文章
相关标签/搜索