1、修改項目使其基於瀏覽器cookie的SSO
1.1、修改回調方法,獲得到token后,由存放到session改為存放到cookie
/** * 回調方法 * 接收認證服務器發來的授權碼,並換取令牌 * * @param code 授權碼 * @param state 請求授權服務器時發送的state */ @GetMapping("/oauth/callback") public void oauthCallback(@RequestParam String code, String state, HttpServletRequest request, HttpServletResponse response) throws IOException { String oauthTokenUrl = "http://gateway.caofanqi.cn:9010/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("webApp", "123456"); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.set("code", code); params.set("grant_type", "authorization_code"); params.set("redirect_uri", "http://web.caofanqi.cn:9000/oauth/callback"); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers); ResponseEntity<TokenInfoDTO> authResult = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class); log.info("tokenInfo : {}", authResult.getBody()); //將token放到session中 //request.getSession().setAttribute("token", authResult.getBody().init()); //將token放到cookie中 Cookie accessTokenCookie = new Cookie("access_token",authResult.getBody().getAccess_token()); accessTokenCookie.setMaxAge(authResult.getBody().getExpires_in().intValue() - 5); accessTokenCookie.setDomain("caofanqi.cn"); accessTokenCookie.setPath("/"); response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("refresh_token",authResult.getBody().getRefresh_token()); refreshTokenCookie.setMaxAge(2592000); refreshTokenCookie.setDomain("caofanqi.cn"); refreshTokenCookie.setPath("/"); response.addCookie(refreshTokenCookie); log.info("state :{}", state); //一般會根據state記錄需要登陸時的路由 response.sendRedirect("/"); }
1.2、寫一個CookieTokenFilter,將token從cookie中取出來
/** * 將cookie中的token取出放到請求頭中 * * @author caofanqi * @date 2020/2/6 0:34 */ @Slf4j @Component public class CookieTokenFilter extends ZuulFilter { private RestTemplate restTemplate = new RestTemplate(); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse response = requestContext.getResponse(); String accessToken = getCookie("access_token"); if (StringUtils.isNotBlank(accessToken)) { // 有值說明沒過期 requestContext.addZuulRequestHeader("Authorization", "bearer " + accessToken); } else { //使用refresh_token刷新令牌 String refreshToken = getCookie("refresh_token"); if (StringUtils.isNotBlank(refreshToken)) { //去認證服務器刷新令牌 String oauthTokenUrl = "http://gateway.caofanqi.cn:9010/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("webApp", "123456"); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.set("grant_type", "refresh_token"); params.set("refresh_token", refreshToken); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers); try { ResponseEntity<TokenInfoDTO> refreshTokenResult = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class); requestContext.addZuulRequestHeader("Authorization", "bearer " + refreshTokenResult.getBody().getAccess_token()); Cookie accessTokenCookie = new Cookie("access_token", refreshTokenResult.getBody().getAccess_token()); accessTokenCookie.setMaxAge(refreshTokenResult.getBody().getExpires_in().intValue() - 5); accessTokenCookie.setDomain("caofanqi.cn"); accessTokenCookie.setPath("/"); response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("refresh_token", refreshTokenResult.getBody().getRefresh_token()); refreshTokenCookie.setMaxAge(2592000); refreshTokenCookie.setDomain("caofanqi.cn"); refreshTokenCookie.setPath("/"); response.addCookie(refreshTokenCookie); log.info("refresh_token......"); } catch (Exception e) { //刷新令牌失敗 log.info("token refresh fail"); requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); requestContext.setResponseBody("{\"message\":\"token refresh fail\"}"); } } else { //過期了,無法刷新令牌 log.info("refresh_token not exist"); requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); requestContext.setResponseBody("{\"message\":\"token refresh fail\"}"); } } return null; } /** * 獲取cookie的值 */ private String getCookie(String cookieName) { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (StringUtils.equals(cookieName, cookie.getName())) { return cookie.getValue(); } } return null; } }
1.3、判斷用戶登陸狀態,從網關中獲取,MeFilter放到授權Filter之后。因為之間基於session,直接從客戶端服務器中獲取就行,現在不急於session,客戶端不知道用戶登陸狀態,去網關獲取。
之前配置了以api開頭的請求會轉發到網關
網關配置
網關過濾器MeFilter
/** * 用戶判斷當前用戶是否認證 * * @author caofanqi * @date 2020/2/7 21:43 */ @Component public class MeFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 6; } /** * 只處理/user/me請求 */ @Override public boolean shouldFilter() { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); return StringUtils.equals(request.getRequestURI(),"/user/me"); } /** * 判斷請求頭中有沒有我們放入的username,后直接返回,不繼續往下走 */ @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); String username = requestContext.getZuulRequestHeaders().get("username"); if(StringUtils.isNotBlank(username)) { requestContext.setResponseBody("{\"username\":\""+username+"\"}"); } requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(HttpStatus.OK.value()); requestContext.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); return null; } }
1.4、啟動各項目,進行測試
過期時間設置如下
訪問http://web.caofanqi.cn:9000/ ,自動跳轉到登陸頁面,進行登陸,間隔時間獲取訂單信息,webApp控制台打印如下
查看瀏覽器cookie如下
1.5、但是現在還有一個問題,認證信息放在cookie中,退出時,也要將cookie刪除
//退出 function logout() { $.get("/logout", function () { }); //將瀏覽器中的cookie也刪除 $.removeCookie('access_token', { domain:'caofanqi.cn', path: '/' }); $.removeCookie('refresh_token', { domain:'caofanqi.cn', path: '/' }); //客戶端session失效后,將認證服務器session也失效掉,添加重定向url location.href = "http://auth.caofanqi.cn:9020/logout?redirect_uri=http://web.caofanqi.cn:9000"; }
2、基於token的SSO優缺點
2.1、優點:
復雜度低,相對於基於session的SSO來說,只需要做access_token和refresh_token的過期處理。
不占用服務器資源,適合用戶量特別大的系統。因為token存在瀏覽器cookie中,只有cookie中的refresh_token失效時,才會去認證服務器登陸。不需要認證服務器設置有效期很長的session。因為通過token就可以訪問微服務。
2.2、缺點:
安全性低:token存在瀏覽器,有一定的風險。可以使用https,縮短access_token的有效期來防范。
可控性低:token存在瀏覽器,沒辦法主動失效掉。
項目源碼:https://github.com/caofanqi/study-security/tree/dev-web-sso-token