Skip to content

Latest commit

 

History

History
293 lines (210 loc) · 8.74 KB

README.md

File metadata and controls

293 lines (210 loc) · 8.74 KB

前后端分离的单点登录系统

项目说明

有两个外部系统想要做单点登录,有一个单点登录认证中心系统 CAS

CAS 采用前后端分离架构,后端使用 Spring Boot,前端使用 nginx 做代理。

环境配置

hosts

127.0.0.1 www.cas.com
127.0.0.1 www.app1.com
127.0.0.1 www.app2.com

nginx

server {
    listen       80;
    server_name  www.cas.com;
    root   html/cas;
}

server {
    listen       80;
    server_name  www.app1.com;

    location / {
        root   html/app1;
        index  index.html index.htm;
	}
}

server {
    listen       80;
    server_name  www.app2.com;

    location / {
        root   html/app2;
        index  index.html index.htm;
    }
}

redis

本地 redis 环境

使用

启动后端Spring Boot,前端 nginx。

浏览器输入www.app1.com,登录后,另起一个窗口,输入www.app2.com,无需登录。

注销也是同理。

单点登录的流程

如图所示。

解释说明(序号与图中不对应):

客户端访问系统1

1、浏览器输入www.app1.com初次访问,发现没有登录,此时携带你想访问的地址(www.app1.com)跳转到 CAS,如下代码所示。

window.location.href = "http://www.cas.com:9000/login?returnUrl=http://www.app1.com"

2、如果之前没有登录,是第一次的登录的话,跳转到 CAS 登录界面。

CASlogin接口的代码如下。

@GetMapping("login")
public void login(String returnUrl, HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 获取全局门票,如果cookie中能够获取到,证明用户登录过
    String userTicket = getCookie(request, COOKIE_USER_TICKET);

    // 验证全局门票是否合法
    boolean isValid = verifyUserTicket(userTicket);
    if (isValid) {
        // 全局门票合法,那么再签发一个临时门票
        String tmpTicket = createTmpTicket();
        response.sendRedirect(returnUrl + "?tmpTicket=" + tmpTicket);
    } else {
        // TODO 前后端可以对returnUrl进行编解码传输
        response.sendRedirect("http://www.cas.com?returnUrl=" + returnUrl);
    }
}

我们只需要关注最后一行,因为使用了 nginx 做代理,所以访问http://www.cas.com时,会跳转到默认首页。同时会附带一个参数returnUrl

3、跳转到 CAS 的登录页后,输入账号密码,点击登录。登录接口代码如下。

@PostMapping("login")
@ResponseBody
public AjaxResult doLogin(String username,
                          String password,
                          String returnUrl,
                          HttpServletRequest request,
                          HttpServletResponse response) throws IOException {
    // 判断用户名密码不为空
    if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
        return AjaxResult.error("用户名或密码不能为空");
    }

    // 1.模拟登录
    UserDO userDO1 = new UserDO().setId("1").setUsername("admin1").setPassword("admin1");
    UserDO userDO2 = new UserDO().setId("2").setUsername("admin2").setPassword("admin2");

    //        if (!userDO.getUsername().equals(username) || !userDO.getPassword().equals(password)) {
    //            return AjaxResult.error("用户名或密码不正确");
    //        }

    UserDO userDO = new UserDO();
    if ("admin1".equals(username)) {
        userDO = userDO1;
    }else if ("admin2".equals(username)) {
        userDO = userDO2;
    }
    // 2.保存用户会话信息到redis中
    UserVO userVO = new UserVO();
    BeanUtils.copyProperties(userDO, userVO);
    redisOperator.set(REDIS_USER_INFO + ":" + userDO.getId(), JsonUtils.objectToJson(userVO));

    // 3.生成全局门票,代表用户在CAS端登录过
    String userTicket = UUID.randomUUID().toString().trim().replace("-", "");

    // 给CAS端设置全局门票cookie
    setCookie(COOKIE_USER_TICKET, userTicket, response);

    // 4.userTicket关联用户id,放入redis中,代表该用户拥有了全局门票
    redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, userDO.getId());

    // 5.生成临时门票,回跳到调用端网站验证使用,是CAS签发的一个一次性门票
    String tmpTicket = createTmpTicket();

    String url = returnUrl + "?tmpTicket=" + tmpTicket;
    Map<String, String> map = new HashMap<>();
    map.put("returnUrl", url);
    return AjaxResult.success(map);
}

4、创建用户会话,以下代码同上。

redisOperator.set(REDIS_USER_INFO + ":" + userDO.getId(), JsonUtils.objectToJson(userVO));

5、创建用户全局门票。

 String userTicket = UUID.randomUUID().toString().trim().replace("-", "");

6、保存全局门票到 cookie。

 setCookie(COOKIE_USER_TICKET, userTicket, response);

7、全局门票关联到用户会话。

 redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, userDO.getId());

8、创建临时门票。

String tmpTicket = createTmpTicket();

9、拿着临时门票回调到之前想要访问的网址(www.app1.com)。

String url = returnUrl + "?tmpTicket=" + tmpTicket;
Map<String, String> map = new HashMap<>();
map.put("returnUrl", url);
return AjaxResult.success(map);

具体的回调是前端页面发起得,代码如下。

if (res.data.code === 200) {
	var returnUrl = res.data.data.returnUrl;
	window.location.href = returnUrl
}

10、校验临时门票,接口如下。

@PostMapping("verifyTmpTicket")
@ResponseBody
public AjaxResult verifyTmpTicket(String tmpTicket, HttpServletRequest request, HttpServletResponse response) {
    // 使用一次性临时门票来验证用户是否登录,如果登录过,把用户会话信息返回给站点
    String tmpTicketValue = redisOperator.get(REDIS_TMP_TICKET + ":" + tmpTicket);
    if (StringUtils.isBlank(tmpTicketValue)) {
        return AjaxResult.error("用户门票异常");
    }

    // 如果临时门票成功获取,则销毁门票,并且拿到CAS端cookie中的全局门票,以此获取用户会话信息
    if (!tmpTicketValue.equals(tmpTicket)) {
        return AjaxResult.error("用户门票异常");
    } else {
        // 销毁临时门票
        redisOperator.del(REDIS_TMP_TICKET + ":" + tmpTicket);
    }

    // 验证全局门票绑定的用户会话信息是否存在
    String userTicket = getCookie(request, COOKIE_USER_TICKET);
    String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
    if (StringUtils.isBlank(userId)) {
        return AjaxResult.error("用户门票异常");
    }
    String userVO = redisOperator.get(REDIS_USER_INFO + ":" + userId);
    if (StringUtils.isBlank(userVO)) {
        return AjaxResult.error("用户门票异常");
    }

    // 验证成功,返回用户会话信息
    return AjaxResult.success(JsonUtils.jsonToPojo(userVO, UserVO.class));
}

校验请求时由www.app1.com的首页发起的,

axios.post('http://www.cas.com:9000/verifyTmpTicket?tmpTicket=' + tmpTicket)

11、如果临时门票存在且有效,则销毁临时门票,同时通过全局门票获取用户会话。

12、返回会话到请求端,请求端保存会话到本地 cookie,以便后续使用。

13、显示登录成功。

在客户端访问完系统1后,客户端又访问系统2

1、浏览器输入www.app2.com初次访问,此时携带你想访问的地址(www.app1.com)跳转到 CAS 的 login接口校验登录状态。

@GetMapping("login")
public void login(String returnUrl, HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 获取全局门票,如果cookie中能够获取到,证明用户登录过
    String userTicket = getCookie(request, COOKIE_USER_TICKET);

    // 验证全局门票是否合法
    boolean isValid = verifyUserTicket(userTicket);
    if (isValid) {
        // 全局门票合法,那么再签发一个临时门票
        String tmpTicket = createTmpTicket();
        response.sendRedirect(returnUrl + "?tmpTicket=" + tmpTicket);
    } else {
        // TODO 前后端可以对returnUrl进行编解码传输
        response.sendRedirect("http://www.cas.com?returnUrl=" + returnUrl);
    }
}

2、由于之前系统1登录过了,因此能够从 cookie 中获取到全局门票,只要全局门票验证合法,则创建一个临时门票用于访问。

3、同样需要校验临时门票,校验通过, 就会获取用户会话,返回会话到请求端,显示登录成功。

也就是说,系统2登录时是不需要输入账号密码的。

完整代码地址:https://github.com/FeiChaoyu/CAS

公众号