单点登录SSO介绍
目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的用户名密码,同时,管理员也需要为同一个用户设置多套系统登录账号,这对系统的使用者来说显然是不方便的。我们期望的是如果存在多个系统,只需要登录一次就可以访问多个系统,只需要在其中一个系统执行注销登录操作,则所有的系统都注销登录,无需重复操作,这就是单点登录(Single Sign On 简称SSO)系统实现的功能。
单点登录是系统功能的定义,而实现单点登录功能,目前开源且流行的有CAS和OAuth2两种方式,过去我们用的最多的是CAS,现在随着SpringCloud的流行,更多人选择使用SpringSecurity提供的OAuth2认证授权服务器实现单点登录功能。
Oauth2介绍
有关Oauth2的详细介绍可以看juejin.cn/post/708380…
SpringSecurity基于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页面放行,进入登录页面时不需要鉴权
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;
}