SSO 轻量级实现指南(原生 Java 实现):SSO Client 部分
根據單點登錄的定義,客戶端可以完全不用創建自己的用戶系統,它只需要接入 SSO 中心的服務就好。SSO 中心關于用戶的常規業務都在其內。那么客戶端接入單點登錄,需要做什么工作呢?首先用戶一般常規操作有:
- 用戶注冊。這部分 SSO 中心提供注冊接口。客戶端自定義自己風格注冊 UI,跨域請求數據到 SSO 中心接口即可;
- 用戶登錄。這部分 SSO 中心提供登錄接口。客戶端自定義自己風格登錄 UI,跨域請求數據到 SSO 中心接口即可;
- 用戶注銷登陸。這部分 SSO 中心提供登錄接口,跨域請求數據到 SSO 中心接口即可;
- 用戶常規查詢操作,例如查詢列表、單個用戶詳情等,這部分 SSO 中心開放相關 API。
一般常規接口上文已經討論過了。可見 SSO 中心一個特性要求便是允許“跨域訪問”,這個問題不大,進行相關配置即可。
SSO 中心,即認證中心,關鍵一點在于用戶的認證。除了上述登錄是重要的認證過程外,每次涉及相關操作都必須進行認證,否則就是非法訪問。
認證的問題
如果按照 OAuth 本來的目的,資源服務器跟認證服務器是在一塊的,比如說微博,它有個開放平臺你可以根據 AccessToken 獲取它微博內容。每次訪問都有提供 AccessToken 參數,看是否合法才允許訪問。
但目前我們搞的不是純粹 OAuth,上文《SSO 與 OAuth 傻傻分不清?》小節已經說過了。SSO 認證中心往往不是跟資源服務器在一起的“單體”結構,而是獨立部署的;而且應用端(即客戶端)肯定都有自己的資源服務,肯定需要用戶認證、權限校驗之類的操作。那么問題來了,校驗客戶端憑證令牌(即 AccessToken)這項工作,——是放在應用端還是 SSO 中心呢?
顯然易見,作為統一的認證中心,SSO 中心無疑擁有最根本的用戶狀態記錄,一切皆以 SSO 中心的為準。但每次訪問資源的認證工作都要通訊 SSO 中心,性能成本會不會太高呢?對于 SSO 中心服務器的性能也是嚴重的考驗。對此,筆者考慮了以下幾個個解決方案。
- 還是在 SSO 中心校驗,但采取優化手段:對已驗證的 token 進行緩存,僅首次訪問時調用 SSO 驗證一次,一般緩存10分鐘這種,便于 SSO 進行 token 撤銷。
- 無須 SSO 校驗 token,采用自描述的 token。這種自描述的 Token 比普通的 Token 的復雜,解密之后包含了更多的信息,根據這些信息對比、校驗便能清楚是否合法,以及一定的用戶信息。舉個例子,如“重置密碼”,在郵件中包含一個帶 token 的連接,后端得到這 token 后其實有時間戳的信息的,再對比一下便能知道是否超時的請求。
- 采用自描述的 Token,其實跟大家說 JWT 就可以了,它就是干這事的。不過筆者說實話還不太懂 JWT,當前方案中還沒有使用 JWT。
- 應用端自建用戶登錄會話。其實就是冗余一套 SSO 中心的,用戶登錄之后回來馬上搞自己的 Session。但怎么同步是個問題,而且隱約好像不是“單點”的意思了。當前我正在使用這方案。
應用端自建用戶登錄會話
既然選定了這個方案,那我們就看看怎么做吧。首先是用戶登錄之后馬上建立 Session。源碼在這里。
這屬于客戶端登錄的一部分,得到授權碼之后在服務端發起請求。
@GetMapping(value = "clientLogin", produces = JSON) public String clientLogin(@RequestParam String code, HttpServletRequest req) {Map<String, Object> params = new HashMap<>();params.put("code", code);params.put("grant_type", GRANT_TYPE);params.put("client_id", clientId);params.put("client_secret", clientSecret);Map<String, Object> result = Post.api(api + "/sso/authorize", params);UserSession saveSession = saveSession(result);// 存入 sessionreq.getSession().setAttribute(saveSession.accessToken.getAccessToken(), saveSession);return "${User.home}".equals(userHome) ? toJson(result) : "redirect:/" + userHome; }/*** JSON 結果轉換為 Session 存儲,形成本地登錄狀態* * @param result* @return*/ static UserSession saveSession(Map<String, Object> result) {AccessToken accessToken = new AccessToken();accessToken.setAccessToken(result.get("access_token").toString());accessToken.setRefreshToken(result.get("refresh_token").toString());accessToken.setScope(result.get("scope").toString());accessToken.setExpiresIn(((Integer) result.get("expires_in")).longValue());@SuppressWarnings("unchecked")Map<String, Object> userJson = (Map<String, Object>) result.get("user");User user = MapTool.map2Bean(userJson, User.class, true);UserSession userSession = new UserSession();userSession.accessToken = accessToken;userSession.user = user;return userSession; }若登錄成功,就在客戶端本地產生 Session。其中重點就是 UserSession ,它包含了用戶和 AccessToken 兩種對象,以 Token 為 key 存到 Session 中。
校驗攔截 Token
有了本地的用戶登錄狀態,就無須訪問 SSO 中心校驗了,于是也變得簡單和高效了。所有校驗都發生在本地進行。我們看看這個攔截器 SsoAccessTokenInterceptor,它是標準的 Spring 攔截器。
你先需要在 yaml 配置中定義一下要保護資源的訪問路徑,即接口,按照 Spring 攔截器的配置。
User:resources: /api/**, /user/** # 要保護的資源excludeResources: /user/login/** # 排除的路徑記得路徑后面要加上 ** 同貝所有子路徑。
/*** 要保護的資源(只有登錄了才能訪問)*/ @Value("${User.resources}") private String[] protectPerfix;/*** 要保護的資源(只有登錄了才能訪問)*/ @Value("${User.excludeResources}") private String[] excludeResources;/*** 加入攔截器*/ @Override public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tokenInterceptor).addPathPatterns(protectPerfix).excludePathPatterns(excludeResources);super.addInterceptors(registry); }攔截器代碼
import java.io.IOException; import java.time.LocalDateTime;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor;import com.ajaxjs.framework.BaseController; import com.ajaxjs.user.sso.model.AccessToken; import com.ajaxjs.user.sso.model.UserSession; import com.ajaxjs.util.date.LocalDateUtils;/*** 校驗 AccessToken 的攔截器* * @author Frank Cheung<sp42@qq.com>**/ @Component public class SsoAccessTokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {String accessToken = req.getParameter("access_token");if (!StringUtils.hasText(accessToken)) {err(resp, "缺少 access_token 參數");return false;}Object object = req.getSession().getAttribute(accessToken);if (object == null) {// TODO 是否拿 Token 去 SSO 中心再校驗一下err(resp, "非法 AccessToken");return false;} else {}UserSession userSess = (UserSession) object;// 如果 Access Token 已經失效,則返回錯誤提示if (checkIfExpire(userSess.accessToken)) {// TODO 是否要刪除過期 token?err(resp, "access_token 已超時");return false;} elsereturn true;}/*** 獲取 expiresIn 與當前時間對比,看是否超時* * @param token 令牌* @return true 表示超時*/static boolean checkIfExpire(AccessToken token) {long expiresIn = token.getExpiresIn();LocalDateTime expiresDateTime = LocalDateUtils.ofEpochSecond(expiresIn);// 過期日期return expiresDateTime.isBefore(LocalDateTime.now());}static void err(HttpServletResponse resp, String msg) {resp.setStatus(HttpStatus.UNAUTHORIZED.value());resp.setHeader("Content-type", "application/json;charset=UTF-8");try {resp.getWriter().write(BaseController.jsonNoOk(msg));} catch (IOException e) {e.printStackTrace();}} }SSO Client
上面所述的所有代碼都在 SSO Client 這個工程中,可以通過 Maven 加入到你的工程中。
設置 Session 超時時間,在 web.xml 配置一下。
<!-- 時間單位為分鐘 --> <session-config><session-timeout>15</session-timeout> </session-config>Spring Boot 設置 yml
server:port: 8089session:timeout: 1800 #以秒為單位Ja
va 設置:
總結
以上是生活随笔為你收集整理的SSO 轻量级实现指南(原生 Java 实现):SSO Client 部分的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 启动ps时,老是出现一个提示框:不能输入
- 下一篇: LED格栅灯市场现状及未来发展趋势分析