栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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 params = new HashMap<>();
	params.put("code", code);
	params.put("grant_type", GRANT_TYPE);
	params.put("client_id", clientId);
	params.put("client_secret", clientSecret);

	Map 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;
}


static UserSession saveSession(Map 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 userJson = (Map) 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
@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;


@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;
	}

	
	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 配置一下。

  

      15

Spring Boot 设置 yml

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

Ja
va 设置:

session.setMaxInactiveInterval(30*60);//以秒为单位
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/842183.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号