一、Oauth 是一個關於授權(authorization)的開網絡標准(規范)
OAuth2: 解決的是不同的企業之間的登錄,本質是授權,如論壇與QQ
要能訪問各種資源重點是要獲取令牌(token),但根據令牌的獲取方式不同,又會有四種授權方式
- 授權碼(authorization-code)
- 隱藏式(implicit)
- 密碼式(password)
- 客戶端憑證(client credentials)
授權碼:這是最常用的一種方式,指的是第三方應用先申請一個授權碼,然后再用該碼獲取令牌,項目中用的就是這種
隱藏式:允許直接向前端頒發令牌。這種方式沒有授權碼這個中間步驟,所以稱為(授權碼)"隱藏式"(implicit),一般應用於純前端項目
密碼式:直接通過用戶名和密碼的方式申請令牌,這方式是最不安全的方式
憑證式:這種方式的令牌是針對第三方應用,而不是針對用戶的,既某個第三方應用的所有用戶共用一個令牌,一般用於沒有前端的命令行應用
授權碼授權流程:
第一步,A 網站提供一個鏈接,用戶點擊后就會跳轉到 B 網站(權限驗證系統)
http://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
第二步,用戶跳轉后,B 網站如果沒有登錄會要求用戶登錄,然后詢問是否同意給予 A 網站授權。用戶表示同意,這時 B 網站就會跳回redirect_uri參數指定的網址,並附加授權碼code
http://a.com/callback?code=AUTHORIZATION_CODE
第三步,A 網站拿到授權碼以后,在后端,向 B 網站請求令牌。
http://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
上面 URL 中,client_id參數和client_secret參數用來讓 B 確認 A 的身份(client_secret參數是保密的,因此只能在后端發請求),grant_type參數的值是AUTHORIZATION_CODE,表示采用的授權方式是授權碼,code參數是上一步拿到的授權碼,redirect_uri參數是令牌頒發后的回調網址。
第四步,B 網站收到請求以后,就會頒發令牌。具體做法是向redirect_uri指定的網址,發送一段 JSON 數據。
{
"access_token":"ACCESS_TOKEN",
"info":{...}
}
接下來用戶就可以根據這個access_token來進行訪問了,
如A網站拿着token,申請獲取用戶信息,B網站確認令牌無誤,同意向A網站開放資源。
對於第三方網站來說 可分為3部分
1、申請code
2、申請token
3、帶着token去請求資源(如:申請獲取用戶信息)
偽代碼
服務端
@RequestMapping("authorize") public Object authorize(Model model, HttpServletRequest request) throws OAuthSystemException, URISyntaxException { //構建OAuth請求 OAuthAuthzRequest oAuthAuthzRequest = null; try { oAuthAuthzRequest = new OAuthAuthzRequest(request); // 根據傳入的clientId 判斷 客戶端是否存在 if(!authorizeService.checkClientId(oAuthAuthzRequest.getClientId())) { return HttpResponseBody.failResponse("客戶端驗證失敗,如錯誤的client_id/client_secret"); } // 判斷用戶是否登錄 Subject subject = SecurityUtils.getSubject(); if(!subject.isAuthenticated()) { if(!login(subject, request)) { return new HttpResponseBody(ResponseCodeConstant.UN_LOGIN_ERROR, "沒有登陸"); } } String username = (String) subject.getPrincipal(); //生成授權碼 String authorizationCode = null; String responseType = oAuthAuthzRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE); if(responseType.equals(ResponseType.CODE.toString())) { OAuthIssuerImpl oAuthIssuer = new OAuthIssuerImpl(new MD5Generator()); authorizationCode = oAuthIssuer.authorizationCode(); shiroCacheUtil.addAuthCode(authorizationCode, username); } Map<String, Object> data = new HashMap<>(); data.put(SsoConstants.AUTH_CODE, authorizationCode); return HttpResponseBody.successResponse("ok", data); } catch(OAuthProblemException e) { return HttpResponseBody.failResponse(e.getMessage()); } }
@RequestMapping("/accessToken") public HttpEntity token(HttpServletRequest request) throws OAuthSystemException { try { // 構建Oauth請求 OAuthTokenRequest oAuthTokenRequest = new OAuthTokenRequest(request); //檢查提交的客戶端id是否正確 if(!authorizeService.checkClientId(oAuthTokenRequest.getClientId())) { OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(OAuthError.TokenResponse.INVALID_CLIENT) .setErrorDescription("客戶端驗證失敗,如錯誤的client_id/client_secret") .buildJSONMessage(); return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } // 檢查客戶端安全Key是否正確 if(!authorizeService.checkClientSecret(oAuthTokenRequest.getClientSecret())){ OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT) .setErrorDescription("客戶端驗證失敗,如錯誤的client_id/client_secret") .buildJSONMessage(); return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } String authCode = oAuthTokenRequest.getParam(OAuth.OAUTH_CODE); // 檢查驗證類型,此處只檢查AUTHORIZATION類型,其他的還有PASSWORD或者REFRESH_TOKEN if(oAuthTokenRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(GrantType.AUTHORIZATION_CODE.toString())){ if(!shiroCacheUtil.checkAuthCode(authCode)){ OAuthResponse response = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST) .setError(OAuthError.TokenResponse.INVALID_GRANT) .setErrorDescription("error grant code") .buildJSONMessage(); return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } } //生成Access Token OAuthIssuer issuer = new OAuthIssuerImpl(new MD5Generator()); final String accessToken = issuer.accessToken(); shiroCacheUtil.addAccessToken(accessToken, shiroCacheUtil.getUsernameByAuthCode(authCode)); logger.info("accessToken generated : {}", accessToken); //需要保存clientSessionId和clientId的關系到redis,便於在Logout時通知系統logout String clientSessionId = request.getParameter("sid"); //System.out.println("clientSessionId = " + clientSessionId); String clientId = oAuthTokenRequest.getClientId(); //System.out.println("clientId = " + clientId); redisTemplate.opsForHash().put(RedisKey.CLIENT_SESSIONS, clientSessionId, clientId); // 生成OAuth響應 OAuthResponse response = OAuthASResponse.tokenResponse(HttpServletResponse.SC_OK) .setAccessToken(accessToken).setExpiresIn(String.valueOf(authorizeService.getExpireIn())) .buildJSONMessage(); return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } catch(OAuthProblemException e) { e.printStackTrace(); OAuthResponse res = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e).buildBodyMessage(); return new ResponseEntity<>(res.getBody(), HttpStatus.valueOf(res.getResponseStatus())); } }
@RequestMapping("/userInfo") public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException { try { //構建OAuth資源請求 OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.QUERY); //獲取Access Token String accessToken = oauthRequest.getAccessToken(); //驗證Access Token if (!shiroCacheUtil.checkAccessToken(accessToken)) { // 如果不存在/過期了,返回未驗證錯誤,需重新驗證 OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm("fxb") .setError(OAuthError.ResourceResponse.INVALID_TOKEN) .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED); } //返回用戶名 String username = shiroCacheUtil.getUsernameByAccessToken(accessToken); SysUser user = userService.selectByAccount(username); return new ResponseEntity<>(user, HttpStatus.OK); } catch (OAuthProblemException e) { //檢查是否設置了錯誤碼 String errorCode = e.getError(); if (OAuthUtils.isEmpty(errorCode)) { OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm("fxb") .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED); } OAuthResponse oauthResponse = OAuthRSResponse .errorResponse(HttpServletResponse.SC_UNAUTHORIZED) .setRealm("fxb") .setError(e.getError()) .setErrorDescription(e.getDescription()) .setErrorUri(e.getUri()) .buildHeaderMessage(); HttpHeaders headers = new HttpHeaders(); headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE)); return new ResponseEntity(HttpStatus.BAD_REQUEST); } }
客戶端
private String extractUsername(String code) { OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); try { OAuthClientRequest accessTokenRequest = OAuthClientRequest.tokenLocation(accessTokenUrl) .setGrantType(GrantType.AUTHORIZATION_CODE) .setClientId(clientId) .setClientSecret(clientSecret) .setCode(code) .setRedirectURI(redirectUrl) .setParameter("sid", SecurityUtils.getSubject().getSession().getId().toString()) .buildQueryMessage(); OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST); String accessToken = oAuthResponse.getAccessToken(); //拿用戶信息 OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(userInfoUrl) .setAccessToken(accessToken).buildQueryMessage(); OAuthResourceResponse resourceResponse = oAuthClient.resource(userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class); String userJson = resourceResponse.getBody(); SysUser user = JsonUtils.json2Obj(userJson, SysUser.class); this.setResource(user, accessToken); return user.getUserName(); } catch(OAuthSystemException e) { e.printStackTrace(); throw new RuntimeException(e); } catch(OAuthProblemException e) { e.printStackTrace(); throw new BusinessException(ResponseCodeConstant.UN_LOGIN_ERROR, "沒有登錄"); } }
<dependency> <groupId>org.apache.oltu.oauth2</groupId> <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.apache.oltu.oauth2</groupId> <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> <version>1.0.2</version> </dependency>
二、單點: 是解決企業內部的一系列產品登錄問題,安全信任度要比oauth2高
(一)session-cookie機制
1、session-cookie機制出現的根源, http連接是無狀態的連接
-------- 同一瀏覽器向服務端發送多次請求,服務器無法識別,哪些請求是同一個瀏覽器發出的
2、為了標識哪些請求是屬於同一個人 ---------- 需要在請求里加一個標識參數
方法1-----------直接在url里加一個標識參數(對前端開發有侵入性),如: token
方法2-----------http請求時,自動攜帶瀏覽器的cookie(對前端開發無知覺),如:jsessionid=XXXXXXX
3、瀏覽器標識在網絡上的傳輸,是明文的,不安全的
-----------安全措施:改https來保障
4、cookie的使用限制---依賴域名
-------------- 頂級域名下cookie,會被二級以下的域名請求,自動攜帶
-------------- 二級域名的cookie,不能攜帶被其它域名下的請求攜帶
5、在服務器后台,通過解讀標識信息(token或jsessionid),來對應會話是哪個session
--------------- 一個tomcat,被1000個用戶登陸,tomcat里一定有1000個session -------》存儲格式map《sessionid,session對象》
--------------- 通過前端傳遞的jsessionid,來對應取的session ------ 動作發生時機request.getsession
(二)session共享方式,實現的單點登陸
1、多個應用共用同一個頂級域名,sessionid被種在頂級域名的cookie里
2、后台session通過redis實現共享(重寫httprequest、httpsession 或使用springsession框架),即每個tomcat都在請求開始時,到redis查詢session;在請求返回時,將自身session對象存入redis
3、當請求到達服務器時,服務器直接解讀cookie中的sessionid,然后通過sessionid到redis中查找到對應會話session對象
4、后台判斷請求是否已登陸,主要校驗session對象中,是否存在登陸用戶信息
5、整個校驗過程,通過filter過濾器來攔截切入,如下圖:
6、登陸成功時,后台需要給頁面種cookie方法如下:
response里,反映的種cookie效果如下:
7、為了request.getsession時,自動能拿到redis中共享的session,
我們需要重寫request的getsession方法(使用HttpServletRequestWrapper包裝原request)
(三)cas單點登陸方案
1、對於完全不同域名的系統,cookie是無法跨域名共享的
2、cas方案,直接啟用一個專業的用來登陸的域名(比如:cas.com)來供所有的系統登陸。
3、當業務系統(如b.com)被打開時,借助cas系統來登陸,過程如下:
cas登陸的全過程:
(1)、b.com打開時,發現自己未登陸 ----》 於是跳轉到cas.com去登陸
(2)、cas.com登陸頁面被打開,用戶輸入帳戶/密碼登陸成功
(3)、cas.com登陸成功,種cookie到cas.com域名下 -----------》把sessionid放入后台redis《ticket,sesssionid》---頁面跳回b.com
String ticket = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(ticket,request.getSession().getId(),20, TimeUnit.SECONDS);//一定要設置過期時間 CookieBasedSession.onNewSession(request,response); response.sendRedirect(user.getBackurl()+"?ticket="+ticket);
(4)、b.com重新被打開,發現仍然是未登陸,但是有了一個ticket值
(5)、b.com用ticket值,到redis里查到sessionid,並做session同步 ------ 》種cookie給自己,頁面原地重跳
(6)、b.com打開自己頁面,此時有了cookie,后台校驗登陸狀態,成功
(7)整個過程交互,列圖如下:
4、cas.com的登陸頁面被打開時,如果此時cas.com本來就是登陸狀態的,則自動返回生成ticket給業務系統
整個單點登陸的關鍵部位,是利用cas.com的cookie保持cas.com是登陸狀態,此后任何第三個系統跳入,都將自動完成登陸過程
5,本示例中,使用了redis來做cas的服務接口,請根據工作情況,自行替換為合適的服務接口(主要是根據sessionid來判斷用戶是否已登陸)
6,為提高安全性,ticket應該使用過即作廢(本例中,會用有效期機制)
public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; MyRequestWrapper myRequestWrapper = new MyRequestWrapper(request,redisTemplate); //如果未登陸狀態,進入下面邏輯 String requestUrl = request.getServletPath(); if (!"/toLogin".equals(requestUrl) && !requestUrl.startsWith("/login") && !myRequestWrapper.isLogin()) { /** * ticket為空,或無對應sessionid為空 * --- 表明不是自動登陸請求--直接強制到登陸頁面 */ String ticket = request.getParameter("ticket"); if (null == ticket || null == redisTemplate.opsForValue().get(ticket)){ HttpServletResponse response = (HttpServletResponse)servletResponse; response.sendRedirect("http://cas.com:8090/toLogin?url="+request.getRequestURL().toString()); return ; } /** * 是自動登陸請求,則種cookie值進去---本次請求是302重定向 * 重定向后的下次請求,自帶本cookie,將直接是登陸狀態 */ myRequestWrapper.setSessionId((String) redisTemplate.opsForValue().get(ticket)); myRequestWrapper.createSession(); //種cookie CookieBasedSession.onNewSession(myRequestWrapper,(HttpServletResponse)servletResponse); //重定向自流轉一次,原地跳轉重向一次 HttpServletResponse response = (HttpServletResponse)servletResponse; response.sendRedirect(request.getRequestURL().toString()); return; } try { filterChain.doFilter(myRequestWrapper,servletResponse); } finally { myRequestWrapper.commitSession(); } }
public static void onNewSession(HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(); String sessionId = session.getId(); Cookie cookie = new Cookie(COOKIE_NAME_SESSION, sessionId); cookie.setHttpOnly(true); cookie.setPath(request.getContextPath() + "/"); cookie.setMaxAge(Integer.MAX_VALUE); response.addCookie(cookie); }
參考:https://my.oschina.net/u/2351011/blog/3058424