SpringBoot实现OAuth客户端

news2024/12/27 3:35:43

      背景

        5 月份的时候,我实践并整理了一篇博客:SpringBoot搭建OAuth2,该博客完成之后,本以为能对OAuth2的认证机制更加清晰,但我却觉得自己更“迷惘”了。
        抛开我在项目中积累的浅薄经验不谈,单从在网上找到的OAuth2资料来看,它更适合应用于“资源所有者,客户端,授权服务器,资源服务器”四方角色存在的场景。那么,在企业级的微服务架构中,它也是这么应用的吗?

        一般的企业分布式微服务架构中,常有认证服务OAuth2、基础平台服务(负责用户信息,权限,菜单等管理),网关服务(负责负载,网关转发),业务资源服务(提供业务服务)等,这些服务在互相调用时的流程是怎么样的?怎么做的授权?用的是OAuth2中的哪种授权模式?服务之间,哪个是客户端,哪个是资服服务……等等,怎么越想脑子越乱呢?   

        于是,我打算结合企业微服务架构中对于OAuth的实际应用整理一篇博客,把自己不懂的全弄清楚。也借此和各位大佬们探讨下,OAuth应用于企业服务需要做哪些调整。

      代码实践

        结合公司对OAuth的实际使用情况,以及网上查阅到的资料,我发现要实现OAuth客户端,有两种方案。一种是官方推建的使用spring-boot-starter-oauth2-client的方式,另一种是公司DIY的网关代理的模式,这两种方式的实现我在这里都会写一下。

      一、spring-boot-starter-oauth2-client方式

        这是网上推荐的OAuth2客户端实现方式,它与OAuth Server的交互时序图如下:

        

        代码实现如下:

        1、pom.xml引入依赖包

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.client.auth2</groupId>
    <artifactId>auth-client</artifactId>
    <version>1.0</version>
    <name>auth-client</name>
    <description>auth-client</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

    </dependencies>

        2、yml添加添加OAuth服务端的配置

server:
  port: 19210
  servlet:
    context-path: /leixi
    session:
      cookie:
        # 需要更换存放sessionId的cookie名字,否则认证服务和客户端的sessionId会相互覆盖
        name: JSESSIONID-2
  max-http-header-size: 102400

spring:
  security:
    oauth2:
      client:
        registration:
          leixi-client:
            provider: auth-server    #与下面provider里的配置呼应
            client-id: client        #在OAuth Server服务端里注测的客户端Id
            client-secret: 123456    #在OAuth Server服务端里注测的客户端Secret
            authorization-grant-type: authorization_code   #客户端访问的授权模式
            redirect-uri: '{baseUrl}/{action}/oauth2/code/{registrationId}'  #客户端获得code后的回调地址,默认该地址不变
            scope: read, write  #授权的scope
            client-name: client  #客户端名称
        provider:
          auth-server:
            authorization-uri: http://127.0.0.1:19200/oauth/authorize    #OAuth Server授权码模式地址
            token-uri: http://127.0.0.1:19200/oauth/token   #OAuth Server获取Token的地址
            user-info-uri: http://127.0.0.1:19200/user/info   #OAuth获取用户信息的地址
            user-name-attribute: name

        3、添加WebSecurityConfig配置

package com.client.auth2.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().logout()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                .and().oauth2Client()
                .and().oauth2Login();
    }
}

        4、编写一个Controller方法用于测试

/**
 *
 * @author leixiyueqi
 * @since 2023/12/5 19:39
 */
@RestController
@Slf4j
public class DemoController {

    @GetMapping("/demo")
    public Object demo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("authentication: {}", authentication);
        return "Hello World";
    }
}

        5、结合OAuth Server进行测试,为方便测试,咱们使用上篇博客中构建的直接跳转登陆页的OAuth Server服务。访问客户端地址:http://127.0.0.1:19210/leixi/demo,测试结果如下:

        

        输入用户名,密码之后,正常跳转到了客户端的请求:

        

      二、网关代理集成方式

        网关代理集成方式是公司在应用OAuth Server时,结合公司的架构进行的一些个性化设计处理,它与其他服务的交互时序图如下:

        以下是一个简化版的实现,其实现逻辑如下:
        1) 通过浏览器访问客户端服务,客户端通过Filter检查请求的cookie中是否有Token,
        2) 如果没有Token或Token校验不通过,则重定向到OAuth Server的登陆页面。
        3) OAuth Server登陆授权后,跳转到客户端的回调方法,回调方法中拿到Code,调用oauth/token来获得token.
        4) 将token封装到cookie中,再重新调用客户端服务,本次Filter检查到有Token,正常放行。
        5) 返回客户端服务的结果到浏览器。

        下面是代码实践:

        1、添加pom.xml依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version> 
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        

        <!--huTool工具箱大全-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.0</version>
        </dependency>

        2、添加检查cookie中Token的过滤器

/**
 *
 * @author leixiyueqi
 * @since 2024/9/18 19:39
 */
@Slf4j
@Component
public class TokenAuthFilter implements Ordered, Filter {

    private static final String authUri = "http://127.0.0.1:19200/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://127.0.0.1:19210/leixi/callback&state=";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        //String token = httpRequest.getParameter("token");  通过url的方式获取Token
        // 获取请求的URI
        String requestURI = CommonUtils.getRequestUriByHttpRequest(httpRequest);
        String token = getCookieFromRequest(httpRequest,"token");

        if (!requestURI.contains("callback")) {
            if (StringUtils.isEmpty(token)) {
                log.info("请求中未携带token信息");
                httpResponse.sendRedirect(authUri + requestURI);
            } else {
                Claims claim = CommonUtils.parseJwt(token);
                if (claim == null) {
                    log.info("token解析失败");
                    httpResponse.sendRedirect(authUri + requestURI);
                }
            }
        }
        chain.doFilter(request, response);

    }

    private String getCookieFromRequest(HttpServletRequest request,String cookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                // 进行其他处理,比如验证Cookie
                if (cookieName.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        } else {
            log.info("No cookies found in the request.");
        }
        return null;
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

        3、添加调用/oauth/token请求的工具类

/**
 * 认证服务交互工具类,用于访问OAuth Server,获得Token
 *
 * @author leixiyueqi
 * @since 2024/9/18 19:39
 */

public class AuthorizationUtils {

    private static String oauthTokenUrl = "http://127.0.0.1:19200/oauth/token";

    private static final String clientId ="client";

    private static final String clientSecret ="123456";

    public static Map getAccessToken(String code, String redirectUri) {
        try {
            // 发送请求
            String body = HttpUtil.createPost(oauthTokenUrl)
                    .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                    .header("Authorization", generateAuthHeader())
                    .form("code", code)
                    .form("redirect_uri", redirectUri)
                    .form("grant_type", "authorization_code")
                    .execute()
                    .body();

            System.out.println("DibAuthorization.getAccessToken:tokenBody " +body);

            Map<String, Object> map = JSON.parseObject(body, HashMap.class);
            return map;
        } catch (Exception ex) {
            System.out.println("get access token failed : {}"+ ex.getMessage());
            throw ex;
        }
    }
    private static String generateAuthHeader() {
        String credentials = clientId + ":" + clientSecret;
        String encoded = new String(Base64.getEncoder().encode(credentials.getBytes()));
        return "Basic " + encoded;
    }

}

        4、添加测试Controller


/**
 *
 * @author leixiyueqi
 * @since 2024/9/18 19:39
 */
@RestController
@Slf4j
public class DemoController {


    @GetMapping("/demo")
    public Object demo() {;
        return "Hello World";
    }

    /**
     * 客户端的回调方法,用于获得code后,通过code获得
     *
     * @param code
     * @param state
     * @param httpRequest
     * @param response
     */
    @GetMapping("/callback")
    public void callback(@RequestParam String code,
                         @RequestParam(required = false) String state,
                         ServletRequest httpRequest, ServletResponse response) {
        try {
            log.info("进入方法,callback");
            String localUri = CommonUtils.getRequestUriByHttpRequest((HttpServletRequest)httpRequest);
            Map<String, Object> map = AuthorizationUtils.getAccessToken(code, localUri);
            String jwtStr = CommonUtils.createJwt(map);
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            //redirectUrl = String.format("%s%s=%s", redirectUrl, redirectUrl.contains("?") ? "&token" : "?token", jwtStr);  //将token拼装到url中。
            addCookieForToken(httpResponse, jwtStr);
            httpResponse.sendRedirect(state);
        } catch (Exception e) {
            log.error("AuthorizationCodeTokenController.callback exception:{}", e.getMessage());
        }
    }

    /**
     * 将token以cookie的形式添加到response中
     *
     * @param response
     * @param token
     * @throws Exception
     */
    private void addCookieForToken(HttpServletResponse response, String token) throws Exception {
        Cookie cookie = new Cookie("token", token);

        // 设置Cookie的有效期(以秒为单位)
        cookie.setMaxAge(60 * 60); // 有效期为1小时
        // 设置Cookie的路径
        cookie.setPath("/");
        // 设置Cookie是否只能通过HTTPS协议传输
        cookie.setSecure(true); // 如果你的应用支持HTTPS,设置为true
        // 设置Cookie是否可以通过JavaScript脚本访问
        cookie.setHttpOnly(true); // 设置为true,增加安全性

        // 添加Cookie到响应中
        response.addCookie(cookie);

        // 输出一些文本,以便查看响应
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().println("<h1>Cookie has been set.</h1>");
    }
}

        5、补充下工具类

/**
 * 工具类
 *
 * @author leixiyueqi
 * @since 2024/9/18 19:39
 */
public class CommonUtils {

    private static final String secretKey = "leixi_2024";

    public static String createJwt(Map<String, Object> map) {
        return Jwts.builder().setClaims(map).setExpiration(new Date(System.currentTimeMillis() + 28800000L)).signWith(SignatureAlgorithm.HS256, secretKey).compact();
    }

    public static Claims parseJwt(String jwtString) {
        try {
            return (Claims)Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtString).getBody();
        } catch (JwtException var2) {
            return null;
        }
    }

    public static String getRequestUriByHttpRequest(HttpServletRequest httpRequest) {
        String requestURI = httpRequest.getRequestURI();

        // 获取服务器的端口号
        int serverPort = httpRequest.getServerPort();

        // 获取请求的协议(HTTP或HTTPS)
        String scheme = httpRequest.getScheme();

        // 构建完整的URL
        StringBuilder fullUrl = new StringBuilder(scheme).append("://")
                .append(httpRequest.getServerName())
                .append(":").append(serverPort)
                .append(requestURI);
        return fullUrl.toString();
    }
}

        6、测试,与上文一致,输入:http://127.0.0.1:19210/leixi/demo,通过Filter重定向跳转到/oauth/authorize,重定向到登陆页。

        

        输入用户名,密码后,经过上文所述的认证,callback,重定向,再Filter,最终进入客户端请求。

        

      后记与致谢

        完成了这篇博客后,我终于对OAuth Server的使用,企业中的应用、与客户端的交互有了一个全盘的理解。道阻且长,行将则至,我也没想到时隔近五个月,我才把OAuth相关的知识链给跑通。参考了网上其他的博客,很多大佬在一篇博客里就把认证,自定义页面,客户端给写好了,但我自认没有能力写得那么简单直白,另一方面也想深入的剖析下它的实现,所以写得有点啰嗦了,请各位看官人多多包涵。

        现在回过头看OAuth Server的四种授权模式,可知本篇博客中的两种实现都是授权码模式,那么,对于在企业内部应用OAuth,是不是可以使用其他模式呢?如更简捷的“简单模式”,这个课题,大家可以结合自己的需要进行实践。

        在实践这篇博客时,我也在网上找到了很多二货,以下是我觉得对我帮助极大的,拜谢大佬!

        SpringBoot+SpringSecurity OAuth2 认证服务搭建实战 (六)OAuth2经典场景~授权码模式

        SpringBoot整合OAuth 2.0

        超级简单的springboot整合springsecurity oauth2第三方登录

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2159024.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

9月16日笔记

访问控制列表 访问控制列表(ACL)是访问控制项(Access Control Entry , ACE)的列表。安全对象的安全描述可以通过两种访问控制列表DACL和SACL进行。 DACL DACL 是由一条条的访问控制项(ACE)条目构成的&#xff0c;每条ACE定义了哪些用户或组对该对象拥有怎样的访问权限。DACL…

进阶SpringBoot之集合 Redis

&#xff08;在跑 Redis 测试之前&#xff0c;需要先安装 Redis&#xff0c;并开启 Redis 服务&#xff09; Spring Boot 项目添加依赖 NoSQL -> Spring Data Redis pom.xml 文件如下 <dependencies><dependency><groupId>org.springframework.boot<…

实时数据的处理一致性

实时数据一致性的定义以及面临的挑战‍‍‍‍‍ 数据一致性通常指的是数据在整个系统或多个系统中保持准确、可靠和同步的状态。在实时数据处理中&#xff0c;一致性包括但不限于数据的准确性、完整性、时效性和顺序性。 下图是典型的实时/流式数据处理的流程&#xff1a; 1、…

佰朔资本:沪港通,深港通,港股通的区别与联系?

沪港通&#xff0c;深港通&#xff0c;港股通的差异与联系&#xff1a; 1、沪港通是“沪港股票市场生意互联互通机制”的简称&#xff0c;包含“沪股通”和“沪港通下的港股通”。 &#xff08;1&#xff09;“沪股通”是指投资者托付香港经纪商&#xff0c;经由联交地址上海…

kettle从入门到精通 第八十六课 ETL之kettle kettle调用https接口忽略SSL校验

1、在使用kettle调用接口的时候不可避免要调用http或者https接口&#xff0c;调用http接口kettle可以正常工作&#xff0c;但是遇到https接口的时候kettle就会提示证书有误&#xff0c;无法正常调用接口&#xff0c;今天咱们一起通过自研插件的方式来解决这个问题。自研插件需要…

启明云端WT32C3-S2模组,乐鑫ESP32-C3芯片开发应用,设备联网通信方案

随着科技的飞速发展&#xff0c;我们正步入一个全新的时代——物联网时代。在这个时代&#xff0c;每一个设备都不再是孤立的个体&#xff0c;而是通过无线网络相互连接、相互沟通的智能节点。 想象一下&#xff0c;当你走进家门&#xff0c;灯光自动亮起&#xff0c;空调调整…

认识NDK

什么是NDK&#xff08;Native Development Kit&#xff09; The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C. &emdp; Android NDK 是一个工具集&#xff0c;可让您使用 C 和 C 等语言以原生代…

CANdela/Diva系列8--如何生成0x27服务解锁的DLL

本系列的上一篇文章中&#xff0c;我们介绍了如何在CDD文件中去根据客户需求来配置诊断服务&#xff0c;其实每个诊断服务的配置方式都是大同小异&#xff0c;但是0x27服务略有不同&#xff0c;为了能够让CDD文件根据ECU返回的种子去自动计算出密钥&#xff0c;需要添加一个解锁…

流域碳中和技术

随着全球气候变化的加剧&#xff0c;碳中和已成为实现可持续发展的重要目标之一。碳中和不仅仅是能源和工业领域的调整&#xff0c;它涉及整个生态系统的转型与再生。在这一过程中&#xff0c;流域的生态系统作为水、土、生物多样性等自然资源的集成体&#xff0c;扮演着至关重…

解密.baxia勒索病毒:.baxia勒索病毒的攻击手法及防护建议

导言 在当前网络安全形势日益严峻的背景下&#xff0c;勒索软件的威胁正不断升级&#xff0c;其中.baxia勒索病毒尤为突出。作为一种新型恶意软件&#xff0c;.baxia病毒通过加密用户的文件并要求支付赎金来获取解密密钥&#xff0c;对个人和企业的安全构成了严重威胁。随着其…

【LLM多模态】视频理解模型Cogvlm-video和MVBench评测基准

note Cogvlm-video模型通过视频抽帧&#xff08;24帧&#xff0c;每帧大小为224 x 224&#xff09;后经过ViT进行图像编码&#xff08;ViT中添加了2x2的卷积核更好的压缩视觉信息&#xff09;&#xff0c;使用adapter模块更好的将视觉特征和文本特征对齐&#xff0c;得到的图像…

基于WebServer的工业数据采集系统

一、项目框架及流程 二、http简介 HTTP协议是Hyper Text Transfer Protocol&#xff08;超文本传输协议&#xff09;的缩写&#xff0c;是用于Web Browser&#xff08;浏览器&#xff09;到Web Server&#xff08;服务器&#xff09;进行数据交互的传输协议。 HTTP是应用层协…

poi生成的ppt,powerPoint打开提示内容错误解决方案

poi生成的ppt&#xff0c;powerPoint打开提示内容错误解决方案 最近做了ppt的生成&#xff0c;使用poi制作ppt&#xff0c;出现一个问题。微软的powerPoint打不开&#xff0c;提示错误信息 通过xml对比工具发现只需要删除幻灯片的某些标签即可解决。 用的是XML Notepand 分…

ai写论文哪个平台好?7款AI写论文软件帮你撰写论文

在当今学术研究和写作领域&#xff0c;AI论文写作工具的出现极大地提高了写作效率和质量。这些工具不仅能够帮助研究人员快速生成论文草稿&#xff0c;还能进行内容优化、查重和排版等操作。以下是七款推荐的AI写论文软件&#xff0c;其中特别推荐千笔-AIPassPaper。 一、千笔…

Java集合(Map篇)

一.Map a.使用Map i.键值&#xff08;key-value&#xff09;映射表的数据结构&#xff0c;能高效通过key快速查找value&#xff08;元素&#xff09;。 ii.Map是一个接口&#xff0c;最常用的实现类是HashMap。 iii.重复放入k-v不会有问题&#xff0c;但是一个…

盈科巴黎办公室开业典礼暨盈科全球一小时法律服务生态圈·法国中心揭牌仪式圆满举办

国际化建设是盈科律师事务所发展的重要战略目标之一&#xff0c;随着中国对外投资、跨境贸易、“一带一路”高质量共建等迅速发展&#xff0c;越来越多中国企业和公民“走出去”&#xff0c;寻找海外市场。今年是中法建交60周年。建交以来&#xff0c;中法坚持做真诚的朋友、共…

【Java】JVM基本组成

一、JDK、JRE、JVM JDK&#xff1a;全称 “Java Development Kit” Java 开发工具包&#xff0c;提供 javac编译器、jheap、jconsole 等监控工具; JRE&#xff1a;全称 “Java Runtime Environment” Java 运行环境&#xff0c;提供 class Library 核心类库JVM; …

文件服务器FastDFS 消息队列中间件RabbitMQ

新标签页 (chinaunix.net) FastDFS - Browse Files at SourceForge.net 一、FastDFS Tracker和Storage&#xff1a; tracker用来管理所有的storage&#xff0c;只是管理服务器&#xff0c;负责负载均衡。 storage是存储服务器&#xff0c;每一个storage服务器都是一个单独的个…

计算机毕业设计之:基于微信小程序的疫苗预约系统的设计与实现(源码+文档+讲解)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Shiro rememberMe反序列化漏洞(Shiro-550)

开启环境抓包验证Shiro框架 使⽤BurpSuite进⾏抓包&#xff0c;在请求包中的cookie字段中添加 rememberMe123; &#xff0c;看响应包 header中是否返回 rememberMedeleteMe 值&#xff0c;若有&#xff0c;则证明该系统使⽤了Shiro框架&#xff1a; 使用工具爆破密钥爆破利用…