2019年初,领导刘哥给了我一个很棘手的任务,让研究下Kerberos
认证原理,因为半年后有个项目会依赖Kerberos
实现SSO
。我心里暗暗骂了一句,我这么忙能不能让那些每天吹牛的人研究下呀,我激动的拍了下桌子,满脸笑容的答应了。
Kerberos
是啥呀,我问了问百度,翻掉前两页的广告之后,我终于查到了一些资料。
- Spring Security Kerberos 配置IWA的关键步骤备忘:https://blog.csdn.net/wwwcomy/article/details/84883086
- Kerberos基本概念及原理汇总:https://cloud.tencent.com/developer/article/1381306
- 第十二章 Spring Security扩展:https://www.iteye.com/blog/lengyun3566-1404943
- spring-security-kerberos官网:https://spring.io/projects/spring-security-kerberos
- …
以上内容讲的都挺好,头一个礼拜我Google了各种网站,终于在我的努力下懂得了原来这就是Kerberos
,我真的懂了,但我应该怎么做呢。就好像我懂得了这就是涡轮增压的原理,行了,那你去设计台车吧,我拿着手中的锤子不知道该砸向谁。其实网上的内容大部分是讲解Kerberos
的认证原理,都是很底层的内容,当然对于开发人员来说掌握其中的原理是必要的,但只需要简单了解即可,不需要深入研究,因为有很多框架已经把内部的逻辑实现了,我们只要按照相关的配置信息简单配置即可。针对Kerberos
认证原理,我们只需了解:
- 大致的认证过程
- 所需要的机器种类
- 所需要的机器环境。
如何将Kerberos用到具体的项目中才是我们真正需要掌握的,而网上能够查到的资料又非常稀少,为了将我2个月的研究过程进行整理,并希望能给恰巧遇到同样问题的你提供一点帮助,特整理了本篇文章,当然内容肯定会由很多错误的地方,还望嘴下留情,我们才好共同探讨。
环境介绍
先说下甲方爸爸的机器环境:
- 服务器操作系统:
Windows Server2016 R2
,服务器已经入域 - 部署中间件:
Tomcat 8.5
,运行环境Java
,框架Spring Boot 2
,B/S
框架 - 客户端操作系统:
Windows 10
,客户端同样已经入域、客户端浏览器为万恶的IE9(当时微软还没有丢弃它)
再说下我的需求:
我们未来要做一个系统,这个系统支持两种登录方式:
- 用户名+密码。访问地址为
http://www.jiafangbaba.com/login
之后跳转到登录页面,输入用户名和密码,之后登录进入主页。这种实现过程就不说了,每个公司都有自己的一套权限认证体系。 - 域认证。访问地址
http://www.jiafangbaba.com
之后,根据当前的域用户信息直接跳转到系统首页。
本篇内容只是关于如何实现SSO
的,对于上述两种登录方式的逻辑实现,以后再写。
实施步骤
整个实现步骤大致分为下面几步:
- 域环境准备
- 服务器端口配置
- 编写测试程序
- 浏览器配置
- 验证成果
域环境准备
先在VM上创建三台虚机,相关配置如下:
KDC/DNS | Server | Client | |
---|---|---|---|
OS | Windows Server 2016 | Windows Server 2016 | Win10 |
IP | 192.168.150.138 | 192.168.150.139 | 192.168.150.140 |
DNS | 127.0.0.1 | 192.168.150.138 | 192.168.150.138 |
AD | jiafangbaba.com | jiafangbaba.com | jiafangbaba.com |
JDK | ----- | Jdk1.8 | ----- |
Tomcat | ----- | apache-tomcat-8.5.42 | ----- |
计算机名 | kdc.jiafangbaba.com | server.jiafangbaba.com | client.jiafangbaba.com |
备注 | AD域控制器 | 应用服务器 | 客户端 |
虚机的创建步骤大家自己找找吧,到处都是:这个不会有人不知道吧。
搭建域环境的步骤大家找找吧,也到处都是:https://www.jb51.net/article/252392.htm
KDC/DNS端环境搭建详细步骤
- 修改计算机名为kdc
- 修改本机IP为静态IP
(192.168.150.138
),DNS地址为127.0.0.1
- 检查登录用户Administrator是否有密码,检查密码是否符合密码策略(默认策略密码要有大小写字母和数字)
- 搭建域服务器,域名为
jiafangbaba.com
- 创建两个域用户
- 用户名:user2(用于client端登录),密码:Tomcat2019
- 用户名:tomcat1(用于生成keytab文件)密码:Tomcat2019
Server端
- 修改计算机名为
server
- 修改本机IP为静态IP(
192.168.150.139
),DNS地址为192.168.150.138
(KDC的IP) - 加入域(
jiafangbaba.com
)
Client端
- DNS地址为
192.168.150.138
(KDC的IP) - 加入域(
jiafangbaba.com
),并用域用户(user2
)登录
服务器端口配置
我没Google到端口配置的相关内容,这也是我在调试的过程中遇到的很头疼的一个问题。同样的包在我本地行,在用户现场死活跑不出来,研究了好长一段时间才明白,我本地都是把防火墙禁掉的,而现场环境是打开的,所以需要提前将相关端口策略开通,才能保证服务器之间的Kerberos验证是没有问题的。我通过抓包工具抓到了需要开放的端口。
相关端口见下表
本端 | 本端端口 | 本端类型 | 对端 | 对端端口 | 对端类型 | 协议 | 功能 |
---|---|---|---|---|---|---|---|
Client | 随机 | 客户端 | KDC | 53 | 服务端 | UDP | DNS解析 |
Client | 随机 | 客户端 | KDC | 88 | 服务端 | TCP | KerberosV5:AS Request |
Client | 随机 | 客户端 | KDC | 389 | 服务端 | UDP | 访问LDAP服务 |
Client | 随机 | 客户端 | KDC | 389 | 服务端 | TCP | 访问LDAP服务 |
Server | 随机 | 客户端 | KDC | 53 | 服务端 | UDP | DNS解析 |
Server | 随机 | 客户端 | KDC | 88 | 服务端 | UDP | KerberosV5:AS Request |
Server | 随机 | 客户端 | KDC | 389 | 服务端 | TCP | 访问LDAP服务 |
Client | 随机 | 客户端 | Server | 80 | 服务端 | TCP | HTTP请求 备注:此处对端端口为Tomcat的端口,默认为8080,测试环境中已修改为80. |
在测试环境中通过telnet命令确定机器之间是互通的,这一步非常关键。上表中的Client相关的端口大家可以忽略,因为本身Client对上面的端口是放开的,特殊情况下才需要考虑Client的上述配置。
编写测试程序
按照上述内容配置相关服务器信息之后,下面就到了真正的测试环节,首先我们要准备测试包部署到Server
服务器上的Tomcat
中。由于我使用的是SpringBoot
框架,而spring-security-kerberos
是Spring
对Kerbeors的实现,对于Spring Security
,上来肯定是先来个WebSecurityConfig
。
此类的目的有两个:1、实现WebSecurityConfigurerAdapter
的自动装载,其中下图红框中的adDomain
、adServer
等参数是我们实现Kerberos
认证的关键参数。
- 实现权限的拦截。
我为了测试简单,把/home
、all-new
两个URL添加到了忽略列表,其中/home
是为了验证kerberos
认证是否通过,all-new
是为了获取所有域用户,本篇文章不谈域用户的获取。
package com.example.demowin.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
import org.springframework.security.kerberos.client.config.SunJaasKrb5LoginConfig;
import org.springframework.security.kerberos.client.ldap.KerberosLdapContextSource;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${app.ad-domain}")
private String adDomain;
@Value("${app.ad-server}")
private String adServer;
@Value("${app.service-principal}")
private String servicePrincipal;
@Value("${app.keytab-location}")
private String keytabLocation;
@Value("${app.ldap-search-filter}")
private String ldapSearchFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(spnegoEntryPoint())
.and()
.authorizeRequests()
.antMatchers( "/all-new/").permitAll()
.and()
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
.logout()
.permitAll()
.and()
.addFilterBefore(
spnegoAuthenticationProcessingFilter(authenticationManagerBean()),
BasicAuthenticationFilter.class);
//初始化krb5config文件位置
// initKrb5Config();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(activeDirectoryLdapAuthenticationProvider())
.authenticationProvider(kerberosServiceAuthenticationProvider());
}
@Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
return new ActiveDirectoryLdapAuthenticationProvider(adDomain, adServer);
}
@Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint("/login");
}
@Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter(
AuthenticationManager authenticationManager) {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
/*@Bean
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator());
provider.setUserDetailsService(ldapUserDetailsService());
return provider;
}*/
@Bean
public SunJaasKerberosTicketValidator sunJaasKerberosTicketValidator() {
SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
ticketValidator.setServicePrincipal(servicePrincipal);
ticketValidator.setKeyTabLocation(new FileSystemResource(keytabLocation));
ticketValidator.setDebug(true);
return ticketValidator;
}
@Bean
public KerberosLdapContextSource kerberosLdapContextSource() throws Exception {
KerberosLdapContextSource contextSource = new KerberosLdapContextSource(adServer);
contextSource.setLoginConfig(loginConfig());
return contextSource;
}
@Bean
public LdapTemplate ldapTemplate() throws Exception {
LdapTemplate template = new LdapTemplate(kerberosLdapContextSource());
return template;
}
public SunJaasKrb5LoginConfig loginConfig() throws Exception {
SunJaasKrb5LoginConfig loginConfig = new SunJaasKrb5LoginConfig();
loginConfig.setKeyTabLocation(new FileSystemResource(keytabLocation));
loginConfig.setServicePrincipal(servicePrincipal);
loginConfig.setDebug(true);
loginConfig.setIsInitiator(true);
loginConfig.afterPropertiesSet();
return loginConfig;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() {
KerberosServiceAuthenticationProvider provider = new KerberosServiceAuthenticationProvider();
provider.setTicketValidator(sunJaasKerberosTicketValidator());
provider.setUserDetailsService(dummyUserDetailsService());
return provider;
}
@Bean
public DummyUserDetailsService dummyUserDetailsService() {
return new DummyUserDetailsService();
}
}
相关配置信息说明如下:
- ad-domain:該參數表示的是域名,可在入域的“計算機屬性”中查找,建議統一使用大寫字母表示,為
JIANGFANGBABA.COM
; - ad-server:該參數表示的是ldap服務地址,對於
windows server
系統,我們在創建DC的時,會自動啓用該服務,所以此處地址應為域控服務器計算機名稱(包含域名),為:ldap://kdc.jiafangbaba.com
;小写即可 - service-principal:該參數表示的是認證主體的名稱,即部署應用程序的計算機名稱(包含域名),為:
HTTP/server@JIAFANGBABA.COM
; - keytab-location:該參數表示的keytab文件位置,此处为
/tmp/server.keytab
,代表Tomcat所在的磁盘下的/tmp/server.keytab
文件,假如Tomcat在D盘,那么需要将keytab文件拷贝到D:/tmp/
下,具體路徑和名稱根據情況而定,此文件根據service-principal
生成; - ldap-search-filter:ldap目錄查詢過濾條件,不需要修改。
创建keytab文件
上面的配置中有一步是需要创建keytab文件,此文件为Server服务器上的秘钥文件,我们知道kerberos
认证中涉及到三方认证,分别是Client-KDC
、Server-KDC
、Client-Server
之间的认证,Client-KDC
、Client-Server
之间的认证其实是登录Client
的域用户与KDC
之间的认证,而Server为一台具体的服务器并没有具体的域用户登录信息,所以我们需要将Server
上部署的应用程序作为一个域用户即principal
进行权限的认证,那么就需要建立Server
上的应用程序和域用户的映射即Server.jiafangbaba.com
与tomcat1
域用户之间的映射关系。简单来说Client
需要有个域用户,Server
上同样需要一个域用户,并通过此域用户创建一个keytab
秘钥文件,每次Server
进行kerberos
认证时,以此秘钥文件代替域用户名、密码进行相关的校验。
KDC中执行下列代码:
- 执行
setspn –a HTTP/server.jiafangbaba.com tomcat1
- 执行
ktpass /out c:\tmp\server.keytab /mapuser tomcat1@JIAFANGBABA.COM /princ HTTP/server.jiafangbaba.com@JIAFANGBABA.COM /pass Tomcat2019 /ptype KRB5_NT_PRINCIPAL /crypto ALL`
- 将生成的keytab文件拷贝到server端
注意:keytab文件所在的目录一定已经被创建好
- /out;指定keytab文件生成的位置
- /mapuser 用户名@域名(大写)
- /princ setspn设置的认证主体名@域名(大写)
- /pass 用户密码
- /ptype 认证主体类型(写这个就行)
- /crypto 支持的加密类型
“/home”等其他地址的详细页面如下:
package com.example.demowin.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home.html");
registry.addViewController("/").setViewName("home.html");
registry.addViewController("/hello").setViewName("hello.html");
registry.addViewController("/login").setViewName("login.html");
}
}
home.html页面代码如下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Kerberos Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
<p>Click <a th:href="@{/all-new/}">all domain users</a> to see all domain users.</p>
</body>
</html>
点击home.html
的Click here
超链接,会跳转到/hello
页面,因为我们在WebSecurityConfig
类中并没有将/hello
页面添加到permitAll
,所以页面进入/hello
之前会进行kerberos
认证,认证通过之后会跳转到/hello
页面,返回hello.html
。同时Spring Security
会将当前登录的域用户信息封装到后端HttpServletRequest
对象中,在hello.html
页面通过thymeleaf
语法 [[${#httpServletRequest.remoteUser}]]
就可将登录的域用户名打印出来。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Kerberos Example</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<h1 th:inline="text">User Principal is [[${#httpServletRequest.userPrincipal}]]!</h1>
</body>
</html>
浏览器配置
在IE浏览器中,将要访问的域名server.jiafangbaba.com
路径添加到本地信任站点,否则浏览器会弹出Windows窗口输入用户名密码。
如果使用的是EDGE
、Chrome
浏览器,同样需要在IE浏览器下设置本地信任站点,因为浏览器都会使用IE中的信任站点进行权限的认证。