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已經登陸直接跳轉登陸成功

相關文章
相關標籤/搜索