掘金 后端 ( ) • 2024-05-03 09:56

基于Spring框架搭建一套含有单点登陆(SSO)的综合服务平台

前言

最近搭建了一套综合服务平台系统,主要是基于Oauth2.0实现单点登陆,用SSO的形式去登录到接入的每个系统中;该平台还包含了(子)系统注册、(子)系统权限控制、网络隔离、数据隔离等等。

接下来就直接上干货吧

一、什么是单点登录?

首先我们得了解一下什么是单点登陆:单点登录(SSO)是一种身份验证和授权机制,允许用户使用一组凭据(如用户名和密码)登录到一个门户网站,然后在不再需要重新输入凭据的情况下访问多个相关业务系统

了解单点登陆

单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。----来源 《百度百科》

换而言之,在公司业务逐渐壮大的过程中,开发了很多的子系统。每个子系统都有自己的用户的登录、注册模块,为了能够在公司内部统一用户登录授权功能,诞生了一个统一的用户登录认证系统,这个系统就可以称之为单点登录系统。

单点登录(SSO)的实现方式主要包括以下三种(我这里用的是基于令牌的方式):

  • 基于
    令牌的单点登录。这种方式使用令牌(Token)作为身份认证的凭证。用户登录认证中心后,认证中心生成一个令牌并发送给用户的浏览器。用户在访问其他应用程序时,将令牌发送给应用程序,应用程序再将令牌发送给认证中心进行验证。如果令牌有效,认证中心返回用户信息,用户便可以访问应用程序。
  • 基于代理的单点登录。在这种方式中,代理服务器作为单点登录的代理。用户在访问代理服务器时,代理服务器将用户的身份信息传递给认证中心进行验证。认证中心验证通过后,代理服务器保存用户的身份信息,并将其传递给其他应用程序。应用程序通过代理服务器获取用户身份信息,完成身份验证。
  • 基于集中式授权的单点登录。这种方式以授权服务器为核心,负责验证用户的身份和权限。授权服务器将用户的授权信息提供给访问应用程序的用户。应用程序通过与授权服务器交互,获取用户的授权信息,完成身份验证。

此外,还有基于session、cookie、token等实现方式。

二、OAuth2.0 协议

OAuth 2.0 是一种授权框架,用于向第三方应用授权访问受保护的资源。单点登录(SSO)是一种身份验证机制,允许用户使用一组凭据登录多个应用程序,而无需为每个应用程序重新进行身份验证。基于 OAuth 2.0 的单点登录将这两个概念结合起来,允许用户使用 OAuth 2.0 进行身份验证,并在多个应用程序之间共享身份验证状态

了解Oauth2.0协议

基于 OAuth 2.0 的单点登录的工作原理

  1. 用户登录请求: 用户首先尝试访问某个需要身份验证的应用程序。如果用户尚未登录,则应用程序将重定向到身份提供者(通常是认证服务器)。
  2. 认证服务器登录页面: 用户被要求在认证服务器上输入其凭据(例如用户名和密码)。这个过程是标准的 OAuth 2.0 授权流程,通常是授权码授权流程或者密码授权流程。
  3. 授权码或令牌颁发: 用户成功进行身份验证后,认证服务器会颁发一个授权码或访问令牌给应用程序。授权码授权流程中,应用程序将使用授权码交换访问令牌。而密码授权流程中,用户的凭据会直接交给认证服务器,认证服务器会直接颁发访问令牌。
  4. 共享身份验证状态: 应用程序将访问令牌存储在本地,并将用户身份验证状态与该访问令牌关联起来。通常,这是通过在用户会话中存储令牌或将令牌存储在浏览器的 Cookie 中实现的。
  5. 访问受保护的资源: 用户继续访问应用程序所需的受保护资源。每当应用程序需要验证用户身份时,它将检查本地存储的访问令牌。如果令牌过期或无效,应用程序可能会尝试使用令牌刷新机制来获取新的访问令牌。
  6. 单点注销: 当用户注销某个应用程序时,应用程序将清除本地存储的访问令牌,并通知认证服务器注销用户的会话。这样,用户在其他与认证服务器信任的应用程序中的会话也将被注销,从而实现单点注销。

总而言之,基于 OAuth 2.0 的单点登录通过将用户身份验证和授权委托给专门的认证服务器来实现身份验证,使得用户可以跨多个应用程序使用相同的身份验证状态,从而提高了用户体验和安全性。

三、使用单点登录解决什么问题?

假设,当我们的 ERP 系统功能模块越来越多的时候,后期可能会拆分出产品库存系统、财务系统、订单系统、工单系统等,那是不是意味着每个系统都需要开发一套登录注册功能呢?如果每个系统有一套自己的用户体系,就会出现用户在使用的时候,需要重复注册、重复登录、重复记住对应的账号密码,一旦子系统多达十几个的时候,对于用户来说这种情况,显然是不能接受的。

其次对应我们开发人员来说,很显然也不可能每个系统开发一套相同的功能,因此我们需要有一套能够统一登录、注册、用户管理、权限等功能的系统。那么这样一套系统就是,今天要分享的单点登录系统。单点登录系统,能够让我们只需拥有一个账号,便可以访问任意的子系统,类似于我们拥有了一张通行证,行便天下,畅通无阻。

四、综合服务介绍

由于部分原因,某些截图不能在此处展示;所以这里简单介绍一下该综合服务的功能,目前的搭建是主要做了五个大板块的处理;第一个就是用户注册,这个地方我设计了两套注册方案,一个是同手机短信号进行注册登录,这种注册的账号只是普通用户;一种是通过填写完注册信息后,提交给管理审批,审批通过后会发邮件给申请时的邮箱,这种用户一般是给接入该系统的子系统管理使用;第二个就是系统管理,一般我们综合服务处理,涉及到父子系统的时候,父级和子级系统除了一个父子的关联关系就没有其他的联系了,这个系统丰富了父子系统间的交互;第三个就是权限控制;通过一些处理使用户、岗位、部门、角色拥有对应的权限,这个现在很多框架都可以实现这种控制;第四个就是单点登录;之前有发过一个基于keycloak实现的sso,那个是基于smal2.0协议进行实现的,而本次系统采用的是oauth2.0协议实现的sso;第五个是网络隔离,通过一些拦截器或者是一些特殊的处理方法实现。接下来具体看一下

(1)用户注册

短信注册

参考链接,我这里使用的是阿里云的短信服务,基本上按照参考链接里面去开通签名和模板,然后接入到Java系统中即可。主要的就是AccessKeyId和AccessKeySecret以及regionID。

demo样例,可以按照这个去操作一下,写在项目里的话最好是配置在yml文件里面。

在项目中引入阿里云短信Java SDK

    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
        <version>4.0.3</version>
    </dependency>
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
        <version>1.1.0</version>
    </dependency>
public class SmsService {
     
        // 设置你的AccessKeyId和AccessKeySecret
        private String accessKeyId = "<your-access-key-id>";
        private String accessKeySecret = "<your-access-key-secret>";
     
        // 初始化acsClient, 设置regionNo为你的短信服务所在region
        IAcsClient acsClient = new DefaultAcsClient(initClientProfile());
     
        public boolean sendSms(String phoneNumber, String signName, String templateCode, String verificationCode) throws ClientException {
            SendSmsRequest request = new SendSmsRequest();
            // 必填: 待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码, 批量调用相对于单条调用成功率更高, 但是单条消息发送成功率一般为99%以上。
            request.setPhoneNumbers(phoneNumber);
            // 必填: 短信签名-可在短信控制台中找到
            request.setSignName(signName);
            // 必填: 短信模板-可在短信控制台中找到
            request.setTemplateCode(templateCode);
            // 必填: 模板中变量的实际值,如模板中有变量为code,此处需传入实际的code值
            request.setTemplateParam("{"code":"" + verificationCode + ""}");
     
            // 选填: 上行短信总开关
            request.setSmsUpExtendCode("DEFAULT");
     
            SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
            if ("OK".equals(sendSmsResponse.getCode())) {
                return true;
            }
            return false;
        }
     
        private IClientProfile initClientProfile() {
            IClientProfile profile = DefaultProfile.getProfile(
                    "<your-region-id>", // 你的regionID
                    accessKeyId,
                    accessKeySecret);
            return profile;
        }
     
        public static void main(String[] args) throws ClientException {
            SmsService smsService = new SmsService();
            boolean isSuccess = smsService.sendSms("138xxxxxxxx", "your-sign-name", "your-template-code", "123456");
            System.out.println(isSuccess ? "短信发送成功" : "短信发送失败");
        }
    }

审批注册

在项目中接入邮件服务的依赖,然后定义一个发送邮件的工具类即可

            <dependency>
                <groupId>javax.mail</groupId>
                <artifactId>mail</artifactId>
                <version>1.4.7</version>
            </dependency>
public class EmailUtil {
        public static void sendEmail(String host, String port,
                                     final String userName, final String password, String toAddress,
                                     String subject, String emailBody) throws AddressException,
                MessagingException {
            // 设置SMTP服务器属性
            Properties properties = new Properties();
            properties.put("mail.smtp.host", host);
            properties.put("mail.smtp.port", port);
            properties.put("mail.smtp.auth", "true");
            properties.put("mail.smtp.starttls.enable", "true");
     
            // 新建一个认证器
            Authenticator auth = new Authenticator() {
                public PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(userName, password);
                }
            };
     
            // 新建一个会话
            Session session = Session.getInstance(properties, auth);
     
            // 新建一个消息,并设置其属性
            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(userName));
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
            message.setSubject(subject);
            message.setText(emailBody);
     
            // 发送消息
            Transport.send(message);
        }
    }

如果需要在工具类上面进行其他的配置可以自行加上,我在正式使用的过程中 ,设置了文件的内容发送格式、细分化了多个收件人的处理,另外加上了邮件服务器验证,一切处理完毕后进行发送邮件。

(2)系统管理

通过统一配置子系统的菜单和角色、用户,从而实现综合服务系统对子系统的控制和管理;这一操作如果是要在实际的运营中实现起来的话,需要提前和各大系统提前沟通好方案,需要多方配合,基于综合服务来操作,有点局限性和一些潜在的坑,需要自己踩一下;或者通过子系统提供API,实现这些处理的同步;

(3)权限控制

通过某些字段、表、或者拦截器进行过滤处理、这个地方不多介绍,很多开源框架都有一套这样的逻辑。

(4)单点登录

本次单点登录是基于oauth2.0实现的,首先在综合服务系统配置好子系统的APPID、APPKEY、URL。

实现流程大概是,在综合服务平台登录的时候,在后台存一个tgt到cookie中,这个tgt包含了用户信息,然后我们选择一个子系统进行免登录跳转,这个时候我们带上APPID(clientId)、APPKEY(秘钥)一起跳转,同时这个时候去校验这个tgt是否存在,如果这个tgt存在,那么我们会根据APPID和APPKEY生成一个唯一的令牌code,这个code就是父系统去访问子系统的令牌,通过携带这个code,去访问子系统。

(5)网络隔离

这一块是用了一些特殊的处理方式去解决这个问题,一般的话就是直接部署在不同的服务器即可,然后要去定时的同步数据和一些其他的东西。这一块感兴趣的大佬们可以在评论区或者私信一起沟通下。

这篇笔记比较偏向于概念化,甚至有点潦草哈哈,因为某些原因无法分享一些具体的东西给大家演示,如果有喜欢玩这一方面的兄弟可以私信我或者在评论区里面留言,大家一起交流一下。

欢迎大家在评论区讨论,今天的干货分享就到此结束了,如果觉得对您有帮助,麻烦给个三连!

以上内容为本人的经验总结和平时操作的笔记。若有错误和重复请联系作者删除!!感谢支持!!