基于 OAuth2.0 思路实现两种单点登录:授权服务端与客户端的落地实践
手撸OAuth2.0,web版,移动端同样适用—基于 OAuth2.0 思路实现两种单点登录:授权服务端与客户端的落地实践摘要在多系统集成场景中,单点登录的目标并不只是“少输一次密码”,更重要的是把“统一认证”和“各系统本地会话”拆分开来。本文介绍一套基于 OAuth2.0 授权码模式思路实现的单点登录方案,其中授权服务端负责统一认证,客户端负责完成本地登录落地。本文实现了两种登录入口:客户端主动发起登录用户已经登录授权服务端后,从应用中心直接进入客户端两种方式前半段不同,但后半段统一收敛为:授权服务端签发 code - 客户端用 code 换 access_token - 客户端再用 access_token 获取用户信息 - 客户端根据用户信息执行本地免密登录 - 客户端生成自己的业务 token - 前端完成系统登录需要说明的是,本文重点放在“统一认证 + 本地登录落地”这条主链路上。对于“客户端基于 access_token 访问受保护资源时的 scope / audience / 资源权限校验”,本文暂未实现,仅在末尾给出后续扩展方向。一、整体设计目标这套实现的核心思想可以概括为两句话:授权服务端负责认证“你是谁”客户端负责决定“你在我系统里如何登录”也就是说,授权服务端并不直接替客户端生成业务登录态,而是通过code - access_token - userinfo的方式,把统一身份传递给客户端。客户端收到身份后,再在自己的用户体系、权限体系和会话体系内完成一次本地登录。这样做有三个直接好处:统一认证与业务系统解耦各客户端仍然保有自己的权限控制能力后续可以平滑演进为更完整的 OAuth2.0 / OpenID Connect 模式二、核心数据模型授权服务端维护一张sso_app应用表,用来保存每个客户端系统的注册信息。核心字段如下:@TableName("mom_sso_app")publicclassSsoAppEntityimplementsSerializable{@TableId("f_id")privateStringid;@TableField("f_system_id")privateStringsystemId;@TableField("f_en_code")privateStringenCode;@TableField("f_app_name")privateStringappName;@TableField("f_client_id")privateStringclientId;@TableField("f_client_secret")privateStringclientSecret;@TableField("f_callback_uri")privateStringcallbackUri;@TableField("f_status")privateStringstatus;}字段职责如下:systemId:关联授权服务端自己的应用表enCode:应用编码,用于标识是哪个应用发起了登录clientId / clientSecret:客户端凭证callbackUri:客户端回调地址在应用创建时,clientId和clientSecret由服务端自动生成:@OverridepublicSsoAppEntitycreate(SsoAppFormform){checkForm(form);checkSystemIdUnique(form.getSystemId(),null);Datenow=newDate();SsoAppEntityentity=newSsoAppEntity();entity.setId(RandomUtil.uuId());entity.setSystemId(form.getSystemId());entity.setAppName(form.getAppName());entity.setEnCode(form.getEnCode());entity.setClientId(generateClientId());entity.setClientSecret(generateClientSecret());entity.setCallbackUri(form.getCallbackUri());entity.setStatus(StringUtil.isEmpty(form.getStatus())?"0":form.getStatus());entity.setRemark(form.getRemark());entity.setCreatorTime(now);entity.setLastModifyTime(now);this.save(entity);returnentity;}客户端侧则维护自己的 SSO 配置:client:sso:oauth:server-url:http://127.0.0.1:30000client-id:sso_client_xxxclient-secret:xxxxxxxxxredirect-uri:http://127.0.0.1:8089/auth/sso/callbackfront-redirect-uri:http://localhost:5666/sso/callbacksys-client-id:e5cd7e4891bf95d1d19206ce24a7b32estate-expire-seconds:300ticket-expire-seconds:60三、登录页中的enCode作用这套实现里,enCode不是一个普通业务字段,而是授权服务端登录页识别“登录来源”的关键标记。授权服务端在构造登录页跳转地址时,会把enCode作为路径片段拼接进去:privateSsoAuthorizeVObuildLoginRedirect(SsoAppEntityapp,Stringstate,StringresponseType){SystemEntitysystem=loadSystemById(app.getSystemId());UriComponentsBuilderbuilder=UriComponentsBuilder.fromUriString(momLoginPageUrl).pathSegment(system.getEnCode());if(StringUtil.isNotEmpty(state)){builder.queryParam("state",state);}SsoAuthorizeVOvo=newSsoAuthorizeVO();vo.setRedirectUrl(builder.build().encode().toUriString());vo.setSystemId(system.getId());vo.setEnCode(system.getEnCode());vo.setClientId(app.getClientId());vo.setRedirectUri(app.getCallbackUri());vo.setState(state);returnvo;}这段逻辑背后的真实含义是:如果登录页 URL 中带有enCode,说明这次登录是某个客户端系统发起的登录页在登录成功后,应继续走单点登录链路,并最终跳回对应客户端如果登录页 URL 中没有enCode,说明用户只是直接打开了授权服务端自己的登录页此时登录成功后,不需要回跳到外部应用,而是正常进入授权服务端本身的首页或工作台因此,enCode的作用可以总结为:它不是权限参数,而是登录来源标记,用于区分“普通登录”与“来自客户端的登录请求”。这也是一个授权中心支持多应用登录时非常重要的前端分流逻辑。四、方式一:客户端主动发起登录这种方式适用于:用户先访问客户端,此时还没有登录授权服务端。1. 调用流程图授权服务端客户端浏览器授权服务端客户端浏览器