基於Oauth2.0實現SSO單點認證

天下寒發表於2020-12-24

什麼是單點?

借用百度百科的話:

單點登入(Single Sign On),簡稱為 SSO,是比較流行的企業業務整合的解決方案之一。SSO的定義是在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統。

單點登入圖解:

什麼是Oauth2.0?

OAuth 就是一種授權機制。資料的所有者告訴系統,同意授權第三方應用進入系統,獲取這些資料。系統從而產生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。

引用阮一峰老師的文章:OAuth 2.0 的四種方式 - 阮一峰的網路日誌

正文:

前段時間在開發自己部落格的時候,需要用到登入,當時的第一想法是做簡單的密碼驗證就行了。不過為了熟悉一下SSO的流程,還是準備試用一下。

讓我從SSO轉變為Oauth的地方,是因為:SSO的限制,SSO需要在同一主域名下才能有效地寫入cookie例如   www.baidu.com,如果試用SSO ,SSO的域名就需要是 ***.baidu.com。當然,不排除有其他方法可以實現cookie的寫入。但正常的SSO流程就是需要在同一主域名下。

而Oauth2.0並沒有限制,而且Oauth2.0更符合我對單點登入的認知。

開發流程:

1:授權系統開發

授權系統,這裡其實就當做是一個最簡單的登入驗證器使用就行,有一個基礎的增刪改查就行

package com.hao.sso.vali.controller;

import java.util.concurrent.TimeoutException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.hao.bios.core.cache.ZboxCacheUtil;
import com.hao.bios.core.utils.StringUtil;
import com.hao.bios.core.utils.ZboxResult;
import com.hao.sso.vali.SsoConstants;
import com.hao.sso.vali.service.SsoLoginService;
import com.hao.sso.vali.vo.SsoLoginValiVO;
import com.hao.sso.vali.vo.ZboxSsoAuthPassVO;

import net.rubyeye.xmemcached.exception.MemcachedException;

@Controller
@RequestMapping("/sso/login")
public class LoginController {
	
	
	@Autowired
	private ZboxCacheUtil cache;
	@Autowired
	private SsoLoginService ssoLoginService;

	@RequestMapping("/isLogin")
	@ResponseBody
	public boolean isLogin(HttpServletRequest req, String accessToken) {
		
		if (StringUtil.isEmpty(accessToken)) {
			return false;
		}
		ZboxSsoAuthPassVO authVO = cache.get(SsoConstants.LOGIN_AUTH_VALI_CACHE_POOL, accessToken, ZboxSsoAuthPassVO.class);
		if (authVO == null) {
			return false;
		}
		long outTime = authVO.getExpires();
		long nowTime = System.currentTimeMillis();
		if (outTime!=0 && nowTime > outTime) {
			return false;
		}
		return true;
	}
	
	
	
	@RequestMapping("/login")
	@ResponseBody
	public String login(ServletRequest request, ServletResponse response, String userName, String pwd) throws TimeoutException, InterruptedException, MemcachedException {
		HttpServletRequest req = (HttpServletRequest) request;
		boolean isLogin = ssoLoginService.checkLogin(userName, pwd);
		if (isLogin) {
			SsoLoginValiVO valiVO = new SsoLoginValiVO();
			String ip = getIpAddress(req);
			valiVO.setIp(ip);
			valiVO.setIsLogin(true);
			valiVO.setLoginTime(StringUtil.getNowTimeString());
			valiVO.setOutTime(0);
			valiVO.setUserId(userName);
			String key = StringUtil.getUUID();
			cache.put(SsoConstants.LOGIN_VALI_CACHE_POOL, key, valiVO);
			String sign = StringUtil.getUUID();
			cache.put(SsoConstants.LOGIN_AUTH_CACHE_POOL, sign, key);
			return sign;
		}
		return "";
	}
	
	@RequestMapping("/getAuthPass")
	@ResponseBody
	public ZboxResult getAuthPass(ServletRequest request, ServletResponse response, String sign) throws TimeoutException, InterruptedException, MemcachedException {
		ZboxResult rul = new ZboxResult();
		String key = cache.get(SsoConstants.LOGIN_AUTH_CACHE_POOL, sign, String.class);
		if (StringUtil.isEmpty(key)) {
			rul.setSuccess(false);
			rul.setMessage("授權碼無效");
			return rul;
		}
		SsoLoginValiVO vali = cache.get(SsoConstants.LOGIN_VALI_CACHE_POOL, key, SsoLoginValiVO.class);
		if (vali == null) {
			rul.setSuccess(false);
			rul.setMessage("未登入");
			return rul;
		}
		if (vali.getIsLogin() == false) {
			rul.setSuccess(false);
			rul.setMessage("登陸失效");
			return rul;
		}
		long outTime = vali.getOutTime();
		long nowTime = System.currentTimeMillis();
		if (outTime!=0 && nowTime > outTime) {
			rul.setSuccess(false);
			rul.setMessage("登陸超時");
			return rul;
		}
		
		String accessToken = StringUtil.getUUID();
		long expires = System.currentTimeMillis() + 60*60*1000L;
		String refreshToken = StringUtil.getUUID();
		//獲取使用者資訊
		String info = "";
		ZboxSsoAuthPassVO authVO= new ZboxSsoAuthPassVO();
		authVO.setAccessToken(accessToken);
		authVO.setExpires(expires);
		authVO.setInfo(info);
		authVO.setRefreshToken(refreshToken);
		authVO.setScope("all");
		authVO.setTokenType("zbox_user_pass");
		authVO.setUid(vali.getUserId());
		cache.put(SsoConstants.LOGIN_AUTH_VALI_CACHE_POOL, accessToken, authVO);
		rul.setSuccess(true);
		rul.setData(authVO);
		return rul;
	}
	
	@RequestMapping("/loginOut")
	@ResponseBody
	public boolean loginOut(ServletRequest request, String accessToken) throws TimeoutException, InterruptedException, MemcachedException {
		ZboxSsoAuthPassVO authVO = cache.get(SsoConstants.LOGIN_AUTH_VALI_CACHE_POOL, accessToken, ZboxSsoAuthPassVO.class);
		if (authVO != null) {
			cache.put(SsoConstants.LOGIN_AUTH_VALI_CACHE_POOL, accessToken, null);
			cache.delete(SsoConstants.LOGIN_AUTH_VALI_CACHE_POOL, accessToken);
		}else {
			return false;
		}
		return true;
	}
	
	public String getSsoCookie(HttpServletRequest req) {
		Cookie[] cookies = req.getCookies();
		if (cookies!=null) {
			for (Cookie cookie : cookies) {
				String name = cookie.getName();
				if (SsoConstants.SSO_LOGIN_VALI_COOKIE_KEY.equals(name)) {
					return cookie.getValue();
				}
			}
		}
		return "";
	}
	
	public String getIpAddress(HttpServletRequest request) {
		String ip = request.getHeader("X-Forward-For");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("x-forwarded-for");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
}

具體其實就是幾個介面:登入、登出、驗證登入、授權令牌

2:業務系統接入

直接上邏輯流程:

1:使用者訪問業務系統

2:業務系統先驗證本地cookie是否有效,

        無效:直接返回前臺未登入;

        有效:訪問認證系統,檢查登入情況,未登入則返回業務系統,業務系統返回前臺未登入

3:業務檢查到未登入,跳轉到認證系統登入頁面進行登入,登入成功之後認證系統返回一個授權碼

4:業務系統攜帶授權碼,在後端使用雙方協議的id和祕鑰(授權系統給業務系統的一個賬戶密碼)攜帶著授權碼,去認證系統獲取令牌。

5:業務系統拿到令牌後使用令牌訪問認證系統的api。

詳情請檢視阮一峰日誌:GitHub OAuth 第三方登入示例教程

相關文章