SSO英文全稱Single Sign On,單點登陸。SSO是在多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。好比天貓和淘寶,都進入登陸頁面,都要求你登陸的,如今你在淘寶處登陸後,直接在天貓處刷新,你會發現,你已經登陸了。vue
同源策略,它是由Netscape提出的一個著名的安全策略。 如今全部支持JavaScript 的瀏覽器都會使用這個策略。 所謂同源是指,域名,協議,端口相同。 當一個瀏覽器的兩個tab頁中分別打開來 百度和谷歌的頁面 當瀏覽器的百度tab頁執行一個腳本的時候會檢查這個腳本是屬於哪一個頁面的, 即檢查是否同源,只有和百度同源的腳本纔會被執行。 若是非同源,那麼在請求數據時,瀏覽器會在控制檯中報一個異常,提示拒絕訪問。 同源策略是瀏覽器的行爲,是爲了保護本地數據不被JavaScript代碼獲取回來的數據污染,所以攔截的是客戶端發出的請求回來的數據接收,即請求發送了,服務器響應了,可是沒法被瀏覽器接收。nginx
cookie 的域(一般對應網站的域名),瀏覽器發送 http 請求時會自動攜帶與該域匹配的 cookie,而不是全部 cookie。redis
一、使用 nginx 反向代理將全部服務同源spring
二、認證登陸成功建立全部服務的會話(資源浪費)apache
三、跨域 cookie 重定向帶參同步json
將cookie先放置在一個域下,須要登陸的請求訪問這個域獲取到該域下的參數時攜帶參數重定向會原系統域下參考跨域
【本demo原理圖】瀏覽器
因爲瀏覽器安全限制訪問非同域下的資源時會拒絕訪問 安全
springboot容許跨域請求springboot
替換控制字符
replaceAll("[\\x00-\\x09\\x11\\x12\\x14-\\x1F\\x7F]","");
複製代碼
配置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 託管,此時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("/**");
}
}
複製代碼
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>
複製代碼
#redis配置
spring:
redis:
host: xxx.xxx.xxx.xxx
port: 6379
lettuce:
pool:
max-active: 8
max-idle: 8
max-wait: -1ms
min-idle: 0
複製代碼
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;
}
}
複製代碼
@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";
}
}
複製代碼
因爲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全局方法
//設置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>
複製代碼
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已經登陸直接跳轉登陸成功