一篇了解SSO单点登录

news/2024/7/5 5:53:45

SSO基础

文章目录

  • SSO基础
    • 1.什么是单点登录?
    • 2.回顾普通系统登录
    • 3.多系统登录的问题与解决?
      • 3.1.Session不共享问题
  • XXL-SSO框架基础入门
    • 1.什么是XXL-SSO
    • 2.特性
    • 3. 官方Demo分析
      • 3.1 SSO Server中央认证服务
      • 3.2 SSO Client应用(Cookie形式)
    • 4.总结
  • 集成SSO服务
    • 引言
    • 1. 集成xxl-sso-core
    • 2. 集成xxl-server
    • 总结
  • 改造SSO登录界面
    • 引言
    • 1. 效果图
    • 2. 登录界面代码(前端+后台)
    • 3.总结
  • SSO单点登录(Client端集成)
    • 1.首页门户集成SSO Client
    • 2. 聚合支付门户集成SSO Client
    • 3. 测试
    • 4.显示登录的用户信息
    • 5.总结
  • SSO单点登录(退出登录)
    • 1. 效果演示
    • 2.退出功能实现
    • 总结
  • XXL-SSO登录逻辑
    • 1.XXL-SSO登录逻辑
    • 2.XXL-SSO注销逻辑
  • CSRF攻击
    • 1.CSRF是什么
    • 2.CSRF可以做什么
    • 3.CSRF漏洞现状
    • 4.CSRF的原理
    • 5.CSRF示例
      • 5.1.示例1:
      • 5.2.示例2:
      • 5.3.示例3:
      • 5.4.总结
    • 6.CSRF的防御
      • 6.1. 尽量使用POST,限制GET
      • 6.2.浏览器Cookie策略
      • 6.3.加验证码
      • 6.4.Referer Check
      • 6.5.Anti CSRF Token
      • 6.6.总结
  • 跨域(CORS)
    • 1.引言
    • 2.什么是跨域(CORS)
    • 3.什么情况会跨域(CORS)
    • 4.跨域流程
    • 5.解决跨域

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);
    }
}


http://lihuaxi.xjx100.cn/news/821435.html

相关文章

XSS Challenges

XSS 挑战 (由 yamagata21) - 阶段 #1 (int21h.jp) 题目要求注入 JavaScript 命令: alert(document.domain); Stage #1 输入321来定位代码的位置,发现是处于<b></b>标签之内,没有任何过滤 // 第一种方法是闭合 b 标签,插入 Script 标签 "</b> <…

《矩阵论》学习笔记

目录线性代数引论线性空间线性变换及矩阵Jordan标准型欧式空间和酉空间最小二乘法矩阵的分解三角分解QR分解正规矩阵及Schur分解满秩分解奇异值分解单纯矩阵的谱分解矩阵的广义逆广义逆矩阵广义逆矩阵AA^AAA^A的几种基本求法广义逆与线性方程组线性方程组的相容性、通解与A{1}A…

9 个视图子类、视图集、 路由系统、认证组件

目录1.9 个视图子类2 视图集2.1 通过ModelViewSet编写5个接口2.3 ViewSetMixin源码分析2.4 from rest_framework.viewsets包下的类2.5 视图层大总结3 路由系统3.1 自动生成路由3.2 action 装饰器4 认证组件4.1 登录接口4.2 认证功能4.3 全局使用4.4 局部使用 1.9 个视图子类 # …

【Leetcode】面试题 08.05. 递归乘法、HJ55 挑7

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 面试题 08.05. 递归乘法 HJ55 挑7 面试题 08.05. 递归乘法 面试题 08.05. 递归乘法 题…

【LeetCode每日一题】【2023/2/6】2331. 计算布尔二叉树的值

文章目录2331. 计算布尔二叉树的值方法1&#xff1a;递归2331. 计算布尔二叉树的值 LeetCode: 2331. 计算布尔二叉树的值 简单\color{#00AF9B}{简单}简单 给你一棵 完整二叉树 的根&#xff0c;这棵树有以下特征&#xff1a; 叶子节点 要么值为 0 要么值为 1 &#xff0c;其中…

[ 2204阅读 ] 真题 - 6

Part 3 真题 Passage The Rise of Florence Paragraph 1 In the Central Middle Ages, starting around 1000 A.D., a number of northern Italian cities, including Venice and Genoa, rode a wave of economic and population growth that saw them become among the most…

【成为架构师课程系列】性能优化技术之“池化技术”:如何减少频繁创建数据库连接的性能损耗?

池化技术:如何减少频繁创建数据库连接的性能损耗? tcpdump 抓包工具 在前面几节课程中,我从宏观的角度带你了解了高并发系统设计的基础知识,你已经知晓了,我们系统设计的目的是为了获得更好的性能、更高的可用性,以及更强的系统扩展能力。 那么从这一讲开始,我们正式…

0基础使用Vite+Vue3创建项目

什么是 Vue&#xff1f; Vue是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;Vue 都可以胜任。 声明式…