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
相关产品推荐
相关产品推荐

