前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于SpringBoot+JWT+Redis跨域单点登录的实现

基于SpringBoot+JWT+Redis跨域单点登录的实现

作者头像
AI码真香
发布2022-09-13 17:43:49
1.7K0
发布2022-09-13 17:43:49
举报
文章被收录于专栏:AI码真香
一、初识单点登录和JWT

项目中涉及到单点登录,通过各方面了解和学习,本篇就来记录下个人对单点登录的理解和实现;当然对于不同的业务场景,单点登录的实现方式可能不同,但是核心思想应该都是差不多的.....

1.1、什么是单点登录

单点登录SSO(Single Sign On),简单来说,就是多个系统共存的一个大环境中,用户单一位置登录,实现多系统同时登录的一种技术,也就是说,用户的一次登录可以获得其它所有子系统的信任。单点登录在大型网站使用非常频繁,例如阿里巴巴(淘宝)、京东等网站,背后都有成百上千个子系统组成,用户一个操作可能会涉及到几个或更多子系统之间的协作。那你想想,如果每个子系统都需要用户去认证(登录)一次,这种体验是极差的;实现单点登录,实际上就是互相授信的系统之间,解决如何产生和存储这个信任,还有就是如何验证这个信任的有效性(安全);

1.2、什么是Session共享

这个‘含义’是在分布式集群的环境下产生的。也就是摒弃了原系统(Tomcat)提供的Session,而使用自定义的类似Session的机制来保存客户端数据的一种解决方案。具体了解可以浏览这篇文章

https://blog.csdn.net/koli6678/article/details/80144702),下面主要通过几个图来理解一下:

传统的单击web系统,部署简单,但是随着用户访问量的增加(并发),一台服务器明显不能够满足庞大的访问量,这时候可以考虑,应用部署多台服务器,实现负载均衡,也就出现了如下这中场景:

web service副本1/2/3 都是同一个系统,只是分别部署在三台服务器上,这样就可以实现用户分流,减轻原来单台服务器的压力。但是这样会有个问题,比如这样的场景:一个用户访问系统,第一次进入系统所在的服务器(web service副本1),当他刷新页面,这时候负载均衡到服务器(web service副本2)上,但是这个服务器上并没有用户信息(session),系统拦截到就需要重新登录认证。那这样就比较尴尬啦.....那么如何解决呢?考虑实现Session共享(同步),

方式一:

用户首次登录负载在tomcat1上,此时保存用户会话信息,同时同步给其它负载服务器tomcat2/3....。这样,当用户由负载 1 到 负载 2 服务器上,由于负载2同步了客户的会话信息给负载1,此时就不需要再次去登录认证了...

说明

  • 优点:配置简单
  • 缺点:负载的服务器多了,就会出现大量的网络传输,甚至容易引起网络风暴,导致系统崩溃,只能适合少数的负载机器,维护成本较高。

方式二:

这种方式与方式一的区别,主要在于各台服务器不通过session同步的机制,而是将session统一的存储在数据库中(mysql/redis),用户会话信息进行统一管理,实现无状态。

这样每次验证的时候,都去redis中去读取Session信息,如果有则放行,反之则需要去登录认证。登录成功后,redis同步更新用户会话信息。

1.3、Session共享就是单点登录吗?

接着上面,我们接下来再来看一下这个图:

这里的 web ServiceA/B/C ,请注意不等同上面的 web service副本1/2/3,它们是不同的三个系统(子系统);此时如果我们还是通过统一管理session的方式实现Session共享的话。当用户登录A系统后,并不能完成自动切换到B系统,这时候他需要再次在B系统中登录认证才行;原因:系统A 和 系统 B 的session id 不一样了;这就说明一个问题:共享Session 并不是单点登录,他不能解决单点登录的问题,但是单点登录就能解决共享Session的问题!

1.4、了解JWT

JWT(JSON Web Token),官网 。它是一种紧凑且自包含的,用于在多方传递JSON对象的技术。传递的数据可以使用数字签名增加其安全行。可以使用HMAC加密算法或RSA公钥/私钥加密方式。

紧凑: 数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快;

自包含: 使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能;

JWT一般用于处理用户身份验证或数据信息交换;

  • 用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
  • 数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据签名来保证数据的有效性和安全性。

JWT的数据结构是 : A.B.C。 由字符点‘.’来分隔三部分数据。

  • A - header 头信息 数据结构: {“alg”: “加密算法名称”, “typ” : “JWT”} alg是加密算法定义内容,如:HMAC SHA256 或 RSA typ是token类型,这里固定为JWT。
  • B - payload (有效荷载?) 在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。 payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。公开数据和私有数据可以由程序员任意定义。

注意:

即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。

  • C - Signature 签名 签名信息。这是一个由开发者提供的信息。是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据的之前。先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head.加密后的payload。再使用相同的加密算法,对加密后的数据和签名信息进行加密。得到最终结果。

JWT的执行流程

二、实现完全跨域单点登录
2.1、了解什么是跨域

跨域:客户端请求的时候,请求的服务器,不是同一个IP,端口,域名主机名以及请求协议,应当都成为跨域; 域:在应用模型,一个完整的,有独立访问路径的功能集合称为一个域。如:百度称为一个应用或系统。百度下有若干的域,如:搜索引擎(www.baidu.com),百度贴吧(tie.baidu.com),百度知道(zhidao.baidu.com),百度地图(map.baidu.com)等。域信息,有时也称为多级域名。域的划分: 以IP,端口,域名,主机名为标准,实现划分。

2.2、跨域单点登录的实现

先看下图:(图大致画了下,比较粗略)

说明:图中有订单系统(order.demo1.com)、vip系统(vip.demo2.com)等子系统,当用户访问订单系统的时候,先会从cookie中获取token信息,如果token信息是空的,则携带访问的url(redirectURL)和设置客户端cookie的url(setCookieUrl)一同重定向到统一认证中心(sso.demo.com),进行统一登录验证;然后,用户登录成功后,认证中心会生成该用户的token信息,并保存到cookie中,同时也将用户信息存入redis缓存中(key=login:+生成的token,value=用户信息),设置有效时间;信息成功保存后,则重定向到携带过来的(setCookieUrl),redirectUrl和产生的token(作为参数)一并带过去;此时从认证中心来到订单系统,拦截到SetCookie的uri,则去设置cookie,将token信息存入cookie;之后再重定向到携带过来的(redirect_url)同时携带token;

每次访问子系统uri都会对token进行校验(1.判断token是否有效或存在;2.token存在也不一定表示有效,还需要通过模拟http请求,这里使用httpclient post请求 sso认证中心对token信息进行校验);校验成功才放行!

2.3、代码实现

上面巴拉巴拉那么多,可能有的地方还是说得不明白,下面直接来看代码:

首先看一下子系统的过滤器(SsoFilter)

代码语言:javascript
复制
package com.xmlvhy.order.filter;

import com.xmlvhy.order.utils.CookiesUtil;
import com.xmlvhy.order.utils.HttpUtil;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * @ClassName SsoFilter
 * @Description TODO:sso服务过滤器
 * @Author 小莫
 * @Date 2019/04/20 16:51
 * @Version 1.0
 **/
@Slf4j
public class SsoFilter implements Filter {

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

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("============== doFilter ==============");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //获取url
        String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getRequestURI();

        String path = request.getContextPath();
        String redirectURL = request.getParameter("redirect_url");
        if (redirectURL == null) {
            redirectURL = url;
        }
        //本地
        String ssoServerURL = "http://sso.demo.com:8083/ssoAuth";
        String ssoUrl = ssoServerURL + "/auth/preLogin?setCookieURL="
                + request.getScheme() + "://"
                + request.getServerName() + ":" + request.getServerPort()
                + path + "/setCookie&redirect_url=" + redirectURL;

        String token = CookiesUtil.getCookieValue(request, "token");

        log.info("uri================>{}", request.getRequestURI());
        log.info("path=================>{}", path);
        log.info("redirectURL=========>{}", redirectURL);
        log.info("token=========>{}", token);

        if (request.getRequestURI().equals(path + "/logout")) {
            //退出登录
            doLogout(ssoServerURL,token,request,response);
        } else if (request.getRequestURI().equals(path + "/modifyPass")) {
            //修改密码
            doModifyPass(ssoServerURL,ssoUrl,token,path,request,response);
        } else if (request.getRequestURI().equals(path + "/setCookie")) {
            //客户端设置cookie
            doSetCookie(redirectURL,request,response);
        } else if (token != null || token != "") {
            //有Token也未必登录了,有可能token已经过期,通过httpClient请求去获取信息
            doCheckUser(ssoServerURL,ssoUrl,token,request,response,filterChain);
        } else {
            response.sendRedirect(ssoUrl);
            return;
        }
    }

    /**
     *功能描述: 退出登录
     * @Author 小莫
     * @Date 20:38 2019/04/27
     * @Param [ssoServerURL, token, request, response]
     * @return void
     */
    private void doLogout(String ssoServerURL,String token,HttpServletRequest request,HttpServletResponse response) throws IOException   {
        Map<String, Object> LogoutRet = HttpUtil.doPost(ssoServerURL + "/auth/user/logout?token=" + token, null, 4000);
        if (LogoutRet == null || LogoutRet.isEmpty()) {
            log.warn("退出登录出错");
        }
        log.info("退出登录,返回信息:{}", LogoutRet);
        //清楚客户端cookie
        CookiesUtil.deleteCookie(request, response, "token");
        response.sendRedirect(ssoServerURL + "/auth/success/logout");
        return;
    }

    /**
     *功能描述: 修改密码
     * @Author 小莫
     * @Date 20:38 2019/04/27
     * @Param [ssoServerURL, ssoUrl, token, path, request, response]
     * @return void
     */
    private void doModifyPass(String ssoServerURL,String ssoUrl,String token,String path,
                              HttpServletRequest request,HttpServletResponse response) throws IOException {
        if (token != "") {
            //重置密码成功后,会访问主页,此时用户信息已更新则会自动跳转到登录页
            String bk = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/index";
            response.sendRedirect(ssoServerURL + "/auth/modifyPass?token=" + token + "&redirect_url=" + bk);
            return;
        }
        response.sendRedirect(ssoUrl);
        return;
    }

    /**
     *功能描述: 设置客户端cookie
     * @Author 小莫
     * @Date 20:38 2019/04/27
     * @Param [redirectURL, request, response]
     * @return void
     */
    private void doSetCookie(String redirectURL,HttpServletRequest request,HttpServletResponse response) throws IOException {
        CookiesUtil.setCookie(request, response, "token", request.getParameter("token"), 0, true);
        if (redirectURL != null) {
            //跳转到页面
            response.sendRedirect(redirectURL + "?token=" + request.getParameter("token"));
            return;
        }
    }

    /**
     *功能描述: 校验用户信息
     * @Author 小莫
     * @Date 20:39 2019/04/27
     * @Param [ssoServerURL, ssoUrl, token, request, response, filterChain]
     * @return void
     */
    private void doCheckUser(String ssoServerURL,String ssoUrl, String token,
                             HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws IOException, ServletException {
        Map<String, Object> ret = HttpUtil.doPost(ssoServerURL + "/auth/user/info/" + token, null, 4000);
        if (ret == null || ret.isEmpty()) {
            //服务器token或cookie失效,子系统应该也要把cookie清除
            CookiesUtil.deleteCookie(request, response, "token");
            response.sendRedirect(ssoUrl);
            return;
        }
        request.setAttribute("userInfo", ret);
        filterChain.doFilter(request,response);
    }
    @Override
    public void destroy() {
    }
}
代码语言:javascript
复制
package com.xmlvhy.order.config;

import com.xmlvhy.order.filter.SsoFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName FilterConfig
 * @Description TODO 过滤器配置类
 * @Author 小莫
 * @Date 2019/04/20 22:02
 * @Version 1.0
 **/
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean registrationBean(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new SsoFilter());
        bean.addUrlPatterns("/*");
        bean.setName("ssoOrderFilter");
        bean.setOrder(1);
        return bean;
    }
}

工具类:CookiesUtil.java

代码语言:javascript
复制
package com.xmlvhy.order.utils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * @ClassName CookiesUtil
 * @Description TODO
 * @Author 小莫
 * @Date 2019/04/20 16:49
 * @Version 1.0
 **/
public class CookiesUtil {

    /**
     * 得到Cookie的值, 不编码
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName) {
        return getCookieValue(request, cookieName, false);
    }

    /**
     * 得到Cookie的值,
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || cookieName == null) {
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookieList.length; i++) {
                if (cookieList[i].getName().equals(cookieName)) {
                    if (isDecoder) {
                        retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
                    } else {
                        retValue = cookieList[i].getValue();
                    }
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 得到Cookie的值,
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || cookieName == null) {
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookieList.length; i++) {
                if (cookieList[i].getName().equals(cookieName)) {
                    retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
                                 String cookieValue) {
        setCookie(request, response, cookieName, cookieValue, -1);
    }

    /**
     * 设置Cookie的值 在指定时间内生效,但不编码
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
                                 String cookieValue, int cookieMaxage) {
        setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
    }

    /**
     * 设置Cookie的值 不设置生效时间,但编码
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
                                 String cookieValue, boolean isEncode) {
        setCookie(request, response, cookieName, cookieValue, -1, isEncode);
    }

    /**
     * 设置Cookie的值 在指定时间内生效, 编码参数
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
                                 String cookieValue, int cookieMaxage, boolean isEncode) {
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
    }

    /**
     * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
                                 String cookieValue, int cookieMaxage, String encodeString) {
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
    }

    /**
     * 删除Cookie带cookie域名
     */
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
                                    String cookieName) {
        doSetCookie(request, response, cookieName, "", -1, false);
    }

    /**
     * 设置Cookie的值,并使其在指定时间内生效
     *
     * @param cookieMaxage cookie生效的最大秒数
     */
    private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
                                          String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
        try {
            if (cookieValue == null) {
                cookieValue = "";
            } else if (isEncode) {
                cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage > 0)
                cookie.setMaxAge(cookieMaxage);
            if (null != request) {// 设置域名的cookie
                String domainName = getDomainName(request);
                if (!"localhost".equals(domainName)) {
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 设置Cookie的值,并使其在指定时间内生效
     *
     * @param cookieMaxage cookie生效的最大秒数
     */
    private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
                                          String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
        try {
            if (cookieValue == null) {
                cookieValue = "";
            } else {
                cookieValue = URLEncoder.encode(cookieValue, encodeString);
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage > 0)
                cookie.setMaxAge(cookieMaxage);
            if (null != request) {// 设置域名的cookie
                String domainName = getDomainName(request);
                if (!"localhost".equals(domainName)) {
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 得到cookie的域名
     */
    private static final String getDomainName(HttpServletRequest request) {
        String domainName = null;

        // 获取完整的请求URL地址。
        String serverName = request.getRequestURL().toString();
        if (serverName == null || serverName.equals("")) {
            domainName = "";
        } else {
            serverName = serverName.toLowerCase();
            if (serverName.startsWith("http://")){
                serverName = serverName.substring(7);
            } else if (serverName.startsWith("https://")){
                serverName = serverName.substring(8);
            }
            //这里有可能域名只有,例如: jwt.io ,spring.io,那么这样end为-1
            final int end = serverName.indexOf("/");
            if (end != -1) {
                // .test.com  www.test.com.cn/sso.test.com.cn/.test.com.cn  spring.io/xxxx/xxx
                serverName = serverName.substring(0, end);
            }
            final String[] domains = serverName.split("\\.");
            int len = domains.length;
            if (len > 3) {
                //spring boot api 不支持这样的domain格式,当然也可以配置
                //参考:https://blog.csdn.net/doctor_who2004/article/details/81750713
                //domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
                domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
            } else if (len <= 3 && len > 1) {
                //domainName = "." + domains[len - 2] + "." + domains[len - 1];
                domainName = domains[len - 2] + "." + domains[len - 1];
            } else {
                domainName = serverName;
            }
        }

        if (domainName != null && domainName.indexOf(":") > 0) {
            String[] ary = domainName.split("\\:");
            domainName = ary[0];
        }
        return domainName;
    }
}

工具类:HttpUtil.java

代码语言:javascript
复制
package com.xmlvhy.order.utils;

import com.google.gson.Gson;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

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

/**
 * 封装 httpClient get post
 */
public class HttpUtil {
    private static  final Gson gson = new Gson();
    /**
     * get方法
     * @param url
     * @return
     */
    public static Map<String,Object> doGet(String url){

        Map<String,Object> map = new HashMap<>();
        CloseableHttpClient httpClient =  HttpClients.createDefault();
        //设置参数
        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(5000) //连接超时
                .setConnectionRequestTimeout(5000)//请求超时
                .setSocketTimeout(5000)
                .setRedirectsEnabled(true)  //允许自动重定向
                .build();

        HttpGet httpGet = new HttpGet(url);
        httpGet.setConfig(requestConfig);

        try{
           // 获取请求响应结果
           HttpResponse httpResponse = httpClient.execute(httpGet);
           if(httpResponse.getStatusLine().getStatusCode() == 200){
               //这里注意设置默认字节编码格式 为 utf-8
               String jsonResult = EntityUtils.toString(httpResponse.getEntity(),"UTF-8");
               map = gson.fromJson(jsonResult,map.getClass());
           }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                httpClient.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return map;
    }

    /**
     * 封装post
     * @return
     */
    public static Map<String, Object> doPost(String url, String data, int timeout){

        Map<String,Object> map = new HashMap<>();

        CloseableHttpClient httpClient =  HttpClients.createDefault();
        //超时设置
        RequestConfig requestConfig =  RequestConfig.custom().setConnectTimeout(timeout) //连接超时
                .setConnectionRequestTimeout(timeout)//请求超时
                .setSocketTimeout(timeout)
                .setRedirectsEnabled(true)  //允许自动重定向
                .build();

        HttpPost httpPost  = new HttpPost(url);
        httpPost.setConfig(requestConfig);
        httpPost.addHeader("Content-Type","text/html; charset=UTF-8");

        if(data != null && data instanceof  String){ //使用字符串传参
            StringEntity stringEntity = new StringEntity(data,"UTF-8");
            httpPost.setEntity(stringEntity);
        }

        try{
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();
            if(httpResponse.getStatusLine().getStatusCode() == 200){
                String result = EntityUtils.toString(httpEntity,"utf-8");
                map = gson.fromJson(result,map.getClass());
                return map;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                httpClient.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }
}

接下来我们来看看,认证中心SSO的核心代码:

代码语言:javascript
复制
package com.zhly.sso.auth.controller;

import com.zhly.sso.auth.entity.ResponseResult;
import com.zhly.sso.auth.entity.User;
import com.zhly.sso.auth.service.SsoAuthService;
import com.zhly.sso.auth.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 * @ClassName SSOAuthController
 * @Description TODO: sso 认证中心控制器
 * @Author 小莫
 * @Date 2019/04/24 19:29
 * @Version 1.0
 **/
@Controller
@RequestMapping("/auth")
@Slf4j
public class SSOAuthController {

    @Autowired
    private SsoAuthService authService;

    @Autowired
    private UserService userService;

    /**
     *功能描述: 统一登录跳转页面
     * @Author 小莫
     * @Date 16:19 2019/04/25
     * @Param [redirect_url, setCookieURL]
     * @return org.springframework.web.servlet.ModelAndView
     */
    @RequestMapping(value = "login", method = RequestMethod.GET)
    public ModelAndView index(@RequestParam(value = "redirect_url",required = true) String redirect_url,
                              @RequestParam(value = "setCookieURL",required = true) String setCookieURL) {
        ModelAndView mav = new ModelAndView("login");
        mav.addObject("redirect_url", redirect_url);
        mav.addObject("setCookieURL", setCookieURL);
        log.info("login=====> redirectUrl: {}, setCookieUrl: {}", redirect_url, setCookieURL);
        return mav;
    }

    /**
     *功能描述: 预登陆,先获取该用户是否已经登录过,如果登录过则去设置客户端cookie
     * @Author 小莫
     * @Date 15:37 2019/04/25
     * @Param [request, response]
     * @return java.lang.String
     */
    @RequestMapping("preLogin")
    public String preLogin(HttpServletRequest request, HttpServletResponse response) {
        String ssoServerURL = authService.preLogin(request, response);
        return "redirect:" + ssoServerURL;
    }

    /**
     *功能描述: 用户统一登录
     * @Author 小莫
     * @Date 16:09 2019/04/25
     * @Param [username, password, redirect_url, setCookieURL, response, request]
     * @return java.lang.String
     */
    @RequestMapping(value = "login", method = RequestMethod.POST)
    @ResponseBody
    public ResponseResult login(String username, String password,
                     String redirect_url, String setCookieURL,
                     HttpServletResponse response, HttpServletRequest request) {
        return authService.ssoLogin(username,password,redirect_url,setCookieURL,request,response);
    }

    /**
     *功能描述: 获取登录用户的信息(校验)
     * @Author 小莫
     * @Date 16:16 2019/04/25
     * @Param [token]
     * @return com.zhly.sso.auth.entity.ResponseResult
     */
    @PostMapping("/user/info/{token}")
    @ResponseBody
    public Object getLoginUserInfo(@PathVariable("token") String token){
        return authService.checkUserInfo(token);
    }

    /**
     *功能描述: 用户统一登出
     * @Author 小莫
     * @Date 16:19 2019/04/25
     * @Param [token, response, request]
     * @return com.zhly.sso.auth.entity.ResponseResult
     */
    @PostMapping("/user/logout")
    @ResponseBody
    public Map logout(@RequestParam(value = "token",required = true) String token, HttpServletResponse response, HttpServletRequest request) {
        return authService.logout(token,request,response);
    }

    /**
     *功能描述: 统一修改密码页面
     * @Author 小莫
     * @Date 12:31 2019/04/27
     * @Param [token]
     * @return org.springframework.web.servlet.ModelAndView
     */
    @RequestMapping(value = "modifyPass",method = RequestMethod.GET)
    public ModelAndView modifyPass(@RequestParam(value = "token",required = true) String token,
                                   @RequestParam(value = "redirect_url",required = true) String redirect_url){
        ModelAndView mav = new ModelAndView("modifyPass");
        User user = userService.getUserInfo(token);
        mav.addObject("userInfo",user);
        mav.addObject("token",token);
        mav.addObject("redirect_url",redirect_url);
        return mav;
    }

    /**
     *功能描述: 处理统一修改密码
     * @Author 小莫
     * @Date 12:31 2019/04/27
     * @Param [password, username, oldPass]
     * @return com.zhly.sso.auth.entity.ResponseResult
     */
    @RequestMapping(value = "modifyPass",method = RequestMethod.POST)
    @ResponseBody
    public ResponseResult modifyPass(String password,String username,String oldPass,String token,
                                     HttpServletRequest request,HttpServletResponse response){
        return authService.modifyPass(password,username,oldPass,token,request,response);
    }

    /**
     *功能描述: 用户登出成功页面
     * @Author 小莫
     * @Date 16:22 2019/04/25
     * @Param []
     * @return java.lang.String
     */
    @RequestMapping("/success/logout")
    public String allLogout(){
        return "logout";
    }
}
代码语言:javascript
复制
package com.zhly.sso.auth.service.impl;

import com.zhly.sso.auth.constant.CommonConstant;
import com.zhly.sso.auth.entity.ResponseResult;
import com.zhly.sso.auth.entity.User;
import com.zhly.sso.auth.service.SsoAuthService;
import com.zhly.sso.auth.service.UserService;
import com.zhly.sso.auth.utils.CookiesUtil;
import com.zhly.sso.auth.utils.JwtUtil;
import com.zhly.sso.auth.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName SsoAuthServiceImpl
 * @Description TODO
 * @Author 小莫
 * @Date 2019/04/27 19:06
 * @Version 1.0
 **/
@Service
@Slf4j
public class SsoAuthServiceImpl implements SsoAuthService {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisUtil redisUtil;

    @Value("${sso.login_url}")
    private String ssoUrl;

    @Value("${sso.token_expire}")
    private Long expireTime;

    @Override
    public String preLogin(HttpServletRequest request, HttpServletResponse response) {
        log.info("========== 进入 preLogin =======");
        String redirect_url = request.getParameter("redirect_url");
        String setCookieURL = request.getParameter("setCookieURL");
        String token = CookiesUtil.getCookieValue(request,"token");
        String ssoServerURL = ssoUrl + "?setCookieURL="+setCookieURL+"&redirect_url="+redirect_url;
        log.info("preLogin=========> redirectUrl: {}, setCookieUrl: {}, token: {}",redirect_url,setCookieURL,token);
        if (token == null || token == "") {
            //表示未登录过,跳转到登录页面
            return ssoServerURL;
        } else {
            //检验是否有效
            User user = (User) redisUtil.get(CommonConstant.REDIS_PRE__KEY + token);
            if (user != null) {
                //去设置客户端的cookie
                if (setCookieURL != null) {
                    return setCookieURL + "?token=" + token + "&redirect_url=" + redirect_url;
                }
            }
        }
        log.info("preLogin===========> soUrl: {}",ssoServerURL);
        return ssoServerURL;
    }

    @Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
    @Override
    public ResponseResult ssoLogin(String username, String password, String redirect_url, String setCookieURL, HttpServletRequest request, HttpServletResponse response) {
        log.info("===== 进入 ssoLogin ====");

        String ssoServerURL = ssoUrl + "?setCookieURL="+setCookieURL+"&redirect_url="+redirect_url;
        ResponseResult result = userService.login(username, password);

        log.info("ssoLogin =====> result:{}",result);
        Map<String,Object> ret = new HashMap<>();

        if (result.getCode() == 0) {
            //登录成功,生成一个token
            User user = (User) result.getData();
            String token = JwtUtil.generateJWT(user.getId(), user.getUsername());
            //写入cookie
            CookiesUtil.setCookie(request,response,"token",token,0,true);
            //写入redis缓存中,并设置有效时间
            user.setPassword(null);
            redisUtil.set(CommonConstant.REDIS_PRE__KEY + token,user,expireTime);
            String url = setCookieURL + "?token=" + token + "&redirect_url=" + redirect_url;
            ret.put("successURL", url);
            result.setData(ret);
            log.info("ssoLogin =====> successURL: {}",url);
            //登录成功后,sso中心保存token,跳转到客户端去保存token 到 cookie 中
            return result;
        }else{
            //登录失败,跳转到登录页面
            ret.put("failURL",ssoServerURL);
            result.setData(ret);
            log.info("ssoLogin =====> failURL: {}",ssoServerURL);
            return result;
        }
    }

    @Override
    public Object checkUserInfo(String token) {
        log.info("===== 进入 checkUserInfo =====");
        User user = (User) redisUtil.get(CommonConstant.REDIS_PRE__KEY + token);
        log.info("getLoginUserInfo ========> token:{}",token);
        return user;
    }

    @Override
    public Map<String, Object> logout(String token, HttpServletRequest request, HttpServletResponse response) {
        log.info("logout========> token:{}",token);
        // 指定允许其他域名访问
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 响应类型
        response.setHeader("Access-Control-Allow-Methods", "POST");
        // 响应头设置
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type");
        redisUtil.del("TOKEN_" + token);
        //清除cookie
        CookiesUtil.deleteCookie(request, response, "token");

        //封装 httpclient 请求响应消息
        Map ret = new HashMap();
        ret.put("code", 200);
        ret.put("msg", "ok");
        return ret;
    }

    @Override
    public ResponseResult modifyPass(String password, String username, String oldPass, String token, HttpServletRequest request, HttpServletResponse response) {
        ResponseResult ret = userService.modifyUserPwd(password, username, oldPass);
        if (ret.getCode() == 0) {
            //表示成功,这里把原来的缓存和cookie清除
            redisUtil.del(CommonConstant.REDIS_PRE__KEY + token);
            //清除cookie
            CookiesUtil.deleteCookie(request, response, "token");
            log.info("密码修改完成,清除cookie和缓存");
        }
        return ret;
    }
}

SSO认证中心相关工具类:

JwtUtil.java

代码语言:javascript
复制
package com.zhly.sso.auth.utils;

import com.alibaba.fastjson.JSONObject;
import com.zhly.sso.auth.constant.CommonConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName JwtUtil
 * @Description TODO: jwt 工具类,用于生成token,用户授权和信息校验
 * @Author 小莫
 * @Date 2019/04/24 10:37
 * @Version 1.0
 * 参考jwt官网 https://jwt.io
 * jwt 的结构,A.B.C三部分,由字符‘.’分割成三部分数据
 * A-header头信息,内容:{"alg":"HS256","typ","JWT"}
 * B-payload 有效负荷,一般用于记录实体(常用用户信息),分为
 * 三个部分:已注册信息(registered claims)/公开数据(public claims)/私有数据(private claims)
 * 常用信息:iss(发行者)、exp(到期时间)、sub(主体)、aud(受众);由于payload是明文暴露的,推荐不要存放隐私数据
 * C-signature 签名信息:是将header 和 payload进行加密生成的
 **/
@Slf4j
public class JwtUtil {

    /**
     * 功能描述: 签发 JWT ,也就是生成token
     *
     * @return java.lang.String
     * @Author 小莫
     * @Date 11:51 2019/04/24
     * @Param [userId, userName, identities]
     * 用户编号(id)/用户名
     * 格式:A.B.C
     * A-header头信息
     * B-payload 有效负荷
     * C-signature 签名信息 是将header和payload进行加密生成的
     */
    public static String generateJWT(Integer userId, String userName) {
        //签名算法,选择SHA-256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //获取当前系统时间
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        //将BASE64SECRET常量字符串使用base64解码成字节数组
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(CommonConstant.BASE64_SECRET);
        //使用HmacSHA256签名算法生成一个HS256的签名秘钥Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //添加构成JWT的参数
        Map<String, Object> headMap = new HashMap<>();

        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                //加密后的客户编号
                .claim("userId", AESSecretUtil.encryptToStr(String.valueOf(userId), CommonConstant.AES_SECRET_KEY))
                //客户名称
                .claim("userName", userName)
                //Signature
                .signWith(signatureAlgorithm, signingKey);
        //添加Token过期时间
        if (CommonConstant.EXPIRE_TIME >= 0) {
            long expMillis = nowTimeMillis + CommonConstant.EXPIRE_TIME;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /**
     * 功能描述: 签发 JWT ,也就是生成token
     *
     * @return java.lang.String
     * @Author 小莫
     * @Date 11:51 2019/04/24
     * @Param [userId, userName, identities]
     * 用户编号(id)/用户名/客户端信息目前包括浏览器信息,用户客户端拦截校验,防止跨域非法访问
     * 格式:A.B.C
     * A-header头信息
     * B-payload 有效负荷
     * C-signature 签名信息 是将header和payload进行加密生成的
     */
    public static String generateJWT(Integer userId, String userName, String... identities) {
        //签名算法,选择SHA-256
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //获取当前系统时间
        long nowTimeMillis = System.currentTimeMillis();
        Date now = new Date(nowTimeMillis);
        //将BASE64SECRET常量字符串使用base64解码成字节数组
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(CommonConstant.BASE64_SECRET);
        //使用HmacSHA256签名算法生成一个HS256的签名秘钥Key
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
        //添加构成JWT的参数
        Map<String, Object> headMap = new HashMap<>();

        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");
        JwtBuilder builder = Jwts.builder().setHeader(headMap)
                //加密后的客户编号
                .claim("userId", AESSecretUtil.encryptToStr(String.valueOf(userId), CommonConstant.AES_SECRET_KEY))
                //客户名称
                .claim("userName", userName)
                //客户端浏览器信息
                .claim("userAgent", identities[0])
                //Signature
                .signWith(signatureAlgorithm, signingKey);
        //添加Token过期时间
        if (CommonConstant.EXPIRE_TIME >= 0) {
            long expMillis = nowTimeMillis + CommonConstant.EXPIRE_TIME;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate).setNotBefore(now);
        }
        return builder.compact();
    }

    /**
     * 功能描述: 解析JWT
     *
     * @return io.jsonwebtoken.Claims 返回  Claims对象
     * @Author 小莫
     * @Date 11:58 2019/04/24
     * @Param [token] JWT生成的token
     */
    public static Claims parseJWT(String token) {
        Claims claims = null;
        try {
            if (StringUtils.isNotBlank(token)) {
                //解析jwt
                claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(CommonConstant.BASE64_SECRET))
                        .parseClaimsJws(token).getBody();
            } else {
                log.warn("[JWTUtil]-json web token 为空");
            }
        } catch (Exception e) {
            log.error("[JWTUtil]-JWT解析异常:可能因为token已经超时或非法token");
        }
        return claims;
    }
    /**
     *功能描述: 校验 token 是否有效
     * @Author 小莫
     * @Date 12:04 2019/04/24
     * @Param [jsonWebToken] 
     * @return java.lang.String
     * 返回json字符串:
     *  {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}
     *  -freshToken:刷新后的新JWT(token)
     *  -userName: 客户名称
     *  - userId: 客户编号
     *  - userAgent: 客户端浏览器信息
     */
    public static String validateLogin(String token) {
        Map<String, Object> retMap = null;
        Claims claims = parseJWT(token);
        if (claims != null) {
            //解密客户编号
            Integer decryptUserId = Integer.valueOf(AESSecretUtil.decryptToStr(String.valueOf(claims.get("userId")), CommonConstant.AES_SECRET_KEY));
            retMap = new HashMap<>();
            //解密后的客户编号
            retMap.put("userId", decryptUserId);
            //客户名称
            retMap.put("userName", claims.get("userName"));
            //客户端浏览器信息
            retMap.put("userAgent", claims.get("userAgent"));
            //刷新 JWT
            retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));
        }else {
            log.warn("[JWTUtil]-JWT解析出claims为空");
        }
        return retMap != null ? JSONObject.toJSONString(retMap) : null;
    }

    public static void main(String[] args) {
        String token = generateJWT(123, "XiaoMo",
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
        System.out.println("生成的jwt: "+token);
        System.out.println("==========================");
        Claims claims = parseJWT(token);
        System.out.println("claims: "+claims);
        System.out.println("校验后产生的新的jwt: "+ validateLogin(token));
    }
}
三、效果展示
3.1、实现效果截图

这里的 sso.demo.com、vip.demo2.com、order.demo1.com 三个不同的域名都是本地映射模拟的,分别对应SSO认证中心,VIP中心以及订单中心。

方法:修改本地 hosts 文件,我的是win10系统,路径:C:\Windows\System32\drivers\etc

代码语言:javascript
复制
127.0.0.1 sso.demo.com
127.0.0.1 vip.demo2.com
127.0.0.1 order.demo1.com

分别启动 vip、order、sso三个应用:

这时候我们浏览器地址栏,输入:order.demo1.com:8081/index,检测未登录,统一到认证中心进行登录。

登录成功,进入订单中心:

点击vip会员中心,进入vip系统页面:

在订单中心中,点击修改密码,进入修改密码页面:

说明:

在这过程中,只经过一次的登录验证(第一次进入订单中心的时候),当我点击进入vip系统就不需要再次进行登录了。实现了单一地点登录(order.demo1.com),全系统有效。这就实现了完全跨域的单点系统!

当然,你可以尝试配置多个子系统验证。各个子系统配置好 SsoFilter (过滤器)即可,你也可以通过拦截器来实现。

以上便是我开发跨域单点登录的实现方式,当然后续还要进一步考虑,伪装一下url信息、token的安全性等...

源码下载

参考

代码语言:javascript
复制
https://www.jianshu.com/p/023a94df16ea
https://www.cnblogs.com/LUA123/p/10126881.html
以及 尚学堂单点登录教程

本文作者: AI码真香

本文标题: 基于SpringBoot+JWT+Redis跨域单点登录的实现

本文网址: https://www.xmlvhy.com/article/64.html

版权说明: 自由转载-非商用-非衍生-保持署名 署名-非商业性使用4.0 国际 (CC BY-NC 4.0)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、初识单点登录和JWT
    • 1.1、什么是单点登录
      • 1.2、什么是Session共享
        • 1.3、Session共享就是单点登录吗?
          • 1.4、了解JWT
          • 二、实现完全跨域单点登录
            • 2.1、了解什么是跨域
              • 2.2、跨域单点登录的实现
                • 2.3、代码实现
                • 三、效果展示
                  • 3.1、实现效果截图
                  相关产品与服务
                  访问管理
                  访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档