JWT单点登录

单点登录

零、用户模块内容以及设计

用户模块一般包含注册、认证、鉴权等功能。

注册一般是指添加用户信息。

认证登录和判断是否登录。

鉴权是验证用户的权限是否符合要求。

一般设计:用户-----角色-----权限,多对多一共5张表。

特殊设计:相同角色可能由于用户的特殊性给与不同的权限。用户-----权限组(角色)----权限。用户最终跟权限挂钩,权限组只是作为一个模板(身份)。

一、问题的提出

场景一:企业里有一系列的系统,由于开发的时间有先后,导致出现了OA、ERP、CRM等系统,每个都有独立的登录模块,用户在使用系统的时候,需要登录多个系统,并且每个系统都不能直接跳转到其他系统中。

提出问题:能否在登录了其中一个系统后,其他的系统都不需要登录,直接认证,那么系统之间也就可以很容易跳转和相互调用了。

场景二:某个大型的企业收购了另外一个企业,本来每一个企业都有其用户数据库。

提出问题:能否直接将被收购的企业的网站嵌入到原来的网站,并且无论是哪个企业的用户登录其中一个系统后,就不需要登录另一个系统,可以直接使用。

场景三:现在,很多企业都是使用微服务架构来实现自己的系统。

提出问题:多个服务之间,如何保证登录了其中一个服务后,就能够直接使用其他的服务,而不需要再次登录。

二、单点登录SSO
1.1 什么是单点登录

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

较大的企业内部,一般都有很多的业务支持系统为其提供相应的管理和IT服务。
例如财务系统为财务人员提供财务的管理、计算和报表服务;
人事系统为人事部门提供全公司人员的维护服务;
各种业务系统为公司内部不同的业务提供不同的服务等等。
这些系统的目的都是让计算机来进行复杂繁琐的计算工作,来替代人力的手工劳动,提高工作效率和质量。这些不同的系统往往是在不同的时期建设起来的,运行在不同的平台上;也许是由不同厂商开发,使用了各种不同的技术和标准。

如果举例说国内一著名的IT公司(名字隐去),内部共有60多个业务系统,这些系统包括两个不同版本的SAP的ERP系统,12个不同类型和版本的数据库系统,8个不同类型和版本的操作系统,以及使用了3种不同的防火墙技术,还有数十种互相不能兼容的协议和标准,你相信吗?不要怀疑,这种情况其实非常普遍。每一个应用系统在运行了数年以后,都会成为不可替换的企业IT架构的一部分,如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbpDdpJ0-1686298555220)(mdpic/p1.PNG)]

随着企业的发展,业务系统的数量在不断的增加,老的系统却不能轻易的替换,这会带来很多的开销。其一是管理上的开销,需要维护的系统越来越多。很多系统的数据是相互冗余和重复的,数据的不一致性会给管理工作带来很大的压力。业务和业务之间的相关性也越来越大,例如公司的计费系统和财务系统,财务系统和人事系统之间都不可避免的有着密切的关系。

为了降低管理的消耗,最大限度的重用已有投资的系统,很多企业都在进行着企业应用集成(EAI)。企业应用集成可以在不同层面上进行:例如在数据存储层面上的“数据大集中”,在传输层面上的“通用数据交换平台”,在应用层面上的“业务流程整合”,和用户界面上的“通用企业门户”等等。事实上,还用一个层面上的集成变得越来越重要,那就是“身份认证”的整合,也就是“单点登录”。

通常来说,每个单独的系统都会有自己的安全体系和身份认证系统。整合以前,进入每个系统都需要进行登录,这样的局面不仅给管理上带来了很大的困难,在安全方面也埋下了重大的隐患。下面是一些著名的调查公司显示的统计数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zj4vyoP0-1686298555221)(mdpic/p2.PNG)]

另外,使用“单点登录”还是SOA时代的需求之一。在面向服务的架构中,服务和服务之间,程序和程序之间的通讯大量存在,服务之间的安全认证是SOA应用的难点之一,应此建立“单点登录”的系统体系能够大大简化SOA的安全问题,提高服务之间的合作效率。

1.2 单点登录的技术实现机制

单点登录的机制其实是比较简单的,用一个现实中的例子做比较。颐和园是北京著名的旅游景点,在颐和园内部有许多独立的景点,例如“苏州街”、“佛香阁”和“德和园”,都可以在各个景点门口单独买票。很多游客需要游玩所有的景点,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿进拿出的,容易丢失,很不安全。于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门口出示一下刚才买的套票就能够被允许进入每个独立的景点

单点登录的机制也一样,如下图所示,当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录(1);根据用户提供的登录信息,认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket(2);用户再访问别的应用的时候(3,5)就会将这个ticket带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性(4,6)。如果通过效验,用户就可以在不用再次登录的情况下访问应用系统2和应用系统3了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aDW1lLyh-1686298555221)(mdpic/p3.PNG)]

从上面的视图可以看出,要实现SSO,需要以下主要的功能:

所有应用系统共享一个身份认证系统。
统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。

所有应用系统能够识别和提取ticket信息
要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。

常见的方式有:

  1. 使用redis之类的实现session共享
  2. 使用jwt实现
  3. 使用CAS实现
二、远程调用方式RPC

指在一个项目中,调用另一个项目中的服务。

【RMI】:RMI是个典型的为java定制的远程通信协议, 我们都知道,在single vm中,我们能够通过直接调用java object instance来实现通信,那么在远程通信时,假设也能依照这样的方式当然是最好了。这样的远程通信的机制成为RPC(RemoteProcedure Call),RMI正是朝着这个目标而诞生的。传输的标准格式是Java Object Stream;基于Java串行化机制将请求的Java Object信息转化为流。传输协议是Socket。

特点:为Java定制,所以在Java中运行速度最快,使用Java语言开发,不能跨语言。通用性较弱。例如微服务dubbo就是采用的此方案。

【REST】:采用http协议进行通信。例如微服务的springcloud就是采用此方案。

特点:由于采用的通用协议,也就意味着通用性比较强。基本上现在大多都采用此方案。

【WS】:webservice。采用SOAP(SimpleObject Access Protocol)协议,采用标准的xml格式传输数据。要求语言格式为WSDL(webservice describe language)。

特点: 使用xml格式,通用性也很强。但是xml传输数据相对json来说,体积比较大,格式解析比较麻烦。早期用得很多。

【JMS】:Java消息服务,传输内容作为一个Message。

三、JWT的使用
3.1 session的使用原理

http协议是无状态的。无状态是指服务器不记录客户端的信息。借助session来确认是否同一个客户端发送的请求。

当客户端第一次访问服务器时,请求中没有包含sessionid,服务器会自动创建一个session,并将该sessionid保存到客户端的cookie中,age采用默认设置(即关闭浏览器失效),下一次客户端访问服务器时,会将sessionid传输到服务器,服务器将会验证该id是否合法,并确认该id所关联的数据(map)。

【缺点:】如果需要跨服务认证,那么可能无法获取之前存在的session信息。不适合分布式环境,除非进行session共享。

3.2 JWT介绍

在分布式环境中,需要模拟session的使用过程,此处采用第三方的框架JWT。

JWT,即JSON WEB TOKEN。是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

3.3 JWT原理

参考JWT官方文档:https://jwt.io/

3.4 JWT的使用

1、导入依赖

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.1</version>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2、编写帮助类

public class TokenUtil {
    private static Long tokenExpireTime = 1000 * 60 * 30L;//  单位毫秒
    public static final String PRIVATEKEY = "privateKey";
    public static final String ACCESSTOKEN = "AccessToken";// 公私钥
    private static final String secretKey = "ueor82739sjsd234759jdfijosd289347sdjklfvjsxdr389wrksjdhfjksdyr9234yu89htsdkhfjksdhf83wy4hrsdjkhfsdjkh8i34wyuirfhsdjkfsxmnfbcvm,xcnskdhfriw3eyrikni12y391y238923y4y89dfhisfhsdjknfk23w4y598hfdjkfkjxcfbnisyer93we5rhkdjsfnjks"; // secret

    // 生成token
    public static String createToken(String userName){
        // 得到当前时间
        Date now = new Date();
        // 通过hs256算法,以及secretKey得到Algorithm对象
        Algorithm algo = Algorithm.HMAC256(secretKey);
        String token = JWT.create()
                .withIssuer("qianfeng")
                .withIssuedAt(now)
                .withExpiresAt(new Date(now.getTime() + tokenExpireTime))
                .withClaim("userName", userName)//保存身份标识
                .sign(algo);
        return token;
    }



    /**
     * JWT验证
     * @param token
     * @return userName
     */
    public static String verifyJWT(String token){
        String userName = null;
        try {
            Algorithm algorithm = Algorithm.HMAC256(secretKey);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("qianfeng")
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            userName = jwt.getClaim("userName").asString();
        } catch (JWTVerificationException e){
            e.printStackTrace();
        }
        return userName;
    }

    public static void main(String[] args) {
        String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJxaWFuZmVuZyIsImV4cCI6MTYwNTYwMzc1NSwidXNlck5hbWUiOiJhZG1pbiIsImlhdCI6MTYwNTYwMTk1NX0.7mgYbJfCSqkauj0Vi7KJba96qHjk7i3CYYj4bCJF5FE";
        System.out.println(verifyJWT(jwt));

//        System.out.println(createToken("admin"));
    }
}

3、在登录时创建token并存入到cookie中,在访问时加载cookie中的数据并发送到服务器验证。

四、CAS实现单点登录的原理

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:

【1】开源的企业级单点登录解决方案。

【2】CAS Server 为需要独立部署的 Web 应用。

【3】CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。下图是 CAS 最基本的协议过程:

CAS原理
img

从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server

SSO单点登录访问流程主要有以下步骤:

  • 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。

  • 定向认证:SSO客户端会重定向用户请求到SSO服务器。

  • 用户认证:用户身份认证。

  • 发放票据:SSO服务器会产生一个随机的Service Ticket。

  • 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。

  • 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。

四、CAS的安装和代码的实现
4.1 CAS Server的安装

CAS服务器安装

可以在下载:https://github.com/apereo/cas-overlay-template/tree/5.3

点击Download ZIP下载并解压
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0rmAOg2U-1686298555222)(mdpic/p4.PNG)]

下载后,将工程用开发工具打开,使用maven进行打包为war

使用开发工具打开并使用maven的package命令打包成war文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FQeYReDY-1686298555222)(mdpic/p5.PNG)]

将cas.war部署到tomcat的webapps目录下,后启动tomcat

在浏览器打开http://localhost:8080/cas 会出现登录页面,默认用户名:casuser,密码:Mellon

4.2 编写客户端

下载一个客户端示例代码:https://github.com/cas-projects/cas-sample-java-webapp

新建一个Java web的maven项目,将刚下载的示例项目中的pom.xml中的依赖复制到自己项目中,导入对应的依赖,将示例项目的web.xml中的内容复制到自己的项目的web.xml中,并将示例项目中的index.jsp和logout.jsp复制到自己的项目中,并修改web.xml中配置,将cas服务端修改成自己配置的tomcat地址,将客户端修改成自己的客户端的地址,例如本项目web.xml中的配置

<filter>
        <filter-name>CAS Single Sign Out Filter</filter-name>
        <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>http://localhost:8080/cas</param-value>
        </init-param>
    </filter>

    <listener>
        <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
    </listener>

    <filter>
        <filter-name>CAS Authentication Filter</filter-name>
        <!--<filter-class>org.jasig.cas.client.authentication.Saml11AuthenticationFilter</filter-class>-->
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>http://localhost:8080/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080/demo</param-value>
        </init-param>
    </filter>

    <filter>
        <filter-name>CAS Validation Filter</filter-name>
        <!--<filter-class>org.jasig.cas.client.validation.Saml11TicketValidationFilter</filter-class>-->
        <filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>http://localhost:8080/cas</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080/demo</param-value>
        </init-param>
        <init-param>
            <param-name>redirectAfterValidation</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>useSession</param-name>
            <param-value>true</param-value>
        </init-param>
        <!--
        <init-param>
            <param-name>acceptAnyProxy</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>proxyReceptorUrl</param-name>
            <param-value>/sample/proxyUrl</param-value>
        </init-param>
        <init-param>
            <param-name>proxyCallbackUrl</param-name>
            <param-value>https://mmoayyed.unicon.net:9443/sample/proxyUrl</param-value>
        </init-param>
        -->
        <init-param>
            <param-name>authn_method</param-name>
            <param-value>mfa-duo</param-value>
        </init-param>
    </filter>

    <filter>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>CAS Single Sign Out Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>CAS Validation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>CAS Authentication Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

启动项目,运行项目的首页,会出现以下问题

未配置https出现的问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09V2cK8A-1686298555223)(mdpic/p6.PNG)]

出现此问题的原因主要是CAS默认支持的服务器和客户端必须是https协议的,但是我们刚写的项目中默认是http协议,此处有两种办法解决:1. 将刚刚的项目配置成https。2:修改服务端配置,让http也可以支持。此处为了简单起见直接采用第二种方式。实际生产环境中可以采取第一种方式。

修改配置:在刚刚配置服务端的tomcat的webapps中找到cas项目,修改cas\WEB-INF\classes\services目录下的HTTPSandIMAPS-10000001.json文件,修改第三行。

#未修改前
"serviceId" : "^(https|imaps)://.*",
#修改成
"serviceId" : "^(https|http|imaps)://.*",

修改cas\WEB-INF\classes目录下的application.properties文件,在最后加上如下内容

cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true

重启服务器端后,再访问客户端的首页,会提示登录

登录页
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RC3Wk2vT-1686298555223)(mdpic/p7.PNG)]

说明客户端和服务器都配置成功,接下来需要验证客户端是否能够实现SSO效果

在tomcat的webapps中将客户端代码demo复制一份,起名为demo1,并修改web.xml中的demo为demo1

重启tomcat

在浏览器中打开:http://localhost:8080/demo,并开启一个新的标签页打开另一个项目:http://localhost:8080/demo1

发现都会出现上图的登录页面,在其中一个登录页面使用用户名casuser和密码Mellon进行登录,会跳转到项目的首页,然后在另一个标签中刷新,发现不用再次登录也会跳转到首页,说明单点登录效果成功。

4.3 使用springboot集成CAS客户端

接下来搭建springboot项目的客户端1

在新建的springboot项目的pom.xml添加如下依赖(匹配对应的版本)

<dependency>
    <groupId>net.unicon.cas</groupId>
    <artifactId>cas-client-autoconfig-support</artifactId>
    <version>2.1.0-GA</version>
</dependency>

修改application.properties文件

server.port=8088
#cas服务端的地址
cas.server-url-prefix=http://localhost:8080/cas
#cas服务端的登录地址
cas.server-login-url=http://localhost:8080/cas/login
#当前服务器的地址(客户端)
cas.client-host-url=http://localhost:8088
#Ticket校验器使用Cas30ProxyReceivingTicketValidationFilter
cas.validation-type=cas3

修改Application类,添加注解@EnableCasClient

@EnableCasClient
@SpringBootApplication
public class TestcasApplication {
	public static void main(String[] args) {
		SpringApplication.run(TestcasApplication.class, args);
	}
}

创建一个测试类

@RestController
public class CasController {
    @RequestMapping("/")
    public String hello(){
        return "hello, cas";
    }
}

打开浏览器启动http://localhost:8088,也会出现登录页面,可以跟前面的客户端一起测试效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值