首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单点登录

单点登录

作者头像
晚上没宵夜
发布2020-04-24 15:20:35
1.7K0
发布2020-04-24 15:20:35
举报

1. SSO

单点登录(Single Sign On),在多个互相信任的Web站点中,只要登录过其中一个,那么其他的站点都可以直接访问而不用登录。举个栗子:淘宝和天猫是两个Web站点,登录淘宝之后就不用登录天猫而可以互相访问。

为什么需要单点登录?

在大型系统架构中,其往往有很多的子站点,各个站点部署在不同的服务器上。那么用户在访问不同站点时就需要逐一登录,用户体验不友好。而且每个站点都需要做登录模块,业务冗余,重复性太高。单点登录就是解决这些问题的,下面说明主要主要是思想,而实现是其次,因为实现方式有多种

2. 回顾单系统登录

HTTP是无状态的,我们可以用Cookie和Session来实现会话跟踪。一般登录功能的流程:

  • 用户输入账号密码正确,用户信息存储在Session中(Session存储在当前Tomcat服务器上)
  • Tomcat服务器根据当前Session发送含唯一JESSIONID的Cookie给浏览器自动保存
  • 下次浏览器再次访问会带上该Cookie,服务器识别JESSIONID对应的Session来跟踪会话

实现单点登录要解决的是Session共享问题,以及Cookie跨域

3. 单点登录简单实现

  • 最简单实现:JWT(单点登录的友好使者)
  • 借助Redis实现Session共享

补充:Session是服务器实现的一种机制,可以用Redis来模拟其功能

登录站点业务层实现

这个站点的功能在于给其他站点提供登录服务

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    // 1. 正常登录流程
    public String login(String username, String password) {

        // 验证账号密码
        User user = userMapper.find(username, password);
        if (user == null) {
            return ResponseHelper.error("0001","账号或密码错误",null);
        } else {
            user.setPassword(null);
        }

        // 在Redis中存入Session
        String token = UUID.randomUUID().toString().replace("-", "");
        Jedis jedis = JedisUtil.getResource();
        jedis.setex("userToken:" + token, 60 * 60 * 24, JSON.toJSONString(user));
        jedis.close();
        return ResponseHelper.success("0002","登录成功",JSON.toJSONString(token));
    }
    
    // 2. Redis中获取Session
    public String getSessionByToken(String token){
        Jedis jedis = JedisUtil.getResource();
        String redisSession = jedis.get("userToken:" + token);
        jedis.close();
        return ResponseHelper.success("0003","获取成功",JSON.toJSONString(redisSession));
    }
}

登录成功后返回token,客户端将该token保存起来,下次访问带上即可

其他站点

其他站点需要登录时,利用HttpClient去登录站点登录,返回token保存到Cookie中

// LOGIN_WEB_URL登录站点的请求地址
public String login(String username, String password, HttpServletResponse response) {

    // 请求参数,与HttpClient登录站点
    Map<String, String> param = new HashMap<>();
    param.put("username", username);
    param.put("password", password);
    String token = HttpClientUtil.doPost(LOGIN_WEB_URL, param);
    
    // 写入Cookie
    Cookie cookie = new Cookie("userToken", token);
    cookie.setMaxAge(60 * 60 * 24);
    response.addCookie(cookie);

    return ResponseHelper.success(0002, "登录成功", null);
}

登录拦截器,拦截没有token

public class LoginHandlerInterceptor implements HandlerInterceptor {
    private BeanContext JedisUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 获取Token,然后去Redis查Session
        boolean auth = false;
        Cookie[] cookies = request.getCookies();
        for (Cookie value : cookies){
            if (value.getName().equals("userToken")){
                String token = value.getValue();
                Jedis jedis = JedisUtil.getResource();
                String redisSession = jedis.get("userToken:" + token);
                if(redisSession != null){
                    // request.getSession().setAttribute("user",redisSession);
                    auth = true;
                }
            }
        }

        // 2. Redis中没有Session,跳转本站登录页面
        if(!auth){
            request.getRequestDispatcher("/user/login.html").forward(request,response);
            return false;
        }
        return true;
    }
}

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 已经boot做好了静态资源放行
        registry.addInterceptor(new LoginHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/","/user/login");

    }
}

注意:为了精简,部分Controller层内容放入Service层操作

流程总结:

  • 进入站点1时,没有Cookie则跳转登录站点,登录后将用户信息存入Redis(模拟Session),生成Token(模拟JESSIONID),返回给浏览器保存
  • 浏览器从上面接收到Token之后写入cookie或LocalStorage或SessionStorage,刷新页面,访问时带上Token即可(写入Token操作由前端进行,前后端分离)
  • 进入站点2时,发现有带上Token,查询Redis后有对应的Session放行。当想要用户信息时,带上Token去Redis查询
  • 上面步骤其实在用Redis模拟Session的机制。若熟悉Session机制,完全可重写request.getSession(),用包装者模式,增加其向Redis获取用户信息的功能,实现Session共享

至此Session共享问题解决了,共享实现还有但不建议:Session绑定(Nginx的Hash_ip绑定服务器),Tomcat集群Session复制

Cookie由于有跨域问题,同域下可以设置domain,不同域则无法携带,但不同域可以用token存放到LocalStorage(永久)或SessionStorage(会话级别),访问时带上token,即验证token即可(跨域可用协议解决或Nginx反向代理)

补充一个重复登录:用户每次登录然后在共享Session中更新一个随机数(singleToken),此singleToken也让客户端保存为Cookie,之后的访问对比自己Cookie中的singleToken与共享Session中的是否一致,不一致则有人登录了,踢出后者。踢出动作可以服务器端发出(长连接),或者客户端主动刷新对比。

4. CAS机制

Central Authentication Service,将登录功能抽取出来单独做一个认证中心,此后的所有相关功能都去认证中心操作,这里提供思路。阿里云的控制台登录,跳转登录再跳转回来的

  • 用户访问需登录的站点1,重定向至认证中心(带上自己访问站点1的url)。若在认证中心也没有登录,跳转登录页面登录,登陆后客户端与认证中间建立全局会话(Cookie和Session),并生成一个ST(Service Ticket),然后带上该ST重定向至站点1的url
  • 回到站点1之后,站点1拿这个ST去认证中心验证,正确则建立局部会话(Session),那么至此站点1是登录状态了。
  • 用户这次访问需登录的站点2,重定向至认证中心(带上自己访问站点2的url),因为已经和认证中心建立全局会话,所以认证中心直接返回ST重定向回站点2,而站点2携带ST去认证中心验证,正确则建立局部会话

这里的局部会话关闭浏览器则会失效,下次再次访问还是需要 重定向至认证中心返回ST,带上ST去验证再建立局部会话

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-04-17 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. SSO
  • 2. 回顾单系统登录
  • 3. 单点登录简单实现
  • 4. CAS机制
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档