Redis + vue + springcloud реализуют междоменный единый вход sso

Spring Cloud

1 Обзор

Полное название SSO на английском языке — Single Sign On. SSO заключается в том, что в системах с несколькими приложениями пользователям необходимо войти в систему только один раз, чтобы получить доступ ко всем взаимно доверенным системам приложений. Например, Tmall и Taobao заходят на страницу входа, и оба требуют, чтобы вы вошли в систему. Теперь, после входа в систему на Taobao, вы можете обновить данные непосредственно на Tmall, и вы обнаружите, что уже вошли в систему.

2. Принципиальная схема реализации sso

1. Принципиальная схема этой демонстрации

2. Схема 2

3. Возникшие проблемы

1. Та же политика происхождения

Политика того же происхождения — хорошо известная политика безопасности, предложенная Netscape. Все браузеры, поддерживающие JavaScript, теперь используют эту стратегию. Так называемый гомологичный означает, что доменное имя, протокол и порт совпадают. Когда две вкладки браузера открыты соответственно страницы Baidu и Google Когда вкладка браузера Baidu выполняет скрипт, она проверяет, к какой странице он принадлежит. То есть для проверки того же происхождения, будут выполняться только скрипты с тем же происхождением, что и Baidu. Если он не из того же источника, при запросе данных браузер сообщит об исключении в консоли, указывающем, что доступ запрещен. Политика того же происхождения — это поведение браузера для защиты локальных данных от загрязнения данными, полученными кодом JavaScript, поэтому он перехватывает данные, полученные из запроса, отправленного клиентом, то есть запрос отправляется и сервер отвечает, но не может быть Браузер получает.

2. домен cookie

Домен файла cookie (обычно соответствующий доменному имени веб-сайта). Когда браузер отправляет HTTP-запрос, он автоматически передает файл cookie, соответствующий домену, а не все файлы cookie.

решение

  • 1. Используйте обратный прокси-сервер nginx для подключения всех сервисов к одному источнику.

  • 2. Вход в систему для аутентификации успешно создает сеансы для всех служб (пустая трата ресурсов).

  • 3. Междоменное перенаправление файлов cookie с синхронизацией параметров

    Файл cookie сначала помещается в домен, и запрос, который должен войти в систему, обращается к этому домену, чтобы получить параметры в этом домене.Когда параметры будут получены, перенаправление будет ссылаться на исходный системный домен.

    [Схема этой демонстрации]

3. Междоменный запрос

Доступ к ресурсам, не относящимся к тому же домену, будет запрещен из-за ограничений безопасности браузера.

Решение для запроса из разных источников

springboot разрешает междоменные запросы

4. После того, как redisTemplate запишет значение redis для установки времени истечения срока действия, полученные данные получат управляющие символы и не могут быть преобразованы в объекты Bean.

Я не знаю, чем это вызвано, пожалуйста, дайте мне знать

решение

заменить управляющие символы

replaceAll("[\\x00-\\x09\\x11\\x12\\x14-\\x1F\\x7F]","");

Ссылка на ссылку

5. RedisTemplate сталкивается с проблемой невозможности сериализации с использованием сериализатора jdkSerializeable по умолчанию.

решение

Настройте redisTemplate для использования сериализатора StringRedisSerializer.

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String,String> redisTemplate(){
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

6. Компонент не может быть введен с помощью @Autowired в перехватчике spring.

решение

Когда перехватчик инициализирован, перехватчик сначала управляется Spring, и bean-компонент будет внедрен в перехватчик.


/**
 * 拦截器初始化配置
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    /**
     * 解决拦截器不能注入bean 问题
     * @return
     */
    @Bean
    WebInterceptor WebInterceptor(){
        return new WebInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(WebInterceptor()).addPathPatterns("/**");
    }
}

4. Процесс реализации

1. Создайте интегрированный redisTemplate с весенней загрузкой для предоставления службы redis.

1. Измените pom, чтобы добавить зависимости

pom в основном увеличивает зависимости

   <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

Единая зависимость облачного сервиса

 <!-- Spring Cloud eureka Begin -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!-- zipkin begin-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
    <!-- Spring Cloud eureka End -->
    <!-- config begin-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <!-- admin begin-->
    <dependency>
        <groupId>org.jolokia</groupId>
        <artifactId>jolokia-core</artifactId>
    </dependency>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-client</artifactId>
        <version>${spring-cloud-admin.version}</version>
    </dependency>
    <!--feign Begin-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--feign End-->

    <!-- config获取不到配置时自动重试 begin-->
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- config获取不到配置时自动重试 end-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>

2. application.yml настраивает параметры Redis

#redis配置
spring:
  redis:
    host: xxx.xxx.xxx.xxx
    port: 6379
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1ms
        min-idle: 0

3. Измените сериализатор redisTemplate по умолчанию.

В демонстрации, поскольку используется сериализатор jdkSerializeable по умолчанию, сериализатор не может быть сериализован, поэтому сериализатор заменяется.

StringRedisSerializer преобразует только между байтом и строкой

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String,String> redisTemplate(){
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

4. Создайте сервис redis restfull

@RestController
public class RedisController {
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 取值
     * @param key
     * @return
     */
    @RequestMapping(value = "get")
    public String get(String key){
        String value;
        try {
            value = (String) redisTemplate.opsForValue().get(key);
            if(StringUtils.isNotBlank(value)){
                //替换控制字符
                value = value.replaceAll("[\\x00-\\x09\\x11\\x12\\x14-\\x1F\\x7F]","");
            }
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
        return value;
    }

    /**
     * 写值
     * @param key
     * @param value
     * @param seconds
     * @return
     */
    @RequestMapping(value = "put")
    public String put(String key,String value,@RequestParam(required = false) Long seconds){
        try {
            if (seconds == null){
                redisTemplate.opsForValue().set(key,value);
            }else {
                redisTemplate.opsForValue().set(key,value,seconds);
            }
        }catch (Exception e){
            e.printStackTrace();
            return "ERROR";
        }
        return "OK";
    }


}

2. Создайте единый центр аутентификации sso

1. Реализация метода входа

Поскольку vue и центр аутентификации больше не находятся в одном домене, файл cookie нельзя использовать совместно, поэтому токен помещается в параметр и возвращается напрямую.

 /**
     * 登录
     *
     * @param sysUser
     * @return
     */
    @RequestMapping(value = "login")
    public Map<String, Object> login(@RequestBody SysUser sysUser, HttpServletRequest request, HttpServletResponse response) {
        Map<String, Object> resultMap = new HashMap<>();
        try {
            if (sysUser != null && StringUtils.isNotBlank(sysUser.getUserName()) && StringUtils.isNotBlank(sysUser.getPassword())) {
                SysUser result = sysUserService.getUserByLoginName(sysUser);
                //登录成功
                if (result != null && StringUtils.isNotBlank(result.getPassword()) && sysUser.getPassword().equals(result.getPassword())) {
                    //登录信息存入redis
                    String token = UUID.randomUUID().toString();
                    String userJson = JSON.toJSONString(result);
                    String flag = loginService.redisPut(token, userJson, 60*60*2L);
                    if("ERROR".equals(flag)){
                        throw new RuntimeException("redis调用异常1");
                    }
                    resultMap.put("code", "1");
                    resultMap.put("data", result);
                    //返回token 值
                    resultMap.put("token",token);
                } else {
                    resultMap.put("code", "-1");
                    resultMap.put("message", "用户或密码错误");
                }
            } else {
                resultMap.put("code", "-99");
                resultMap.put("message", "参数错误");
            }
            return resultMap;
        } catch (Exception e) {
            e.printStackTrace();
            resultMap.clear();
            resultMap.put("code", "-999");
            resultMap.put("message", "系统错误稍后重试");
            return resultMap;
        }
    }

3. Код ключа Vue

глобальный метод vue

//设置cookie
Vue.prototype.setCookie = function(c_name,value,expiredays) {
  var exdate=new Date()
  exdate.setDate(exdate.getDate()+expiredays)
  document.cookie=c_name+ "=" +escape(value)+
    ((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
};

//获取cookie
Vue.prototype.getCookie=function(c_name) {
  if (document.cookie.length>0)
  {
    var  c_start=document.cookie.indexOf(c_name + "=")
    if (c_start!=-1)
    {
      c_start=c_start + c_name.length+1
      var c_end=document.cookie.indexOf(";",c_start)
      if (c_end==-1) c_end=document.cookie.length
      return unescape(document.cookie.substring(c_start,c_end))
    }
  }
  return ""
};

//获取url中的参数
Vue.prototype.getUrlKey=function(name) {
    return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ""])[1].replace(/\+/g, '%20')) || null
};

Запишите токен, возвращенный центром аутентификации, в файл cookie.

 //将token 写入cookie
this.setCookie("token",repos.data.token);

ключевой код vue

<template>
    
</template>

<script>
    export default {
      name: "SsoIndex",
      //钩子函数用于同步不同域之间的cookie 同步
      beforeCreate:function () {
        let token = this.getCookie("token");
        let url = this.getUrlKey("redirect");
        //如果有存在token则直接响应给后台
        if(token){
          location.href = url+"?token="+token;
        }
        //否则返回不存在
        else{
          location.href = url+"?token=not";
        }

      }
    }
</script>

<style scoped>

</style>

4. Система A, код перехватчика системы B

код перехватчика инициализации конфига

**
 * 拦截器初始化配置
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    /**
     * 解决拦截器不能注入bean 问题
     * @return
     */
    @Bean
    WebInterceptor WebInterceptor(){
        return new WebInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(WebInterceptor()).addPathPatterns("/**");
    }
}

код перехватчика


/***
 * 未登录请求拦截
 */
@Component
public class WebInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisService redisService;
    /**
     * 未执行请求方法前拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws IOException
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        SysUser sysUser = (SysUser) request.getSession().getAttribute("loginUser");
        //子系统不存在局部会话   尝试获取统一认证中心会话信息
        if(sysUser == null){
            String token = request.getParameter("token");
            //如果没有token到统一认证页获取
            if(StringUtils.isBlank(token)){
                response.sendRedirect("http://localhost:8080/ssoIndex?redirect="+request.getRequestURL());
                return false;
            }
            //如果token 等于not 说明未登录 跳转sso登录
            else if("not".equals(token)){
                response.sendRedirect("http://localhost:8080/login?redirect="+request.getRequestURL());
                return false;
            }
            //根据 token 获取redis 登录数据
            String json = redisService.redisGet(token);
            //token 有效已登录
            if(StringUtils.isNotBlank(json)){
                try {
                    SysUser user = MapperUtils.json2pojo(json,SysUser.class);
                    //创建局部会话信息
                    request.getSession().setAttribute("loginUser",user);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //验证局部会话是否创建完毕
            sysUser = (SysUser) request.getSession().getAttribute("loginUser");
            //没有局部会话说明认证失效跳转sso重新认证
            if(sysUser == null){
                response.sendRedirect("http://localhost:8080/login?redirect="+request.getRequestURL());
                return false;
            }
        }
        return true;
    }
}

5. Осознайте эффект

1. Система 1

2. Система 2

Поскольку система 1 уже зарегистрирована, перейдите непосредственно к успешному входу в систему.