You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot无状态REST服务与Shibboleth的手动认证实现问询

手动实现Shibboleth OAuth2认证与令牌验证(无Spring Security)

前置准备

  • 引入依赖:使用OkHttp处理HTTP请求、Jackson Databind做JSON序列化/反序列化,可在pom.xml中添加:
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.11.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>
  • 提前在Shibboleth IDP配置OAuth2客户端:获取客户端ID、客户端密钥,确认令牌端点(token endpoint)和令牌验证端点(introspection endpoint)的URL。

1. 登录接口:用户名密码换令牌并返回认证结果

核心逻辑

接收前端自定义表单提交的用户名密码,构造OAuth2密码模式请求调用Shibboleth令牌端点,成功则返回true及令牌,失败返回false,全程无重定向。

代码实现

先定义接收登录参数的DTO:

public class LoginRequest {
    private String username;
    private String password;
    // 生成getter、setter方法
}

编写登录接口的Controller方法:

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.Map;

@RestController
public class AuthController {
    // 建议从application.yml读取,此处为示例硬编码
    private static final String SHIBBOLETH_TOKEN_ENDPOINT = "https://your-shibboleth-idp/oauth2/token";
    private static final String CLIENT_ID = "your-client-id";
    private static final String CLIENT_SECRET = "your-client-secret";
    
    private final OkHttpClient httpClient = new OkHttpClient();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody LoginRequest loginRequest) throws IOException {
        // 构造OAuth2密码模式表单请求
        FormBody formBody = new FormBody.Builder()
                .add("grant_type", "password")
                .add("username", loginRequest.getUsername())
                .add("password", loginRequest.getPassword())
                .add("client_id", CLIENT_ID)
                .add("client_secret", CLIENT_SECRET)
                .build();

        Request request = new Request.Builder()
                .url(SHIBBOLETH_TOKEN_ENDPOINT)
                .post(formBody)
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                return Map.of("success", false);
            }
            // 解析令牌响应并返回结果
            Map<String, Object> tokenResponse = objectMapper.readValue(response.body().string(), Map.class);
            return Map.of(
                    "success", true,
                    "access_token", tokenResponse.get("access_token"),
                    "expires_in", tokenResponse.get("expires_in")
            );
        }
    }
}

注意:Shibboleth默认可能禁用OAuth2密码模式,需提前在IDP配置中开启该授权类型。


2. 后续接口:令牌验证与用户信息获取

核心逻辑

前端请求时携带Authorization: Bearer {token}请求头,后端提取令牌后调用Shibboleth的令牌验证端点,校验有效性并获取用户信息。

代码实现

先编写令牌验证工具类:

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.Map;

public class TokenValidator {
    private static final String SHIBBOLETH_INTROSPECTION_ENDPOINT = "https://your-shibboleth-idp/oauth2/introspect";
    private static final String CLIENT_ID = "your-client-id";
    private static final String CLIENT_SECRET = "your-client-secret";
    
    private final OkHttpClient httpClient = new OkHttpClient();
    private final ObjectMapper objectMapper = new ObjectMapper();

    public Map<String, Object> validateToken(String accessToken) throws IOException {
        FormBody formBody = new FormBody.Builder()
                .add("token", accessToken)
                .add("client_id", CLIENT_ID)
                .add("client_secret", CLIENT_SECRET)
                .build();

        Request request = new Request.Builder()
                .url(SHIBBOLETH_INTROSPECTION_ENDPOINT)
                .post(formBody)
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                return Map.of("valid", false, "error", "令牌验证请求失败");
            }
            Map<String, Object> introspectResponse = objectMapper.readValue(response.body().string(), Map.class);
            boolean isValid = (Boolean) introspectResponse.get("active");
            if (!isValid) {
                return Map.of("valid", false, "error", "令牌无效或已过期");
            }
            // 返回用户核心信息,字段根据Shibboleth配置调整
            return Map.of(
                    "valid", true,
                    "userInfo", Map.of(
                            "userId", introspectResponse.get("sub"),
                            "username", introspectResponse.get("username")
                    )
            );
        }
    }
}

在需要验证的业务接口中使用该工具类:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.Map;

@RestController
public class ResourceController {
    private final TokenValidator tokenValidator = new TokenValidator();

    @GetMapping("/api/user-info")
    public Map<String, Object> getUserInfo(@RequestHeader("Authorization") String authHeader) throws IOException {
        // 提取Bearer令牌
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return Map.of("success", false, "error", "缺少或无效的Authorization请求头");
        }
        String accessToken = authHeader.substring(7);
        
        Map<String, Object> validationResult = tokenValidator.validateToken(accessToken);
        if (!(Boolean) validationResult.get("valid")) {
            return Map.of("success", false, "error", validationResult.get("error"));
        }
        return Map.of("success", true, "userInfo", validationResult.get("userInfo"));
    }
}

关键注意事项

  • 无状态保证:后端不存储任何会话信息,完全依赖令牌的有效性验证,符合无状态REST服务要求。
  • 安全性:
    • 与Shibboleth的通信必须使用HTTPS,避免敏感信息明文传输。
    • 客户端ID、密钥不要硬编码,建议通过@Value从Spring Boot配置文件注入。
    • 前端存储令牌时,优先使用HttpOnly Cookie或内存存储,降低XSS风险。
  • 错误处理:示例仅做基础判断,实际场景可解析Shibboleth返回的错误码,细化错误类型(如用户名密码错误、客户端权限不足等)。

内容的提问来源于stack exchange,提问作者AhmadWabbi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 04:11:02