一篇了解SSO单点登录

导读:本篇文章讲解 一篇了解SSO单点登录,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

SSO基础

1.什么是单点登录?

单点登录的英文名叫做:Single Sign On(简称SSO)。

在初学/以前的时候,一般我们就单系统,所有的功能都在同一个系统上。
在这里插入图片描述
后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。
在这里插入图片描述
比如阿里系的淘宝天猫,很明显地我们可以知道这是两个系统,但是你在使用的时候,登录了天猫,淘宝也会自动登录。
在这里插入图片描述

简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录,只要在一个业务中退出,所有系统都退出

2.回顾普通系统登录

众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。

如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”

HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是乎:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户

所以,一般我们单系统实现登录会这样做:

登录:将用户信息保存在Session对象中

  • 如果在Session对象中能查到,说明已经登录
  • 如果在Session对象中查不到,说明没登录(或者已经退出了登录)
    注销(退出登录):从Session中删除用户的信息
    记住我(关闭掉浏览器后,重新打开浏览器还能保持登录状态):配合Cookie来用

在这里插入图片描述

3.多系统登录的问题与解决?

3.1.Session不共享问题

单系统登录功能主要是用Session保存用户信息来实现的,但我们清楚的是:多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是不共享的。

在这里插入图片描述

解决系统之间Session不共享问题有一下几种方案:

  • Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能呢,不建议
  • 根据请求的IP进行Hash映射到对应的机器上(这就相当于请求的IP一直会访问同一个服务器)【如果服务器宕机了,会丢失了一大部分Session的数据,不建议】
  • 把Session数据放在Redis中(使用Redis模拟Session)【建议】

我们可以将登录功能单独抽取出来,做成一个子系统。
在这里插入图片描述
总结:

  • SSO系统生成一个token,并将用户信息存到Redis中,并设置过期时间
  • 其他系统请求SSO系统进行登录,得到SSO返回的token,写到Cookie中
  • 每次请求时,Cookie都会带上,拦截器得到token,判断是否已经登录

到这里,其实我们会发现其实就两个变化:

  • 将登陆功能抽取为一个系统(SSO),其他系统请求SSO进行登录
  • 本来将用户信息存到Session,现在将用户信息存到Redis

XXL-SSO框架基础入门

1.什么是XXL-SSO

XXL-SSO 是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。 拥有”轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持”等特性。现已开放源代码,开箱即用。

我们先登录XXL-SSO官网:https://www.xuxueli.com/xxl-sso/

在这里插入图片描述

2.特性

1、简洁:API直观简洁,可快速上手
2、轻量级:环境依赖小,部署与接入成本较低
3、单点登录:只需要登录一次就可以访问所有相互信任的应用系统
4、分布式:接入SSO认证中心的应用,支持分布式部署
5、HA:Server端与Client端,均支持集群部署,提高系统可用性
6、跨域:支持跨域应用接入SSO认证中心
7、Cookie+Token均支持:支持基于Cookie基于Token两种接入方式,并均提供Sample项目
8、Web+APP均支持:支持Web和APP接入
9、实时性:系统登陆、注销状态,全部Server与Client端实时共享
10、CS结构:基于CS结构,包括Server”认证中心”Client”受保护应用
11、记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期
12、路径排除:支持自定义多个排除路径,支持Ant表达式,用于排除SSO客户端不需要过滤的路径

3. 官方Demo分析

首先我们从Github 克隆XXL-SSO的源码到本地(https://github.com/xuxueli/xxl-sso.git):
在这里插入图片描述
下载完源码,我们可以看到目录结构如下:
在这里插入图片描述

3.1 SSO Server中央认证服务

打开xxl-sso-server目录,可以看到有如下结构:
在这里插入图片描述
他们分别表示:
在这里插入图片描述
打开xxl-sso-server的配置文件,可以看到需要配置Redis地址,在这里配置好Redis地址:
在这里插入图片描述
启动xxl-sso-server
在这里插入图片描述

日志文件的位置!

可以看到启动成功:
在这里插入图片描述

3.2 SSO Client应用(Cookie形式)

SSO 认证中心已经配置好并打开了,下面我们来看看SSO Client端。

打开samples下的xxl-sso-web-sample-springboot项目,并配置redis路径(与认证中心的一致):
在这里插入图片描述
在上图可以看到xxl.sso.server对应的值为:http://xxlssoserver.com:8080/xxl-sso-server,这里用到了域名,所以要在我们本地localhost文件里配置域名
在这里插入图片描述

启动成功:
在这里插入图片描述
浏览器输入:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot
在这里插入图片描述
可以看到自动跳转到了SSO 认证服务中心的登录页面了,url地址变为如下,可以看到携带了一个redirect_url,指的就是登录成功后重定向的地址:

http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/

为了更好的验证单点登录,我们复制xxl-sso-web-sample-springboot项目命名为xxl-sso-web-sample-springboot8083,并设置端口号为8083
在这里插入图片描述

并在hosts文件增加配置:

在这里插入图片描述启动复制的项目

好了,可以开始验证了。首先浏览器输入Client1服务地址:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot,会自动跳转到授权中心:
在这里插入图片描述
点击登录,可看到登录成功,而且登录成功后的sessionid在地址栏也能看到。
在这里插入图片描述
接下来看看Client2是否需要再次登录,浏览器输入:http://xxlssoclient2.com:8083/xxl-sso-web-sample-springboot

可以看到Client2也登录成功了,而且sessionid与Client1的一样。

最后,我们看看浏览器的Cookie信息,观察发现他们的sessionid也是一致的:

clinent1
在这里插入图片描述
client2
在这里插入图片描述
打开Redis可视化窗口,可以看到Redis服务器有保存SessionId:
在这里插入图片描述

4.总结

本文主要讲解了单点登录的相关概念,已经使用xxl-sso框架来做演示。

集成SSO服务

引言

主要讲解了SSO单点登录的一些概念,以及使用国产的XXL-SSO单点登录例子来熟悉了单点登录的整个流程。

本文将把XXL-SSO框架集成到我们的项目中,本文先集成SSO 认证服务。

1. 集成xxl-sso-core

本来我是不打算把xxl-core集成到电商项目的,阅读文档里也没发现有最新的版本发布到仓库,只是更新了代码。远程maven仓库最新的版本为1.1.0,而代码最新版本为1.1.1了,如下图:
在这里插入图片描述
所以我打算把xxl-sso-core最新的代码直接复制到我们的项目使用。

首先在电商项目通用模块里添加xxl-core模块:
在这里插入图片描述
把xxl-core源码复制过去,包括maven依赖:
在这里插入图片描述
复制成功,没报错。

2. 集成xxl-server

在基础设施包里新增xxl-sso-server:
在这里插入图片描述
添加xxl-core的maven依赖:

<dependency>
    <groupId>com.guoranxinxian</groupId>
    <artifactId>guoranxinxian-shop-common-xxlsso-core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

 <!-- freemarker -->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-freemarker</artifactId>
 </dependency>

复制代码和resources里面的内容:
在这里插入图片描述
修改配置文件:

### web
server.port=8099
#server.servlet.context-path=/xxl-sso-server

### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########

### xxl-sso
xxl.sso.redis.address=redis://127.0.0.1:6379
xxl.sso.redis.expire.minute=1440
eureka.client.service-url.defaultZone=http://127.0.0.1:8080/eureka

spring.application.name=guoranxinxian-shop-basics-xxlsso-server

启动类增加@EnableEurekaClient注解,启动注册中心,和SSO Server:

package com.xxl.sso.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;


@SpringBootApplication
@EnableEurekaClient
public class XxlSsoServerApplication {

	public static void main(String[] args) {
        SpringApplication.run(XxlSsoServerApplication.class, args);
	}
}

在这里插入图片描述
浏览器输入地址:http://localhost:8099/,会自动跳转到认证授权中心登录页面

在这里插入图片描述
点击Login,登录成功:
在这里插入图片描述

总结

本文主要讲解集成SSO认证服务。

改造SSO登录界面

引言

在上一篇主要讲解了如何集成SSO认证中心,集成成功后,登录界面和登录成功界面如下图所示:

登录
在这里插入图片描述
登录成功
在这里插入图片描述
但是这个登录和主界面并不是我们想要的,本文先来来讲解如何改造登录界面。

注意:我在hosts文件里添加了如下内容,之后的博客都用这些域名:
在这里插入图片描述

1. 效果图

下面先贴上效果图(主界面先暂时替代,涉及其它的知识点,下篇博客继续完善):

登录界面
在这里插入图片描述

登录成功界面
在这里插入图片描述

2. 登录界面代码(前端+后台)

先贴上前端代码(核心代码,注意里面携带了redirect_url,隐藏起来了),改造原来自带的登录页面

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="shortcut icon" href="/static/img/page-common/favicon.ico" type="image/x-icon" />
    <title>果然新鲜 - 登录</title>
    <link rel="stylesheet" type="text/css" href="/static/css/page-common.css" />
    <link rel="stylesheet" type="text/css" href="/static/css/page-login.css" />
    <link rel="stylesheet" type="text/css" href="/static/css/page-login-header.css" />
</head>

<body>
<!-- 网页头部开始 -->
<script type="text/javascript" src="/static/js/page-login-header.js" charset="UTF-8"></script>
<!-- 网页头部结束 -->

<!-- 网页主体开始 -->
<div class="fresh-main-fluid" style="width: 100%;height:100%;background:#2663b6;">
    <div class="fresh-main fresh-center fresh-clearfix">
        <div class="fresh-body-1">
            <div class="fresh-img"> <img src="/static/img/page-login/bg.png" /> </div>
            <div class="fresh-loginbox">
                <h2>账号登录<span style="color: red">${error!''}</span></h2>
                <form action="doLogin" method="post">
                    <div class="fresh-loginbox-text"> <p>手机号</p>
                        <div> <img src="/static/img/page-login/denglu.png" />
                            <input type="text" name="mobile" value="${(loginVo.mobile)!''}" id="mobile" placeholder="请输入手机号码" />
                        </div>
                    </div>
                    <div class="fresh-loginbox-text"> <p>密码</p>
                        <div> <img src="/static/img/page-login/mima.png" />
                            <input type="password" name="password" id="password" value="${(loginVo.password)!''}" placeholder="请输入密码" />
                        </div>
                    </div>
                    <div class="fresh-loginbox-text"> <p>验证码</p>
                        <div> <img src="/static/img/page-login/mima.png" />
                            <input type="text" name="graphicCode" id="graphicCode" placeholder="请输入验证码" />
                            <img src="/getVerify" style="width: 80px;" id="getverification" onclick="getVerify(this);"/>
                        </div>
                    </div>
                    <div class="fresh-login-forget"> <a href="forget.html">忘记密码</a> </div>
                    <div class="fresh-login-submit">
                        <input type="hidden" name="redirect_url" value="${RequestParameters['redirect_url']!''}" />
                        <input type="submit" value="登录" />
                    </div>
                    <div class="fresh-login-thirdlogin"> <a href="#">——&nbsp;&nbsp;第三方登录&nbsp;&nbsp; ——</a> </div>
                    <div class="fresh-login-loginmode">
                        <div> <a href="/qqAuth"> <img src="/static/img/page-login/qq.png" /> </a>
                            <a href="#"> <img src="/static/img/page-login/weixin.png" /> </a>
                            <a href="#"> <img src="/static/img/page-login/weibo.png" /> </a>
                        </div>
                    </div>
                    <div class="fresh-login-Register"> <a href="register.html">立即注册</a> </div>
                </form>
            </div>
        </div>
    </div>
</div>
<!-- 网站主体结束 -->

<!-- 网页底部开始 -->
<script type="text/javascript" src="/static/js/page-footer.js" charset="UTF-8"></script>
<!-- 网页底部结束 -->

<script type="text/javascript" src="/static/plugins/jquery/jquery-1.12.4.min.js"></script>

<script>
    //获取验证码
    function getVerify(obj) {
        obj.src = "getVerify?" + Math.random();
    }

</script>

</body>
</html>

WebController层代码(现在业务系统查询用户是否存在,然后使用XXL-SSO框架登录):

package com.xxl.sso.server.controller;

import com.guoranxinxian.api.BaseResponse;
import com.guoranxinxian.common.base.BaseWebController;
import com.guoranxinxian.common.util.RandomValidateCodeUtil;
import com.guoranxinxian.common.util.WebBeanUtils;
import com.guoranxinxian.constants.Constants;
import com.guoranxinxian.member.dto.input.UserLoginInDTO;
import com.guoranxinxian.member.dto.output.UserLoginInOutDTO;
import com.xxl.sso.core.conf.Conf;
import com.xxl.sso.core.login.SsoWebLoginHelper;
import com.xxl.sso.core.store.SsoLoginStore;
import com.xxl.sso.core.store.SsoSessionIdHelper;
import com.xxl.sso.core.user.XxlSsoUser;
import com.xxl.sso.server.controller.req.vo.LoginVo;
import com.xxl.sso.server.feign.MemberLoginServiceFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;

@Controller
public class WebController extends BaseWebController {

    /**
     * 跳转到登陆页面页面
     */
    private static final String MB_LOGIN_FTL = "login";

    @Autowired
    private MemberLoginServiceFeign memberLoginServiceFeign;
    /**
     * 重定向到首页
     */
    private static final String REDIRECT_INDEX = "redirect:/";

    @RequestMapping("/")
    public String index(Model model, HttpServletRequest request, HttpServletResponse response) {
        XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);
        if (xxlUser == null) {
            return "redirect:/login";
        } else {
            model.addAttribute("xxlUser", xxlUser);
            return "index";
        }
    }

    @RequestMapping(Conf.SSO_LOGIN)
    public String login(Model model, HttpServletRequest request, HttpServletResponse response) {
        // login check
        XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);
        if (xxlUser != null) {
            // success redirect
            String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
            if (redirectUrl!=null && redirectUrl.trim().length()>0) {

                String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request);
                String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;;

                return "redirect:" + redirectUrlFinal;
            } else {
                return "redirect:/";
            }
        }
        model.addAttribute("errorMsg", request.getParameter("errorMsg"));
        model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
        return "login";
    }


    /**
     * 接受请求参数
     *
     * @return
     */
    @PostMapping("/doLogin")
    public String postLogin(@ModelAttribute("loginVo") @Validated LoginVo loginVo,
                            BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes, HttpServletRequest request,
                            HttpServletResponse response, HttpSession httpSession, String ifRemember) {
        if (bindingResult.hasErrors()) {
            // 如果参数有错误的话
            // 获取第一个错误!
            String errorMsg = bindingResult.getFieldError().getDefaultMessage();
            setErrorMsg(model, errorMsg);
            return MB_LOGIN_FTL;
        }
        // 1.图形验证码判断
        String graphicCode = loginVo.getGraphicCode();
        if (!RandomValidateCodeUtil.checkVerify(graphicCode, httpSession)) {
            setErrorMsg(model, "图形验证码不正确!");
            return MB_LOGIN_FTL;
        }
        // 2.将vo转换dto调用会员登陆接口
        UserLoginInDTO userLoginInpDTO = WebBeanUtils.voToDto(loginVo, UserLoginInDTO.class);
        userLoginInpDTO.setLoginType(Constants.MEMBER_LOGIN_TYPE_PC);
        String info = webBrowserInfo(request);
        userLoginInpDTO.setDeviceInfor(info);
        BaseResponse<UserLoginInOutDTO> login = memberLoginServiceFeign.ssoLogin(userLoginInpDTO);
        if (!isSuccess(login)) {
            setErrorMsg(model, login.getMsg());
            return MB_LOGIN_FTL;
        }
        UserLoginInOutDTO data = login.getData();
        XxlSsoUser xxlUser = new XxlSsoUser();
        xxlUser.setUserid(data.getToken());
        xxlUser.setUsername(data.getUserName());
        xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", ""));
        xxlUser.setExpireMinute(SsoLoginStore.getRedisExpireMinute());
        xxlUser.setExpireFreshTime(System.currentTimeMillis());

        // 设置sessionid
        String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);
        // 认证服务登录
        boolean ifRem = (ifRemember != null && "on".equals(ifRemember)) ? true : false;
        SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);

        // 4、return, redirect sessionId
        String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
        if (redirectUrl != null && redirectUrl.trim().length() > 0) {
            String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;
            return "redirect:" + redirectUrlFinal;
        } else {
            return "redirect:/";
        }
    }

    @RequestMapping(Conf.SSO_LOGOUT)
    public String logout(HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) {
        // logout
        SsoWebLoginHelper.logout(request, response);
        redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
        return "redirect:/login";
    }

}

3.总结

本文主要讲解了XXL-SSO认证服务的登录界面改造。

SSO单点登录(Client端集成)

1.首页门户集成SSO Client

1.Maven添加xxl-sso-core模块:

<dependency>
    <artifactId>guoranxinxian-shop-common-xxlsso-core</artifactId>
    <groupId>com.guoranxinxian</groupId>
    <version>1.0-SNAPSHOT</version>
</dependency>

2.配置applicatoin.yml,完整内容如下(注意要在hosts文件里配置好域名):
在这里插入图片描述

3.添加配置文件

spring.redis.hostName=127.0.0.1
spring.redis.port=6379

xxl.sso.logout.path=/logout
xxl.sso.server=http://guoranxinxian.ssoserver.com:8099
xxl-sso.excluded.paths=
package com.guoranxinxian.config;

import com.xxl.sso.core.conf.Conf;
import com.xxl.sso.core.filter.XxlSsoWebFilter;
import com.xxl.sso.core.util.JedisUtil;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class XxlSsoConfig implements DisposableBean {


    @Value("${xxl.sso.server}")
    private String xxlSsoServer;

    @Value("${xxl.sso.logout.path}")
    private String xxlSsoLogoutPath;

    @Value("${xxl-sso.excluded.paths}")
    private String xxlSsoExcludedPaths;

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String port;


    @Bean
    public FilterRegistrationBean xxlSsoFilterRegistration() {

        // xxl-sso, redis init
        JedisUtil.init(String.format("redis://%s:%s", redisHost, port));

        // xxl-sso, filter init
        FilterRegistrationBean registration = new FilterRegistrationBean();

        registration.setName("XxlSsoWebFilter");
        registration.setOrder(1);
        registration.addUrlPatterns("/*");
        registration.setFilter(new XxlSsoWebFilter());
        registration.addInitParameter(Conf.SSO_SERVER, xxlSsoServer);
        registration.addInitParameter(Conf.SSO_LOGOUT_PATH, xxlSsoLogoutPath);
        registration.addInitParameter(Conf.SSO_EXCLUDED_PATHS, xxlSsoExcludedPaths);

        return registration;
    }

    @Override
    public void destroy() throws Exception {

        // xxl-sso, redis close
        JedisUtil.close();
    }

}

2. 聚合支付门户集成SSO Client

创建聚合支付门户模块guoranxinxian-shop-portal-pay-web,具体的代码不再详述,可以clone代码下来看,SSO Client方式与上面一样:

在这里插入图片描述

3. 测试

1.启动Eureka服务、SSO认证服务、会员服务门户服务聚合支付服务`。
在这里插入图片描述
2.浏览器访问门户服务(注意:hosts文件已经配置了域名)http://guoranxinxian.com:8080/,浏览器自动跳转到登录界面:
在这里插入图片描述
3.输入登录信息,执行登录操作,登录成功,可以看到登录成功后,地址栏的url也发生改变了http://guoranxinxian.com:8080/?xxl_sso_sessionid=27_c11ef89924a4465cbf395bfefcafc63d

在这里插入图片描述

同时,看下cookie信息,也把session id自动写入了浏览器的cookie:
在这里插入图片描述

4.访问聚合支付门户http://guoranxinxian.pay.com:8079/,可以看到直接就跳转到了聚合支付的首页了,而且浏览器的Session id与门户服务的session id一样:
在这里插入图片描述

4.显示登录的用户信息

     @GetMapping("/")
    public String index(HttpServletRequest request, HttpServletResponse response, Model model){
        XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);
        if (xxlUser != null && StringUtils.isNotEmpty(xxlUser.getUserid())) {
            DataResults<Users> results = usersFeign.getByUserId(Long.valueOf(xxlUser.getUserid()));
            if(results.getData()!=null){
                String mobile = results.getData().getMobile();
                // 对手机号码实现脱敏
                String desensMobile = mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
                model.addAttribute("desensMobile", desensMobile);
            }
        }

        model.addAttribute("goods_fresh_fruits",itemServiceFeign.findGoodsByCategory1Id(1001).getData()); // 新鲜水果 1001
        model.addAttribute("goods_fresh_fish",itemServiceFeign.findGoodsByCategory1Id(1038).getData()); // 海鲜水产 1038

        List<Content> content_top= (List<Content>) redisTemplate.opsForValue().get("redis_content_top");
        if(content_top==null||content_top.size()==0){
            content_top=contentServiceFeign.findContentBycategoryId(1).getData();
            redisTemplate.opsForValue().set("redis_content_top",content_top,3, TimeUnit.MINUTES);  //3分刷新缓存
        }
        model.addAttribute("content_top",content_top); // 轮播图

        model.addAttribute("content_fresh_fruits",contentServiceFeign.findContentBycategoryId(3).getData()); // 新鲜水果主体
        return "index";
    }

在这里插入图片描述

<li th:if="${desensMobile==null}"><a href="login.html">您好,请登录</a></li>
       <li th:if="${desensMobile!=null}"><a href="login.html" th:text="|您好,${desensMobile}|">您好,请登录</a></li>
<li>
<a href="register.html">免费注册</a>
       </li>
       <li>
           <a href="home-order.html">我的订单</a>
       </li>
       <li th:if="${desensMobile!=null}"><a href="javascript:void(0);" onclick="logout();">退出</a></li>
       <li>
           <a href="home-person-footprint.html">我的足迹</a>
       </li>

5.总结

本文主要讲解SSO Client集成与测试。

SSO单点登录(退出登录)

1. 效果演示

首先启动Eureka注册中心、SSO服务、会员服务、门户服务、聚合支付服务
在这里插入图片描述
登录门户,浏览器输入http://guoranxinxian.com:8080,登录成功。
在这里插入图片描述
访问聚合支付门户,浏览器输入:http://guoranxinxian.pay.com:8079/,可以看到没走登录直接就进入了。
在这里插入图片描述
好的,可以看退出效果的演示了,在门户首页点击退出
在这里插入图片描述
点击后,自动跳转到了登录页了:
在这里插入图片描述

刷新聚合支付页面,可以看到也自动跳转到了登录页面了:
在这里插入图片描述

从上面演示效果可以看出:一端退出,所有端都退出。

2.退出功能实现

前端代码(核心代码):

 <!--引入JQuery-->
    <script type="text/javascript" src="plugins/jquery/jquery-1.12.4.min.js"></script>

    <script type="text/javascript">
        function logout() {
            if(confirm("确定退出吗?")){
                $.ajax({
                    type: "delete",
                    //url: "exit",
                    url: "ssoExit",
                    contentType: "application/json",
                    dataType: "json",
                    success: function (result) {
                        if(result.code==200){
                            window.location.href = "/";
                        }
                    },
                    error: function (result) {
                    }
                });
            }
        }
    </script>

Controller层代码:

@RestController
public class LogoutController {

    @DeleteMapping("/ssoExit")
    @ResponseBody
    public DataResults logout(HttpServletRequest request, HttpServletResponse response, Model model) {
        // logout
        XxlSsoUser xxlUser = (XxlSsoUser) request.getAttribute(Conf.SSO_USER);
        SsoWebLoginHelper.logout(request, response);
        return DataResults.success(ResultCode.SUCCESS);
    }
}

退出成功后,可以看到浏览器Cookie信息为空,Redis保存的内容也移除了,数据库更新为未登录。

Cookie
在这里插入图片描述

Redis
在这里插入图片描述

总结

本文主要讲解SSO单点退出的功能。

XXL-SSO登录逻辑

1.XXL-SSO登录逻辑

在这里插入图片描述
代码逻辑描述

  1. 访问pro.com,获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie为空;
  2. 获取用户信息为空,重定向sso服务;
  3. sso服务,获取sso.com域cook’ie为空,获取用户信息为空,跳转登陆页
  4. 登录页输入用户名密码登陆,登陆成功,
1、创建用户对象,
2、创建sessionid(userId_user版本号),
3、response设置cookie,
4、radis设置key(xxl_sso_sessionid,#,usrid组成),用户对象, 失效时间
  1. 重定向pro.com?xxl_sso_sessionid=xxl_sso_sessionid;
  2. 获取pro.com域的cookie(xxl_sso_sessionid,由userId_随机数码组成)为空,从请求参数获取cookie,根据cookie查询raids获取用户对象;
  3. 如果当前时间超过刷新时间一半的时候,重新设置radis数据的有效时间;设置pro.com,cookie值
  4. 跳转请求页面;
  5. 访问pro1.com,获取pro1.com,cookie以及url参数cookie失败,获取对象失败,重定向sso.com服务
  6. sso服务,获取sso.com域cook’ie,根据cookie查询raids获取用户对象
  7. 重定向pro1.com?xxl_sso_sessionid=xxl_sso_sessionid;
  8. 后面逻辑与6,7,8相同
  9. 再次访问pro.com,pro1.com,只需要验证本域下的cookie;

2.XXL-SSO注销逻辑

在这里插入图片描述

代码逻辑

  1. 用户注销pro.com,销毁pro.com下的cookie;重定向sso.com,销毁sso.com下的cookie,删除radis下的用户信息,跳转登录页。
  2. 用户访问pro1.com,从pro1.com下获取cookie,从raids查询用户信息失败,无法返回用户信息登陆失败,重定向sso.com服务,获取sso.com域下cookie失败,从raids查询用户信息失败,跳转登陆页。

cookie可能会受到防跨站请求伪造(CSRF)攻击,token可以解决这个问题

举个CSRF攻击的例子,在网页中有这样的一个链接
(http://bank.com?withdraw=1000&to=tom),假设你已经通过银行的验证并且cookie中存在验证信息,同时银行网站没有CSRF保护。一旦用户点了这个图片,就很有可能从银行向tom这个人转1000块钱。

但是如果银行网站使用了token作为验证手段,攻击者将无法通过上面的链接转走你的钱。(因为攻击者无法获取正确的token)

CSRF攻击

1.CSRF是什么

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

2.CSRF可以做什么

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。

3.CSRF漏洞现状

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI…而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

4.CSRF的原理

下图简单阐述了CSRF攻击的思想:
在这里插入图片描述
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1. 登录受信任网站A,并在本地生成Cookie。
  2. 在不登出A的情况下,访问危险网站B。

看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

  1. 你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。

  2. 你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…)

5.CSRF示例

5.1.示例1:

银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000

危险网站B,它里面有一段HTML的代码如下:

<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块…

为什么会这样呢?

原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前你已经登录了银行网站A,而B中的<img/>以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作…

5.2.示例2:

为了杜绝上面的问题,银行决定改用POST请求完成转账操作。

银行网站A的WEB表单如下:

<form action="Transfer.php" method="POST">
    <p>ToBankId: <input type="text" name="toBankId" /></p>
    <p>Money: <input type="text" name="money" /></p>
    <p><input type="submit" value="Transfer" /></p>
</form>

后台处理页面Transfer.php如下:

<?php    
session_start();    
if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money'])) {       
      buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']);    
 } 
?>

危险网站B,仍然只是包含那句HTML代码:

<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果.....和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。

5.3.示例3:

经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:

<?php
    session_start();
    if (isset($_POST['toBankId'] && isset($_POST['money']))
    {
        buy_stocks($_POST['toBankId'], $_POST['money']);
    }
  ?>

然而,危险网站B与时俱进,它改了一下代码:

<html>
  <head>
<script type="text/javascript">
      function steal()
      {
               iframe = document.frames["steal"];
               iframe.document.Submit("transfer");
      }
    </script>
  </head>

  <body onload="steal()">
    <iframe name="steal" display="none">
      <form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php">
        <input type="hidden" name="toBankId" value="11">
        <input type="hidden" name="money" value="1000">
      </form>
    </iframe>
  </body>
</html>

如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块…因为这里危险网站B暗地里发送了POST请求到银行!

5.4.总结

上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个<img>就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。

CSRF攻击的本质原因

CSRF攻击是源于Web的隐式身份验证机制!Web的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的。CSRF攻击的一般是由服务端解决。

6.CSRF的防御

6.1. 尽量使用POST,限制GET

GET接口太容易被拿来做CSRF攻击,看第一个示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。

当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

6.2.浏览器Cookie策略

IE6、7、8、Safari会默认拦截第三方本地Cookie(Third-party Cookie)的发送。但是Firefox2、3、Opera、Chrome、Android等不会拦截,所以通过浏览器Cookie策略来防御CSRF攻击不靠谱,只能说是降低了风险。

PS:Cookie分为两种,Session Cookie(在浏览器关闭后,就会失效,保存到内存里),Third-party Cookie(即只有到了Exprie时间后才会失效的Cookie,这种Cookie会保存到本地)。

6.3.加验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

6.4.Referer Check

Referer Check在Web最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就极可能是CSRF攻击。

但是因为服务器并不是什么时候都能取到Referer,所以也无法作为CSRF防御的主要手段。但是用Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。

6.5.Anti CSRF Token

现在业界对CSRF的防御,一致的做法是使用一个Token。
例子:

  1. 用户访问某个表单页面。

  2. 服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。

  3. 在页面表单附带上Token参数。

  4. 用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。

这个Token的值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。另外使用Token时应注意Token的保密性,尽量把敏感操作由GET改为POST,以form或AJAX形式提交,避免Token泄露。

6.6.总结

CSRF攻击是攻击者利用用户的身份操作用户帐户的一种攻击方式,通常使用Anti CSRF Token来防御CSRF攻击,同时要注意Token的保密性和随机性。

跨域(CORS)

1.引言

我们在开发过程中经常会遇到前后端分离而导致的跨域问题,导致无法获取返回结果。跨域就像分离前端和后端的一道鸿沟,君在这边,她在那边,两两不能往来.

2.什么是跨域(CORS)

跨域(CORS)是指不同域名之间相互访问。跨域,指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略所造成的,是浏览器对于JavaScript所定义的安全限制策略。

3.什么情况会跨域(CORS)

  • 同一协议, 如http或https
  • 同一IP地址, 如127.0.0.1
  • 同一端口, 如8080

以上三个条件中有一个条件不同就会产生跨域问题。

在这里插入图片描述

4.跨域流程

在这里插入图片描述

参考地址:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

5.解决跨域

配置当次请求允许跨域
在这里插入图片描述

解决方法:在网关中定义“CorsConfig”类,该类用来做过滤,允许所有的请求跨域。

package com.microservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


//配置过滤器,解决跨域问题
@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); //允许任何域名使用
        corsConfiguration.addAllowedHeader("*"); //允许任何头
        corsConfiguration.addAllowedMethod("*"); //允许任何方法(post、get等)
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/94151.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!