SSO 轻量级实现指南(原生 Java 实现):SSO Client 部分

SSO 轻量级实现指南(原生 Java 实现):SSO Client 部分,第1张

根据单点登录的定义,客户端可以完全不用创建自己的用户系统,它只需要接入 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);
	// 存入 session
	req.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
 *
 */
@Component
public class SsoAccessTokenInterceptor implements HandlerInterceptor {
	@Override
	public 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;
		} else
			return 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>15session-timeout>
session-config>

Spring Boot 设置 yml

server:
   port: 8089
   session:
      timeout: 1800  #以秒为单位

Ja
va 设置:

session.setMaxInactiveInterval(30*60;//以秒为单位

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/790527.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-05
下一篇 2022-05-05

发表评论

登录后才能评论

评论列表(0条)

保存