【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 {@Autowiredprivate SSOLoginService ssoLoginService;/*** 获取 SSO 登录的 URL* @param response* @throws IOException*/@GetMapping("/login")public void login(HttpServletResponse response) throws IOException {try {// 通过 ssoLoginService 获取登录 URLString loginUrl = ssoLoginService.buildLoginUrl();// 检查登录 URL 是否有效if (loginUrl != null && !loginUrl.isEmpty()) {// 重定向到 SSO 登录 URLresponse.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;@Autowiredpublic SSOLoginServiceImpl(RedisCache redisCache) {this.redisCache = redisCache;}// 用户数据访问接口@Autowiredprivate UserMapper userMapper;// RestTemplate用于HTTP请求@Autowiredprivate RestTemplate restTemplate;/*** 构建SSO登录URL。** @return 生成的SSO登录URL。*/@Overridepublic 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 如果在处理请求过程中发生输入输出异常*/@Overridepublic void handleSSOCallback(String code, HttpSession session, HttpServletResponse response) throws IOException {try {// 构造获取访问令牌的URLString 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编码回调URIbody.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);// 构建获取用户信息的URLString 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 异常*/@Overridepublic 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:# 单点登录系统的基本URLurl: http://192.168.91.130:8882oauth:# 获取code的API路径(get)authorizeAPI: /sso/oauth/authorize# 获取access_token的API路径accessTokenAPI: /sso/oauth/accessToken# 获取userInfo的API路径userInfoAPI: /sso/oauth/userInfoclient:# 客户端IDid: APP016#客户端请求的响应类型response_type: code# 客户端密钥secret: f997855e-c449-49a5-84a3-26d317eb# 重定向URIredirect_uri: http://localhost:8888/SSOlogin/callback# 登录成功后跳转的地址web_uri: http://localhost:9528/callback