• 【转】单点登录实现


    一、单点登录介绍

      单点登录的英文简称为SSO(single sign on),单点登录功能使得用户只要登录了其中一个系统,就可以访问其他相关系统,而不用进行身份验证登录。即用户只要登陆系统一次,该用户的身份信息就可以被系统中的多个主机上的应用所识别,不需要在访问每个应用时再分别进行登陆。

    二、工程目录

      

      该工程采用spring session+redis完成session共享; sso-core中主要是一些sso需要的过滤器和工具类,缓存和session共享的一些XML配置文件,还有springmvc需要的一下jar包的管理。sso-cache中配置了redis缓存策略。

    三、SSO原理图

      

      简单描述:

      用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

      sso认证中心发现用户未登录,将用户引导至登录页面

      用户输入用户名密码提交登录申请

      sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌

      sso认证中心带着令牌跳转会最初的请求地址(系统1)

      系统1拿到令牌,去sso认证中心校验令牌是否有效

      sso认证中心校验令牌,返回有效,注册系统1

      系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源

      用户访问系统2的受保护资源

      系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数

      sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌

      系统2拿到令牌,去sso认证中心校验令牌是否有效

      sso认证中心校验令牌,返回有效,注册系统2

      系统2使用该令牌创建与用户的局部会话,返回受保护资源

    四、SSO实现

      1.SSOFilter.java(sso client 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.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import com.alibaba.fastjson.JSONObject;
    import com.hjz.sso.utils.RestTemplateUtil;
    publicclass SSOFilter implements Filter{
    publicstatic Logger logger = LoggerFactory.getLogger(SSOFilter.class);
    private String SSO_SERVER_URL;
    private String SSO_SERVER_VERIFY_URL;
     @Override
    publicvoid init(FilterConfig filterConfig) throws ServletException {
     SSO_SERVER_URL = filterConfig.getInitParameter("SSO_SERVER_URL");
     SSO_SERVER_VERIFY_URL = filterConfig.getInitParameter("SSO_SERVER_VERIFY_URL");
    if(SSO_SERVER_URL == null) logger.error("SSO_SERVER_URL is null.");
    if(SSO_SERVER_VERIFY_URL == null) logger.error("SSO_SERVER_VERIFY_URL is null.");
     }
     @Override
    publicvoid doFilter(ServletRequest req, ServletResponse res,
     FilterChain chain) throws IOException, ServletException {
     HttpServletRequest request = (HttpServletRequest) req;
     HttpServletResponse response = (HttpServletResponse) res;
    //请求中带有token,去sso-server验证token是否有效 String authority = null;
    if(request.getParameter("token") != null) {
    boolean verifyResult = this.verify(request, SSO_SERVER_VERIFY_URL, request.getParameter("token"));
    if (verifyResult) {
     chain.doFilter(req, res);
    return;
     } else {
     authority = "token->" + request.getParameter("token") + " is invalidate.";
     }
     }
     HttpSession session = request.getSession();
    if (session.getAttribute("login") != null && (boolean)session.getAttribute("login") == true) {
     chain.doFilter(req, res);
    return;
     }
    //跳转至sso认证中心 String callbackURL = request.getRequestURL().toString();
     StringBuilder url = new StringBuilder();
     url.append(SSO_SERVER_URL).append("?callbackURL=").append(callbackURL);
    if(authority != null) {
     url.append("&authority=").append(authority);
     }
     response.sendRedirect(url.toString());
     }
    privateboolean verify(HttpServletRequest request, String verifyUrl, String token) {
     String result = RestTemplateUtil.get(request, verifyUrl + "?token=" + token, null);
     JSONObject ret = JSONObject.parseObject(result);
    if("success".equals(ret.getString("code"))) {
    returntrue;
     }
     logger.error(request.getRequestURL().toString() + " : " + ret.getString("msg"));
    returnfalse;
     }
     @Override
    publicvoid destroy() {
     }
    }
    View Code

      2.LoginController.java(sso server登录controller)

    import java.util.UUID;
    import javax.servlet.http.HttpSession;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    @Controller
    @RequestMapping("sso")
    publicclass LoginController {
    private Logger logger = LoggerFactory.getLogger(LoginController.class);
     @RequestMapping(value="login", method={RequestMethod.GET, RequestMethod.POST})
    public String login(HttpSession session, Model model,
     @RequestParam(value="name", required=false) String name,
     @RequestParam(value="password", required=false) String password) {
    if(name == null && password == null) return "login";
    if("admin".equals(name) && "admin".equals(password)) {
     String token = UUID.randomUUID().toString();
     session.setAttribute("login", true);
     session.setAttribute("token", token);
    return "index";
     } else {
     model.addAttribute("error", true);
     model.addAttribute("message", "用户名或密码错误。");
    return "login";
     }
     }
    }
    View Code

          3.ValidateController.java(sso server验证token controller)

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    import com.alibaba.fastjson.JSONObject;
    @Controller
    @RequestMapping("sso")
    publicclass ValidateController {
     @RequestMapping("verify")
     @ResponseBody
    public JSONObject verify(HttpServletRequest request, @RequestParam String token) {
     HttpSession session = request.getSession();
     JSONObject result = new JSONObject();
    if(session.getAttribute("token") != null && token.equals(session.getAttribute("token"))) {
     result.put("code", "success");
     result.put("msg", "认证成功");
     } else {
     result.put("code", "failure");
     result.put("msg", "token已失效,请重新登录!");
     }
    return result;
     }
    }
    View Code
          4.在sso client工程中加上SSOFilter(web.xml部分配置)
    <filter><filter-name>ssoFilter</filter-name><filter-class>com.hjz.sso.filter.SSOFilter</filter-class><init-param><param-name>SSO_SERVER_URL</param-name><param-value>http://localhost:8088/sso-server/sso/login</param-value></init-param><init-param><param-name>SSO_SERVER_VERIFY_URL</param-name><param-value>http://localhost:8088/sso-server/sso/verify</param-value></init-param></filter><filter-mapping><filter-name>ssoFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
    View Code

      基本模型已经出来了,启动sso-client 和 sso-server(本人都部署到了同一个tomcat下),试图去验证单点登录。测试的时候,从浏览器中的cookie中查看,可以看到 localhost域下有多个JSESSIONID。这也难怪, Tomcat中的每一个application都会创建自己的session会话。那接下来的事情就是解决 session 共享的问题,这样我们就可以完成我们的单点登陆了。

      为完成 session共享,这里推荐两种方案。一个是 tomcat+redis实现session共享,一个是 spring session+redis实现session共享。我这里采用了第二种方案,详情请接着看下面的步骤。

      5.为每个工程的web.xml中增加spring session代理filter的配置

    复制代码
    <!-- session 代理 --> <filter> <filter-name>springSessionRepositoryFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSessionRepositoryFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
    复制代码

      6.在sso-core中加入 缓存和spring session的xml配置(cache-config.xml)

    复制代码
    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd" default-lazy-init="false"> <description>Cache公共配置</description> <bean id="cookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer"> <property name="cookiePath" value="/"></property> </bean> <bean class="com.sso.cache.config.CacheConfig"/> <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="1800"></property> </bean> </beans>
    复制代码

      这里说一下为什么有定义一个 cookieSerializer 这个bean。参看RedisHttpSessionConfiguration的源码,发现它继承了SpringHttpSessionConfiguration,继续查看源码,发现SpringHttpSessionConfiguration中实现了我们配置的spring session代理filter,如下所示。

    复制代码
    SpringHttpSessionConfiguration.java
    @Bean public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter( SessionRepository<S> sessionRepository) { SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) { sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy); } else { sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy); } return sessionRepositoryFilter; }
    复制代码

      查看源码,可以发现 SpringHttpSessionConfiguration使用的默认会话策略(httpSessionStrategy)是CookieHttpSessionStrategy。继续查看CookieHttpSessionStrategy的源码,如新建session写入cookie。

    复制代码
    public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) { Set sessionIdsWritten = getSessionIdsWritten(request); if (sessionIdsWritten.contains(session.getId())) { return; } sessionIdsWritten.add(session.getId()); Map sessionIds = getSessionIds(request); String sessionAlias = getCurrentSessionAlias(request); sessionIds.put(sessionAlias, session.getId()); String cookieValue = createSessionCookieValue(sessionIds); this.cookieSerializer.writeCookieValue(new CookieSerializer.CookieValue(request, response, cookieValue)); }
    复制代码

      cookieSerializer 默认是 DefaultCookieSerializer。查看DefaultCookieSerializer 的 writeCookieValue方法如下。

    复制代码
    public void writeCookieValue(CookieSerializer.CookieValue cookieValue) { HttpServletRequest request = cookieValue.getRequest(); HttpServletResponse response = cookieValue.getResponse(); String requestedCookieValue = cookieValue.getCookieValue(); String actualCookieValue = requestedCookieValue + this.jvmRoute; Cookie sessionCookie = new Cookie(this.cookieName, actualCookieValue); sessionCookie.setSecure(isSecureCookie(request));  sessionCookie.setPath(getCookiePath(request)); String domainName = getDomainName(request); if (domainName != null) { sessionCookie.setDomain(domainName); } if (this.useHttpOnlyCookie) { sessionCookie.setHttpOnly(true); } if ("".equals(requestedCookieValue)) { sessionCookie.setMaxAge(0); } else { sessionCookie.setMaxAge(this.cookieMaxAge); } response.addCookie(sessionCookie); }
    复制代码

      sessionCookie.setPath(getCookiePath(request));这块有一个问题,看一下getCookiePath方法的实现,如下。

    private String getCookiePath(HttpServletRequest request) { if (this.cookiePath == null) { return request.getContextPath() + "/"; } return this.cookiePath; }

      如果要实现单点登录,就不要使用默认的 cookiePath 的值。所以,我定义了一个 cookieSerializer 的bean,并指定了 cookiePath 的值。 SpringHttpSessionConfiguration中如下方法可以自动装配 我们配置的cookieSerializer,而不是使用默认的。

    @Autowired(required = false) public void setCookieSerializer(CookieSerializer cookieSerializer) { this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer); }

       7.在每个工程中的spring公共配置文件中增加如下配置。

    <import resource="classpath*:cache-config.xml"/>

      8.后端之间rest请求传递 session ID。

    复制代码
    private static ResponseEntity<String> request(ServletRequest req, String url, HttpMethod method, Map<String, ?> params) { HttpServletRequest request = (HttpServletRequest) req; //获取header信息 HttpHeaders requestHeaders = new HttpHeaders(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = (String) headerNames.nextElement(); String value = request.getHeader(key); requestHeaders.add(key, value); } HttpEntity<String> requestEntity = new HttpEntity<String>(params != null ? JSONObject.toJSONString(params) : null, requestHeaders); ResponseEntity<String> rss = restTemplate.exchange(url, method, requestEntity, String.class); return rss; }
    复制代码

      使用RestTemplate发送rest请求,发送之前复制request中的header信息,保证session ID可以传递。

      9.最后,启动工程,测试结果如下。

       

    原博地址: https://yq.aliyun.com/ziliao/356277

  • 相关阅读:
    java的原子类 AtomicInteger 实现原理是什么?
    Tomcat性能调优
    JVM性能调优
    vue下载和上传excle数据文件,解析excel文件数据并存在数据库中
    07----popo up 弹窗
    06----fiter
    05 ---批量操作
    04
    stark组件03
    stack组件03
  • 原文地址:https://www.cnblogs.com/hirampeng/p/9671262.html
Copyright © 2020-2023  润新知