基于Oauth2的SSO单点登录---后端

【springboot+vue项目(十三)】 Springboot整合Spring Security+JWT

【springboot+vue项目(十四)】基于Oauth2的SSO单点登录(一)整体流程介绍

【springboot+vue项目(十五)】基于Oauth2的SSO单点登录(二)vue-element-admin框架改造整合Oauth2.0

一、整体流程

整体的流程大概为:

  1. 用户请求访问应用系统的前端。
  2. 重定向到应用系统的后端。
  3. 应用系统后端将认证请求发送到认证服务器,认证服务器判断是否认证,如果没有认证过,则重定向到认证登录页面进行统一认证。
  4. 认证成功后,重定向到应用系统后端指定URL,并返回code。
  5. 应用系统后端根据返回的code请求认证服务器获取access_token和refresh_token。
  6. 应用系统根据返回的access_token请求认证服务器获取用户信息。
  7. 应用系统的后端根据用户信息生成token返回前端,
  8. 应用系统前端接受token并持久化,调用userinfo请求后端 接口,获取用户信息
  9. 后端验证解析token,将用户信息返回前端。
  10. 登录到应用系统主页。

二 、代码流程

    在【springboot+vue项目(十三)】 Springboot整合Spring Security+[JWT](https://so.csdn.net/so/search?q=JWT&spm=1001.2101.3001.7020) 已经配置的基础上进行编写代码。

(一)SSOLoginController

代码定义了一个 SSOLoginController,用于处理 SSO 登录、回调和登出请求。以下是每个方法的功能说明:

  1. login 方法:构建 SSO 登录 URL 并重定向到第三方认证系统URL。如果 URL 无效,返回 400 错误。如果出现异常,返回 500 错误。

  2. callback 方法:处理 SSO 回调请求,使用授权码完成登录过程。如果处理过程中出现异常,返回 500 错误。

  3. 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值