单点登录
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。一句话概括:一次登陆,处处访问。
解决:
- 用户身份信息独立管理,更好的分布式管理。
- 可以自己扩展安全策略
- 跨域问题
单点登录容易造成认证服务器的压力过大,因为单点登录的核心是需要一个认证中心来验证和生成token,大量的请求容易造成服务器压力过大。
认证中心需要做的就是将请求拦截下来,验证token的有效性,如果token不正确或为空打回到登录页面重新登陆。
流程:
认证中心步骤:
- 用接受的用户名密码核对后台数据库。
- 将用户信息加载到写入redis,redis中有该用户视为登录状态。
- 用userId+当前用户登录ip地址+密钥生成token。
- 重定向用户到之前的来源地址,同时把token作为参数附上。
生成token
利用JWT工具生成token,一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT。
- 公共部分主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
- 私有部分是用户自定义的内容,根据实际需要真正要封装的信息。
- 签名部分根据用户信息+盐值+密钥生成的签名。如果想知道JWT是否是真实的只要把JWT的信息取出来,加上盐值和服务器中的密钥就可以验证真伪。所以不管由谁保存JWT,只要没有密钥就无法伪造。
验证token
用JWT解析,当业务模块某个页面要检查当前用户是否登录时,提交到认证中心,认证中心进行检查校验,返回登录状态、用户Id和用户名称。
步骤:
- 利用密钥和IP检验token是否正确,并获得里面的userId
- 用userId检查Redis中是否有用户信息,如果有延长它的过期时间。
- 登录成功状态返回。
业务模块的登陆检查
首先要定义一个拦截器,因为并不是任何一个模块都需要检查登录状态,通过拦截请求来确认放不放行。
定义一个注释。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
// true代表拦截校验必须通过,false表示拦截器校验不通过也可以继续访问
boolean loginSuccess() default true;
}
拦截器判断是否注释,如果存在注释则进行拦截判断,否则放行。
// 判断被拦截请求的访问的方法的注解(是否需要拦截的)
// 反射得到方法的注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
LoginRequired methodAnnotation = handlerMethod.getMethodAnnotation(LoginRequired.class);
// 是否拦截
if (methodAnnotation == null) {
return true;
}
在拦截器中定义一个token,判断是否登陆过,登陆过的用户会将token存入cookie中,而新验证的用户会通过login请求携带的参数产生新的token,二者非此即彼。
String token = "";
// oldtoken代表之前登陆过
String oldToken = CookieUtil.getCookieValue(request, "oldToken", true);
if (StringUtils.isNotBlank(oldToken)) {
token = oldToken;
}
// newtoken代表地址栏携带验证过后的token
String newToken = request.getParameter("token");
if (StringUtils.isNotBlank(newToken)) {
token = newToken;
}
boolean loginSuccess = methodAnnotation.loginSuccess();// 获取该请求是否必须登陆成功,来决定拦截器拦截成功请求是否继续访问
在拦截器中远程调用验证中心,验证token的有效性,返回状态map。
String success = "fail";
Map<String, String> map = new HashMap<>();
if (StringUtils.isNotBlank(token)) {
// 调用认证中心验证
String ip = "127.0.0.1";
if (StringUtils.isNotBlank(request.getRemoteHost())) {
ip = request.getRemoteHost();
}
String successJson = HttpclientUtil.doGet("http://127.0.0.1:8085/verify?token=" + token + "¤tIp=" + ip);
map = JSON.parseObject(successJson, Map.class);
success=map.get("status");
}
验证中心的验证判断jwt解析token形成的用户信息是否存在,不存在返回fail状态,否则在map中加入解析的用户信息和success标志位。
public String verify(@RequestParam String token, @RequestParam String currentIp) {
// 通过jwt验证
HashMap<String, String> map = new HashMap<>();
Map<String, Object> gmall = JwtUtil.decode(token, "gmall", currentIp);
if (gmall != null) {
map.put("status", "success");
map.put("memberId", (String) gmall.get("memberId"));
map.put("nickname", (String) gmall.get("nickname"));
} else {
map.put("status", "fail");
}
return JSON.toJSONString(map);
}
在拦截器中判断返回的状态,失败打回登录页面重新登陆,否则将用户信息写入请求属性,更新cookie。
if (!success.equals("success")) {
// 重定向passport登陆
response.sendRedirect("http://127.0.0.1:8085/index?ReturnUrl=" + request.getRequestURL());
return false;
} else {
// 将token携带的信息写入
request.setAttribute("memberId", map.get("memberId"));
request.setAttribute("nickname", map.get("nickname"));
if (StringUtils.isNotBlank(token)) {
// 覆盖cookie中token
CookieUtil.setCookie(request, response, "oldToken", token, 60 * 60 * 2, true);
}
}
至此单点登录流程结束,有拦截器远程调用验证中心判断请求的合法性,通过注释实现了登不登录的区别,只需要通过注释而不是路径匹配判断,减少了代码量又增加效率。