什么是单点登陆系统
- 一处登录,多处使用
即用户登陆某系统后,访问其他相互信任的应用系统时不需要再次手动登录
为什么要有单点登陆系统
服务器集群时,每个服务器有自己独立的session,用户每次只能在其中一台服务器登录
- 解决集群情况下的session共享问题
集群服务器配置session复制 | 单点登录系统 |
---|---|
广播session信息 | 将登录信息存放到redis中,系统间共享存放token的cookie |
容易引起网络风暴,集群节点受限(5个左右) | SSO需要提供服务供其他系统调用 |
单点登陆系统做了什么
- 将当前登录映射到该用户在其他应用中的登录
- SSO查询数据库进行校验
- SSO生成token作为用户唯一标识(类似sessionId)
- token作为key,用户名密码作为value存入redis,设置过期时间
- 存入成功后将token写入cookie
- 其他系统从cookie中取token,通过jsonp向SSO跨域发送token进行查询
- SSO从缓存中读取数据,若过期则重新登陆,否则更新过期时间并用jsonp返回相关返回已登陆
- 用户退出登录时向SSO发送token,删除redis中的用户信息
项目中实现单点登陆系统
- 第一次登录
@Override public TaotaoResult login(String username, String password, HttpServletRequest request, HttpServletResponse response) { //校验用户名密码是否正确 TbUserExample example = new TbUserExample(); Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(username); List<TbUser> list = userMapper.selectByExample(example); if (list == null || list.isEmpty()) { return TaotaoResult.build(400, "用户名或密码错误"); } TbUser user = list.get(0); if(!user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))) { return TaotaoResult.build(400, "用户名或密码错误"); } //校验成功后生成token String token = UUID.randomUUID().toString(); //把用户信息写入redis //key:REDIS_SESSION:{TOKEN} value:user转json user.setPassword(null); jedisClient.set(REDIS_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user)); //设置用户信息的过期时间 jedisClient.expire(REDIS_SESSION_KEY + ":" + token, SESSION_EXPIRE); //将token写入cookie CookieUtils.setCookie(request, response, "TT_TOKEN", token); return TaotaoResult.ok(token); }
- 其他系统再次登录
SSO查询redis
当前系统跨域调用SSO查询服务public TaotaoResult getUserByToken(String token) { // 根据token取用户信息 String json = jedisClient.get(REDIS_SESSION_KEY + ":" + token); // 判断是否查询到结果 if (StringUtils.isBlank(json)) { return TaotaoResult.build(400, "用户session已经过期"); } //若缓存中存在用户信息,则更新token的过期时间 jedisClient.expire(REDIS_SESSION_KEY + ":" + token, SESSION_EXPIRE); //并返回对应用户信息 TbUser user = JsonUtils.jsonToPojo(json, TbUser.class); return TaotaoResult.ok(user); }
@RequestMapping("/user/token/{token}") @ResponseBody public Object getUserByToken(@PathVariable String token, String callback) { try { TaotaoResult result = loginService.getUserByToken(token); //支持jsonp调用 if (StringUtils.isNotBlank(callback)) { MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue; } return result; } catch (Exception e) { e.printStackTrace(); return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); } }
- 门户系统整合SSO
其他系统跳转到SSO登页面,或SSO登录成功后跳转到其他系统页面:js中指定跳转url即可
登录拦截器
-
使用场景: 在进行某操作前必须先登录
-
实现步骤:
- 当前页面拦截请求url(创建一个类XXX继承HandlerInterceptor)
- 当前页面从cookie中取token,若token已过期则跳转到登录
- 若有则向SSO发送跨域请求
- SSO根据传过来的token到redis查询信息并返回响应数据
- 若响应数据显示token已过期,则跳转到登录
- 否则获取取返回数据中的用户信息
- 回调到当前页面
//配置拦截器设置 <mvc:interceptors> <mvc:interceptor> //需要拦截的url <mvc:mapping path="/item/*" /> <bean class="com.taotao.portal.LoginInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
public class LoginInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Value("SSO_LOGIN_URL") private String SSO_LOGIN_URL; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 拦截请求url,查询用户信息 TbUser user = userService.getUserByToken(request, response); // 如果无法取到用户数据,跳转到登录页面 if (user == null) { //为了让登录后回到当前页面,将当前页面的url传给SSO以便回调 response.sendRedirect(SSO_LOGIN_URL+"?redirect="+request.getRequestURL()); return false; } // 若取到用户信息,放行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
public TbUser getUserByToken(HttpServletRequest request, HttpServletResponse response) { try { //从cookie中取token String token = CookieUtils.getCookieValue(request, "TT_TOKEN"); //判断浏览器token是否过期 if (StringUtils.isBlank(token)) { return null; } //调用sso的服务在redis中查询用户信息 String json = HttpClientUtils.doGet(SSO_BASE_URL + SSO_USER_TOKEN_SERVICE + token); TaotaoResult result = TaotaoResult.format(json); if (result.getStatus() != 200) { return null; } //取用户对象数据 result = TaotaoResult.formatToPojo(json, TbUser.class); TbUser user = (TbUser) result.getData(); return user; } catch (Exception e) { e.printStackTrace(); return null; } }
@RequestMapping("/page/login")
public String showLogin(String redirectURL, Model model) {
//将回调页面的url传给前端,通过前端js回调
model.addAttribute("redirect", redirectURL);
return "login";
}