单点登录(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); } }