总体概览 FreeRADIUS 3.2.10 的配置设计看似繁杂,但核心逻辑非常明确:将工具定义(Modules)、接入客户端(Clients)和业务流程(Sites)彻底解耦。如果把 FreeRADIUS 类比为一个 Spring Boot 框架:
radiusd.conf = application.yml(全局基础配置、线程池、环境变量)
clients.conf = 防火墙/白名单(允许哪些 NAS/交换机/AP 连进来)
mods-enabled/ = Service 层(具体干活的插件/工具,如数据库连接、REST 调用、密码哈希)
sites-enabled/ = Controller + Interceptor(定义请求进来后的执行管道与路由)
整体结构鸟瞰图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /etc/freeradius/ ├── radiusd.conf <-- [全局] 主配置文件(全局参数、线程池、主日志) ├── clients.conf <-- [安全] 允许接入的 NAS 交换机 / AP / 网关列表 │ ├── mods-available/ <-- [模块库] 放置所有可用的功能模块定义文件 ├── mods-enabled/ <-- [已激活模块] 软链接指向 mods-available/ │ ├── rest <-- REST 模块配置 (定义 URI、JSON 属性映射) │ ├── sql <-- SQL 模块配置 (数据库连接池、SQL 语句) │ ├── pap / chap / eap <-- 各类加密/认证算法模块 │ ├── sites-available/ <-- [流程库] 放置所有可用的虚拟服务器(流水线) └── sites-enabled/ <-- [已激活流程] 软链接指向 sites-available/ ├── default <-- 主流水线 (监听 1812/1813,处理明文/常规请求) │ ├── authorize { ... } <-- 阶段 1: 预检查、调 rest/sql、设定 Auth-Type │ ├── authenticate { ... }<-- 阶段 2: 根据 Auth-Type 执行密码比对 │ └── post-auth { ... } <-- 阶段 3: 认证成功后下发属性 (如 VLAN) │ └── inner-tunnel <-- 隧道内部流水线 (处理 802.1X PEAP 隧道解密后的请求)
radiusd.conf 整个 FreeRADIUS 服务的入口配置文件。它定义了服务的全局行为:
运行用户与组:user = freeradius / group = freeradius。
主日志配置:log { destination = files, file = ${logdir}/radius.log }。
线程池参数:thread pool { start_servers = 5, max_servers = 32 }。
文件引入($INCLUDE):负责将 clients.conf、mods-enabled/、sites-enabled/ 等零散文件拼装成完整运行上下文。
clients.conf 定义允许与 RADIUS 服务器通信的客户端设备(Network Access Server,如华为/华三交换机、无线 AP、VPN 网关或测试工具 radtest)。
1 2 3 4 5 client local_network { ipaddr = 192.168.1.0/24 secret = testing123 shortname = dev-switches }
模块层配置 FreeRADIUS 采用 “可用” 与 “启用” 分离的策略:所有功能模块配置文件存放在 mods-available/ 中,当需要使用某个功能时,在 mods-enabled/ 下建一个同名软链接即可。
核心常见模块一览:
模块名称
作用说明
常见应用场景
rest
发送 HTTP GET/POST/PUT 请求与外部 REST API 交互
对接 Spring Boot、Go 或微服务后端
sql
直接连接 MySQL / PostgreSQL 数据库
读取 radcheck、radreply 表或记录计费日志
pap
处理明文密码认证
最基础的密码比对
chap
处理 CHAP 挑战应答式认证
PPPoE、部分 VPN 认证
mschap
处理 MS-CHAPv1/v2 认证
结合 Samba/AD 做 Windows 域认证或 VPN 认证
eap
处理 802.1X / EAP 框架(PEAP, EAP-TLS, EAP-TTLS)
企业级 WiFi / 园区网有线准入
files
读取本地静态文本文件(/etc/freeradius/users)
简易测试或静态规则兜底
以 mods-enabled/rest 为例,REST 模块内部又按 FreeRADIUS 的生命周期切分了不同的 HTTP 映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rest { connect_uri = "http://192.168.1.3:8080/api/v1/radius" authorize { uri = "${..connect_uri} /check-asset" method = 'post' body = 'json' data = '{"username": "%{User-Name}", "mac": "%{Calling-Station-Id}"}' } authenticate rest_pap { uri = "${..connect_uri} /auth-pap" method = 'post' body = 'json' data = '{"username": "%{User-Name}", "password": "%{User-Password}"}' } }
站点层配置 两个主要配置文件 站点代表一个虚拟服务器(Virtual Server),也是请求真正的处理流水线。两个核心默认站点:
default :监听 1812(认证)与 1813(计费)端口,处理绝大部分未加密/单层的常规 RADIUS 请求(如 PAP、CHAP、MAC 准入)。
inner-tunnel :不直接暴露公网端口,只在内部被 eap 模块调用。当进行 802.1X PEAP 认证时,外层建立 TLS 隧道后,剥离出来的内层明文请求会送入 inner-tunnel 处理。
五大阶段 在一个站点文件(如 sites-enabled/default)中,请求会按顺序穿过以下 5 个主要阶段:
1 2 3 4 5 6 7 8 9 10 11 12 13 [ 客户端请求入站 ] │ ▼ 1. authorize ──► 决定“能否接入”及“用什么方式认证” │ ▼ 2. authenticate ──► 根据 Auth-Type 校验凭据/密码 │ ├───────────────────────┐ ▼ (认证成功) ▼ (认证失败) 3. post-auth 4. Post-Auth-Type REJECT │ │ └───────────────────────┴──► 返回 Access-Accept / Reject
① authorize(授权与准备阶段)
② authenticate(认证比对阶段)
③ post-auth(认证成功后处理)
④ accounting(计费阶段)
任务:处理客户端发来的 Accounting-Request 报文(Start / Interim-Update / Stop),记录上下线时间与流量统计。
⑤ pre-proxy / post-proxy(代理转发阶段)
任务:当 RADIUS 充当代理服务器(Proxy)需要将请求转发给上级 RADIUS 时使用。
企业典型案例(REST 前置) 具体需求 下面我们来实现一个真正的企业级案例,这个案例是典型的企业级零信任准入系统架构,通过 FreeRADIUS 作为统一准入网关,将一切身份鉴权、设备资产校验和 VLAN 动态授权全量收口到后端 Spring Boot REST 服务。它的具体要求如下:
支持普通的 PAP 、CHAP 认证
支持 EAP-TTLS + PAP 基于用户密码的认证方式
支持 EAP-TLS 证书认证
支持哑终端的 mac 认证
支持 portal server PAP 和 CHAP 认证
所有认证过程都需要走 REST 模块。
数据源接入 mysql
Freeradius 配置实现 Freeradius 3.2.10 测试环境的快速构建可以参考之前的两篇文章 :《Freeradius 3.2.10 环境搭建以及 PAP 和 CHAP 两种认证方式的测试》 、《Freeradius 3.2.10 基于密码的企业级安全认证实现》 ,这里不再赘述。
第一,/etc/freeradius/clients.conf:定义允许接入 FreeRADIUS 的交换机/AP/Portal 网关(也可以使用 nas 表代替)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 client localhost { ipaddr = 127.0.0.1 proto = * secret = testing123 nas_type = other limit { max_connections = 16 lifetime = 0 idle_timeout = 900 } } client localhost_ipv6 { ipv6addr = ::1 secret = testing123 } client docker_net { ipaddr = 172.0.0.0/8 secret = testing123 } client enterprise_network { ipaddr = 192.168.0.0/16 secret = radius_secret_2026 shortname = ent-switches }
第二,/etc/freeradius/mods-enabled/sql。配置 MySQL 数据库连接池(处理系统基础数据或备用账密)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 sql { dialect = "mysql" driver = "rlm_sql_mysql" mysql { warnings = auto } server = "192.168.1.251" port = 3306 login = "xxx" password = "xxx" radius_db = "radius3" acct_table1 = "radacct" acct_table2 = "radacct" postauth_table = "radpostauth" authcheck_table = "radcheck" groupcheck_table = "radgroupcheck" authreply_table = "radreply" groupreply_table = "radgroupreply" usergroup_table = "radusergroup" delete_stale_sessions = yes pool { start = ${thread[pool].start_servers} min = ${thread[pool].min_spare_servers} max = ${thread[pool].max_servers} spare = ${thread[pool].max_spare_servers} uses = 0 retry_delay = 30 lifetime = 0 idle_timeout = 60 max_retries = 5 } read_clients = yes client_table = "nas" group_attribute = "SQL-Group" $INCLUDE ${modconfdir} /${.:name} /main/${dialect} /queries.conf }
第三,/etc/freeradius/mods-enabled/eap。配置 EAP-TLS(证书认证) 与 EAP-TTLS(外层 TLS 隧道 + 内层 PAP)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 eap { default_eap_type = ttls timer_expire = 60 ignore_unknown_eap_types = no cisco_accounting_username_bug = no max_sessions = ${max_requests} tls-config tls-common { private_key_password = whatever private_key_file = ${certdir} /server.key certificate_file = ${certdir} /server.pem ca_file = ${cadir} /ca.pem ca_path = ${cadir} tls_min_version = "1.2" cipher_list = "DEFAULT@SECLEVEL=1" } tls { tls = tls-common } ttls { tls = tls-common default_eap_type = pap copy_request_to_tunnel = yes use_tunneled_reply = yes virtual_server = "inner-tunnel" } peap { tls = tls-common default_eap_type = mschapv2 copy_request_to_tunnel = no use_tunneled_reply = no virtual_server = "inner-tunnel" } }
第四,/etc/freeradius/mods-enabled/rest。统一定义传给 Spring Boot 后端的全量参数(包含用户、密码、MAC 地址、设备编号、NAS IP、证书序列号、认证类型等),并从 REST 响应中提取 VLAN-ID 进行动态授权。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 rest { connect_uri = "http://192.168.1.3:8080/api/v1/radius" tls { timeout = 10 } authorize { uri = "${..connect_uri} /check-asset" method = 'post' body = 'json' data = '{\ "username": "%{User-Name}",\ "chapPassword": "%{CHAP-Password}",\ "mac": "%{Calling-Station-Id}",\ "nasIp": "%{NAS-IP-Address}",\ "nasPort": "%{NAS-Port}",\ "authType": "%{control:Auth-Type}",\ "certSerialNumber": "%{TLS-Client-Cert-Serial}",\ "nasPortType": "%{NAS-Port-Type}"\ }' response = 'json' } authenticate rest_pap { uri = "${..connect_uri} /auth-pap" method = 'post' body = 'json' data = '{\ "username": "%{User-Name}",\ "password": "%{User-Password}",\ "mac": "%{Calling-Station-Id}",\ "nasIp": "%{NAS-IP-Address}"\ }' } preacct { uri = "${..connect_uri} /user/%{User-Name}/sessions/%{Acct-Unique-Session-ID}?action=preacct" method = 'post' tls = ${..tls} } accounting { uri = "${..connect_uri} /user/%{User-Name}/sessions/%{Acct-Unique-Session-ID}?action=accounting" method = 'post' tls = ${..tls} } post-auth { uri = "${..connect_uri} /user/%{User-Name}/mac/%{Called-Station-ID}?action=post-auth" method = 'post' tls = ${..tls} } pre-proxy { uri = "${..connect_uri} /user/%{User-Name}/mac/%{Called-Station-ID}?action=pre-proxy" method = 'post' tls = ${..tls} } post-proxy { uri = "${..connect_uri} /user/%{User-Name}/mac/%{Called-Station-ID}?action=post-proxy" method = 'post' tls = ${..tls} } xlat { body_uri_encode = yes tls = ${..tls} } pool { start = ${thread[pool].start_servers} min = ${thread[pool].min_spare_servers} max = ${thread[pool].max_servers} spare = ${thread[pool].max_spare_servers} uses = 0 retry_delay = 30 lifetime = 0 idle_timeout = 60 } }
第五,/etc/freeradius/sites-enabled/default。主虚拟服务器逻辑:识别 PAP/CHAP/Portal/MAC/EAP-TLS/EAP-TTLS 请求,并路由到 REST 校验和 VLAN 下发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 server default { listen { type = auth ipaddr = * port = 1812 limit { max_connections = 16 lifetime = 0 idle_timeout = 900 } } listen { type = acct ipaddr = * port = 1813 } authorize { filter_username preprocess if (User-Name == Calling-Station-Id) { update control { Auth-Type := Accept } } eap { ok = return } rest if (User-Password) { update control { Auth-Type := rest_pap } } if (reply:Cleartext-Password) { update control { &Cleartext-Password := "%{reply:Cleartext-Password}" } update reply { &Cleartext-Password !* ANY } } if (CHAP-Password) { chap } } authenticate { Auth-Type rest_pap { rest } Auth-Type CHAP { chap } eap Auth-Type Accept { pap } } preacct { preprocess acct_unique suffix files } accounting { detail -sql exec attr_filter.accounting_response } session { } post-auth { if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) { update reply { &User-Name !* ANY } } update { &reply: += &session-state: } if (reply:Tunnel-Private-Group-Id) { update reply { &Tunnel-Type := VLAN &Tunnel-Medium-Type := IEEE-802 } } -sql exec eap remove_reply_message_if_eap Post-Auth-Type REJECT { -sql attr_filter.access_reject eap remove_reply_message_if_eap } Post-Auth-Type Challenge { } Post-Auth-Type Client-Lost { } if (EAP-Key-Name && &reply:EAP-Session-Id) { update reply { &EAP-Key-Name := &reply:EAP-Session-Id } } } pre-proxy { } post-proxy { } }
第六,/etc/freeradius/sites-enabled/inner-tunnel。内层隧道:处理 EAP-TTLS 剥离 TLS 隧道 后的内层 PAP 认证请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 server inner-tunnel { listen { ipaddr = 127.0.0.1 port = 18120 type = auth } authorize { filter_username suffix eap { ok = return } rest if (&User-Password) { update control { Auth-Type := rest_pap } } if (reply:Cleartext-Password) { update control { &Cleartext-Password := "%{reply:Cleartext-Password}" } update reply { &Cleartext-Password !* ANY } } if (&CHAP-Password) { chap } files -sql } authenticate { Auth-Type rest_pap { rest } Auth-Type CHAP { chap } Auth-Type PAP { pap } Auth-Type CHAP { chap } Auth-Type MS-CHAP { mschap } mschap eap } session { } post-auth { -sql if (reply:Tunnel-Private-Group-Id) { update outer.session-state { &Tunnel-Private-Group-Id := "%{reply:Tunnel-Private-Group-Id}" &Tunnel-Type := VLAN &Tunnel-Medium-Type := IEEE-802 } } update { &outer.session-state: += &reply: } Post-Auth-Type REJECT { -sql attr_filter.access_reject update outer.session-state { &Module-Failure-Message := &request:Module-Failure-Message } } } pre-proxy { } post-proxy { } }
Mysql 表和数据准备 这部分内容可以参考 EAP-TTLS + PAP 建库建表 。另外,为了测试 chap 情况,我们也需要在数据库中再插入一条数据。账号只在数据库存在,其他之前配置的,比如 users 配置文件的中的用户数据全部删除。
1 2 3 4 INSERT INTO radius3.radcheck(username, attribute, op, value ) VALUES ('owlias01' , 'Crypt-Password' , ':=' , '$2b$12$1dKv2bZWGBPN58J1R4hFBODkMY1S.UhVYQKbqE5IiVO.jDkizVdLe' );INSERT INTO radcheck (username, attribute, op, value ) VALUES ('owlias_chap' , 'Cleartext-Password' , ':=' , '1234567' );
REST 服务实现 依赖配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 3.3.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.46</version > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jdbc</artifactId > <version > 3.3.0</version > </dependency > <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <version > 8.3.0</version > <scope > runtime</scope > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-crypto</artifactId > <version > 6.3.0</version > </dependency > <dependency > <groupId > com.github.oshi</groupId > <artifactId > oshi-core</artifactId > <version > 7.2.0</version > </dependency > </dependencies >
启动类和配置文件:
1 2 3 4 5 6 @SpringBootApplication public class App { public static void main (String[] args) { SpringApplication.run(App.class, args); } }
1 2 3 4 5 6 7 8 9 server: port: 8080 spring: datasource: url: jdbc:mysql://192.168.1.251:3306/radius3?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: xxx password: xxx driver-class-name: com.mysql.cj.jdbc.Driver
业务控制器:RadiusController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 @Slf4j @RestController @RequestMapping("/api/v1/radius") public class RadiusController { @Resource private RadiusAuthService radiusAuthService; @PostMapping("/check-asset") public ResponseEntity<RadiusCheckAssetResp> checkAsset (@RequestBody RadiusCheckAssetReq req) { try { String vlanId = radiusAuthService.checkAssetAndGetVlan(req); if (vlanId == null ) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } String userCleartextPassword = "" ; if (Objects.nonNull(req.getChapPassword())) { userCleartextPassword = radiusAuthService.getUserCleartextPassword(req.getUsername()) .orElse("" ); } RadiusCheckAssetResp resp = RadiusCheckAssetResp.builder() .tunnelType(List.of("VLAN" )) .tunnelMediumType(List.of("IEEE-802" )) .tunnelPrivateGroupId(List.of(vlanId)) .cleartextPassword(userCleartextPassword.isBlank() ? Collections.emptyList() : Collections.singletonList(userCleartextPassword)) .build(); return ResponseEntity.ok(resp); } catch (Exception e) { log.error("资产检查处理异常" , e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @PostMapping("/auth-pap") public ResponseEntity<?> authPap(@RequestBody RadiusAuthPapReq req) { boolean passed = radiusAuthService.authenticatePap(req); if (passed) { return ResponseEntity.ok().build(); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } @PostMapping("/user/{username}/sessions/{sessionId}") public ResponseEntity<?> handleSessionEvent( @PathVariable("username") String username, @PathVariable("sessionId") String sessionId, @RequestParam("action") String action, @RequestBody(required = false) Map<String, Object> body) { log.info("[RADIUS 会话事件] action={}, username={}, sessionId={}, body={}" , action, username, sessionId, body); return ResponseEntity.ok().build(); } @PostMapping("/user/{username}/mac/{mac}") public ResponseEntity<?> handleMacEvent( @PathVariable("username") String username, @PathVariable("mac") String mac, @RequestParam("action") String action, @RequestBody(required = false) Map<String, Object> body) { log.info("[RADIUS 设备事件] action={}, username={}, mac={}, body={}" , action, username, mac, body); return ResponseEntity.ok().build(); } }
请求和响应的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Data public class RadiusCheckAssetReq { private String username; private String chapPassword; private String mac; private String nasIp; private String nasPort; private String authType; private String certSerialNumber; private String nasPortType; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RadiusCheckAssetResp { @JsonProperty("Tunnel-Type") private List<String> tunnelType; @JsonProperty("Tunnel-Medium-Type") private List<String> tunnelMediumType; @JsonProperty("Tunnel-Private-Group-Id") private List<String> tunnelPrivateGroupId; @JsonProperty("Cleartext-Password") private List<String> cleartextPassword; } @Data public class RadiusAuthPapReq { private String username; private String password; private String mac; private String nasIp; }
业务接口和实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface RadiusAuthService { String checkAssetAndGetVlan (RadiusCheckAssetReq req) ; boolean authenticatePap (RadiusAuthPapReq req) ; Optional<String> getUserCleartextPassword (String username) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 @Slf4j @Service public class RadiusAuthServiceImpl implements RadiusAuthService { @Resource private RadCheckRepository radCheckRepository; private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder (); @Override public String checkAssetAndGetVlan (RadiusCheckAssetReq req) { log.info("[RADIUS 资产校验] 收到请求: req={}" , req); if ("Accept" .equalsIgnoreCase(req.getAuthType()) || req.getUsername().equalsIgnoreCase(req.getMac())) { log.info("[RADIUS 资产校验] 哑终端 MAC 认证通过, mac={}" , req.getMac()); return "102" ; } if (req.getCertSerialNumber() != null && !req.getCertSerialNumber().isBlank()) { log.info("[RADIUS 资产校验] 证书认证通过, sn={}" , req.getCertSerialNumber()); return "200" ; } List<RadCheck> userAttributes = radCheckRepository.findByUsername(req.getUsername()); if (userAttributes.isEmpty()) { log.warn("[RADIUS 资产校验失败] 用户在数据库中不存在: username={}" , req.getUsername()); return null ; } log.info("[RADIUS 资产校验成功] username={}" , req.getUsername()); return "100" ; } @Override public boolean authenticatePap (RadiusAuthPapReq req) { log.info("[RADIUS PAP 认证] 收到 PAP 密码比对请求: req={}" ,req); List<RadCheck> checks = radCheckRepository.findByUsername(req.getUsername()); if (checks.isEmpty()) { log.warn("[RADIUS PAP 认证失败] 用户不存在: username={}" , req.getUsername()); return false ; } String rawInputPassword = req.getPassword(); for (RadCheck check : checks) { String attr = check.getAttribute(); String dbVal = check.getValue(); if ("Crypt-Password" .equalsIgnoreCase(attr)) { if (dbVal.startsWith("$2a$" ) || dbVal.startsWith("$2b$" ) || dbVal.startsWith("$2y$" )) { if (passwordEncoder.matches(rawInputPassword, dbVal)) { log.info("[RADIUS PAP 认证成功] BCrypt 匹配: username={}" , req.getUsername()); return true ; } } else { log.warn("[RADIUS PAP 认证] 暂不支持的 Crypt-Password 格式: {}" , dbVal); } } else if ("Cleartext-Password" .equalsIgnoreCase(attr) || "User-Password" .equalsIgnoreCase(attr)) { if (dbVal.equals(rawInputPassword)) { log.info("[RADIUS PAP 认证成功] 明文匹配: username={}" , req.getUsername()); return true ; } } else if ("MD5-Password" .equalsIgnoreCase(attr)) { String inputMd5 = DigestUtils.md5DigestAsHex(rawInputPassword.getBytes(StandardCharsets.UTF_8)); if (dbVal.equalsIgnoreCase(inputMd5)) { log.info("[RADIUS PAP 认证成功] MD5 匹配: username={}" , req.getUsername()); return true ; } } } log.warn("[RADIUS PAP 认证失败] 密码比对不匹配: username={}" , req.getUsername()); return false ; } @Override public Optional<String> getUserCleartextPassword (String username) { return radCheckRepository.findCleartextPasswordByUsername(username); } }
数据库 Repository:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Repository public interface RadCheckRepository extends CrudRepository <RadCheck, Long> { List<RadCheck> findByUsername (String username) ; @Query("SELECT value FROM radcheck WHERE username = :username AND attribute IN ('Cleartext-Password', 'User-Password') ORDER BY id DESC LIMIT 1") Optional<String> findCleartextPasswordByUsername (@Param("username") String username) ; }
实体类 PO:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table("radcheck") public class RadCheck { @Id private Long id; private String username; private String attribute; private String op; private String value; }
测试验证 测试对 PAP 认证的支持,在 radius-client 执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ echo "User-Name = owlias01, User-Password = 123456, Calling-Station-Id = 00-11-22-33-44-55, NAS-IP-Address = 172.18.0.2" | radclient -x 172.18.0.3:1812 auth testing123 $ radtest -t pap owlias01 123456 172.18.0.3:1812 0 testing123 Sent Access-Request Id 244 from 0.0.0.0:48710 to 172.18.0.3:1812 length 91 User-Name = "owlias01" User-Password = "123456" Calling-Station-Id = "00-11-22-33-44-55" NAS-IP-Address = 172.18.0.2 Cleartext-Password = "123456" Received Access-Accept Id 244 from 172.18.0.3:1812 to 172.18.0.2:48710 length 55 Message-Authenticator = 0xf2614ab62d71f981bc8b6bed680e151f Tunnel-Type:0 = VLAN Tunnel-Medium-Type:0 = IEEE-802 Tunnel-Private-Group-Id:0 = "100"
测试对 CHAP 认证的支持,在 radius-client 执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ radtest -t chap owlias_chap 1234567 172.18.0.3:1812 0 testing123 Sent Access-Request Id 246 from 0.0.0.0:33841 to 172.18.0.3:1812 length 82 User-Name = "owlias_chap" CHAP-Password = 0xd3fac3c260bb45733b7534a8bb1b480054 NAS-IP-Address = 172.18.0.2 NAS-Port = 0 Message-Authenticator = 0x00 Cleartext-Password = "1234567" Received Access-Accept Id 246 from 172.18.0.3:1812 to 172.18.0.2:33841 length 55 Message-Authenticator = 0x4b5d91e24f1a03853f73d1ddd4bbbeec Tunnel-Type:0 = VLAN Tunnel-Medium-Type:0 = IEEE-802 Tunnel-Private-Group-Id:0 = "100"
测试对 TTLS+PAP 认证的支持,在 radius-client 执行:
1 2 3 4 5 6 7 $ eapol_test -c ttls-pap.conf -a 172.18.0.3 -p 1812 -s testing123 ... WPA: Clear old PMK and PTK EAP: deinitialize previously used EAP method (21, TTLS) at EAP deinit MPPE keys OK: 1 mismatch: 0 SUCCESS
测试对 EAP-TLS 认证的支持,先将一个测试证书 client.key 和 client.pem 复制到测试机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ docker cp ./client.key radius-client:/root/test/ $ docker cp ./client.pem radius-client:/root/test/ $ docker exec -it radius-client bash cd /root/testvim eap-tls.conf network={ ssid="Corporate-WiFi" key_mgmt=IEEE8021X eap=TLS identity="owlias01" ca_cert="/root/test/ca.pem" client_cert="/root/test/client.pem" private_key="/root/test/client.key" private_key_passwd="whatever" } eapol_test -c eap-tls.conf -a 172.18.0.3 -p 1812 -s testing123 ... MPPE keys OK: 1 mismatch: 0 SUCCESS
再来看 springboot REST 服务的日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 20:36.070+08:00 INFO 48897 --- [nio-8080-exec-7] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.074+08:00 INFO 48897 --- [nio-8080-exec-7] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.092+08:00 INFO 48897 --- [nio-8080-exec-8] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.097+08:00 INFO 48897 --- [nio-8080-exec-8] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.108+08:00 INFO 48897 --- [nio-8080-exec-9] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.115+08:00 INFO 48897 --- [nio-8080-exec-9] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.127+08:00 INFO 48897 --- [io-8080-exec-10] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.132+08:00 INFO 48897 --- [io-8080-exec-10] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.139+08:00 INFO 48897 --- [nio-8080-exec-1] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.142+08:00 INFO 48897 --- [nio-8080-exec-1] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.165+08:00 INFO 48897 --- [nio-8080-exec-2] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.169+08:00 INFO 48897 --- [nio-8080-exec-2] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.177+08:00 INFO 48897 --- [nio-8080-exec-3] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.180+08:00 INFO 48897 --- [nio-8080-exec-3] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01 20:36.190+08:00 INFO 48897 --- [nio-8080-exec-4] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验] 收到请求: req=RadiusCheckAssetReq(username=owlias01, chapPassword=, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, authType=eap, certSerialNumber=, nasPortType=Wireless-802.11) 20:36.198+08:00 INFO 48897 --- [nio-8080-exec-4] z.radius.service.RadiusAuthServiceImpl : [RADIUS 资产校验成功] username=owlias01
糟了:证书认证虽然通过了,但它存在两个很要命的问题:
重读请求问题: 一次 EAP-TLS 认证触发了多达 7 次 REST /check-asset 请求!原因是 EAP-TLS 是一个多回合的 TLS 握手过程。客户端和 RADIUS 服务器之间需要交换多个 EAP-TLS 报文(Client Hello -> Server Hello -> Certificate -> Key Exchange…)。 FreeRADIUS 的 authorize 模块在每一个 EAP 握手包到达时都会重新执行一遍。rest 模块被挂载在了 authorize 域(section)里,结果就是每进行一轮 TLS 握手,FreeRADIUS 就会调一次 Spring Boot 的 /check-asset 接口。对于这个问题,虽然可以在 REST 处使用 redis 进行去重处理,但是不够优雅!
证书编号获取不到: 因为是在 authorize 阶段嵌入的 REST,所以是拿不到证书编号的!这个问题更加致命,因为我们需要靠证书编号唯一定位一台设备(mac地址存在动态变动和伪造的问题),进而对资产进行精准的校验,甚至对证书进行动态吊销,在拿不到证书唯一编号的情况下,这一切都是妄想。
看来要对 REST 重构了!😭
对上述案例的完善(REST 后置) 问题和解决思路 上述企业案例中,我们将 REST 绑定到了 authorize 阶段 。存在的问题是:
对于 PAP 或者 TTLS-PAP 认证,需要手动查库校验用户名密码;
对于 CHAP 认证,需要手动从数据库中捞出 Cleartext-Password 再传递给 freeradius;
对于 EAP-TLS 证书认证,问题更加致命,存在重复请求和获取不到证书序列号的问题。
为此,为了彻底解决上述问题,我们将 REST 绑定到 post-auth 阶段,这样做的好处:
彻底解决 EAP 暴击问题:EAP-TLS / TTLS 无论中间交互多少个报文,post-auth 阶段只在 TLS 握手最终成功时触发 1 次,日志瞬间变干净,数据库压力暴降 80% 以上。
提取到的证书属性更完整:在 EAP-TLS 的 authorize 初始阶段,证书还没发过来;只有握手结束进入 post-auth 时,FreeRADIUS 才能百分之百拿到客户端证书的序列号(TLS-Client-Cert-Serial)或 CN。
防止无效资产校验:如果用户密码输错了,在 authorize 阶段调 REST 校验资产是纯粹浪费性能。放在 post-auth 能确保 “只有密码/证书是对的,才去查资产和发 VLAN”。
改造 FreeRadius 配置 第一,改造 rest 模块本身:/etc/freeradius/mods-enabled/rest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 rest { connect_uri = "http://192.168.1.3:8080/api/v1/radius" tls { timeout = 10 } post-auth { uri = "${..connect_uri} /check-asset" method = 'post' body = 'json' data = '{\ "username": "%{User-Name}",\ "mac": "%{Calling-Station-Id}",\ "nasIp": "%{NAS-IP-Address}",\ "nasPort": "%{NAS-Port}",\ "nasPortType": "%{NAS-Port-Type}",\ "calledStationId": "%{Called-Station-Id}",\ "certSerialNumber": "%{TLS-Client-Cert-Serial}"\ }' response = 'json' } preacct { uri = "${..connect_uri} /user/%{User-Name}/sessions/%{Acct-Unique-Session-ID}?action=preacct" method = 'post' tls = ${..tls} } accounting { uri = "${..connect_uri} /user/%{User-Name}/sessions/%{Acct-Unique-Session-ID}?action=accounting" method = 'post' tls = ${..tls} } pre-proxy { uri = "${..connect_uri} /user/%{User-Name}/mac/%{Called-Station-ID}?action=pre-proxy" method = 'post' tls = ${..tls} } post-proxy { uri = "${..connect_uri} /user/%{User-Name}/mac/%{Called-Station-ID}?action=post-proxy" method = 'post' tls = ${..tls} } xlat { body_uri_encode = yes tls = ${..tls} } pool { start = ${thread[pool].start_servers} min = ${thread[pool].min_spare_servers} max = ${thread[pool].max_servers} spare = ${thread[pool].max_spare_servers} uses = 0 retry_delay = 30 lifetime = 0 idle_timeout = 60 } }
第二,改造主虚拟服务器逻辑:/etc/freeradius/sites-enabled/default
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 server default { listen { type = auth ipaddr = * port = 1812 limit { max_connections = 16 lifetime = 0 idle_timeout = 900 } } listen { type = acct ipaddr = * port = 1813 } authorize { filter_username preprocess if (User-Name == Calling-Station-Id) { update control { Auth-Type := Accept } } eap { ok = return } sql pap chap } authenticate { Auth-Type PAP { pap } Auth-Type CHAP { chap } eap Auth-Type Accept { pap } } preacct { preprocess acct_unique suffix files } accounting { detail -sql exec attr_filter.accounting_response } session { } post-auth { if (!EAP-Type || EAP-Type == TLS) { rest } if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) { update reply { &User-Name !* ANY } } update { &reply: += &session-state: } if (reply:Tunnel-Private-Group-Id) { update reply { &Tunnel-Type := VLAN &Tunnel-Medium-Type := IEEE-802 } } -sql exec eap remove_reply_message_if_eap Post-Auth-Type REJECT { -sql attr_filter.access_reject eap remove_reply_message_if_eap } Post-Auth-Type Challenge { } Post-Auth-Type Client-Lost { } if (EAP-Key-Name && &reply:EAP-Session-Id) { update reply { &EAP-Key-Name := &reply:EAP-Session-Id } } } pre-proxy { } post-proxy { } }
第三,改造内层隧道逻辑:/etc/freeradius/sites-enabled/inner-tunnel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 server inner-tunnel { listen { ipaddr = 127.0.0.1 port = 18120 type = auth } authorize { filter_username suffix eap { ok = return } sql pap chap files } authenticate { Auth-Type PAP { pap } Auth-Type CHAP { chap } Auth-Type MS-CHAP { mschap } mschap eap } session { } post-auth { rest -sql if (reply:Tunnel-Private-Group-Id) { update outer.session-state { &Tunnel-Private-Group-Id := "%{reply:Tunnel-Private-Group-Id}" &Tunnel-Type := VLAN &Tunnel-Medium-Type := IEEE-802 } } update { &outer.session-state: += &reply: } Post-Auth-Type REJECT { -sql attr_filter.access_reject update outer.session-state { &Module-Failure-Message := &request:Module-Failure-Message } } } pre-proxy { } post-proxy { } }
REST 服务改造 依赖配置精简,暂时用不到 jdbc、加解密之类的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 3.3.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.46</version > <optional > true</optional > </dependency > </dependencies >
业务类控制器改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Slf4j @RestController @RequestMapping("/api/v1/radius") public class RadiusController { @Resource private RadiusAssetService radiusAssetService; @PostMapping("/check-asset") public ResponseEntity<RadiusAssetCheckResp> checkAssetAndGetVlan (@RequestBody RadiusAssetCheckReq req) { log.info("[RADIUS Post-Auth] 收到资产鉴权请求req: {}" , req); try { String vlanId = radiusAssetService.validateAssetAndAssignVlan(req); log.info("[RADIUS Post-Auth] 资产校验通过 | 用户: {} | 匹配 VLAN: {}" , req.getUsername(), vlanId); RadiusAssetCheckResp resp = RadiusAssetCheckResp.builder() .tunnelPrivateGroupId(vlanId) .build(); return ResponseEntity.ok(resp); } catch (AssetAccessDeniedException e) { log.warn("[RADIUS Post-Auth] 资产校验不通过 | 用户: {} | 原因: {}" , req.getUsername(), e.getMessage()); RadiusAssetCheckResp resp = RadiusAssetCheckResp.builder() .replyMessage(e.getMessage()) .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(resp); } } @PostMapping("/user/{username}/sessions/{sessionId}") public ResponseEntity<?> handleSessionEvent( @PathVariable("username") String username, @PathVariable("sessionId") String sessionId, @RequestParam("action") String action, @RequestBody(required = false) Map<String, Object> body) { log.info("[RADIUS 会话事件] action={}, username={}, sessionId={}, body={}" , action, username, sessionId, body); return ResponseEntity.ok().build(); } @PostMapping("/user/{username}/mac/{mac}") public ResponseEntity<?> handleMacEvent( @PathVariable("username") String username, @PathVariable("mac") String mac, @RequestParam("action") String action, @RequestBody(required = false) Map<String, Object> body) { log.info("[RADIUS 设备事件] action={}, username={}, mac={}, body={}" , action, username, mac, body); return ResponseEntity.ok().build(); } }
校验入参和响应定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 @Data public class RadiusAssetCheckReq { private String username; private String mac; private String nasIp; private String nasPort; private String nasPortType; private String calledStationId; private String certSerialNumber; } @Data @Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class RadiusAssetCheckResp { @JsonProperty("Tunnel-Private-Group-Id") private String tunnelPrivateGroupId; @JsonProperty("Reply-Message") private String replyMessage; }
1 2 3 4 5 public class AssetAccessDeniedException extends RuntimeException { public AssetAccessDeniedException (String message) { super (message); } }
业务类实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j @Service public class RadiusAssetService { public String validateAssetAndAssignVlan (RadiusAssetCheckReq req) { if (req.getUsername() != null && req.getUsername().equalsIgnoreCase(req.getMac())) { return processMacAccess(req); } if (StringUtils.hasText(req.getCertSerialNumber())) { return processCertAccess(req); } return processUserAccess(req); } private String processMacAccess (RadiusAssetCheckReq req) { return "300" ; } private String processCertAccess (RadiusAssetCheckReq req) { return "100" ; } private String processUserAccess (RadiusAssetCheckReq req) { return "200" ; } }
测试验证 测试环境搭建参考:《Freeradius 3.2.10 环境搭建以及 PAP 和 CHAP 两种认证方式的测试 - 搭建基本的调试环境》 。
再次测试对 PAP 认证的支持,在 radius-client 执行:
1 2 3 4 5 $ echo "User-Name = owlias01, User-Password = 123456, Calling-Station-Id = 00-11-22-33-44-55, NAS-IP-Address = 172.18.0.2" | radclient -x 172.18.0.3:1812 auth testing123 23:56:09.463+08:00 INFO 50911 --- [nio-8080-exec-5] z.radius.controller.RadiusController : [RADIUS Post-Auth] 收到资产鉴权请求req: RadiusAssetCheckReq(username=owlias01, mac=00-11-22-33-44-55, nasIp=172.18.0.2, nasPort=, nasPortType=, calledStationId=, certSerialNumber=) 23:56:09.465+08:00 INFO 50911 --- [nio-8080-exec-5] z.radius.controller.RadiusController : [RADIUS Post-Auth] 资产校验通过 | 用户: owlias01 | 匹配 VLAN: 200
再次测试对 CHAP 认证的支持,在 radius-client 执行:
1 2 3 4 5 $ radtest -t chap owlias_chap 1234567 172.18.0.3:1812 0 testing123 23:58:33.513+08:00 INFO 50911 --- [nio-8080-exec-8] z.radius.controller.RadiusController : [RADIUS Post-Auth] 收到资产鉴权请求req: RadiusAssetCheckReq(username=owlias_chap, mac=, nasIp=172.18.0.2, nasPort=0, nasPortType=, calledStationId=, certSerialNumber=) 23:58:33.513+08:00 INFO 50911 --- [nio-8080-exec-8] z.radius.controller.RadiusController : [RADIUS Post-Auth] 资产校验通过 | 用户: owlias_chap | 匹配 VLAN: 200
再次测试对 EAP-TTLS+PAP 认证的支持:也可以参考 《基于密码的企业级安全认证实现 - 测试验证》 )
1 2 3 4 5 $ eapol_test -c ttls-pap.conf -a 172.18.0.3 -p 1812 -s testing123 23:59:45.529+08:00 INFO 50911 --- [nio-8080-exec-3] z.radius.controller.RadiusController : [RADIUS Post-Auth] 收到资产鉴权请求req: RadiusAssetCheckReq(username=owlias01, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, nasPortType=Wireless-802.11, calledStationId=, certSerialNumber=) 23:59:45.529+08:00 INFO 50911 --- [nio-8080-exec-3] z.radius.controller.RadiusController : [RADIUS Post-Auth] 资产校验通过 | 用户: owlias01 | 匹配 VLAN: 200
再次测试对 EAP-TLS 证书认证的支持:看到日志中也获取到了 certSerialNumber 证书编号!并且请求只有一次,世界又重新回归了美好!测试参考 《配置文件说明以及一个企业级网络认证案例 - 测试验证》 。(注意,证书认证依赖的是证书之间的握手,所以它在认证的时候也是不需要查询数据库的用户密码的)。
1 2 3 4 5 $ eapol_test -c eap-tls.conf -a 172.18.0.3 -p 1812 -s testing123 00:01:42.655+08:00 INFO 50911 --- [nio-8080-exec-2] z.radius.controller.RadiusController : [RADIUS Post-Auth] 收到资产鉴权请求req: RadiusAssetCheckReq(username=owlias01, mac=02-00-00-00-00-01, nasIp=127.0.0.1, nasPort=, nasPortType=Wireless-802.11, calledStationId=, certSerialNumber=02) 00:01:42.656+08:00 INFO 50911 --- [nio-8080-exec-2] z.radius.controller.RadiusController : [RADIUS Post-Auth] 资产校验通过 | 用户: owlias01 | 匹配 VLAN: 100
在 RADIUS 的设计中,MAC 认证本质上就是一个 PAP 认证,只不过它的用户名和密码(或者仅用户名)都是终端的 MAC 地址(专门适用于哑终端接入)。所以它本质上根本没有所谓的 “密码校验”,认证时也不需要查库。这个案例对 MAC 认证的资产校验也完美支持,在此也进行一并测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ echo "User-Name = '02-00-00-00-00-01', User-Password = '02-00-00-00-00-01', Calling-Station-Id = '02-00-00-00-00-01', NAS-IP-Address = 192.168.1.1, NAS-Port-Type = Ethernet" | radclient -x 172.18.0.3:1812 auth testing123 Sent Access-Request Id 94 from 0.0.0.0:41337 to 172.18.0.3:1812 length 122 User-Name = "02-00-00-00-00-01" User-Password = "02-00-00-00-00-01" Calling-Station-Id = "02-00-00-00-00-01" NAS-IP-Address = 192.168.1.1 NAS-Port-Type = Ethernet Cleartext-Password = "02-00-00-00-00-01" Received Access-Accept Id 94 from 172.18.0.3:1812 to 172.18.0.2:41337 length 55 Message-Authenticator = 0x859ffef171be35027f1b74bfcd35a30c Tunnel-Private-Group-Id:0 = "300" Tunnel-Type:0 = VLAN Tunnel-Medium-Type:0 = IEEE-802
标题:
Freeradius 3.2.10 配置文件说明以及一个企业级网络认证案例