mirror of
https://github.com/zfile-dev/zfile.git
synced 2026-09-01 15:30:09 +08:00
feat(WIP): 添加单点登录(SSO)功能
- 新增 SSO 服务逻辑,包括授权地址生成、回调处理和用户信息获取。 - 新增 SSO Controller,提供登录、回调以及成功/失败处理的 API。 - 定义 SSO 模型类,用于 Access Token 和用户信息的封装。 - 当前逻辑以使用 Logto OSS 测试无误。 - 本功能未全部完成,待后续开发。
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package im.zhaojun.zfile.module.sso.controller;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import im.zhaojun.zfile.module.sso.service.SsoService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "单点登录")
|
||||
@RestController
|
||||
@RequestMapping("/sso")
|
||||
@RequiredArgsConstructor
|
||||
class SsoController
|
||||
{
|
||||
private final SsoService ssoService;
|
||||
|
||||
@GetMapping("/login/{provider}")
|
||||
public RedirectView login(@PathVariable String provider, HttpSession session)
|
||||
{
|
||||
var state = IdUtil.fastSimpleUUID();
|
||||
session.setAttribute("state", state);
|
||||
|
||||
var url = ssoService.getAuthRedirectUrl(state);
|
||||
|
||||
var redirect = new RedirectView();
|
||||
redirect.setUrl(url);
|
||||
redirect.setStatusCode(HttpStatus.SEE_OTHER);
|
||||
|
||||
return redirect;
|
||||
}
|
||||
|
||||
@GetMapping("/callback")
|
||||
public RedirectView callback(@RequestParam("code") String code, @RequestParam("state") String state, HttpSession session)
|
||||
{
|
||||
if (!state.equals(session.getAttribute("state").toString()))
|
||||
{
|
||||
var err = URLUtil.encode("state 参数不一致");
|
||||
return new RedirectView("/sso/error?err=" + err);
|
||||
}
|
||||
|
||||
var url = ssoService.callbackHandler(code);
|
||||
|
||||
var redirect = new RedirectView();
|
||||
redirect.setUrl(url);
|
||||
redirect.setStatusCode(HttpStatus.SEE_OTHER);
|
||||
return redirect;
|
||||
}
|
||||
|
||||
@GetMapping("/success")
|
||||
public String success()
|
||||
{
|
||||
return "单点登录成功, 当前用户 ID: [" + StpUtil.getLoginIdAsString() + "]!";
|
||||
}
|
||||
|
||||
@GetMapping("/error")
|
||||
public String error(@RequestParam("err") String err)
|
||||
{
|
||||
return "单点登录失败: " + err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package im.zhaojun.zfile.module.sso.model.response;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public class TokenResponse
|
||||
{
|
||||
private String accessToken;
|
||||
private String idToken;
|
||||
private String scope;
|
||||
private String tokenType;
|
||||
private Long expiresIn;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package im.zhaojun.zfile.module.sso.model.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UserInfoResponse
|
||||
{
|
||||
private String sub;
|
||||
private String name;
|
||||
private String picture;
|
||||
private String email;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package im.zhaojun.zfile.module.sso.service;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import im.zhaojun.zfile.module.sso.model.response.TokenResponse;
|
||||
import im.zhaojun.zfile.module.sso.model.response.UserInfoResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* 单点登录服务
|
||||
*
|
||||
* @author OnEvent
|
||||
*/
|
||||
@Service
|
||||
public class SsoService
|
||||
{
|
||||
// TODO 这里需要修改为从数据库或配置文件中读取
|
||||
// TODO 同时考虑是否需要利用 Well-Known 来获取相关信息
|
||||
private static final String CLIENT_ID = "<Client ID>";
|
||||
private static final String CLIENT_SECRET = "<Client Secret>";
|
||||
private static final String AUTH_URL = "<Auth URL>";
|
||||
private static final String TOKEN_URL = "<Token URL>";
|
||||
private static final String USER_INFO_URL = "<User Info URL>";
|
||||
private static final String REDIRECT_URI = "http://localhost:8080/sso/callback";
|
||||
private static final String SCOPE = "openid profile email";
|
||||
|
||||
/**
|
||||
* 获取 OIDC/OAuth2 的授权地址<br/>
|
||||
* 并由后端控制页面重定向到 OIDC/OAuth2 服务器的授权页面
|
||||
*
|
||||
* @param state 状态值
|
||||
* @return 授权地址
|
||||
*/
|
||||
public String getAuthRedirectUrl(String state)
|
||||
{
|
||||
var authParamsMap = new HashMap<String, String>()
|
||||
{{
|
||||
put("response_type", "code");
|
||||
put("client_id", CLIENT_ID);
|
||||
put("redirect_uri", REDIRECT_URI);
|
||||
put("state", state);
|
||||
put("scope", SCOPE);
|
||||
}};
|
||||
var authParamsStr = HttpUtil.toParams(authParamsMap);
|
||||
return AUTH_URL + "?" + authParamsStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 OIDC/OAuth2 的回调,并同时获取用户信息,利用用户信息中的邮箱完成登录<br/>
|
||||
* 若一切顺利则返回到成功页面<br/>
|
||||
* 当在获取 AK 和用户信息时发生错误时,返回错误页面
|
||||
*
|
||||
* @param code 授权码
|
||||
* @return 重定向的页面路径
|
||||
*/
|
||||
public String callbackHandler(String code)
|
||||
{
|
||||
// 获取 Access Token
|
||||
var tokenParamsMap = new HashMap<String, String>()
|
||||
{{
|
||||
put("code", code);
|
||||
put("client_id", CLIENT_ID);
|
||||
put("client_secret", CLIENT_SECRET);
|
||||
put("redirect_uri", REDIRECT_URI);
|
||||
put("grant_type", "authorization_code");
|
||||
}};
|
||||
var authParamsStr = HttpUtil.toParams(tokenParamsMap);
|
||||
var result = HttpUtil.post(TOKEN_URL, authParamsStr);
|
||||
|
||||
var token = JSONUtil.toBean(result, TokenResponse.class);
|
||||
|
||||
if (ObjectUtil.isNull(token) || StrUtil.isEmpty(token.getAccessToken()))
|
||||
{
|
||||
return "/sso/error?err=获取 Access Token 失败"; // TODO 这里要确认,是否需要从后端控制跳转到错误页面,全部由后端控制可能会更简单一些
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
var userInfoStr = HttpUtil.createGet(USER_INFO_URL).bearerAuth(token.getAccessToken()).execute().body();
|
||||
var userInfo = JSONUtil.toBean(userInfoStr, UserInfoResponse.class);
|
||||
|
||||
if (ObjectUtil.isNull(userInfo) || StrUtil.isEmpty(userInfo.getEmail()))
|
||||
{
|
||||
return "/sso/error?err=获取用户信息失败"; // TODO 同上一个错误处理
|
||||
}
|
||||
|
||||
// 调用 Sa Token 的登录方法
|
||||
// TODO 这里要处理一下,如果没有对应用户,则创建用户
|
||||
StpUtil.login(userInfo.getEmail(), new SaLoginParameter().setToken(token.getAccessToken()));
|
||||
|
||||
return "/sso/success";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user