微服务之单点登录 SSO 详解

简介

较大的企业内部,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务。通常来说,每个单独的系统都会有自己的安全体系和身份认证系统。进入每个系统都需要进行登录,这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好。那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢。“单点登录”就是专为解决此类问题的。其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息

实现思路

业务服务设计

在每一个需要身份认证的服务中,定义一个SSOFilter用于拦截非登录请求。对于每个拦截的请求,会先从当前请求的Session中确认是否能够拿到用户信息,拿不到用户信息又会确认当前请求中是否携带ticket票据这个参数,如果携带就会尝试从Redis中根据该票据拿到用户信息。如果最终都获取不到用户信息就会被重定向到SSO登录服务的登录页面进行登录处理

代码语言:javascript
复制
    private RedisTemplate redisTemplate;
public static final String USER_INFO = "user";

public SSOFilter(RedisTemplate redisTemplate){
    this.redisTemplate = redisTemplate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
                     ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;

    Object userInfo = request.getSession().getAttribute(USER_INFO);;

    //如果未登陆,则拒绝请求,转向登陆页面
    String requestUrl = request.getServletPath();
    if (!"/toLogin".equals(requestUrl)
            && !requestUrl.startsWith("/login")
            && null == userInfo) {

        String ticket = request.getParameter("ticket");
        //有票据,则使用票据去尝试拿取用户信息
        if (null != ticket){
            userInfo = redisTemplate.opsForValue().get(ticket);
        }
        //无法得到用户信息,则去CAS服务的登陆页面
        if (null == userInfo){
            response.sendRedirect("http://cas.com:8080/toLogin?url="+request.getRequestURL().toString());
            return ;
        }

        /**
         * 将用户信息,加载进session中
         */
        request.getSession().setAttribute(SSOFilter.USER_INFO,userInfo);
        //登录成功需要将ticket从redis中删除
        redisTemplate.delete(ticket);
    }

    filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}

}

SSO服务设计

在SSO登录服务中,只需要简单定义一个Filter,进行拦截非登录请求,然后确认当前请求的Session中是否能够拿到用户信息,如果能拿到用户信息,那么就是登录状态,否则,认定当前请求无效,将请求转发到登录页面即可

代码语言:javascript
复制
    public static final String USER_INFO = "user";
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
                     ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;

    Object userInfo = request.getSession().getAttribute(USER_INFO);;

   //非登陆页面并且不是登陆状态
    String requestUrl = request.getServletPath();
    if (!"/toLogin".equals(requestUrl)
            && !requestUrl.startsWith("/login")
            && null == userInfo) {
        //则拒绝当前请求,请求转发到登陆页面 
        request.getRequestDispatcher("/toLogin").forward(request,response);
        return ;
    }

    filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}

}

在SSO登录处理过程中,当请求已认证登录成功后,会先生成一个ticket票据,并将ticket票据和用户信息存放到Redis中,然后重定向回原先请求服务的Url,并携带上ticket票据参数

代码语言:javascript
复制
public class IndexController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping("/toLogin")
public String toLogin(Model model,HttpServletRequest request) {
    Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);
    //不为空,则是已登陆状态
    if (null != userInfo){
        String ticket = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(ticket,userInfo,2, TimeUnit.SECONDS);
        return "redirect:"+request.getParameter("url")+"?ticket="+ticket;
    }
    UserForm user = new UserForm();
    user.setUsername("username");
    user.setPassword("password");
    user.setBackurl(request.getParameter("url"));
    model.addAttribute("user", user);

    return "login";
}

@PostMapping("/login")
public void login(@ModelAttribute UserForm user,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
    request.getSession().setAttribute(LoginFilter.USER_INFO,user);

    //登陆成功,创建用户信息票据
    String ticket = UUID.randomUUID().toString();
    //将ticket和用户信息写入到redis中
    redisTemplate.opsForValue().set(ticket,user,20, TimeUnit.SECONDS);
    //重定向,回原请求的url,并携带ticket信息
    if (null == user.getBackurl() || user.getBackurl().length()==0){
        response.sendRedirect("/index");
    } else {
        response.sendRedirect(user.getBackurl()+"?ticket="+ticket);
    }
}

@GetMapping("/index")
public ModelAndView index(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView();

    modelAndView.setViewName("index");
    modelAndView.addObject("user", request.getSession().getAttribute(LoginFilter.USER_INFO));

    request.getSession().setAttribute("test","123");
    return modelAndView;
}

}

单点登录流程

1.用户访问服务A某个页面时,服务A发现自己未登录,重定向到CAS单点登录服务,CAS服务也发现未登录,则跳转到相应的登录页面
2.用户输入用户名和密码登录成功后,CAS服务进行认证,将登录状态记录CAS服务的session中,并写入当前CAS服务域名下的Cookie中
3.CAS服务登录完成后会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务A,同时将Ticket作为参数传递给服务A
4.服务A拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务A再将登录状态写入session并设置到服务A域名下的Cookie中
5.至此,单点登录就完成了,之后再访问服务A时,服务A就是登录状态的
6.当有一个新的服务B被用户访问时,服务B发现自己也未登录,此时也重定向到CAS单点登录服务,但是此时CAS服务发现已经登录了,此时就不需要进行登录认证
7.CAS服务会生成一个Ticket票据,并将Ticket和用户信息记录到Redis中,之后再重定向回服务B,同时将Ticket作为参数传递给服务B
8.服务B拿到Ticket后,从Redis中进行查找,查询Ticket对应的用户信息,之后服务B再将登录状态写入session并设置到服务B域名下的Cookie中
9.因此服务B不需要进行登录过程,就能完成用户登录认证

参考

https://blog.csdn.net/wzljiayou/article/details/109550565 *****
https://www.cnblogs.com/zh94/p/8352943.html *** 没看,觉得不错