单点登录(SSO)原理

在整个SSO流程当,有两个流程非常重要:

  第一个是用户没有登录系统到登录系统的过程;

  第二是用户在一个系统当中已经登录(例如在OA系统中登录 了),但又想进入另一个系统(例如进入PRO系统)的过程

一、用户没有登录系统到登录系统的过程:

1:用户通过URL访问OA系统。

 2:在OA系统中的filter发现这个URL没有ticket,此时就会跳转到SSO Server。

 3:SSO Server中的filter发现该客户端中的cookie中没有相应信息,也即是一个没有登录的用户,那么会跳转到登录页面。

 4:用户在登录页面填写相应信息,然后通过post方式提交到SSO Server中。

 5:SSO Server会校验用户信息,同时在cookie中放username。

 6:将生成ticket和username放到JVMCache中,在实际项目应该放到Memcached中,它的用处等下分析。

 7、8:就是在用户访问OA系统的URL基础上加上了一个ticket参数,这样跳转到OA系统。

(此时进入OA系统时,filter发现URL是带ticket的,则filter会根据带过来的ticket并通过HttpClient的形式去调用SSO Server中的TicektServlet,这样就会返回用户名,

  其实这个用户名就是从JVMCache拿到的,同时马上将这个ticket从JVMCache中移除,这样保证一个ticket只会用一次,然后把返回的用户名放到session中)

 9:session中有了用户名,说明用户登录成功了,则会去本应该返问的servlet。

10,11:将OA系统返回的视图给用户。

二、用户已经登录成功了,但要访问另一个系统

 1:用户通过URL访问PRO系统。

 2:在PRO系统中的filter发现这个URL没有ticket,此时就会跳转到SSO Server。此时,由于用户登录了,所以cookie中有相应的信息(例如用户名),此时SSO Server中的filter会生成一个ticket。

 3:将生成的ticket和username放到JVMCache中。

 4:就是在用户访问PRO系统的URL基础上加上了一个ticket参数,这样跳转到PRO系统。

(此时进入PRO系统时,filter发现URL是带ticket的,则filter会根据带过来的ticket并通过HttpClient的形式去调用SSO Server中的TicektServlet,这样就会返回用户名,其实这个用户名就是从JVMCache拿到的,同时马上将这个ticket从JVMCache中移除,这样保证一个ticket只会用一次,然后把返回的用户名放到session中)

 5:session中有了用户名,说明用户登录成功了,则会去本应该返问的servlet。

 6、7:将PRO系统返回的视图给用户。

关键代码:

SSOServer:

SSOServerFilter.java(认证中心的过滤器):

package 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.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import util.JVMCache;

public class SSOServerFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String service = request.getParameter("service");
        //String ticket = request.getParameter("ticket");
        Cookie[] cookies = request.getCookies();
        String username = "";
        //判断用户是否已经登陆认证中心并认证过
        if (cookies!=null) {
            for (Cookie cookie : cookies) {
                if ("SSO".equals(cookie.getName())) {//如果cookie中有"sso",则已生成了认证凭证
                    username = cookie.getValue();
                    System.out.println("扫描cookie中的SSO:"+username);
                    break;
                }
            }
        }
        
        //实现一处登录处处登录
        if (username!=null && !"".equals(username)) {
            System.out.println("从cookie中获取的username:"+username);
            long time = System.currentTimeMillis();
            //生成认证凭据--ticket
            String ticket = username + time;
            JVMCache.TICKET_AND_NAME.put(ticket, username);
            StringBuilder url = new StringBuilder();
            url.append(service);
            if (service.indexOf("?")>=0) {//请求url带了参数
                url.append("&");
            }else{
                url.append("?");
            }
            //返回给用户一个认证的凭据--ticket
            url.append("ticket="+ticket);
            //重定向
            response.sendRedirect(url.toString());
        }else {
            chain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
        
    }

}

LoginServlet.java(验证登录的servlet):

package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import util.JVMCache;

public class LoginServlet extends HttpServlet{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String service = request.getParameter("service");
        //判断用户名和密码是否正确
        if ("admin".equals(username)&&"123456".equals(password)) {//用户名和密码正确
            Cookie cookie = new Cookie("SSO", username);
            cookie.setPath("/");
            response.addCookie(cookie);
            
            long time = System.currentTimeMillis();
            //生成认证凭据--ticket
            String ticket = username+time;
            JVMCache.TICKET_AND_NAME.put(ticket, username);
            
            if (service!=null) {//目的url不为空
                StringBuilder url = new StringBuilder();
                url.append(service);
                if (service.indexOf("?")>=0) {
                    url.append("&");
                }else{
                    url.append("?");
                }
                //返回给用户一个认证的凭据--ticket
                url.append("ticket="+ticket);
                response.sendRedirect(url.toString());
            }else {//如果用户没填跳转目的的url,则返回当前页面
                response.sendRedirect("/sso_server/index.jsp");
            }
        }else{//用户名或者密码错误
            response.sendRedirect("/sso_server/index.jsp?service="+service);
        }
    }
    
}

TicketServlet.java(获取认证凭证的servlet):

package servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import util.JVMCache;

/**
 * HttpClient调用这个Servlet获取username
 * @author 尐蘇
 *
 */
public class TicketServlet extends HttpServlet{

    /**
     * 
     */
    private static final long serialVersionUID = -5580725166413724608L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String ticket = req.getParameter("ticket");
        String username = JVMCache.TICKET_AND_NAME.get(ticket);
        System.out.println("获取令牌username:"+username);
        //保证一个ticket只会用一次
        JVMCache.TICKET_AND_NAME.remove(ticket);
        PrintWriter writer = resp.getWriter();
        writer.println(username);
        writer.close();
    }
    
    
}

JVMCache.java(存储ticket的工具类)

package util;

import java.util.HashMap;
import java.util.Map;

/*
 * Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。
 * 它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。
 * Memcached基于一个存储键/值对的hashmap。其守护进程(daemon )是用C写的,但是客户端可以用任何语言来编写,并通过memcached协议与守护进程通信。
 */


public class JVMCache {
    //存放username,再通过HttpClient获取(在实际项目应该放到Memcached中)
    public static Map<String, String> TICKET_AND_NAME = new HashMap<>();
}

OA系统:

SSOClientFilter.java(客户端过滤器):

package filter;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

public class SSOClientFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        String username = (String) session.getAttribute("username");
        String ticket = request.getParameter("ticket");
        System.out.println("ticket:"+ticket);
        String url = URLEncoder.encode(request.getRequestURL().toString(), "UTF-8");
        System.out.println("username:"+username);
        //判断用户是否已登录OA系统
        if (username == null) {//如果没有username这个参数,说明不是登录请求,不直接放行,最好是在配置的时候不拦截登录请求
            //1.判断用户是否有认证凭据--ticket(认证中心生成)
            if (ticket!=null && !"".equals(ticket)) {//有认证凭据,连接认证中心认证
                CloseableHttpClient httpClient = HttpClients.createDefault();
                HttpPost post = new HttpPost("http://localhost:8085/sso_server/ticket");
                //给url添加新的参数
                List<NameValuePair> params = new ArrayList<>();
                params.add(new BasicNameValuePair("ticket", ticket));
                post.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
                //通过httpClient调用SSO Server中的TicektServlet
                CloseableHttpResponse closeableHttpResponse = httpClient.execute(post);
                //将HTTP方法的响应正文(如果有)返回为String
                HttpEntity entity = closeableHttpResponse.getEntity();
                username = EntityUtils.toString(entity, "UTF-8");
                System.out.println("认证中心返回的username:"+username);
                //释放连接
                closeableHttpResponse.close();
                httpClient.close();
                
                //2.判断认证凭据是否有效
                if (username!=null && !"".equals(username)) {
                    //session设置用户名,说明用户登录成功了
                    session.setAttribute("username", username);
                    chain.doFilter(request, response);
                }else{
                    response.sendRedirect("http://localhost:8085/sso_server/index.jsp?service="+url);
                }
            }else{//第一次访问OA系统,需要到sso-server系统验证
                response.sendRedirect("http://localhost:8085/sso_server/index.jsp?service="+url);
            }
        }else{
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
        
    }

}

OAServlet.java

package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class OAServlet extends HttpServlet{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("WEB-INF/jsp/welcome.jsp").forward(req, resp);
        System.out.println("请求oa系统资源");
    }
    
}

PRO系统:

SSOClientFilter.java:

package filter;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

public class SSOClientFilter implements Filter{

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO Auto-generated method stub
        
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        String username = request.getParameter("username");
        String ticket = request.getParameter("ticket");
        String url = URLEncoder.encode(request.getRequestURL().toString(), "UTF-8");
        System.out.println("用户名:"+username);
        //判断用户是否已登录OA系统
        if (username == null) {//如果没有username这个参数,说明不是登录请求,不直接放行,最好是在配置的时候不拦截登录请求
            //1.判断用户是否有认证凭据--ticket(认证中心生成)
            if (ticket!=null && !"".equals(ticket)) {
                CloseableHttpClient httpClient = HttpClients.createDefault();
                HttpPost post = new HttpPost("http://localhost:8085/sso_server/ticket");
                //给url添加新的参数
                List<NameValuePair> params = new ArrayList<>();
                params.add(new BasicNameValuePair("ticket", ticket));
                post.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
                //通过httpClient调用SSO Server中的TicektServlet
                CloseableHttpResponse closeableHttpResponse = httpClient.execute(post);
                //将HTTP方法的响应正文(如果有)返回为String
                HttpEntity entity = closeableHttpResponse.getEntity();
                username = EntityUtils.toString(entity, "UTF-8");
                //释放连接
                closeableHttpResponse.close();
                httpClient.close();
                
                //2.判断认证凭据是否有效
                if (username!=null && !"".equals(username)) {
                    //session设置用户名,说明用户登录成功了
                    session.setAttribute("username", username);
                    chain.doFilter(request, response);
                }else{
                    response.sendRedirect("http://localhost:8085/sso_server/index.jsp?service="+url);
                }
            }else{//第一次访问OA系统,需要到sso-server系统验证
                response.sendRedirect("http://localhost:8085/sso_server/index.jsp?service="+url);
            }
        }else{
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub
        
    }

}

ProServlet.java

package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PROServlet extends HttpServlet{

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher("WEB-INF/jsp/welcome.jsp").forward(req, resp);
    }
    
}
原文地址:https://www.cnblogs.com/a591378955/p/8489661.html