Information Security 18 min read

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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Single Sign-On (SSO) with Ticket‑Based Authentication in Java

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/yi5AsDub

Configuration – 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.

backendJavaauthenticationencryptiontokenSSOSingle Sign-On
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.