您好,登錄后才能下訂單哦!
前言
作為分布式項(xiàng)目,單點(diǎn)登錄是必不可少的,文本基于之前的的博客(猛戳:SpringCloud系列——Zuul 動(dòng)態(tài)路由,SpringBoot系列——Redis)記錄Zuul配合Redis實(shí)現(xiàn)一個(gè)簡單的sso單點(diǎn)登錄實(shí)例
sso單點(diǎn)登錄思路:
1、訪問分布式系統(tǒng)的任意請求,被Zuul的Filter攔截過濾
2、在run方法里實(shí)現(xiàn)過濾規(guī)則:cookie有令牌accessToken且作為key存在于Redis,或者訪問的是登錄頁面、登錄請求則放行
3、否則,將重定向到sso-server的登錄頁面且原先的請求路徑作為一個(gè)參數(shù);response.sendRedirect("http://localhost:10010/sso-server/sso/loginPage?url=" + url);
4、登錄成功,sso-server生成accessToken,并作為key(用戶名+時(shí)間戳,這里只是demo,正常項(xiàng)目的令牌應(yīng)該要更為復(fù)雜)存到Redis,value值存用戶id作為value(或者直接存儲(chǔ)可暴露的部分用戶信息也行)設(shè)置過期時(shí)間(我這里設(shè)置3分鐘);設(shè)置cookie:new Cookie("accessToken",accessToken);,設(shè)置maxAge(60*3);、path("/");
5、sso-server單點(diǎn)登錄服務(wù)負(fù)責(zé)校驗(yàn)用戶信息、獲取用戶信息、操作Redis緩存,提供接口,在eureka上注冊
代碼編寫
sso-server
首先我們創(chuàng)建一個(gè)單點(diǎn)登錄服務(wù)sso-server,并在eureka上注冊(創(chuàng)建項(xià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; } } /** * 校驗(yàn)用戶名密碼,成功則返回通行令牌(這里寫死huanzi/123456) */ @RequestMapping("/sso/checkUsernameAndPassword") private String checkUsernameAndPassword(String username, String password) { //通行令牌 String flag = null; if ("huanzi".equals(username) && "123456".equals(password)) { //用戶名+時(shí)間戳(這里只是demo,正常項(xiàng)目的令牌應(yīng)該要更為復(fù)雜) flag = username + System.currentTimeMillis(); //令牌作為key,存用戶id作為value(或者直接存儲(chǔ)可暴露的部分用戶信息也行)設(shè)置過期時(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); }
啟動(dòng)類加入@EnableFeignClients注解,否則啟動(dòng)會(huì)報(bào)錯(cuò),無法注入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過濾器,實(shí)現(xiàn)了路由檢查 */ public class AccessFilter extends ZuulFilter { @Autowired private SsoFeign ssoFeign; /** * 通過int值來定義過濾器的執(zhí)行順序 */ @Override public int filterOrder() { // PreDecoration之前運(yùn)行 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; } } /** * 返回一個(gè)boolean類型來判斷該過濾器是否要執(zhí)行 */ @Override public boolean shouldFilter() { return true; } }
修改配置文件,映射sso-server代理路徑,超時(shí)時(shí)間與丟失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=
測試效果
啟動(dòng)eureka、zuul-server、sso-server、config-server、myspringboot、springdatajpa(由兩個(gè)應(yīng)用組成,實(shí)現(xiàn)了ribbon負(fù)載均衡),記得啟動(dòng)我們的RabbitMQ服務(wù)和Redis服務(wù)!
剛開始,沒有cookie且無Redis的情況下,瀏覽器訪問http://localhost:10010/myspringboot/feign/ribbon,被zuul-server攔截重定向到sso-server登錄頁面
開始登錄校驗(yàn),為了方便演示,我將密碼的type改成text
登錄失敗,返回提示語
登錄成功,重定向到之前的請求
cookie的值,以及過期時(shí)間
3分鐘后我們再次訪問http://localhost:10010/myspringboot/feign/ribbon,cookie、Redis失效,需要從新登錄
后記
sso單點(diǎn)登錄就記錄到這里,這里只是實(shí)現(xiàn)了單機(jī)版的sso,以后在進(jìn)行升級吧。
問題報(bào)錯(cuò):我們在sso-server設(shè)置cookie后,在zuul-server的run方法里獲取不到設(shè)置的cookie,去瀏覽器查看,cookie沒有設(shè)置成功,Zuul丟失Cookie
解決方案
我們是使用spring cloud zuul作為api-gateway實(shí)踐中,發(fā)現(xiàn)默認(rèn)zuul會(huì)過濾掉cookie等header信息,有些業(yè)務(wù)場景需要傳遞這些信息該怎么處理呢?
處理方式 在api-gateway的application.properties文件中添加 zuul.sensitive-headers=
問題原因
負(fù)責(zé)根據(jù)ServiceId來路由的RibbonRoutingFilter在route之前會(huì)調(diào)用ProxyRequestHelper的buildZuulRequestHeaders(request)來重新組裝一個(gè)新的Header。
在buildZuulRequestHeaders方法中會(huì)對RequsetHeader中的每一項(xiàng)調(diào)用isIncludedHeader(name)來判斷當(dāng)前項(xiàng)是否應(yīng)該留在新的Header中,如下圖,如果當(dāng)前項(xiàng)在IGNORED_HEADERS(需要忽略的信息)中,就不會(huì)在新header中保留。
PreDecorationFilter過濾器會(huì)調(diào)用ProxyRequestHelper的addIgnoredHeaders方法把敏感信息(ZuulProperties的sensitiveHeaders屬性)添加到請求上下文的IGNORED_HEADERS中
sensitiveHeaders的默認(rèn)值初始值是"Cookie", "Set-Cookie", "Authorization"這三項(xiàng),可以看到Cookie被列為了敏感信息,所以不會(huì)放到新header中
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。