OAuth2.0協議入門(三):OAuth2.0授權與單點登入(SSO)的區別以及單點登入服務端從設計到實現

zifangsky發表於2018-09-11

一 OAuth2.0授權與單點登入(SSO)的區別

在前兩篇文章中我介紹了OAuth2.0協議的基本概念(www.zifangsky.cn/1309.html)以及OAuth2.0授權服務端從設計到實現(www.zifangsky.cn/1313.html)。這篇文章中我將介紹OAuth2.0授權與單點登入的區別,這兩個概念看似很相似,實際上卻有很大區別,而很多人往往把二者混為一談。

什麼是單點登入?

單點登入的英文名是 Single Sign On,因此一般簡稱為SSO。它的用途在於,不管多麼複雜的應用群,只要在使用者許可權範圍內,那麼就可以做到,使用者只需要登入一次就可以訪問許可權範圍內的所有應用子系統。對於使用者而言,訪問多個應用子系統只需要登入一次,同樣在需要登出的時候也只需要登出一次。舉個簡單的例子,你在百度首頁登入成功之後,你再訪問百度百科、百度知道、百度貼吧等網站也會處於登入狀態了,這就是一個單點登入的真實案例。

OAuth2.0授權與單點登入的區別

根據OAuth2.0授權與單點登入的概念,我們可以得知二者至少存在以下幾點區別:

  1. 從信任角度來看。OAuth2.0授權服務端和第三方客戶端不屬於一個互相信任的應用群(通常都不是同一個公司提供的服務),第三方客戶端的使用者不屬於OAuth2.0授權服務端的官方使用者;而單點登入的服務端和接入的客戶端都在一個互相信任的應用群(通常是同一個公司提供的服務),各個子系統的使用者屬於單點登入服務端的官方使用者。
  2. 從資源角度來看。OAuth2.0授權主要是讓使用者自行決定——“我”在OAuth2.0服務提供方的個人資源是否允許第三方應用訪問;而單點登入的資源都在客戶端這邊,單點登入的服務端主要用於登入,以及管理使用者在各個子系統的許可權資訊。
  3. 從流程角度來看。OAuth2.0授權的時候,第三方客戶端需要拿預先“商量”好的密碼去獲取Access Token;而單點登入則不需要。

二 單點登入服務端的設計

對於一個接入單點登入的子系統而言,進行單點登入需要以下兩個步驟:

  1. client請求單點登入服務端,獲取Access Token
  2. client因為不能判斷給它的Access Token是單點登入服務端返回還是使用者偽造,所以需要再次請求單點登入服務端,校驗Access Token是否有效,如果有效則返回使用者基本資訊以及相應的使用者在client上所屬的角色、許可權等資訊

因此,單點登入服務端的設計主要圍繞這兩個介面展開,其主要流程是這樣的:

單點登入的主要流程

資料庫的表結構設計

提示:我在下面只介紹一些表的主要欄位,這個Demo中使用的完整的表結構可以參考:gitee.com/zifangsky/O…

(1)sso_client_details

接入單點登入的子系統詳情表。類似於百度的百度百科、百度知道、百度貼吧等應用子系統,每個想要接入單點登入的子系統都需要事先在服務端這裡“備案”。一方面是為了防止使用者在服務端登入成功生成的Access Token被重定向到非法網站,從而導致使用者的Access Token被竊取;另一方面是記錄接入的子系統的登出URL,便於開發單點登出功能。所以主要需要以下幾個欄位:

  • client_name:子系統的名稱
  • redirect_url:獲取Access Token成功後的回撥URL
  • logout_url:使用者在子系統的登出URL(使用者登入狀態可以分為:全域性登入——在單點登入服務端的登入狀態;區域性登入——在子系統的登入狀態,登出的時候需要同時登出使用者在單點登入服務端和應用子系統的登入狀態)

(2)sso_access_token

單點登入的Access Token資訊表。這個表主要體現出哪個使用者在哪個子系統登入,以及生成的令牌的結束日期是哪天。所以主要需要以下幾個欄位:

  • access_tokenAccess Token欄位
  • user_id:表明是哪個使用者登入
  • client_id:表明是在哪個子系統登入
  • expires_in:過期時間戳,表明這個Token在哪一天過期

(3)sso_refresh_token

單點登入的Refresh Token資訊表。這個表主要用來記錄Refresh Token,在設計表結構的時候需要關聯它對應的sso_access_token表的記錄。所以主要需要以下幾個欄位:

  • refresh_tokenRefresh Token欄位
  • token_id:它對應的sso_access_token表的記錄
  • expires_in:過期時間戳

三 單點登入服務端主要介面的程式碼實現

這個Demo的單點登入服務端的完整可用原始碼可以參考:gitee.com/zifangsky/O…

(1)獲取Access Token:

client請求單點登入服務端,獲取Access Token,獲取完成之後重定向到請求中的回撥URL。

介面地址http://127.0.0.1:7000/sso/token?redirect_uri=http://192.168.197.130:6080/login

/**
 * 獲取Token
 * @author zifangsky
 * @date 2018/8/30 16:30
 * @since 1.0.0
 * @param request HttpServletRequest
 * @return org.springframework.web.servlet.ModelAndView
 */
@RequestMapping("/token")
public ModelAndView authorize(HttpServletRequest request){
	HttpSession session = request.getSession();
	User user = (User) session.getAttribute(Constants.SESSION_USER);

	//過期時間
	Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
	//回撥URL
	String redirectUri = request.getParameter("redirect_uri");
	//查詢接入客戶端
	SsoClientDetails ssoClientDetails = ssoService.selectByRedirectUrl(redirectUri);

	//獲取使用者IP
	String requestIp = SpringContextUtils.getRequestIp(request);

	//生成Access Token
	String accessTokenStr = ssoService.createAccessToken(user, expiresIn, requestIp, ssoClientDetails);
	//查詢已經插入到資料庫的Access Token
	SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessTokenStr);
	//生成Refresh Token
	String refreshTokenStr = ssoService.createRefreshToken(user, ssoAccessToken);
	logger.info(MessageFormat.format("單點登入獲取Token:username:【{0}】,channel:【{1}】,Access Token:【{2}】,Refresh Token:【{3}】"
			,user.getUsername(),ssoClientDetails.getClientName(),accessTokenStr,refreshTokenStr));

	String params = "?code=" + accessTokenStr;
	return new ModelAndView("redirect:" + redirectUri + params);
}
複製程式碼

相應地,呼叫的cn/zifangsky/service/impl/SsoServiceImpl.java類裡面的生成邏輯:

@Override
public String createAccessToken(User user, Long expiresIn, String requestIP, SsoClientDetails ssoClientDetails) {
	Date current = new Date();
	//過期的時間戳
	Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.ACCESS_TOKEN.getTime(), null);

	//1. 拼裝待加密字串(username + 渠道CODE + 當前精確到毫秒的時間戳)
	String str = user.getUsername() + ssoClientDetails.getClientName() + String.valueOf(DateUtils.currentTimeMillis());

	//2. SHA1加密
	String accessTokenStr = "11." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;

	//3. 儲存Access Token
   SsoAccessToken savedAccessToken = ssoAccessTokenMapper.selectByUserIdAndClientId(user.getId(), ssoClientDetails.getId());
	//如果存在匹配的記錄,則更新原記錄,否則向資料庫中插入新記錄
	if(savedAccessToken != null){
		savedAccessToken.setAccessToken(accessTokenStr);
		savedAccessToken.setExpiresIn(expiresAt);
		savedAccessToken.setUpdateUser(user.getId());
		savedAccessToken.setUpdateTime(current);
		ssoAccessTokenMapper.updateByPrimaryKeySelective(savedAccessToken);
	}else{
		savedAccessToken = new SsoAccessToken();
		savedAccessToken.setAccessToken(accessTokenStr);
		savedAccessToken.setUserId(user.getId());
		savedAccessToken.setUserName(user.getUsername());
		savedAccessToken.setIp(requestIP);
		savedAccessToken.setClientId(ssoClientDetails.getId());
		savedAccessToken.setChannel(ssoClientDetails.getClientName());
		savedAccessToken.setExpiresIn(expiresAt);
		savedAccessToken.setCreateUser(user.getId());
		savedAccessToken.setUpdateUser(user.getId());
		savedAccessToken.setCreateTime(current);
		savedAccessToken.setUpdateTime(current);
		ssoAccessTokenMapper.insertSelective(savedAccessToken);
	}

	//4. 返回Access Token
	return accessTokenStr;
}

@Override
public String createRefreshToken(User user, SsoAccessToken ssoAccessToken) {
	Date current = new Date();
	//過期時間
	Long expiresIn = DateUtils.dayToSecond(ExpireEnum.REFRESH_TOKEN.getTime());
	//過期的時間戳
	Long expiresAt = DateUtils.nextDaysSecond(ExpireEnum.REFRESH_TOKEN.getTime(), null);

	//1. 拼裝待加密字串(username + accessToken + 當前精確到毫秒的時間戳)
	String str = user.getUsername() + ssoAccessToken.getAccessToken() + String.valueOf(DateUtils.currentTimeMillis());

	//2. SHA1加密
	String refreshTokenStr = "12." + EncryptUtils.sha1Hex(str) + "." + expiresIn + "." + expiresAt;

	//3. 儲存Refresh Token
	SsoRefreshToken savedRefreshToken = ssoRefreshTokenMapper.selectByTokenId(ssoAccessToken.getId());
	//如果存在tokenId匹配的記錄,則更新原記錄,否則向資料庫中插入新記錄
	if(savedRefreshToken != null){
		savedRefreshToken.setRefreshToken(refreshTokenStr);
		savedRefreshToken.setExpiresIn(expiresAt);
		savedRefreshToken.setUpdateUser(user.getId());
		savedRefreshToken.setUpdateTime(current);
		ssoRefreshTokenMapper.updateByPrimaryKeySelective(savedRefreshToken);
	}else{
		savedRefreshToken = new SsoRefreshToken();
		savedRefreshToken.setTokenId(ssoAccessToken.getId());
		savedRefreshToken.setRefreshToken(refreshTokenStr);
		savedRefreshToken.setExpiresIn(expiresAt);
		savedRefreshToken.setCreateUser(user.getId());
		savedRefreshToken.setUpdateUser(user.getId());
		savedRefreshToken.setCreateTime(current);
		savedRefreshToken.setUpdateTime(current);
		ssoRefreshTokenMapper.insertSelective(savedRefreshToken);
	}

	//4. 返回Refresh Tokens
	return refreshTokenStr;
}
複製程式碼

(2)校驗Access Token,並返回使用者資訊:

client在獲取到Access Token後,再次呼叫單點登入服務端介面,用於“校驗Access Token,並返回使用者資訊”。

介面地址http://127.0.0.1:7000/sso/verify?access_token=11.ad51132688b5be3f476592356c78aef71d235f07.2592000.1539143183

返回如下

{
	"access_token": "11.ad51132688b5be3f476592356c78aef71d235f07.2592000.1539143183",
	"refresh_token": "12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183",
	"expires_in": 2592000,
	"user_info": {
		"id": 2,
		"username": "zifangsky",
		"password": "$5$toOBSeX2$hSnSDyhJmVVRpbmKuIY4SxDgyeGRGacQaBYGrtEBnZA",
		"mobile": "110",
		"email": "admin@zifangsky.cn",
		"createTime": "2017-12-31T16:00:00.000+0000",
		"updateTime": "2017-12-31T16:00:00.000+0000",
		"status": 1,
		"roles": [{
				"id": 2,
				"roleName": "user",
				"description": "普通使用者",
				"funcs": null
			}
		]
	}
}
複製程式碼

首先在一個攔截器裡校驗Access Token是否有效:

package cn.zifangsky.interceptor;

import cn.zifangsky.enums.ErrorCodeEnum;
import cn.zifangsky.model.SsoAccessToken;
import cn.zifangsky.service.SsoService;
import cn.zifangsky.utils.DateUtils;
import cn.zifangsky.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * 用於校驗Access Token是否為空以及Access Token是否已經失效
 *
 * @author zifangsky
 * @date 2018/8/30
 * @since 1.0.0
 */
public class SsoAccessTokenInterceptor extends HandlerInterceptorAdapter{
    @Resource(name = "ssoServiceImpl")
    private SsoService ssoService;

    /**
     * 檢查Access Token是否已經失效
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String accessToken = request.getParameter("access_token");

        if(StringUtils.isNoneBlank(accessToken)){
            //查詢資料庫中的Access Token
            SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessToken);

            if(ssoAccessToken != null){
                Long savedExpiresAt = ssoAccessToken.getExpiresIn();
                //過期日期
                LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);
                //當前日期
                LocalDateTime nowDateTime = DateUtils.now();

                //如果Access Token已經失效,則返回錯誤提示
                return expiresDateTime.isAfter(nowDateTime) || this.generateErrorResponse(response, ErrorCodeEnum.EXPIRED_TOKEN);
            }else{
                return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_GRANT);
            }
        }else{
            return this.generateErrorResponse(response, ErrorCodeEnum.INVALID_REQUEST);
        }
    }
    
    /**
     * 組裝錯誤請求的返回
     */
    private boolean generateErrorResponse(HttpServletResponse response,ErrorCodeEnum errorCodeEnum) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        Map<String,String> result = new HashMap<>(2);
        result.put("error", errorCodeEnum.getError());
        result.put("error_description",errorCodeEnum.getErrorDescription());

        response.getWriter().write(JsonUtils.toJson(result));
        return false;
    }

}
複製程式碼

然後返回使用者資訊:

/**
 * 校驗Access Token,並返回使用者資訊
 * @author zifangsky
 * @date 2018/8/30 16:07
 * @since 1.0.0
 * @param request HttpServletRequest
 * @return java.util.Map<java.lang.String,java.lang.Object>
 */
@RequestMapping(value = "/verify", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> verify(HttpServletRequest request) {
	Map<String, Object> result = new HashMap<>(8);

	//獲取Access Token
	String accessToken = request.getParameter("access_token");

	try {
		//過期時間
		Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
		//查詢Access Token
		SsoAccessToken ssoAccessToken = ssoService.selectByAccessToken(accessToken);
		//查詢Refresh Token
		SsoRefreshToken ssoRefreshToken = ssoService.selectByTokenId(ssoAccessToken.getId());
		//查詢使用者資訊
		UserBo userBo = userService.selectUserBoByUserId(ssoAccessToken.getUserId());

		//組裝返回資訊
		result.put("access_token", ssoAccessToken.getAccessToken());
		result.put("refresh_token", ssoRefreshToken.getRefreshToken());
		result.put("expires_in", expiresIn);
		result.put("user_info", userBo);
		return result;
	}catch (Exception e){
		logger.error(e.getMessage());
		this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);
		return result;
	}
}
複製程式碼

(3)通過Refresh Token重新整理Access Token介面:

如果客戶端的Access Token過期了,那麼就可以通過這個介面重新整理Access Token。

介面地址http://127.0.0.1:7000/sso/refreshToken?refresh_token=12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183

返回如下

{
	"access_token": "11.40f0270697c37db4570e41e0f6f335bf6c2f8902.2592000.1539164947",
	"refresh_token": "12.c10cb9001bf0e2c7f808580318715fc089673279.31536000.1568087183",
	"expires_in": 2592000,
	"user_info": {
		"id": 2,
		"username": "zifangsky",
		"password": "$5$toOBSeX2$hSnSDyhJmVVRpbmKuIY4SxDgyeGRGacQaBYGrtEBnZA",
		"mobile": "110",
		"email": "admin@zifangsky.cn",
		"createTime": "2017-12-31T16:00:00.000+0000",
		"updateTime": "2017-12-31T16:00:00.000+0000",
		"status": 1,
		"roles": [{
				"id": 2,
				"roleName": "user",
				"description": "普通使用者",
				"funcs": null
			}
		]
	}
}
複製程式碼
/**
 * 通過Refresh Token重新整理Access Token
 * @author zifangsky
 * @date 2018/8/30 16:07
 * @since 1.0.0
 * @param request HttpServletRequest
 * @return java.util.Map<java.lang.String,java.lang.Object>
 */
@RequestMapping(value = "/refreshToken", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> refreshToken(HttpServletRequest request){
	Map<String,Object> result = new HashMap<>(8);

	//獲取Refresh Token
	String refreshTokenStr = request.getParameter("refresh_token");
	//獲取使用者IP
	String requestIp = SpringContextUtils.getRequestIp(request);

	try {
		SsoRefreshToken ssoRefreshToken = ssoService.selectByRefreshToken(refreshTokenStr);

		if(ssoRefreshToken != null) {
			Long savedExpiresAt = ssoRefreshToken.getExpiresIn();
			//過期日期
			LocalDateTime expiresDateTime = DateUtils.ofEpochSecond(savedExpiresAt, null);
			//當前日期
			LocalDateTime nowDateTime = DateUtils.now();

			//如果Refresh Token已經失效,則需要重新生成
			if (expiresDateTime.isBefore(nowDateTime)) {
				this.generateErrorResponse(result, ErrorCodeEnum.EXPIRED_TOKEN);
				return result;
			} else {
				//獲取儲存的Access Token
				SsoAccessToken ssoAccessToken = ssoService.selectByAccessId(ssoRefreshToken.getTokenId());
				//查詢接入客戶端
				SsoClientDetails ssoClientDetails = ssoService.selectByPrimaryKey(ssoAccessToken.getClientId());
				//獲取對應的使用者資訊
				User user = userService.selectByUserId(ssoAccessToken.getUserId());

				//新的過期時間
				Long expiresIn = DateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
				//生成新的Access Token
				String newAccessTokenStr = ssoService.createAccessToken(user, expiresIn, requestIp, ssoClientDetails);
				//查詢使用者資訊
				UserBo userBo = userService.selectUserBoByUserId(ssoAccessToken.getUserId());

				logger.info(MessageFormat.format("單點登入重新重新整理Token:username:【{0}】,requestIp:【{1}】,old token:【{2}】,new token:【{3}】"
						,user.getUsername(),requestIp,ssoAccessToken.getAccessToken(),newAccessTokenStr));

				//組裝返回資訊
				result.put("access_token", newAccessTokenStr);
				result.put("refresh_token", ssoRefreshToken.getRefreshToken());
				result.put("expires_in", expiresIn);
				result.put("user_info", userBo);
				return result;
			}
		}else {
			this.generateErrorResponse(result, ErrorCodeEnum.INVALID_GRANT);
			return result;
		}
	}catch (Exception e){
		e.printStackTrace();
		this.generateErrorResponse(result, ErrorCodeEnum.UNKNOWN_ERROR);
		return result;
	}
}

/**
 * 組裝錯誤請求的返回
 */
private void generateErrorResponse(Map<String,Object> result, ErrorCodeEnum errorCodeEnum) {
	result.put("error", errorCodeEnum.getError());
	result.put("error_description",errorCodeEnum.getErrorDescription());
}
複製程式碼

(4)單點登出介面:

在這個Demo專案中,我沒有提供單點登出功能的示例程式碼,但是我可以簡單說一下單點登出功能的主要流程,如果需要這個功能可以自行使用程式碼實現:

  1. 使用者在應用子系統請求登出登入;
  2. 使用者在應用子系統登出完成後,應用子系統後臺請求單點登入服務端的登出介面;
  3. 單點登入服務端的登出介面根據使用者的Token在資料庫中查詢當前使用者登入的所有應用子系統的登出介面,然後依次呼叫登出即可。

四 接入單點登入的子系統的關鍵程式碼

這個Demo的單點登入子系統的完整可用原始碼可以參考:gitee.com/zifangsky/O…

其實,對於接入單點登入的子系統來說,登入模組呼叫單點登入服務端提供的介面就可以了。

登入校驗過濾器:

package cn.zifangsky.interceptor;

import cn.zifangsky.common.Constants;
import cn.zifangsky.common.SpringContextUtils;
import cn.zifangsky.model.bo.UserBo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 定義一些頁面需要做登入檢查
 *
 * @author zifangsky
 * @date 2018/7/26
 * @since 1.0.0
 */
public class LoginInterceptor extends HandlerInterceptorAdapter{

    /**
     * 檢查是否已經登入
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();

        //獲取session中儲存的使用者資訊
        UserBo user = (UserBo) session.getAttribute(Constants.SESSION_USER);

        if(user != null){
            return true;
        }else{
            //如果token不存在,則跳轉等登入頁面
            response.sendRedirect(request.getContextPath() + "/login?redirectUrl=" + SpringContextUtils.getRequestUrl(request));

            return false;
        }
    }
}
複製程式碼

登入相關的程式碼邏輯:

package cn.zifangsky.controller;

import cn.zifangsky.common.Constants;
import cn.zifangsky.model.SsoResponse;
import cn.zifangsky.model.User;
import cn.zifangsky.utils.CookieUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 登入
 * @author zifangsky
 * @date 2018/7/9
 * @since 1.0.0
 */
@Controller
public class LoginController {

    @Autowired
    private RestTemplate restTemplate;

    @Value("${own.sso.access-token-uri}")
    private String accessTokenUri;

    @Value("${own.sso.verify-uri}")
    private String verifyUri;

    /**
     * 登入驗證(實際登入呼叫認證伺服器)
     * @author zifangsky
     * @date 2018/8/30 18:02
     * @since 1.0.0
     * @param request HttpServletRequest
     * @return org.springframework.web.servlet.ModelAndView
     */
    @RequestMapping("/login")
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response){
        //當前系統登入成功之後的回撥URL
        String redirectUrl = request.getParameter("redirectUrl");
        //當前系統請求認證伺服器成功之後返回的Token
        String code = request.getParameter("code");

        //最後重定向的URL
        String resultUrl = "redirect:";
        HttpSession session = request.getSession();

        //1. code為空,則說明當前請求不是認證伺服器的回撥請求,則重定向URL到認證伺服器登入
        if(StringUtils.isBlank(code)){
            //如果存在回撥URL,則將這個URL新增到session
            if(StringUtils.isNoneBlank(redirectUrl)){
                session.setAttribute(Constants.SESSION_LOGIN_REDIRECT_URL,redirectUrl);
            }

            //拼裝請求Token的地址
            resultUrl += accessTokenUri;
        }else{
            //2. 驗證Token,並返回使用者基本資訊、所屬角色、訪問許可權等
            SsoResponse verifyResponse = restTemplate.getForObject(verifyUri, SsoResponse.class
                    ,code);

            //如果正常返回
            if(StringUtils.isNoneBlank(verifyResponse.getAccess_token())){
                //2.1 將使用者資訊存到session
                session.setAttribute(Constants.SESSION_USER,verifyResponse.getUser_info());

                //2.2 將Access Token和Refresh Token寫到cookie
                CookieUtils.addCookie(response,Constants.COOKIE_ACCESS_TOKEN, verifyResponse.getAccess_token(),request.getServerName());
                CookieUtils.addCookie(response,Constants.COOKIE_REFRESH_TOKEN, verifyResponse.getRefresh_token(),request.getServerName());
            }

            //3. 從session中獲取回撥URL,並返回
            redirectUrl = (String) session.getAttribute(Constants.SESSION_LOGIN_REDIRECT_URL);
            session.removeAttribute("redirectUrl");
            if(StringUtils.isNoneBlank(redirectUrl)){
                resultUrl += redirectUrl;
            }else{
                resultUrl += "/user/userIndex";
            }
        }

        return new ModelAndView(resultUrl);
    }

}
複製程式碼

當然,上面程式碼中使用到的一些配置就是我們單點登入服務端的介面地址:

own.sso.access-token-uri=http://10.0.5.22:7000/sso/token?redirect_uri=http://192.168.197.130:6080/login
own.sso.verify-uri=http://10.0.5.22:7000/sso/verify?access_token={1}
複製程式碼

測試:

  1. SsoClientDemo專案部署在跟ServerDemo專案不同的伺服器;
  2. 第一次啟動SsoClientDemo專案並訪問需要登入的頁面,比如:http://192.168.197.130:6080/user/userIndex
  3. 可以發現這時跳轉到ServerDemo專案,在服務端登入成功之後,跳轉到SsoClientDemo專案/user/userIndex,說明客戶端也登入成功了;
  4. 重啟SsoClientDemo專案,並再次訪問http://192.168.197.130:6080/user/userIndex,可以發現這次是直接登入了(當然也可以把SsoClientDemo專案部署到多個伺服器上面,先後登入檢視效果),這說明單點登入功能已經實現。

本篇文章到此結束,感謝大家的閱讀。

參考:

相關文章