Implementing Single Sign-On (SSO) with Ticket‑Based Authentication in Java
This article explains the concept of Single Sign-On, its advantages, and two practical implementations using ticket‑based authentication and encrypted user data exchange between Service A and Service B, complete with database schema, configuration files, and full Java code examples.
Concept
Single Sign‑On (SSO) is an authentication service that allows a user to log in once with a username/password and then access multiple applications or systems (System A, System B, System C) without re‑entering credentials.
In traditional login each application requires its own username/password, which is inconvenient and vulnerable. SSO solves this by authenticating once and sharing the authentication result across systems, improving user experience and security.
Advantages of SSO
User experience improvement – one login grants access to all systems, saving time.
Enhanced security – a single, centrally managed authentication reduces the attack surface.
Simplified management – administrators can manage users and permissions from a single point.
Implementation approaches
Shared authentication – multiple systems rely on a common authentication server.
Proxy authentication – one system authenticates on behalf of others.
Token‑based authentication – after login a token is issued and accepted by all systems.
Practice 1: Ticket‑Based SSO (Service A → Service B)
Architecture diagram
User logs in to Service A with username/password.
User clicks a button in Service A, which redirects to Service B and passes the ticket issued by Service A.
Service B uses the ticket to request user information from Service A.
Service A validates the ticket and returns the user data to Service B.
Service B generates a token, attaches a redirect URL, and returns it to Service A.
Service A uses the token to access resources in Service B.
Database
Initialize a table to store SSO client details.
CREATE TABLE `sso_client_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`platform_name` varchar(64) DEFAULT NULL COMMENT '应用名称',
`platform_id` varchar(64) NOT NULL COMMENT '应用标识',
`platform_secret` varchar(64) NOT NULL COMMENT '应用秘钥',
`encrypt_type` varchar(32) NOT NULL DEFAULT 'RSA' COMMENT '加密方式:AES或者RSA',
`public_key` varchar(1024) DEFAULT NULL COMMENT 'RSA加密的应用公钥',
`sso_url` varchar(128) DEFAULT NULL COMMENT '单点登录地址',
`remark` varchar(1024) DEFAULT NULL COMMENT '备注',
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`update_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`del_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '删除标志,0:正常;1:已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='单点登陆信息表';Insert test data:
INSERT INTO cheetah.sso_client_detail (
id, platform_name, platform_id, platform_secret, encrypt_type, public_key, sso_url, remark, create_date, create_by, update_date, update_by, del_flag)
VALUES (
1, 'serviceA', 'A9mQUjun', 'Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq', 'RSA', NULL, 'http://127.0.0.1:8081/sso/url', NULL, '2023-05-23 16:55:26', 'system', '2023-05-30 13:16:16', NULL, 0);Code implementation – Service A (jump to Service B)
@PostMapping("/jumpB")
public WrapperResult
jump(@RequestBody @Validated SsoJumpReq req) {
log.debug("单点登录:{}", req.getPlatformName());
// 1. Verify platform exists
SsoClientDetail one = iSsoClientDetailService.getOne(new LambdaQueryWrapper
()
.eq(SsoClientDetail::getPlatformName, req.getPlatformName()));
if (Objects.isNull(one)) {
return WrapperResult.faild("不存在的app");
}
// 2. Generate ticket and store user info in Redis
String ticket = UUID.randomUUID().toString().replaceAll("-", "");
UserInfo userInfo = new UserInfo();
userInfo.setId(1L);
userInfo.setUsername("阿Q");
redisTemplate.opsForValue().set(RedisConstants.TICKET_PREFIX + ticket, userInfo, 5, TimeUnit.MINUTES);
// 3. Call Service B with ticket
Map
data = new HashMap<>(1);
data.put("ticket", ticket);
WrapperResult
ssoRespDto = HttpRequest
.get(one.getSsoUrl())
.queryMap(data)
.connectTimeout(Duration.ofSeconds(120))
.readTimeout(Duration.ofSeconds(120))
.execute()
.asValue(new TypeReference
>() {});
log.info("请求ServiceB 结果:{}", JsonUtils.toPrettyString(ssoRespDto));
return WrapperResult.success(ssoRespDto.getData().getRedirectUrl());
}Code implementation – Service B (receive ticket, return token)
@GetMapping("/url")
public WrapperResult
sso(@RequestParam("ticket") String ticket) throws JsonProcessingException {
log.info("收到票据:{}", ticket);
// 1. Exchange ticket for user info from Service A
Map
param = new HashMap<>(1);
param.put("ticket", ticket);
String s = HttpRequest.get("http://localhost:8081/getUser")
.queryMap(param)
.connectTimeout(Duration.ofSeconds(120))
.readTimeout(Duration.ofSeconds(120))
.execute()
.asString();
WrapperResult
ssoUserInfoWrapperResult = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.readValue(s, new TypeReference
>() {});
log.info("ticket登录结果:{}", new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(ssoUserInfoWrapperResult));
// 2. (Optional) sync user info to local DB
// 3. Generate token and return redirect URL
SsoRespDto respDto = new SsoRespDto();
respDto.setRedirectUrl("http://localhost:8082/index?token=123456");
return WrapperResult.success(respDto);
}Code implementation – Service A (provide user info by ticket)
@GetMapping("/getUser")
public WrapperResult
loginByTicket(@RequestParam("ticket") String ticket) {
log.info("收到票据:{}", ticket);
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(RedisConstants.TICKET_PREFIX + ticket);
if (Objects.isNull(userInfo)) {
return WrapperResult.faild("无法识别的票据信息");
}
SsoUserInfo ssoUserInfo = new SsoUserInfo();
BeanUtil.copyProperties(userInfo, ssoUserInfo);
return WrapperResult.success(ssoUserInfo);
}Practice 2: Encrypted User Data Exchange (Service B → Service A)
Architecture diagram
Steps:
User logs in to Service B.
User clicks a button in Service B; the system encrypts user information and redirects to Service A.
Service A verifies the signature, decrypts the data, and synchronizes the user.
Service A generates a token and returns a redirect URL to Service B.
Service B uses the token to access Service A resources.
Configuration – Service B (application.yml excerpt)
appId: A9mQUjun
appSecret: Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq
encrypt:
type: RSA
rsa:
publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KLYE2Tv4qx/duxu8Qvq5ZN58yEjj/uwsxfs96pj+9iOOAUKLur8IIKjR/bi54GICUy0BHO6dzpWc0xqGK170F9NTv0bHe0qbh7jHgzq9MJrfcVD+XZAH17ho5tCGIo+z7CiC+rMWGTqmRopd/EQuzfx4Op4/85hoPlpKxdcxAfys0jpZ9tBMtROPsYKhCz01iDnHV2K95s4UwaQLbbx0VALVaXv1/4Yjw/PW4xK0syW/nqUtVqpfwPuX+fHf+bJ2s4kLnFBNwYAKFSU6znGmtJuq6aoxCunu2PbzI8xc7SYxHEfDqG8Zp29wtZcTJecWSDMBmywlaXjkXLzapvE7QIDAQAB
privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQotgTZO/irH927G7xC+rlk3nzISOP+7CzF+z3qmP72I44BQou6vwggqNH9uLngYgJTLQEc7p3OlZzTGoYrXvQX01O/Rsd7SpuHuMeDOr0wmt9xUP5dkAfXuGjm0IYij7PsKIL6sxYZOqZGil38RC7N/Hg6nj/zmGg+WkrF1zEB/KzSOln20Ey1E4+xgqELPTWIOcdXYr3mzhTBpAttvHRUAtVpe/X/hiPD89bjErSzJb+epS1Wql/A+5f58d/5snaziQucUE3BgAoVJTrOcaa0m6rpqjEK6e7Y9vMjzFztJjEcR8Oobxmnb3C1lxMl5xZIMwGbLCVpeORcvNqm8TtAgMBAAECggEBAKMhoQfRFYxMSkIHbluFcP5eyKylDbRoHOp726pvDUx/L/x3XFYBIHCfFOKRFSvk6SQ0WFFe176f27a9Wfu/sh7kVYNcflZw+YsvFXCKsy/70KZ/lr24izy8KHuPSyf6+E/WkW32Ah9fkNtzTFdfIzDv9m1hiIijq0x9l5C87KjNELnbvC0I6vwFOx0ak+JBbpaJ7IRjZxKZup7UIPvt9nbLzcbKelI83An2JUe8HNhrfWxH9UIyMOBoAY+bKCuAbUtHqSlImPiWyiCwE2/Fh7dmPSOAYYp9aZelnhd25jlR+eh4yaUoIID9ubmYVYbjcPW5SSNdfSZMfQ3oa79QeRUCgYEA6K4L+VLRiX8Dg7NCO1fM2+FTv2csTkPX6n7z/uu7kh0+wQDws+/C6Q906OtizvJBIJqFm2jPACNQCvnRixY1srgMJJlH/Rpeb4LtZGwdM1k0jAZIYQcBlGfaq3RaRI/+6+T0xdsh+7VF5A/smp/VXdK2xI3+JbLQ2wm9uN+3yZcCgYEA5Yvly7veDJYf2+8HIQkRhjWrWm1y5lCSe+HG+1ktfqnhN8YEOiPa71u0TXealL0T8EoKsqhWEjomxZ7n0jLigogz7OxxsGAE6HXAiKX0REINNYrq+1qNaqmkfLrhAJyg3JNgTSlb0xd56w7FSqOBttVL9INawGb1P98kYc5OzhsCgYBEfIY1urTGPcZxC2BhSzSXO7mEyv91ge6ZrQhwbj5lgYopEPfIXrgGFXCZ5j7NHu0ghZrx5WWYasxyjpmo0L65fgbE9wEDdLF7LRRmzJPDu2wGEwtW09MZNYBdmv++0ot8L4YEfr1/8xlBSZag5I7O8Oiu7gRyYDGtZy6are7QvQKBgQCaUZnUhOF7/rU+a4yUZf9VBeHD8k7LjaFdDWVzdvmB7P1PPJ185Lv8LN+jMORIWHD+GxjkEQ2ERXnpY7If+zuSW7Tk8/Reib7i9L7SXxc/iFRPCax9/NuTuKavgAdiHOp8P8v/M+3alS7OmuiCDDhZTT46DNDHBrCcFwzjgAo0vwKBgECBs6hEUVsYU74Uc64he8Zgkvj7wZ/yxnFlWmRRERprfBsuiY/y+DAf5ehezSRFpHXUrAkpeVXq2ydnr9BKTs6TV3AxlDMBNSndXsUYHENncR7tEHCSGRFTTu5jxdYA+k47R865Jh+2vQvPaPaXsEKSkDegvcFeUVR/yi5AsDubConfiguration – Service A (application.yml excerpt)
serviceA:
rsa:
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYEX0RkKxwo1dgAO26UgM0doyejFXli73WOm/4uJZptbYQ7nVvxLHz4SLn0R5sziv5IxqXSTgvZn5w1qBBI/ha+myzzb8yQY7xucAVLFLZI4rD5r5zY/EHJZTG8ejilvJgYVVYAYNcuIj4iqI798QV+nLqthDMwsOd+AwWeB1LgBmZHJN0wptCzyZaoETmcLQ+hTf2j+UWjB+B6USMcT2+GO1OFrrsqFhjef6567Gxh1FDjQyDtpDT4q3+oqhKf3JFbLHT0OF4UCRiXhkJB93Swyi0tDgFm2TRhYw0c5zXD58vVs66ZqPsBomLqcz9W/DKUsnKJrYaMkgOt1N8eI6fAgMBAAECggEBA5f23o3rcEwnLd+WFJ08lGjMWe63lwPF+oQqTJa1Wbi9+HYe2ecJlqbN79EYknKzZIdi79U17APmYnYPYEX64Xh8yljHr0xL1lVijneYQShILI3v6PdmkNndKZnoZ6xfB59WzgnoZ2hiTs/vdtPeHQd3VdQFX4J1wnDXsp/4zMKi1fDPt7rhqWrP5W6PXcoGGKIkN9zBlqrd1RBdnKXcwfFoHcFf2ikk6g3Kn50YMRe324eiHMm8z7W34Y3iSvZYHcKBMgsDklFerw1WOGHTN61oMr+8/NTtCsy1AnCH4PrwX/ryO17mh5xNzo/ZSZRRezR92/hmwUIuOO+3FWIE4QKBgQD05wYMVlGKn1fm+sn4hn+ErC6NifXj3MkNdjs8oSHzLrYr6ea6xIvbxesZvqzqz1Fh68bHjpJPOBKwgFnl7+dLXYLNmKjry1iK0o/MMZTtrGUwMEnWHRrpmxXH6B0cnBecZUReuJ9XfKZIfd9ksHHsUY7IGv1CHcblVP/IhrpnxwKBgQDh2/n0cAh1jygGevlXGK/rxuRSlbVgtxJWLAtY8Yolf2BklSiTwmqtp7nzNn8sxRvgfQCZaLqpjC/o/wtC3Ba5b4StJQejoXkCNhVmRdLbIQ2tUxwAElPjFhWf3C5/4B6uBeLyC9izp4wTSYbNbPKxcUGkkfpPbWdHsFZOG4gSaQKBgA/me/cLF6o3ZD6j478WBGt5vmAEKAnOSONt3LS4BXtDeiJpwkg4AJiZRgVa4uEv6qm/5B0KvacVDemVu8B5DfxPqvFsSvNcNXh16U4pnfC8c6loSTL0ms21+vkKsfEslT/bN1ArDnVgq28jdQCVkB/2v51wWycSxdoX5a+AR9P7AoGAMvTwZefI4M0VmLCyBKZ7OlS7Oq6wJ0vmhS6WuNB1/JPKaacFaqDYdKl82JSZCL7H1VQeiH4KbypDvOud3M3PCrNQWcga+x35MTiGh3aFZg8FCO/RR2rbJkbbRh/lFdC420ZUt4tYrt/ESK20DjDgaIxG5RxSPw1N2ey87A5mGtECgYEAlA12yuxBb6qmG3OUSlacSfcKnxZIC3L1IMqxlXL8eG3MB4dI6QYesc3odmaxmy9csgHs+pTyLfM3yB9Ocl572OW5WcEnod5o1EIup9hxB4IG/xSECYVFHlGKfIgbd/JhWtqloYZrwx+kVX/Iw02z18R32DRqBtK4MQ3klOYH86s=Code – Service B redirects to Service A with encrypted data
@GetMapping
public WrapperResult
redirectToServiceA() {
// 1. Build user info
SsoUserInfo data = buildSsoUserInfo();
Long timestamp = System.currentTimeMillis();
String flowId = UUID.randomUUID().toString();
String businessId = "sso";
String encryptType = configProperties.getEncryptType();
String dataEncrypt;
// 2. Encrypt according to configuration
switch (encryptType) {
case "AES":
AES aes = new AES(configProperties.getAppSecret().getBytes(StandardCharsets.UTF_8));
dataEncrypt = aes.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8);
break;
case "RSA":
RSA rsa = new RSA(AsymmetricAlgorithm.RSA_ECB_PKCS1.getValue(), null, configProperties.getServiceAPublicKey());
dataEncrypt = rsa.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8, KeyType.PublicKey);
break;
default:
return WrapperResult.faild("未配置加密方式");
}
// 3. Sign the request
SsoSignSource build = SsoSignSource.builder()
.platformId(configProperties.getAppId())
.platformSecret(configProperties.getAppSecret())
.businessId(businessId)
.data(dataEncrypt)
.flowId(flowId)
.timestamp(timestamp)
.build();
String sign = build.sign();
// 4. Build request body
ToServiceAReq req = ToServiceAReq.builder()
.platformId(configProperties.getAppId())
.businessId("sso")
.flowId(flowId)
.timestamp(timestamp)
.sign(sign)
.data(dataEncrypt)
.build();
// 5. Call Service A
String s = HttpRequest.post("http://localhost:8081/serviceA")
.bodyString(JsonUtils.toString(req))
.execute()
.asString();
log.info("结果:{}", s);
return WrapperResult.success(s);
}Code – Service A receives encrypted data, verifies signature, decrypts, and returns token
@PostMapping
public WrapperResult
sso(@VerifySign ToServiceAReq req) {
log.info("收到单点登录ServiceA的请求:{}", JsonUtils.toPrettyString(req));
// (User sync omitted)
// Simulate login and generate token
String url = "127.0.0.1:8081/index?token=xxx";
SsoRespDto ssoRespDto = new SsoRespDto();
ssoRespDto.setRedirectUrl(url);
return WrapperResult.success(ssoRespDto);
}Supplementary knowledge
The RSA keys used in this article were generated with the online tool https://www.bchrt.com/tools/rsa/ ; you can also generate them with Hutool's RSA class or Java's built‑in security APIs.
Promotion (not part of the technical content)
Follow the public account "码猿技术专栏" and reply with the keyword "sso" to obtain the source code. Support the author by liking, sharing, or subscribing.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.