一,、Oauth 是一個(gè)關(guān)于授權(quán)(authorization)的開網(wǎng)絡(luò)標(biāo)準(zhǔn)(規(guī)范) OAuth2: 解決的是不同的企業(yè)之間的登錄,,本質(zhì)是授權(quán),,如論壇與QQ 要能訪問(wèn)各種資源重點(diǎn)是要獲取令牌(token),,但根據(jù)令牌的獲取方式不同,,又會(huì)有四種授權(quán)方式
授權(quán)碼:這是最常用的一種方式,指的是第三方應(yīng)用先申請(qǐng)一個(gè)授權(quán)碼,,然后再用該碼獲取令牌,,項(xiàng)目中用的就是這種 隱藏式:允許直接向前端頒發(fā)令牌。這種方式?jīng)]有授權(quán)碼這個(gè)中間步驟,,所以稱為(授權(quán)碼)"隱藏式"(implicit),,一般應(yīng)用于純前端項(xiàng)目 密碼式:直接通過(guò)用戶名和密碼的方式申請(qǐng)令牌,這方式是最不安全的方式 憑證式:這種方式的令牌是針對(duì)第三方應(yīng)用,,而不是針對(duì)用戶的,,既某個(gè)第三方應(yīng)用的所有用戶共用一個(gè)令牌,一般用于沒(méi)有前端的命令行應(yīng)用 授權(quán)碼授權(quán)流程: 第一步,,A 網(wǎng)站提供一個(gè)鏈接,,用戶點(diǎn)擊后就會(huì)跳轉(zhuǎn)到 B 網(wǎng)站(權(quán)限驗(yàn)證系統(tǒng)) http:///oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read 第二步,用戶跳轉(zhuǎn)后,,B 網(wǎng)站如果沒(méi)有登錄會(huì)要求用戶登錄,,然后詢問(wèn)是否同意給予 A 網(wǎng)站授權(quán)。用戶表示同意,,這時(shí) B 網(wǎng)站就會(huì)跳回redirect_uri參數(shù)指定的網(wǎng)址,,并附加授權(quán)碼code http://a.com/callback?code=AUTHORIZATION_CODE 第三步,A 網(wǎng)站拿到授權(quán)碼以后,在后端,,向 B 網(wǎng)站請(qǐng)求令牌。 http:///oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL 上面 URL 中,,client_id參數(shù)和client_secret參數(shù)用來(lái)讓 B 確認(rèn) A 的身份(client_secret參數(shù)是保密的,,因此只能在后端發(fā)請(qǐng)求),grant_type參數(shù)的值是AUTHORIZATION_CODE,,表示采用的授權(quán)方式是授權(quán)碼,,code參數(shù)是上一步拿到的授權(quán)碼,redirect_uri參數(shù)是令牌頒發(fā)后的回調(diào)網(wǎng)址,。 第四步,,B 網(wǎng)站收到請(qǐng)求以后,就會(huì)頒發(fā)令牌,。具體做法是向redirect_uri指定的網(wǎng)址,,發(fā)送一段 JSON 數(shù)據(jù)。 { "access_token":"ACCESS_TOKEN", "info":{...} } 接下來(lái)用戶就可以根據(jù)這個(gè)access_token來(lái)進(jìn)行訪問(wèn)了,, 如A網(wǎng)站拿著token,,申請(qǐng)獲取用戶信息,B網(wǎng)站確認(rèn)令牌無(wú)誤,,同意向A網(wǎng)站開放資源,。 對(duì)于第三方網(wǎng)站來(lái)說(shuō) 可分為3部分 1、申請(qǐng)code 2,、申請(qǐng)token 3,、帶著token去請(qǐng)求資源(如:申請(qǐng)獲取用戶信息) 偽代碼 服務(wù)端 @RequestMapping("authorize") public Object authorize(Model model, HttpServletRequest request) throws OAuthSystemException, URISyntaxException { //構(gòu)建OAuth請(qǐng)求 OAuthAuthzRequest oAuthAuthzRequest = null; try { oAuthAuthzRequest = new OAuthAuthzRequest(request); // 根據(jù)傳入的clientId 判斷 客戶端是否存在 if(!authorizeService.checkClientId(oAuthAuthzRequest.getClientId())) { return HttpResponseBody.failResponse("客戶端驗(yàn)證失敗,如錯(cuò)誤的client_id/client_secret"); } // 判斷用戶是否登錄 Subject subject = SecurityUtils.getSubject(); if(!subject.isAuthenticated()) { if(!login(subject, request)) { return new HttpResponseBody(ResponseCodeConstant.UN_LOGIN_ERROR, "沒(méi)有登陸"); } } String username = (String) subject.getPrincipal(); //生成授權(quán)碼 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 { // 構(gòu)建Oauth請(qǐng)求 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("客戶端驗(yàn)證失敗,,如錯(cuò)誤的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("客戶端驗(yàn)證失敗,,如錯(cuò)誤的client_id/client_secret") .buildJSONMessage(); return new ResponseEntity<>(response.getBody(), HttpStatus.valueOf(response.getResponseStatus())); } String authCode = oAuthTokenRequest.getParam(OAuth.OAUTH_CODE); // 檢查驗(yàn)證類型,此處只檢查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的關(guān)系到redis,,便于在Logout時(shí)通知系統(tǒng)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響應(yīng) 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 { //構(gòu)建OAuth資源請(qǐng)求 OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.QUERY); //獲取Access Token String accessToken = oauthRequest.getAccessToken(); //驗(yàn)證Access Token if (!shiroCacheUtil.checkAccessToken(accessToken)) { // 如果不存在/過(guò)期了,返回未驗(yàn)證錯(cuò)誤,,需重新驗(yàn)證 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) { //檢查是否設(shè)置了錯(cuò)誤碼 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, "沒(méi)有登錄"); } } <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> 二,、單點(diǎn): 是解決企業(yè)內(nèi)部的一系列產(chǎn)品登錄問(wèn)題,安全信任度要比oauth2高 (一)session-cookie機(jī)制 1,、session-cookie機(jī)制出現(xiàn)的根源,, http連接是無(wú)狀態(tài)的連接 -------- 同一瀏覽器向服務(wù)端發(fā)送多次請(qǐng)求,服務(wù)器無(wú)法識(shí)別,,哪些請(qǐng)求是同一個(gè)瀏覽器發(fā)出的 2,、為了標(biāo)識(shí)哪些請(qǐng)求是屬于同一個(gè)人 ---------- 需要在請(qǐng)求里加一個(gè)標(biāo)識(shí)參數(shù) 方法1-----------直接在url里加一個(gè)標(biāo)識(shí)參數(shù)(對(duì)前端開發(fā)有侵入性),如: token 方法2-----------http請(qǐng)求時(shí),自動(dòng)攜帶瀏覽器的cookie(對(duì)前端開發(fā)無(wú)知覺(jué)),,如:jsessionid=XXXXXXX 3,、瀏覽器標(biāo)識(shí)在網(wǎng)絡(luò)上的傳輸,是明文的,,不安全的 -----------安全措施:改https來(lái)保障 4,、cookie的使用限制---依賴域名 -------------- 頂級(jí)域名下cookie,會(huì)被二級(jí)以下的域名請(qǐng)求,,自動(dòng)攜帶 -------------- 二級(jí)域名的cookie,,不能攜帶被其它域名下的請(qǐng)求攜帶 5、在服務(wù)器后臺(tái),,通過(guò)解讀標(biāo)識(shí)信息(token或jsessionid),,來(lái)對(duì)應(yīng)會(huì)話是哪個(gè)session --------------- 一個(gè)tomcat,被1000個(gè)用戶登陸,,tomcat里一定有1000個(gè)session -------》存儲(chǔ)格式map《sessionid,,session對(duì)象》 --------------- 通過(guò)前端傳遞的jsessionid,來(lái)對(duì)應(yīng)取的session ------ 動(dòng)作發(fā)生時(shí)機(jī)request.getsession (二)session共享方式,,實(shí)現(xiàn)的單點(diǎn)登陸 1,、多個(gè)應(yīng)用共用同一個(gè)頂級(jí)域名,sessionid被種在頂級(jí)域名的cookie里 2,、后臺(tái)session通過(guò)redis實(shí)現(xiàn)共享(重寫httprequest,、httpsession 或使用springsession框架),即每個(gè)tomcat都在請(qǐng)求開始時(shí),,到redis查詢session;在請(qǐng)求返回時(shí),,將自身session對(duì)象存入redis 3、當(dāng)請(qǐng)求到達(dá)服務(wù)器時(shí),,服務(wù)器直接解讀cookie中的sessionid,,然后通過(guò)sessionid到redis中查找到對(duì)應(yīng)會(huì)話session對(duì)象 4、后臺(tái)判斷請(qǐng)求是否已登陸,,主要校驗(yàn)session對(duì)象中,,是否存在登陸用戶信息 5、整個(gè)校驗(yàn)過(guò)程,,通過(guò)filter過(guò)濾器來(lái)攔截切入,,如下圖: 6、登陸成功時(shí),,后臺(tái)需要給頁(yè)面種cookie方法如下: response里,,反映的種cookie效果如下: 7、為了request.getsession時(shí),,自動(dòng)能拿到redis中共享的session,, 我們需要重寫request的getsession方法(使用HttpServletRequestWrapper包裝原request) (三)cas單點(diǎn)登陸方案 1,、對(duì)于完全不同域名的系統(tǒng),cookie是無(wú)法跨域名共享的 2,、cas方案,,直接啟用一個(gè)專業(yè)的用來(lái)登陸的域名(比如:)來(lái)供所有的系統(tǒng)登陸。 3,、當(dāng)業(yè)務(wù)系統(tǒng)(如)被打開時(shí),,借助cas系統(tǒng)來(lái)登陸,過(guò)程如下: cas登陸的全過(guò)程: (1),、打開時(shí),發(fā)現(xiàn)自己未登陸 ----》 于是跳轉(zhuǎn)到去登陸 (2),、登陸頁(yè)面被打開,,用戶輸入帳戶/密碼登陸成功 (3)、登陸成功,,種cookie到域名下 -----------》把sessionid放入后臺(tái)redis《ticket,,sesssionid》---頁(yè)面跳回 String ticket = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(ticket,request.getSession().getId(),20, TimeUnit.SECONDS);//一定要設(shè)置過(guò)期時(shí)間 CookieBasedSession.onNewSession(request,response); response.sendRedirect(user.getBackurl()+"?ticket="+ticket); (4)、重新被打開,,發(fā)現(xiàn)仍然是未登陸,,但是有了一個(gè)ticket值 (5)、用ticket值,,到redis里查到sessionid,,并做session同步 ------ 》種cookie給自己,頁(yè)面原地重跳 (6),、打開自己頁(yè)面,,此時(shí)有了cookie,后臺(tái)校驗(yàn)登陸狀態(tài),,成功 (7)整個(gè)過(guò)程交互,,列圖如下: 4、的登陸頁(yè)面被打開時(shí),如果此時(shí)本來(lái)就是登陸狀態(tài)的,則自動(dòng)返回生成ticket給業(yè)務(wù)系統(tǒng) 整個(gè)單點(diǎn)登陸的關(guān)鍵部位,,是利用的cookie保持是登陸狀態(tài),此后任何第三個(gè)系統(tǒng)跳入,都將自動(dòng)完成登陸過(guò)程 5,本示例中,使用了redis來(lái)做cas的服務(wù)接口,請(qǐng)根據(jù)工作情況,自行替換為合適的服務(wù)接口(主要是根據(jù)sessionid來(lái)判斷用戶是否已登陸) 6,為提高安全性,ticket應(yīng)該使用過(guò)即作廢(本例中,會(huì)用有效期機(jī)制) public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; MyRequestWrapper myRequestWrapper = new MyRequestWrapper(request,redisTemplate); //如果未登陸狀態(tài),,進(jìn)入下面邏輯 String requestUrl = request.getServletPath(); if (!"/toLogin".equals(requestUrl) && !requestUrl.startsWith("/login") && !myRequestWrapper.isLogin()) { /** * ticket為空,或無(wú)對(duì)應(yīng)sessionid為空 * --- 表明不是自動(dòng)登陸請(qǐng)求--直接強(qiáng)制到登陸頁(yè)面 */ String ticket = request.getParameter("ticket"); if (null == ticket || null == redisTemplate.opsForValue().get(ticket)){ HttpServletResponse response = (HttpServletResponse)servletResponse; response.sendRedirect("http://:8090/toLogin?url="+request.getRequestURL().toString()); return ; } /** * 是自動(dòng)登陸請(qǐng)求,,則種cookie值進(jìn)去---本次請(qǐng)求是302重定向 * 重定向后的下次請(qǐng)求,,自帶本cookie,將直接是登陸狀態(tài) */ myRequestWrapper.setSessionId((String) redisTemplate.opsForValue().get(ticket)); myRequestWrapper.createSession(); //種cookie CookieBasedSession.onNewSession(myRequestWrapper,(HttpServletResponse)servletResponse); //重定向自流轉(zhuǎn)一次,,原地跳轉(zhuǎn)重向一次 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); } |
|
來(lái)自: 愛(ài)吃魚的俊懶貓 > 《IT》