【springboot+vue项目(十三)】 Springboot整合Spring Security+JWT
【springboot+vue项目(十四)】基于Oauth2的SSO单点登录(一)整体流程介绍
【springboot+vue项目(十五)】基于Oauth2的SSO单点登录(二)vue-element-admin框架改造整合Oauth2.0
一、整体流程
整体的流程大概为:
- 用户请求访问应用系统的前端。
- 重定向到应用系统的后端。
- 应用系统后端将认证请求发送到认证服务器,认证服务器判断是否认证,如果没有认证过,则重定向到认证登录页面进行统一认证。
- 认证成功后,重定向到应用系统后端指定URL,并返回code。
- 应用系统后端根据返回的code请求认证服务器获取access_token和refresh_token。
- 应用系统根据返回的access_token请求认证服务器获取用户信息。
- 应用系统的后端根据用户信息生成token返回前端,
- 应用系统前端接受token并持久化,调用userinfo请求后端 接口,获取用户信息
- 后端验证解析token,将用户信息返回前端。
- 登录到应用系统主页。
二 、代码流程
在【springboot+vue项目(十三)】 Springboot整合Spring Security+[JWT](https://so.csdn.net/so/search?q=JWT&spm=1001.2101.3001.7020) 已经配置的基础上进行编写代码。
(一)SSOLoginController
代码定义了一个
SSOLoginController
,用于处理 SSO 登录、回调和登出请求。以下是每个方法的功能说明:
login
方法:构建 SSO 登录 URL 并重定向到第三方认证系统URL。如果 URL 无效,返回 400 错误。如果出现异常,返回 500 错误。
callback
方法:处理 SSO 回调请求,使用授权码完成登录过程。如果处理过程中出现异常,返回 500 错误。
logout
方法:处理登出请求并重定向到指定 URL。如果提供了redirectUrl
,将用户重定向到该 URL。这些方法都通过
ssoLoginService
与业务逻辑交互,确保处理登录、回调和登出的逻辑。
@RestController
@RequestMapping("/SSOlogin")
public class SSOLoginController {
@Autowired
private SSOLoginService ssoLoginService;
/**
* 获取 SSO 登录的 URL
* @param response
* @throws IOException
*/
@GetMapping("/login")
public void login(HttpServletResponse response) throws IOException {
try {
// 通过 ssoLoginService 获取登录 URL
String loginUrl = ssoLoginService.buildLoginUrl();
// 检查登录 URL 是否有效
if (loginUrl != null && !loginUrl.isEmpty()) {
// 重定向到 SSO 登录 URL
response.sendRedirect(loginUrl);
} else {
// 如果登录 URL 无效,发送 400 错误响应
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效的登录 URL");
}
} catch (IOException e) {
// 记录异常并发送 500 错误响应
e.printStackTrace(); // 或者使用日志框架记录
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "重定向到登录页面时发生错误");
}
}
/**
* 处理SSO回调请求
*
* @param code 从SSO系统返回的授权码
* @param session 当前会话
* @param response HTTP响应
* @throws IOException 如果处理请求时出错
*/
@GetMapping("/callback")
public void callback(@RequestParam("code") String code, HttpSession session, HttpServletResponse response) throws IOException {
try {
// 调用服务层处理回调请求
ssoLoginService.handleSSOCallback(code, session, response);
} catch (Exception e) {
e.printStackTrace();
// 处理异常并返回500错误
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "处理回调请求时出错");
}
}
/**
* 处理登出请求并进行重定向
*
* @param redirectUrl 重定向的 URL
* @param request HTTP 请求对象
* @param response HTTP 响应对象
* @throws IOException 可能抛出的 IO 异常
*/
@GetMapping("/portal/sso/logout.html")
public void logout(@RequestParam(value = "redirectUrl", required = false) String redirectUrl,
HttpServletRequest request, HttpServletResponse response) throws IOException {
// 执行登出操作
ssoLoginService.handleLogout(request, response, redirectUrl);
}
}
(二)SSOLoginService
public interface SSOLoginService {
String buildLoginUrl() throws UnsupportedEncodingException;;
/**
* 处理SSO回调请求
*
* @param code 从SSO系统返回的授权码
* @param session 当前会话
* @param response HTTP响应
* @throws IOException 如果处理请求时出错
*/
void handleSSOCallback(String code, HttpSession session, HttpServletResponse response) throws IOException;
/**
* 处理登出操作
*
* @param request HTTP 请求对象
* @param response HTTP 响应对象
* @param redirectUrl 重定向的 URL
* @throws IOException 可能抛出的 IO 异常
*/
void handleLogout(HttpServletRequest request, HttpServletResponse response, String redirectUrl) throws IOException;
}
(三)SSOLoginServiceImpl
1、构建SSO登录URL。
2、处理从SSO系统回调的请求,包括获取访问令牌、用户信息和生成JWT令牌。
3、根据访问令牌获取用户信息
4、处理退出注销操作
/**
* SSO服务实现类,用于生成SSO登录URL。
*/
@Service
public class SSOLoginServiceImpl implements SSOLoginService {
// SSO基础URL,从配置文件中读取
@Value("${sso.base.url}")
private String ssoBaseUrl;
// 授权API路径,从配置文件中读取
@Value("${sso.oauth.authorizeAPI}")
private String authorizeApi;
// 客户端ID,从配置文件中读取
@Value("${sso.client.id}")
private String clientId;
// 响应类型,从配置文件中读取
@Value("${sso.client.response_type}")
private String responseType;
// 重定向URI,从配置文件中读取
@Value("${sso.client.redirect_uri}")
private String redirectUri;
// 获取令牌的API路径
@Value("${sso.oauth.accessTokenAPI}")
private String tokenApi;
// 客户端密钥
@Value("${sso.client.secret}")
private String clientSecret;
// Web应用的URI
@Value("${sso.client.web_uri}")
private String webUri;
//用户信息API
@Value("${sso.oauth.userInfoAPI}")
private String userInfoApi;
// Redis缓存
private final RedisCache redisCache;
@Autowired
public SSOLoginServiceImpl(RedisCache redisCache) {
this.redisCache = redisCache;
}
// 用户数据访问接口
@Autowired
private UserMapper userMapper;
// RestTemplate用于HTTP请求
@Autowired
private RestTemplate restTemplate;
/**
* 构建SSO登录URL。
*
* @return 生成的SSO登录URL。
*/
@Override
public String buildLoginUrl() {
return UriComponentsBuilder.fromHttpUrl(ssoBaseUrl + authorizeApi)
.queryParam("response_type", responseType)
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.toUriString();
}
/**
* 处理从SSO系统回调的请求,包括获取访问令牌、用户信息和生成JWT令牌。
*
* @param code 从SSO系统返回的授权码
* @param session 当前会话,用于存储访问令牌
* @param response HTTP响应,用于重定向用户或返回错误信息
* @throws IOException 如果在处理请求过程中发生输入输出异常
*/
@Override
public void handleSSOCallback(String code, HttpSession session, HttpServletResponse response) throws IOException {
try {
// 构造获取访问令牌的URL
String tokenUrl = ssoBaseUrl + tokenApi;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // 设置内容类型为表单编码
// 构造请求体,包含授权码及其他必需参数
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
body.add("grant_type", "authorization_code"); // 指明授权类型
body.add("redirect_uri", URLEncoder.encode(redirectUri, "UTF-8")); // URL编码回调URI
body.add("code", code); // 授权码
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
// 发送POST请求以获取访问令牌
ResponseEntity<Map> responseEntity = restTemplate.exchange(tokenUrl, HttpMethod.POST, request, Map.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
Map<String, Object> responseMap = responseEntity.getBody();
if (responseMap != null && responseMap.containsKey("access_token")) {
String accessToken = (String) responseMap.get("access_token");
session.setAttribute("access_token", accessToken); // 存储访问令牌
// 使用访问令牌获取用户信息
ResponseEntity<Map> userInfoResponse = getUserInfo(accessToken);
if (userInfoResponse.getStatusCode() == HttpStatus.OK) {
Map<String, Object> userInfo = userInfoResponse.getBody();
String userId = (String) userInfo.get("uid");
// 创建JWT令牌并缓存
String token = JwtUtil.createJWT(userId);
redisCache.setCacheObject("loginToken:" + userId, token);
// 从数据库获取用户信息并缓存
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getId, userId);
User user = userMapper.selectOne(queryWrapper);
String userJson = JSON.toJSONString(user);
redisCache.setCacheObject("userInfo:" + userId, userJson);
// 重定向到Web应用并附带JWT令牌
response.sendRedirect(webUri + "?token=" + token);
return;
}
}
}
// 获取令牌失败,返回401错误
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "获取访问令牌失败");
} catch (Exception e) {
// 记录异常以帮助调试
e.printStackTrace();
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "处理回调请求时出错");
}
}
/**
* 根据访问令牌获取用户信息
*
* @param accessToken 访问令牌
* @return 包含用户信息的ResponseEntity
*/
private ResponseEntity<Map> getUserInfo(String accessToken) {
// 创建并设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
// 构建获取用户信息的URL
String userInfoUrl = String.format("%s%s?access_token=%s", ssoBaseUrl, userInfoApi, accessToken);
try {
// 发送请求并返回响应
return restTemplate.exchange(userInfoUrl, HttpMethod.POST, requestEntity, Map.class);
} catch (Exception e) {
// 捕获并记录异常
e.printStackTrace();
// 返回内部服务器错误状态
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 处理退出注销操作
*
* @param request HTTP 请求对象
* @param response HTTP 响应对象
* @param redirectUrl 重定向的 URL
* @throws IOException 可能抛出的 IO 异常
*/
@Override
public void handleLogout(HttpServletRequest request, HttpServletResponse response, String redirectUrl) throws IOException {
// 获取当前用户认证信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
// 清除认证信息
SecurityContextHolder.clearContext();
// 使当前会话无效
request.getSession().invalidate();
}
// 根据 URL 验证结果进行重定向
if (redirectUrl != null && isValidRedirectUrl(redirectUrl)) {
response.sendRedirect(redirectUrl);
} else {
response.sendRedirect("/login?logout");
}
}
/**
* 验证重定向 URL 的有效性
*
* @param url 需要验证的 URL
* @return 如果 URL 合法则返回 true,否则返回 false
*/
private boolean isValidRedirectUrl(String url) {
// 示例:只允许特定的 URL 前缀
return url.startsWith("http://127.0.0.1:8080/") || url.startsWith("https://trusted-domain.com/");
}
}
(四)application.yml
sso:
base:
# 单点登录系统的基本URL
url: http://192.168.91.130:8882
oauth:
# 获取code的API路径(get)
authorizeAPI: /sso/oauth/authorize
# 获取access_token的API路径
accessTokenAPI: /sso/oauth/accessToken
# 获取userInfo的API路径
userInfoAPI: /sso/oauth/userInfo
client:
# 客户端ID
id: APP016
#客户端请求的响应类型
response_type: code
# 客户端密钥
secret: f997855e-c449-49a5-84a3-26d317eb
# 重定向URI
redirect_uri: http://localhost:8888/SSOlogin/callback
# 登录成功后跳转的地址
web_uri: http://localhost:9528/callback