redis + vue + springcloud 实现跨域 sso 单点登陆

一、概述

SSO英文全称Single Sign On,单点登陆。SSO是在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统。好比天猫和淘宝,都进入登陆页面,都要求你登陆的,如今你在淘宝处登陆后,直接在天猫处刷新,你会发现,你已经登陆了。vue

二、sso实现原理图

一、本demo原理图

二、原理图2

三、遇到的问题

一、同源策略

同源策略,它是由Netscape提出的一个著名的安全策略。 如今全部支持JavaScript 的浏览器都会使用这个策略。 所谓同源是指,域名,协议,端口相同。 当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面 当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪一个页面的, 即检查是否同源,只有和百度同源的脚本才会被执行。 若是非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。 同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,所以拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,可是没法被浏览器接收。nginx

二、cookie 域

cookie 的域(一般对应网站的域名),浏览器发送 http 请求时会自动携带与该域匹配的 cookie,而不是全部 cookie。redis

解决方案

  • 一、使用 nginx 反向代理将全部服务同源spring

  • 二、认证登陆成功建立全部服务的会话(资源浪费)apache

  • 三、跨域 cookie 重定向带参同步json

    将cookie先放置在一个域下,须要登陆的请求访问这个域获取到该域下的参数时携带参数重定向会原系统域下参考跨域

    【本demo原理图】浏览器

三、跨域请求

因为浏览器安全限制访问非同域下的资源时会拒绝访问 安全

跨域请求解决方案

springboot容许跨域请求springboot

四、redisTemplate 写入redis 值设置过时时间后,获取数据会获得控制字符致使没法转化成Bean对象

不知道什么缘由致使的,求大佬告知

解决方案

替换控制字符

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

参考连接

五、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;
    }
}
复制代码

六、spring 拦截器中没法@Autowired 注入bean

解决方案

初始化拦截器时将拦截器先交由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("/**");
    }
}
复制代码

四、实现过程

一、建立spring boot 集成 redisTemplate 提供redis 服务

一、修改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>
复制代码

cloud 服务统一依赖

<!-- 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>
复制代码

二、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
复制代码

三、修改redisTemplate 默认序列化器

demo中因为使用默认的jdkSerializeable 序列化器遇到没法序列化问题因此更换序列化器

StringRedisSerializer 只在 byte 与 String 之间转化

@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;
    }
}
复制代码

四、建立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";
    }


}
复制代码

二、建立sso统一认证中心

一、登陆方法实现

因为vue 与 认证中心再也不同域下 cookie 没法共享因此 token 放置在参数中直接返回

/**
     * 登陆
     *
     * @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;
        }
    }
复制代码

三、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
};
复制代码

将认证中心返回的token写入到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>

复制代码

四、系统A、系统B 拦截器代码

config初始化拦截器代码

**
 * 拦截器初始化配置
 */
@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;
    }
}
复制代码

五、实现效果

一、系统1

二、系统2

因为系统1已经登陆直接跳转登陆成功

相关文章
相关标签/搜索