SpringCloud實現(xiàn)SSO 單點登錄的示例代碼
前言
作為分布式項目,單點登錄是必不可少的,文本基于之前的的博客(猛戳:SpringCloud系列——Zuul 動態(tài)路由,SpringBoot系列——Redis)記錄Zuul配合Redis實現(xiàn)一個簡單的sso單點登錄實例
sso單點登錄思路:
1、訪問分布式系統(tǒng)的任意請求,被Zuul的Filter攔截過濾
2、在run方法里實現(xiàn)過濾規(guī)則:cookie有令牌accessToken且作為key存在于Redis,或者訪問的是登錄頁面、登錄請求則放行
3、否則,將重定向到sso-server的登錄頁面且原先的請求路徑作為一個參數(shù);response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);
4、登錄成功,sso-server生成accessToken,并作為key(用戶名+時間戳,這里只是demo,正常項目的令牌應(yīng)該要更為復(fù)雜)存到Redis,value值存用戶id作為value(或者直接存儲可暴露的部分用戶信息也行)設(shè)置過期時間(我這里設(shè)置3分鐘);設(shè)置cookie:new Cookie("accessToken",accessToken);,設(shè)置maxAge(60*3);、path("/");
5、sso-server單點登錄服務(wù)負責(zé)校驗用戶信息、獲取用戶信息、操作Redis緩存,提供接口,在eureka上注冊
代碼編寫
sso-server
首先我們創(chuàng)建一個單點登錄服務(wù)sso-server,并在eureka上注冊(創(chuàng)建項目請參考之前的SpringCloud系列博客跟SpringBoot系列——Redis)

login.html
我們這里需要用到頁面,要先maven引入thymeleaf
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
</head>
<body>
<form action="/sso-server/sso/login" method="post">
<input name="url" type="hidden" th:value="${url}"/>
用戶名:<input name="username" type="text"/>
密碼:<input name="password" type="password"/>
<input value="登錄" type="submit"/>
</form>
</body>
</html>
提供如下接口
@RestController
@EnableEurekaClient
@SpringBootApplication
public class SsoServerApplication {
public static void main(String[] args) {
SpringApplication.run(SsoServerApplication.class, args);
}
@Autowired
private StringRedisTemplate template;
/**
* 判斷key是否存在
*/
@RequestMapping("/redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key) {
try {
return template.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 校驗用戶名密碼,成功則返回通行令牌(這里寫死huanzi/123456)
*/
@RequestMapping("/sso/checkUsernameAndPassword")
private String checkUsernameAndPassword(String username, String password) {
//通行令牌
String flag = null;
if ("huanzi".equals(username) && "123456".equals(password)) {
//用戶名+時間戳(這里只是demo,正常項目的令牌應(yīng)該要更為復(fù)雜)
flag = username + System.currentTimeMillis();
//令牌作為key,存用戶id作為value(或者直接存儲可暴露的部分用戶信息也行)設(shè)置過期時間(我這里設(shè)置3分鐘)
template.opsForValue().set(flag, "1", (long) (3 * 60), TimeUnit.SECONDS);
}
return flag;
}
/**
* 跳轉(zhuǎn)登錄頁面
*/
@RequestMapping("/sso/loginPage")
private ModelAndView loginPage(String url) {
ModelAndView modelAndView = new ModelAndView("login");
modelAndView.addObject("url", url);
return modelAndView;
}
/**
* 頁面登錄
*/
@RequestMapping("/sso/login")
private String login(HttpServletResponse response, String username, String password, String url) {
String check = checkUsernameAndPassword(username, password);
if (!StringUtils.isEmpty(check)) {
try {
Cookie cookie = new Cookie("accessToken", check);
cookie.setMaxAge(60 * 3);
//設(shè)置域
// cookie.setDomain("huanzi.cn");
//設(shè)置訪問路徑
cookie.setPath("/");
response.addCookie(cookie);
//重定向到原先訪問的頁面
response.sendRedirect(url);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
return "登錄失敗";
}
}
zuul-server
引入feign,用于調(diào)用sso-server服務(wù)
<!-- feign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
創(chuàng)建SsoFeign.java接口
@FeignClient(name = "sso-server", path = "/")
public interface SsoFeign {
/**
* 判斷key是否存在
*/
@RequestMapping("redis/hasKey/{key}")
public Boolean hasKey(@PathVariable("key") String key);
}
啟動類加入@EnableFeignClients注解,否則啟動會報錯,無法注入SsoFeign對象
@EnableZuulProxy
@EnableEurekaClient
@EnableFeignClients
@SpringBootApplication
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}
修改AccessFilter過濾邏輯,注入feign接口,用于調(diào)用sso-server檢查Redis,修改run方法的過濾邏輯
/**
* Zuul過濾器,實現(xiàn)了路由檢查
*/
public class AccessFilter extends ZuulFilter {
@Autowired
private SsoFeign ssoFeign;
/**
* 通過int值來定義過濾器的執(zhí)行順序
*/
@Override
public int filterOrder() {
// PreDecoration之前運行
return PRE_DECORATION_FILTER_ORDER - 1;
}
/**
* 過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型:
* public static final String ERROR_TYPE = "error";
* public static final String POST_TYPE = "post";
* public static final String PRE_TYPE = "pre";
* public static final String ROUTE_TYPE = "route";
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 過濾器的具體邏輯
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
//訪問路徑
String url = request.getRequestURL().toString();
//從cookie里面取值(Zuul丟失Cookie的解決方案:https://blog.csdn.net/lindan1984/article/details/79308396)
String accessToken = request.getParameter("accessToken");
for (Cookie cookie : request.getCookies()) {
if ("accessToken".equals(cookie.getName())) {
accessToken = cookie.getValue();
}
}
//過濾規(guī)則:cookie有令牌且存在于Redis,或者訪問的是登錄頁面、登錄請求則放行
if (url.contains("sso-server/sso/loginPage") || url.contains("sso-server/sso/login") || (!StringUtils.isEmpty(accessToken) && ssoFeign.hasKey(accessToken))) {
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
return null;
} else {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
//重定向到登錄頁面
try {
response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
/**
* 返回一個boolean類型來判斷該過濾器是否要執(zhí)行
*/
@Override
public boolean shouldFilter() {
return true;
}
}
修改配置文件,映射sso-server代理路徑,超時時間與丟失cookie的解決
zuul.routes.sso-server.path=/sso-server/** zuul.routes.sso-server.service-id=sso-server zuul.host.socket-timeout-millis=60000 zuul.host.connect-timeout-millis=10000 #Zuul丟失Cookie的解決方案:https://blog.csdn.net/lindan1984/article/details/79308396 zuul.sensitive-headers=
測試效果
啟動eureka、zuul-server、sso-server、config-server、myspringboot、springdatajpa(由兩個應(yīng)用組成,實現(xiàn)了ribbon負載均衡),記得啟動我們的RabbitMQ服務(wù)和Redis服務(wù)!

剛開始,沒有cookie且無Redis的情況下,瀏覽器訪問http://localhost:10010/myspringboot/feign/ribbon,被zuul-server攔截重定向到sso-server登錄頁面


開始登錄校驗,為了方便演示,我將密碼的type改成text
登錄失敗,返回提示語

登錄成功,重定向到之前的請求

cookie的值,以及過期時間

3分鐘后我們再次訪問http://localhost:10010/myspringboot/feign/ribbon,cookie、Redis失效,需要從新登錄



后記
sso單點登錄就記錄到這里,這里只是實現(xiàn)了單機版的sso,以后在進行升級吧。
問題報錯:我們在sso-server設(shè)置cookie后,在zuul-server的run方法里獲取不到設(shè)置的cookie,去瀏覽器查看,cookie沒有設(shè)置成功,Zuul丟失Cookie
解決方案
我們是使用spring cloud zuul作為api-gateway實踐中,發(fā)現(xiàn)默認(rèn)zuul會過濾掉cookie等header信息,有些業(yè)務(wù)場景需要傳遞這些信息該怎么處理呢?
處理方式 在api-gateway的application.properties文件中添加 zuul.sensitive-headers=
問題原因
負責(zé)根據(jù)ServiceId來路由的RibbonRoutingFilter在route之前會調(diào)用ProxyRequestHelper的buildZuulRequestHeaders(request)來重新組裝一個新的Header。
在buildZuulRequestHeaders方法中會對RequsetHeader中的每一項調(diào)用isIncludedHeader(name)來判斷當(dāng)前項是否應(yīng)該留在新的Header中,如下圖,如果當(dāng)前項在IGNORED_HEADERS(需要忽略的信息)中,就不會在新header中保留。
PreDecorationFilter過濾器會調(diào)用ProxyRequestHelper的addIgnoredHeaders方法把敏感信息(ZuulProperties的sensitiveHeaders屬性)添加到請求上下文的IGNORED_HEADERS中
sensitiveHeaders的默認(rèn)值初始值是"Cookie", "Set-Cookie", "Authorization"這三項,可以看到Cookie被列為了敏感信息,所以不會放到新header中
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Springcloud eureka搭建高可用集群過程圖解
- SpringCloud Feign服務(wù)調(diào)用請求方式總結(jié)
- springCloud服務(wù)注冊Eureka實現(xiàn)過程圖解
- SpringCloud-Alibaba-Nacos啟動失敗解決方案
- 詳解springcloud 基于feign的服務(wù)接口的統(tǒng)一hystrix降級處理
- springboot2.0和springcloud Finchley版項目搭建(包含eureka,gateWay,F(xiàn)reign,Hystrix)
- SpringCloud Zuul實現(xiàn)動態(tài)路由
- SpringCloud Feign 服務(wù)調(diào)用的實現(xiàn)
- Spring boot GC實現(xiàn)過程原理解析
相關(guān)文章
java中Pulsar?InterruptedException?異常
這篇文章主要為大家介紹了java中Pulsar?InterruptedException?異常分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
利用spring-boot-maven-plugin插件打包SpringBoot應(yīng)用方式
spring-boot-maven-plugin插件可以將SpringBoot應(yīng)用打成帶依賴的jar包,該包中不僅包含應(yīng)用自身的代碼,還包含了pom.xml中配置的依賴,修改pom.xml打包后,生成的jar包就包含了項目依賴,生成的jar包位于項目的target文件夾下2025-02-02
你必須得會的SpringBoot全局統(tǒng)一處理異常詳解
程序在運行的過程中,不可避免會產(chǎn)生各種各樣的錯誤,這個時候就需要進行異常處理,本文主要為大家介紹了SpringBoot實現(xiàn)全局統(tǒng)一處理異常的方法,需要的可以參考一下2023-06-06
詳解Java中CountDownLatch異步轉(zhuǎn)同步工具類
今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識,文章圍繞著CountDownLatch異步轉(zhuǎn)同步工具類展開,文中有非常詳細的介紹及代碼示例,需要的朋友可以參考下2021-06-06

