Spring Security使用Oauth2、Redis实现SSO单点登录

669 阅读5分钟

单点登录SSO介绍

目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的用户名密码,同时,管理员也需要为同一个用户设置多套系统登录账号,这对系统的使用者来说显然是不方便的。我们期望的是如果存在多个系统,只需要登录一次就可以访问多个系统,只需要在其中一个系统执行注销登录操作,则所有的系统都注销登录,无需重复操作,这就是单点登录(Single Sign On 简称SSO)系统实现的功能。

单点登录是系统功能的定义,而实现单点登录功能,目前开源且流行的有CAS和OAuth2两种方式,过去我们用的最多的是CAS,现在随着SpringCloud的流行,更多人选择使用SpringSecurity提供的OAuth2认证授权服务器实现单点登录功能。

Oauth2介绍

有关Oauth2的详细介绍可以看juejin.cn/post/708380…

SpringSecurity基于Oauth2实现单点登录服务端和客户端实现流程解析

单点登录业务流程时序图如下图所示: spring-security-oauth2单点登录

A系统(单点登录客户端)首次访问受保护的资源触发单点登录流程说明

  • 用户通过浏览器访问A系统被保护的资源链接
  • A系统判断当前会话是否登录,如果没有登录则跳转到A系统登录地址/login
  • A系统首次接收到/login请求时没有state和code参数,此时A系统拼接系统配置的单点登录服务器授权url,并重定向至授权链接。
  • 授权服务器判断此会话是否登录,如果没有登录,那么返回单点登录服务器的登录页面。
  • 用户在登录页面填写用户名、密码等信息执行登录操作。
  • 授权服务器校验用户名、密码并将登录信息设置到上下文会话中
  • 授权服务器重定向到A系统的/login链接,此时链接带有code和state参数。
  • A系统再次接收到/login请求,此请求携带state和code参数,系统A通过OAuth2RestTemplate请求授权服务端/oauth/token接口获取token。
  • A系统获取到token后,首先会对token进行解析,并使用配置的公钥对token进行校验(非对称加密),如果校验通过,则将token设置到上下文,下次访问请求时直接从上下文中获取。
  • A系统处理完上下问会话之后重定向到登录前请求的受保护资源链接

B系统(单点登录客户端)访问受保护的资源流程说明

  • 用户通过浏览器访问B系统被保护的资源链接
  • B系统判断当前会话是否登录,如果没有登录则跳转到B系统登录地址/login
  • B系统首次接收到/login请求时没有state和code参数,此时B系统拼接系统配置的单点登录服务器授权url,并重定向至授权链接。
  • 单点登录服务器判断此会话是否登录,因上面访问A系统时登陆过,所以此时不会再返回登录界面
  • 单点登录服务器重定向到B系统的/login链接,此时链接带有code和state参数
  • B系统再次接收到/login请求,此请求携带state和code参数,系统B通过OAuth2RestTemplate请求单点登录服务端/oauth/token接口获取token
  • B系统获取到token后,首先会对token进行解析,并使用配置的公钥对token进行校验(非对称加密),如果校验通过,则将token设置到上下文,下次访问请求时直接从上下文中获取
  • B系统处理完上下问会话之后重定向到登录前请求的受保护资源链接

spring security基于redis实现单点登录流程剖析

1、首先/login页面放行,进入登录页面时不需要鉴权

image.png 2、当用户登录的时候,进行用户名和密码的判断

//验证账户是否存在(账户类型可能为手机号、登录名、邮箱)
RiderInfo riderInfo = selectRiderInfoByAccount(riderLoginVM.getLoginAccount());
PreconditionUtil.checkParameter(riderInfo!=null,"loginAccount");
//验证密码是否正确
PreconditionUtil.checkParameter(passwordEncoder.matches(riderLoginVM.getLoginPassword(),riderInfo.getLoginPassword()),"loginPassword is error");
//构造AuthUser类进行返回
AuthUser authUser = RiderInfo2AuthUser.INSTANCE.convertTo(riderInfo);
PreconditionUtil.checkParameter(authUser != null && authUser.getUserDetail() != null);
System.out.println("用户手机号" + authUser.getLoginMobile());


//Collection<AuthUserAuthority> authorities = new ArrayList<>();
//AuthUserAuthority authUserAuthority = new AuthUserAuthority("RIDER");
//authorities.add(authUserAuthority);
//authUser.setAuthorities(authorities);

//修改登录时间
riderInfo.setLastLoginTime(riderInfo.getLoginTime());
riderInfo.setLoginTime(new java.util.Date());
riderService.updateRiderInfo(riderInfo);

return riderService.detailAuthUser(riderInfo,authUser);

3、当登录信息正确后,生成token,并将相关信息保存到redis中

public Token createToken(AuthUser authUser) {
    String secret = this.genTokenKey();

    String accessToken = this.createJWTAccessToken(authUser, secret);
    stringRedisTemplate.opsForHash().put(
            getRedisKey("user", authUser.getId().toString()),
            accessToken,
            secret);

    String refreshToken = this.createJWTRefreshToken(authUser, secret);
    stringRedisTemplate.opsForHash().put(
            getRedisKey("user", authUser.getId().toString()),
            refreshToken,
            accessToken);

    return Token.builder()
            .accessToken(accessToken)
            .refreshToken(refreshToken)
            .build();
}

4、用户下次访问受保护资源的时候,则会携带生成的token,访问前会从redis中读取用户信息,没有读取到则代表未登录。读取到则返回用户信息,存入上下文中。

public AuthUser userInfo(@RequestHeader("Authorization") String authorizationHeader) {
    if (StringUtils.isEmpty(authorizationHeader)) {
        throwAccessDeniedException();
    }

    String[] strings = authorizationHeader.split(" ");
    if (strings.length != 2) {
        throwAccessDeniedException();
    }

    if (!BEARER.equals(strings[0].toLowerCase())) {
        throwAccessDeniedException();
    }

    String token = strings[1];
    AuthUser authUser = authUserDetailFacade.checkAccessToken(token);
    if (authUser == null) {
        throwAccessDeniedException();
    }
    return authUser;
}