原创

OAuth2客户端

温馨提示:
本文最后更新于 2024年12月07日,已超过 133 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

 

一.简介

在当今互联网应用中,安全地访问第三方服务的数据变得越来越重要。OAuth 2.0 是一种广泛采用的协议,它允许应用程序以安全的方式获取对API的有限访问权限,而无需直接处理用户的凭证。本文将详细介绍如何通过 OAuth 2.0 实现用户授权、获取访问令牌及用户信息的过程。

二.实现

2.1 用户授权

用户授权是 OAuth2 流程的第一步,应用程序通过重定向用户到授权服务器来发起认证请求,用户在此过程中输入凭证以验证身份。此外,状态参数(state)用于防止跨站请求伪造(CSRF)攻击,增强安全性。

实现细节

/**
 * 发起 OAuth2 授权请求。
 *
 * @return 重定向到授权服务器的授权 URL。
 */
@GetMapping("/authorize")
public String authorize(HttpSession session) {
    // 生成并保存状态参数
    String state = UUID.randomUUID().toString();
    session.setAttribute("oauth_state", state);

    // 构建授权 URL
    String authorizationUrl = String.format("http://localhost:8080/oauth2/authorize?response_type=code&client_id=%s&scope=%s&redirect_uri=%s&state=%s",
            "oidc-client", "message.read openid", "http://192.168.1.8:9001/callback", state);
    return "redirect:" + authorizationUrl;
}

2.2 授权回调

授权回调发生在用户在授权服务器完成认证后,授权服务器将用户重定向回客户端应用的指定URL,并附带授权码和状态参数。这一步骤负责接收授权结果、验证状态参数,并准备交换访问令牌。

实现细节

/**
 * 处理 OAuth2 授权回调请求。
 *
 * @param code 授权码,由授权服务器颁发给客户端,用于交换访问令牌。
 * @param state 状态参数,用于防止 CSRF 攻击。它应该与发起授权请求时生成并保存的状态值匹配。
 * @return 重定向到应用的首页。
 */
@GetMapping("/callback")
public String handleCallback(@RequestParam("code") String code,
                             @RequestParam(value = "state", required = false) String state,
                             HttpSession session) {
    // 验证状态参数
    String storedState = (String) session.getAttribute("oauth_state");
    if (!state.equals(storedState)) {
        System.out.println("接收到无效的状态参数: " + state);
        return "redirect:/error?msg=无效的状态参数";
    }

    // 移除会话中的状态参数
    session.removeAttribute("oauth_state");

    // 获取访问令牌
    Map<String, Object> tokenResponse  = oAuth2Service.exchangeCodeForAccessToken(code);
    String accessToken = tokenResponse .get("access_token").toString();
    System.out.println("访问令牌:" + accessToken);

    // 使用访问令牌请求用户信息
    Map<String, Object> userInfo = oAuth2Service.getUserInfo(accessToken);
    Map<String, Object> claims = (Map<String, Object>)userInfo.get("claims");
    Integer id = Integer.valueOf(claims.get("id").toString());
    String username = claims.get("username").toString();
    String name = claims.get("name").toString();
    System.out.println(String.format("ID:%s,用户名:%s,姓名:%s", id, username, name));

    // 认证和授权的逻辑
    oAuth2Service.processUserInfo(claims, session);

    return "redirect:/index";
}

2.3 获取Token

获取Token是指客户端应用使用授权码向授权服务器发送请求,以换取访问令牌的过程。这一步骤对于安全传输至关重要,通常通过 Basic Auth 来保证通信的安全性。根据授权时定义的 scope,访问令牌只授予特定资源的访问权限。

封装服务方法

/**
 * 使用授权码交换访问令牌。
 *
 * @param code 授权码
 * @return 包含访问令牌的 Map
 */
public Map<String, Object> exchangeCodeForAccessToken(String code) {
    // 客户端凭证
    String clientId = "oidc-client";
    String clientSecret = "secret";
    String grantType = "authorization_code";
    String redirectUri = "http://192.168.1.8:9001/callback";

    // 构建 Basic Auth Header
    String auth = clientId + ":" + clientSecret;
    byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes());
    String authHeader = "Basic " + new String(encodedAuth);

    // 创建表单数据
    Map<String, String> formData = new LinkedHashMap<>();
    formData.put("grant_type", grantType);
    formData.put("code", code);
    formData.put("redirect_uri", redirectUri);

    // 设置请求头
    MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
    headers.add(HttpHeaders.AUTHORIZATION, authHeader);

    // 获取访问令牌
    return webClientUtil.postFormData("http://localhost:8080/oauth2/token", formData, headers, Map.class).block();
}

2.4 获取用户信息

获取用户信息是利用获得的访问令牌调用授权服务器提供的用户信息端点,以检索用户的个人信息或其他数据。基于用户信息可以提供个性化的内容和服务,并建立用户会话。

封装服务方法

/**
 * 使用访问令牌获取用户信息。
 *
 * @param accessToken 访问令牌
 * @return 包含用户信息的 Map
 */
public Map<String, Object> getUserInfo(String accessToken) {
    // 设置请求头
    MultiValueMap<String, String> userinfoHeaders = new LinkedMultiValueMap<>();
    userinfoHeaders.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);

    // 创建空请求体
    Map<String, Object> requestBody = new LinkedHashMap<>();

    // 使用访问令牌请求用户信息
    return webClientUtil.post("http://localhost:8080/userinfo", requestBody, userinfoHeaders, Map.class).block();
}

2.5 认证授权

认证授权是在成功获取用户信息后,根据业务需求设置认证标志或更新数据库的过程。这包括确认用户的身份、创建活跃会话以及根据用户的认证状态和角色分配适当的权限。

处理逻辑

/**
 * 认证授权逻辑
 * @param claims 用户信息
 * @param session HttpSession 对象,用来存储与当前HTTP会话相关的数据。
 */
public void processUserInfo(Map<String, Object> claims, HttpSession session) {
    // 设置会话属性
    Integer id = (Integer) claims.get("id");
    session.setAttribute("user_id", id);
    session.setAttribute("user_info", claims);

    // 其他认证授权逻辑...
}

2.6 首页

首页是用户成功登录后的第一个界面,通常展示欢迎信息和其他个性化的用户内容。首页不仅能够根据用户的认证状态显示个性化的内容,还能提供清晰的导航选项,帮助用户快速找到他们感兴趣的功能或信息。

实现细节

/**
 * 首页
 * @return 模板名称
 */
@GetMapping("/index")
public String index(Model model, HttpSession session) {
    // 检查是否已认证
    if (session.getAttribute("user_id") == null) {
        return "redirect:/authorize";
    }

    model.addAttribute("user", session.getAttribute("user_info"));

    return "index";
}

正文到此结束
本文目录