SpringBoot模拟单点登录

SSO Single Sign On,官方的概念:web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。

简言之,系统内部通过某种技术实现用户统一登录和注销,所以单点登录技术一定要包括两部分:登录、注销。

 

1为什么要用单点登录?因为Cookie不能跨域。

Cookie为什么不能跨域请参考《Cookie详解与跨域问题》。

2如何实现单点登录?

建立权限认证中心来处理登录和注销的问题,真正提供服务的应用服务端通过Filter将鉴权任务重定向给认证中心。

原理图:

登录:

注销:

首先看得出分为2个角色,一个是应用服务端,也就是认证客户端;另一个是认证服务端。根据上图来分析2个角色应该具备的功能。

 

认证客户端应该具备的能力:

1必须以Filter或者插件等形式提供,方便系统接入SSO

2未登陆的用户重定向到SSO认证中心

3接收SSO发来的令牌并将该令牌发回给SSO做令牌认证

4处理令牌认证结果并创建局部会话

5拦截用户注销请求并重定向到SSO

6处理SSO发来的注销会话请求

 

认证服务端应该具备的能力:

0独立的web服务

1提供登陆页面,和对用户的校验

2创建全局会话、提供token

3校验token有效性并维护client端地址

4处理注销请求、销毁全局会话、并通知维护的client端地址

5判断用户有没有登录


代码模拟:

总共4个项目,核心的是2SSO-serverSSO-client,其中client不是独立的web应用而是代码插件。SSO-mock-app1SSO-mock-app2是安装了插件的2个独立应用。

Sso-server:https://github.com/yejingtao/forblog/tree/master/sso-server

Sso-client:https://github.com/yejingtao/forblog/tree/master/sso-client

Sso-mock-app1:https://github.com/yejingtao/forblog/tree/master/sso-mock-app1

Sso-moke-app2:https://github.com/yejingtao/forblog/tree/master/sso-mock-app2

 

Sso-client包括3部分:filter负责判断用户是否登陆和重定向到sso-servercontroller负责与sso-server传递令牌等通信;service维护局部会话状态。

Sso-server包括3部分:controller负责与sso-client传递令盘、校验用户登陆情况等通信;service维护全局用户会话状态;templates为用户登陆提供页面入口


下面我们按照图中的顺序来对照代码看一下:

1 客户端filter判断如果用户没有登录,重定向到sso-server端:

登陆过放行

if(userName!=null 
				&& String.valueOf(userName).trim().length()>0 
				&& userAccessService.checkUserStatus(userName.toString())) {
			chain.doFilter(req, response);
		}

没登陆过重定向:

String originalUrl = req.getRequestURL().toString();
				 httpResponse.sendRedirect(ssoServerPath+"/index?originalUrl="+originalUrl+"&ssoUser="+userName);

2 sso-server端收到请求也需要判断用户是否登陆,用户不存在就给到自己的登陆页面,同时返回请求过来的连接在成功登陆后做3秒自动跳转。

@RequestMapping("/index")
	public String firstCheck(HttpServletRequest request) {
		String originalUrl = request.getParameter("originalUrl");
		String ssoUser = request.getParameter("ssoUser");
		String token = null;
		boolean loginFlag = false;
		if(ssoUser!=null && ssoUser.trim().length()>0) {
			//对用户先判断是否已经登陆过
			token = authSessionService.getUserToken(ssoUser);
			if(token!=null) {
				loginFlag = true;
			}
		}
		if(loginFlag) {
			//判断如果用户已经在SSO-Server认证过,直接发送token
			if(tokenTrans(request,originalUrl,ssoUser,token)) {
				if(originalUrl!=null) {
					if(originalUrl.contains("?")) {
						originalUrl = originalUrl + "&ssoUser="+ssoUser;
					}else {
						originalUrl = originalUrl + "?ssoUser="+ssoUser;
					}
				}
			}
			return "redirect:"+originalUrl;
		}else {
			//需要替换成专业点的路径,自己登陆下了
			return "redirect:/loginPage?originalUrl="+request.getParameter("originalUrl");
		}
	}

3 用户登陆页面登录后,验证用户名、创建token

//登陆逻辑,返回的是令牌
	@RequestMapping(value="/doLogin",method=RequestMethod.POST)
	public String login(HttpServletRequest request, HttpServletResponse response,
			String userName, String password, String originalUrl) {
		if(authSessionService.verify(userName,password)) {
			String token = authSessionService.cacheSession(userName);
			if(tokenTrans(request,originalUrl,userName,token)) {
				//跳转到提示成功的页面
				request.setAttribute("helloName", userName);
				if(originalUrl!=null) {
					if(originalUrl.contains("?")) {
						originalUrl = originalUrl + "&ssoUser="+userName;
					}else {
						originalUrl = originalUrl + "?ssoUser="+userName;
					}
					request.setAttribute("originalUrl", originalUrl);
				}
			}
			return "hello";//TO-DO 三秒跳转
		}
		//验证不通过,重新来吧
		if(originalUrl!=null) {
			request.setAttribute("originalUrl", originalUrl);
		}
		return "loginIndex";
	}

重点是token如何传给sso-client端并从clinet端注册上来地址的:

4 server端将token传给client端:

private boolean tokenTrans(HttpServletRequest request, String originalUrl,String userName, String token) {
		String[] paths = originalUrl.split("/");
		String shortAppServerUrl = paths[2];
		String returnUrl = "http://"+shortAppServerUrl+"/receiveToken?ssoToken="+token+"&userName="+userName;
		//http://peer1:8088/receiveToken?ssoToken=80414bcb-a71d-48c8-bfee-098a303324d4&userName=xixi
		return "success".equals(restTemplate.getForObject(returnUrl, String.class));
		
	}
5 client端将收到的token和自己的服务地址传递给server端:
@RequestMapping("/receiveToken")
	@ResponseBody
	public String receiveToken(HttpServletRequest request, String ssoToken,String userName) {
		if(ssoToken!=null && ssoToken.toString().trim().length()>0) {
			String realUrl = request.getRequestURL().toString();
			String[] paths = realUrl.split("/");
			String realUrlUrls = paths[2];
			String returnUrl = ssoServerPath+"/varifyToken?address="+realUrlUrls+"&token="+ssoToken;
			//http://peer2:8089/varifyToken?address=peer1:8088&token=c2ce29be-5adb-4aaf-82cc-2ba24330176e
			String resultStr =  restTemplate.getForObject(returnUrl, String.class);
			if("true".equals(resultStr)) {
				//创建局部会话,保存用户状态为已登陆
				userAccessService.putUserStatus(userName, resultStr);
				return "success";
			}
		}
		return "error";
	}
6 server端检查token是否是自己发放的令牌并维护客户端的地址
//校验token并注册地址
	@RequestMapping(value="/varifyToken",method=RequestMethod.GET)
	@ResponseBody
	public String varifyToken(String token, String address) {
		return String.valueOf(authSessionService.checkAndAddAddress(token, address));
	}

7 令牌交互完毕后,提示用户登陆成功并自动跳转回用户的app服务页面
onload = function(){
		setInterval(go,1000);
	};
	var x = 3;
	function go(){
		x--;
		if(x>0){
			document.getElementById('sp').innerHTML = x;
		}else{
			var returnUrl = [[${originalUrl}]];
			location.href = returnUrl;
		}
	}

8 sso-client要有根据用户名发起注销的能力
@RequestMapping("/ssoLogout")
	@ResponseBody
	public String ssoLogout(String userName) {
		String userToken = userAccessService.getUserToken(userName);
		if(userToken!=null) {
			String returnUrl = ssoServerPath+"/logoutByToken?ssoToken="+userToken;
			return restTemplate.getForObject(returnUrl, String.class);
		}
		return "None Token";
	}


9 注销在sso-server端提供2种,可以用用户注销,也可以用token注销,代码中都有提供这里主要讲根据token注销,我们假定注销请求都是从客户端发起的
@RequestMapping(value="/logoutByToken",method=RequestMethod.GET)
	@ResponseBody
	public String logoutByToken(String ssoToken) {
		List<String> addressList = authSessionService.logoutByToken(ssoToken);
		if(addressList!=null) {
			addressList.stream().forEach(s -> sendLogout2Client(s,ssoToken));
		}
		return "logout";
	}
	
	private void sendLogout2Client(String address,String ssoToken) {
		String returnUrl = "http://"+address+"/ssoDeleteToken?ssoToken="+ssoToken;
		try {
			restTemplate.getForObject(returnUrl, String.class);
		}catch(Exception e) {
			//Log and do nothing
		}
	}

10  对应的客户端要有个删除token的接收动作
@RequestMapping("/ssoDeleteToken")
	@ResponseBody
	public String ssoDeleteToken(String ssoToken) {
		userAccessService.deleteToken(ssoToken);
		return "success";
	}

11 细节需要注意下:

a 客户端filter过滤范围要控制好,不要将sso-server发来的请求进行过滤,否则会死循环

b client端和server端的Redis要维护好usertoken


启动测试:

 Sso-mock-app1,植入sso-client插件,域名使用peer1,服务地址peer1:9001

 Sso-mock-app2,植入sso-client插件,域名使用peer2,服务地址peer2:9002

 Sso-Server,独立部署,域名使用peer3,服务地址peer3:9003

 

 都启动之后,开始测试:

 步骤一:无论我浏览器访问app1被重定向到http://peer3:9003/loginPage?originalUrl=http://peer1:9001/hello地址进行登录

步骤二:在peer3,也就是SSO-Server的登陆页面上用yejingtao账户登陆,登陆成功后3秒钟后页面自动跳转到peer1的步骤一中的请求页面。

步骤三:在app2服务尝试yejingtao账号直接访问,成功。

步骤四:在SSO-Server端注销该账号

步骤五:注销后app1app2又要重新登录了。

整个过程中可以配合redisclient观察redis数据帮助自己理解。


由于只是验证和示例代码,好多可以优化的地方,例如

1 token已经预留了失效时间,可以在逻辑中增加对token时效的判断,并定时清理

2 sso-serversso-client的通信可以改成post请求,虽然不会经过浏览器,从使用效果和安全级别上没有差别,但是从http协议设计来考虑还是post更好

3 Redis序列化部分可以换成Jackson2JsonRedisSerializer节省内存空间

4 sso-client拦截的权限范围可以放在application.yml中让用户自己设置


如果自己产品中用SSO的话当然不用自己写,成型有CAS等,原理当然差别不大。
还有一种解决系统间统一认证的思路,OAuth2.0,虽然也是解决多系统登录的,但是跟这里的单点登录思路完全不一样,等后面有机会再博文介绍。




  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
### 回答1: SpringBoot是一种Java框架,可以简化Java应用程序的开发。单点登录SSO)是一种身份验证技术,允许用户通过一次登录访问多个应用程序。SpringBoot支持单点登录,可以为用户提供更方便的访问体验。 要集成单点登录,您需要选择一个适合您的SSO解决方案。一些流行的选择包括CAS和OAuth。在此之后,您需要将SSO客户端库集成到SpringBoot应用程序中。 一旦SSO客户端库到位,您需要配置客户端库以与SSO服务器通信并验证用户凭据。然后,您需要设置SpringBoot应用程序以编写受保护的端点,这些端点要求用户进行身份验证才能访问。 最后,您需要配置SpringBoot应用程序将用户重定向到SSO服务器进行身份验证。如果身份验证成功,则用户将被重定向回您的SpringBoot应用程序,可以访问他们所请求的受保护端点。 总之,SpringBoot可以集成单点登录,这能够提升用户访问体验,让他们更方便地访问多个应用程序。要集成单点登录,您需要选择SSO解决方案,集成SSO客户端库,配置受保护的端点以及重定向用户到SSO服务器进行身份验证。 ### 回答2: Spring Boot 是目前非常流行的一种 Java 应用程序开发框架,它非常便捷、简单,同时它支持集成多种类型的单点登录框架。单点登录SSO)可以让用户在登录以后即可访问多个应用,而且不需要重复登录,这样可以减轻用户的负担,增强应用程序的易用性。 要在 Spring Boot 中集成单点登录,可以使用Spring Security,它是专门用于处理安全性问题的框架。它提供的多种单点登录机制包括 CAS(Central Authentication Service)、OAuth(开放授权)、SAML(Security Assertion Markup Language)等等,可以根据需求选择相应的方案。 在集成 Spring Security 后,需要进行一些必要的配置,例如配置安全拦截器、设置认证提供器、定义用户、角色以及权限等等。一旦完成配置后,这个单点登录框架就可以和其他应用程序集成,从而实现给用户提供统一的登录入口,同时可以方便地管理用户身份认证和访问权限控制。 总结来说,Spring Boot 集成单点登录可以让用户在多个应用程序间实现方便的访问,同时还可以增强应用程序的安全性,提高管理效率。而实现它需要配置 Spring Security,并选择相应的单点登录机制。 ### 回答3: Spring Boot 是一个快速开发框架,而单点登录是在Web应用系统中实现用户重用身份认证的方法。在Spring Boot中,搭建单点登录系统可以使用spring-security模块。Spring Security 是一个安全框架,主要为 Web 应用程序提供身份认证和授权、攻击防御等安全功能。 在Spring Security中,可以使用OAuth2.0或JWT(JSON Web Token)实现单点登录。OAuth2.0 是一种进行授权的标准协议,它允许用户让第三方应用(不包括密码)获取 limited access tokens 而不是提供他们的密码。这意味着只要用户已经登录了一个 OAuth2.0 应用程序,他们将能够无需再次登录即可访问其他应用程序。 另一种方式是使用JWT,JWT是一种安全的身份验证和授权方法,采用JSON格式传输信息,而且JWT token可以被多个应用程序使用,从而实现单点登录。使用Spring Boot集成JWT可以使用Spring Security,在使用JWT时需要对token进行签发、验签和鉴权等步骤进行验证。 另外,如果企业内部使用的是不同的身份认证服务,可以使用Shiro实现单点登录。 Shiro 是一个领先的框架,支持多种身份认证和授权方式,如LDAP、Kerberos和Active Directory等。在Shiro中,我们可以使用SSO集成身份认证服务,从而实现单点登录。 总而言之,在Spring Boot中集成单点登录,可以根据不同需求选择OAuth2.0、JWT或Shiro等多种方式来实现。而且Spring Security 和 Shiro 都支持多种身份认证和授权方式, 特别适合提供多个角色和权限的企业级应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值