从0搭建SpringCloud集群与OAuth2.1前后分离框架(3/12)

一、概要

        实战文章,主要分为两部分。 一是搭建SpringCloud微服务与集群;二是基于OAuth2.1(Spring Authorization Server)搭建前后分离安全框架。

        后端主要采用的技术栈:SpringBoot3.0,Security6.0,OAuth2.1。

        前端主要采用的技术栈:Vue3.0+Vite,ElementPlus。

二、环境

        系统:MAC Apple M2 13.3

        开发工具:IntelliJ IDEA 2022.3.3 (Ultimate Edition)

        JDK版本:OpenJDK 17.0.8

三、文章内容

本章内容,编写通用服,并实现统一响应类。 基于OAuth2.1,实现安全框架(重点)。

10、编写通用服

前面已经创建了一个通用服cloud-common,主要用于存放一些共用东西,如统一响应类,枚举类或工具类等等。

(1)添加依赖

这里需要添加一个SpringWeb依赖,主要需要用到一些相关的注解。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-common</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

(2)编写统一响应HttpCode

在src.main.java.com.example.enums包中,创建一个枚举类HttpCode,并创建一个构造方法。

package com.example.enums;

import lombok.Getter;

@Getter
public enum HttpCode {
    // 成功相关
    SUCCESS(1000, "操作成功"),

    S_CODE_SEND(1010, "验证码已发送"),


    // 失败相关
    FAILURE(2000, "操作失败"),
    F_USERNAME_EMPTY(2101, "用户名不能为空"),
    F_USER_INCORRECT(2102, "用户名或密码错误"),
    F_MAIL_SEND(2201, "邮件发送失败"),


    // 数据错误
    DB_ERROR(3000, "数据错误"),


    // 系统错误
    SYS_ERROR(5000, "系统错误"),

    // 非法操作
    USER_ERROR(6000, "非法操作");

    final private Integer code;
    final private String msg;

    HttpCode(Integer code, String msg){
        this.code = code;
        this.msg = msg;
    }
}

(3)编写统一响应类

在src.main.java.com.example.domain包中,创建一个抽象类ResponseBase。

  • 需要实现Set和Get方法
  • 需要实现序列化Serializable接口
package com.example.domain;

import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

/**
 * 响应属性模版
 */

@Getter
@Setter
public abstract class ResponseBase<T> implements Serializable {
    /**
     * 请求响应是否成功
     * @mock true
     * @since v1.0
     */
    private Boolean success;
    /**
     * 请求返回code码
     * @mock 200
     * @since v1.0
     */
    private Integer code;
    /**
     * 请求返回一句话信息,用于toast
     * @mock 登录成功
     * @since v1.0
     */
    private String msg;
    /**
     * 请求返回携带的数据,通常是json数据
     * @mock {"token" : "JWTToken"}
     * @since v1.0
     */
    private T data;
    /**
     * 响应时间戳(目前没有用)
     * @mock 1685507117
     * @since v1.0
     */
    private String timestamp;
}

在src.main.java.com.example.domain包中,创建一个类ResponseResult,继承ResponseBase,后续直接调用ResponseResult的静态方法。

package com.example.domain;


import com.example.enums.HttpCode;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;

/**
 * 统一响应类
 */
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_NULL)
@SuppressWarnings("unused")
public class ResponseResult<T> extends ResponseBase<T>{

    public ResponseResult(Boolean success, Integer code, String msg, T data) {
        this.setSuccess(success);
        this.setCode(code);
        this.setMsg(msg);
        this.setData(data);
    }

    /** 请求成功(默认返回success=true, code=200;自定义msg) */
    public static <T> ResponseResult<T> success(String msg){
        return new ResponseResult<>(true, 200, msg, null);
    }

    /** 请求成功(默认返回success=true, code=200;自定义msg、data) */
    public static <T> ResponseResult<T> success(String msg, T data){
        return new ResponseResult<>(true, 200, msg, data);
    }

    /** 请求失败(默认返回success=false, code=400;自定义msg) */
    public static <T> ResponseResult<T> failure(String msg){
        return new ResponseResult<>(false, 400, msg, null);
    }

    /** 请求失败(默认返回success=false;自定义code、msg) */
    public static <T> ResponseResult<T> failure(Integer code, String msg){
        return new ResponseResult<>(false, code, msg, null);
    }


    /* ---- 直接使用自定义HttpCode返回 ---- */
    /** 请求成功(默认返回success=true, 模板code与msg) */
    public static <T> ResponseResult<T> success(HttpCode httpCode){
        return new ResponseResult<>(true, httpCode.getCode(), httpCode.getMsg(), null);
    }

    /** 请求成功(默认返回success=true, 模板code与msg;自定义data) */
    public static <T> ResponseResult<T> success(HttpCode httpCode, T data){
        return new ResponseResult<>(true, httpCode.getCode(), httpCode.getMsg(), data);
    }

    /** 请求失败(默认返回success=false;模板code与msg) */
    public static <T> ResponseResult<T> failure(HttpCode httpCode){
        return new ResponseResult<>(false, httpCode.getCode(), httpCode.getMsg(), null);
    }

}

(4)启动类

在src.main.java.com.example包中,将启动类Main.class删除,通用服主要用于提供公共模块引用,不需要启动服务,因此可以删除掉启动类。

这时,打包会出现提示没有启动类的问题,可以在依赖文件中,增加配置忽略掉。

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!-- 解决main不存在的问题 -->
                    <mainClass>none</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

(5)测试

这时候,通用服基本已经完成。使用测试服引入响应模块试试效果。

打开测试服(cloud-test)的依赖,增加以下配置引用cloud-common。

    <dependencies>
        

        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-common</artifactId>
            <version>${project.version}</version>
        </dependency>

    </dependencies>

完整依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-test</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-user</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  Eureka客户端依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--  openfeign依赖(内部服调用)  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--  JDBC驱动  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--  mysql驱动  -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-common</artifactId>
            <version>${project.version}</version>
        </dependency>

    </dependencies>

</project>

修改src.main.java.com.example.controller包中的TestController,使用ResponseResult来

响应:

package com.example.controller;

import com.example.domain.ResponseResult;
import com.example.domain.entity.User;
import com.example.enums.HttpCode;
import com.example.service.client.UserClient;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    UserClient userClient;

    @GetMapping("/info/{username}")
    public ResponseResult<?> testOne(@PathVariable String username) {
        User user = userClient.getUser(username);
        // 使用统一响应类 响应访问,将user放入data里面
        return ResponseResult.success(HttpCode.SUCCESS, user);
    }

    @GetMapping("/my")
    public String testOne() {
        return "访问成功";
    }
}

重启服务,打开浏览器访问http://127.0.0.1:8101/test/info/user后,可以成功拿到JSON格式的数据,如下:

 11、OAuth2.1 授权

(1)主要技术栈:

        使用框架:Spring Security6.0,Spring Authorization Server1.0(OAuth2.1)

        官方地址:Spring Authorization Server

(2)简述:

        当用户需要访问集群的服务时,我们不可能像独立服一样,让用户在每个服务上都进行鉴权,Oauth2就提供了一套解决方案。如下图(官方图例):

        上图中,大致流程解释:

  • 当用户通过API接口(或前端页面)请求集群的业务服时(图中Client),业务服会判断用户是否已经鉴权,若没有则会重定向至授权服(图中AuthorizationServer)。
  • 授权服,判断用户没有授权,则会给这次请求返回一个登录页(授权服自己的页面)。
  • 用户进行登录操作,确认身份后,还会确认授予的权限(若有)。
  • 鉴权完成后,授权服会给业务服发送一个Code,业务服拿到Code后,会向授权服请求获取一个Access Token(以及Refresh Token)
  • 业务服会向用户最初访问的接口重新发起一次请求。这时候有两种情况:
    • 若访问的接口在业务服中,那么请求通过,并获取接口的数据;
    • 若访问的接口在资源服(Resouce Server),则会携带Access Token去请求,资源服会审核Access Token是否合法,如果通过则返回数据。

        整个大概流程是这样,中间还有很多细节,甚至有很多作者也还在探索当中。有条件,可以翻翻官方说明。

        在这个框架当中,会有三个角色(参考oauth2.1规范文档 Roles):

  • Authorization server(认证服务器):即用来认证与颁发令牌(如token)的服务。
  • Client(客户端):即访问的客户端,如集群内的业务服、网关服,或第三方网站。
  • Resource server(资源服务器):托管受保护资源的服务器,能够使用访问令牌接受和响应受保护的资源请求。
  • Resource Owner(资源拥有者):能够授予对受保护资源的访问权限的实体,通常指的是终端用户。

        授权方式,会有下面几种:

  • 授权码模式(authorization_code):最正规的模式,上述举例说明的就是授权码模式。比如微信、Gitee、Github都是采用此方式。
  • 刷新模式(refresh_token):用Refresh Token获取Access Token。
  • 客户端模式(client_credentials):第三方应用使用自己的客户端凭证(client_id 和 client_secret)向授权服务器获取令牌。
  • 密码模式(password):该方式已被OAuth2.1弃用,主要是因为在第三方中直接输入用户的账号密码是不安全的。

(3)搭建说明:

        这里,先简单概括需要分配的服务:        

服务名称地址作用
授权服cloud-auth127.0.0.1:9000身份认证及授权、颁发令牌
业务服cloud-test

127.0.0.1:8101

127.0.0.1:8102

提供业务数据接口
资源服cloud-user

127.0.0.1:8001

127.0.0.1:8002

提供内部资源接口
授权服前端master127.0.0.1:5666

提供授权服自定义页面

如登录页面、权限页面

Gitee第三方联合登录

12、父工程依赖

        本章中,父工程相对于上一章,需要添加的依赖如下:

  • spring-security-oauth2-authorization-server 1.0.3        授权服依赖

13、搭建OAuth2授权服(cloud-auth)

        开始搭建一个授权服

(1)创建授权服

        老规矩,创建一个子项目cloud-auth作为授权服。        

(2)添加依赖

主要涉及依赖说明:

  • cloud-common,公共模块
  • spring-boot-starter-web,SpringWeb。
  • spring-security-oauth2-authorization-server,授权服依赖
  • spring-cloud-starter-netflix-eureka-client,Eureka客户端。
  • spring-boot-starter-jdbc,JDBC驱动。
  • mysql-connector-j,Mysql驱动。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-auth</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  SpringSecurity依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--  OAuth2依赖(授权服)  -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
        </dependency>

        <!--  Eureka客户端依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--  JDBC驱动  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--  mysql驱动  -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

    </dependencies>

</project>

(3)配置参数

在src.main.resources包中,创建application.yml配置文件,主要配置说明:

  • server.port,分配端口9000。
  • server.servlet.session.cookie.name,管理session名称,避免前端浏览器多个session导致相互覆盖。
  • spring.application.name,修改服务名称,后续方便在注册中心查看。
  • spring.application.datasource,mysql参数配置,本服务使用的数据库。
  • eureka.client.service-url.defaultZone,填写两个Eureka服务地址。
server:
  port: 9000
  servlet:
    session:
      cookie:
        name: "JSESSIONID-${spring.application.name}-${server.port}"

spring:
  application:
    name: AUTH-SERVICE

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8
    username: cloud
    password: c123456

eureka:
  client:
    service-url:
      defaultZone: http://eureka01:7101/eureka, http://eureka02:7102/eureka

(4)修改启动类

在src.main.java.com.example包中,将Main启动类修改为:

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

(5)配置类编写

这里主要编写两个配置类(其实是一个拆成两个),按照官方文档说明,可以归纳为以下配置:

  • SpringAuthorizationServer的配置,实现OAuth2授权服功能。
  • SpringSecurity的配置,实现基础登录功能(与单服类似)
  • UserDetailsService的配置,实现内存缓存一个初始用户,后期可改为Mysql获取。
  • RegisteredClientRepository的配置,实现内存缓存一个OAuth2客户端及其配置参数,后期可改为Mysql获取。
  • JWK、JWT的相关配置,用于生成密钥、处理token。
  • AuthorizationServerSettings的配置,AuthorizationServer基础配置,暂时可以不管。
  • 官方例子地址:Spring Authorization Server

在src.main.java.com.example.config包中,编写第一个配置类SecurityOAuth2ServerConfig。

package com.example.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
public class SecurityOAuth2ServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        /* 使用OAuth2.1默认配置,默认关闭端点csrf校验 */
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http
                /* Enable OpenID Connect 1.0 */
                .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        http
                // Redirect to the login page when not authenticated from the
                // authorization endpoint
                .exceptionHandling((exceptions) -> exceptions
                                .defaultAuthenticationEntryPointFor(
                                        // 设置未登录重定向页面,重写LoginUrlAuthenticationEntryPoint
                                        new LoginUrlAuthenticationEntryPoint("/login"),
                                        new MediaTypeRequestMatcher(MediaType.TEXT_HTML)

                                )
                )
                // Accept access tokens for User Info and/or Client Registration
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer -> OAuth2ResourceServerConfigurer
                        .jwt(Customizer.withDefaults())
                );

        http
                // 禁用 csrf
                .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    /**
     * 配置客户端Repository
     *
     * @return 基于数据库的repository
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id
                .clientId("oidc-client")
                // 客户端秘钥,使用密码解析器加密
                .clientSecret("{noop}secret")
                .clientName("MyAuth")
                // 客户端认证方式,基于请求头的认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 配置资源服务器使用该客户端获取授权时支持的方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost
                .redirectUri("http://127.0.0.1:8101/login/oauth2/code/myOauth2")
                .redirectUri("https://www.baidu.com")
                // 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                // 客户端设置,设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        // 基于内存实现客户端信息存储
        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    /**
     * 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
     * @return JWKSource
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {

        KeyPair keyPair = generateRsaKey();
        // 公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        // 生成jwk令牌
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);

        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * 生成rsa密钥对,提供给jwk
     *
     * @return 密钥对
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * 配置jwt解析器
     *
     * @param jwkSource jwk源
     * @return JwtDecoder
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 添加认证服务器配置,设置jwt签发者、默认端点请求地址等
     *
     * @return AuthorizationServerSettings
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

}

再编写第二个配置类SecurityDefaultConfig。

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityDefaultConfig {

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());
        http
                // 禁用 csrf
                .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    @SuppressWarnings("deprecation")
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.withDefaultPasswordEncoder()
                .username("user")
                .password("123456")
                .roles("USER")
                .authorities("app", "web")
                .build();

        return new InMemoryUserDetailsManager(userDetails);
    }

}

14、搭建OAuth2客户端

将测试服(cloud-test)作为OAuth2客户端

(1)添加配置

在原来的基础上,增加配置说明:

  • spring-boot-starter-security,security安全框。
  • spring-boot-starter-oauth2-client,OAuth2客户端依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>cloud</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-test</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-user</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--  SpringWeb依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--  Eureka客户端依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <!--  openfeign依赖(内部服调用)  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--  JDBC驱动  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--  mysql驱动  -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!--	导入公共模块	-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>cloud-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--  SpringSecurity依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--  OAuth2依赖  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

    </dependencies>

</project>

(2)配置参数

在原来的基础上,增加的参数说明:

  • spring.security.oauth2.client,OAuth2客户端配置(基本就是填写上述授权服生成的客户端参数)。
  • 其中registration.myOauth2与registration.myOauth2.redirect-uri最末尾的myOauth2,需要保持一致,并且这个地址需要与授权服填写的回调地址要保持一致。其中地址/login/oauth2/code是固定格式,其他可以自定义。
  • registration.myOauth2.provider与provider.myOauth2的myOauth2需要保持一致,可以自定义。
  • scope,权限范围,需要授权服提供的范围之内。该参数会向用户展示需要获取的权限范围。
  • provider.myOauth2.issuer-uri,这里需要填写授权服地址,会自动提供所有获取内容的地址(后面会展示)。
server:
  port: 8101

spring:
  application:
    name: CLOUD-TEST
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=utf-8
    username: cloud
    password: c123456
  cloud:
    openfeign:
      client:
        config:
          default:
            # 连接超时时间
            connectTimeout: 5000
            # 读取超时时间
            readTimeout: 5000
  security:
    oauth2:
      client:
        registration:
          myOauth2:
            provider: myOauth2
            client-id: oidc-client
            client-secret: secret
            client-name: MyAuth
            authorization-grant-type: authorization_code
            redirect-uri: http://127.0.0.1:8101/login/oauth2/code/myOauth2
            #redirect-uri: https://www.baidu.com
            scope:
              - openid
              - profile
              - message.read
              - message.write
        provider:
          myOauth2:
            issuer-uri: http://127.0.0.1:9000

eureka:
  client:
    service-url:
      defaultZone: http://eureka01:7101/eureka, http://eureka02:7102/eureka

(3)编写配置类

在src.main.java.com.example.config包中,编写一个SpringSecurity的配置类SecurityConfigurationa。

主要是配置oauth2Login登录方式以及配置为oauth2Client客户端。

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                // 所有请求都需经过授权认证
                .authorizeHttpRequests(authorize -> authorize
                        // 放行登录页面及授权确认页面
                        .requestMatchers( "/login").permitAll()
                        .requestMatchers("/oauth2/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(Customizer.withDefaults())
                .oauth2Client(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }
}

到这,OAuth2的授权服和客户端,最基础的配置已经编写好,我们可以将两个服务启动起来。(这里优先启动授权服,再启动客户端,主要是与issuer-uri接口需要提供地址有关系,后续再填坑)

15、授权过程

(1)访问->授权服登录

打开浏览器访问http://127.0.0.1:8101/test/my (由于用户信息服未接入OAuth2,这里我们只访问测试服自己的资源)。确认后,发现重定向至授权服的登录页(这里相信学过单服安全框架的小伙伴应该很熟悉,就是Security自带的登录页)。

这里,我们可以打开开发者模式,查看Network请求资源的情况,发现总共发起了四次请求(含重定向)。

查看第一次请求my(http://127.0.0.1:8101/test/my)时,出现302重定向,再查看重定向地址为http://127.0.0.1:8101/oauth2/authorization/myOauth2的,这个地址是我们配置的OAuth2客户端,发现用户没有鉴权后,重定向至这个接口,它会帮我们组装携带指定的参数(如client_id、redirect_uri等)去访问授权服,这里需要注意,这个过程仍然发生在测试服中。

我们继续查看第二次请求myOauth2(http://127.0.0.1:8101/oauth2/authorization/myOauth2)时,仍然出现302重定向。查看重定向地址为http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=openid%20profile%20message.read%20message.write&state=71kUPmwZMzQEazxGi39vcIdHgiEVG9hjVMcB8HZ7Nxo%3D&redirect_uri=http://127.0.0.1:8101/login/oauth2/code/myOauth2&nonce=qIKLinWDzSMdz-gP6H8xTiMlXtd7aqNbW0QBNNGy6KI,访问的地址是授权服的地址http://127.0.0.1:9000/oauth2/authorize,并且携带了这些参数:

response_type=code        这个是请求模式,本次请求是为了获取code码

client_id=oidc-client        这里是我们授权服和测试服填写的client_id

scope=openid%20profile%20message.read%20message.write        这里是我们测试服填写的权限范围

state=71kUPmwZMzQEazxGi39vcIdHgiEVG9hjVMcB8HZ7Nxo%3D        这是state随机码,主要用于防止 CSRF 攻击

redirect_uri=http://127.0.0.1:8101/login/oauth2/code/myOauth2        这里是我们测试服的回调地址

nonce=qIKLinWDzSMdz-gP6H8xTiMlXtd7aqNbW0QBNNGy6KI        这里nonce随机码,用于确保请求的唯一性。

 最后第三次请求authorize?response_ty……(http://127.0.0.1:9000/oauth2/authorize?……)时,再次出现302重定向。查看重定向地址为http://127.0.0.1:9000/login,这正是我们当前页面的地址。

输入帐号密码(user与123456) 提交后,再次出现302重定向,重定向地址与第二次请求相同(最后附带了continue,证明是再次访问)。再次请求http://127.0.0.1:9000/oauth2/authorize?……地址时,授权服已经确认我们身份,回调给我们一个确认权限的页面。

 (2)确认授权范围->访问成功

选择权限确认后,浏览器会向http://127.0.0.1:9000/oauth2/authorize发起post请求,并携带client_id、state以及权限信息scope表单。授权服收到post请求,会先确认用户身份(通过session),接受并确认参数后,又再次(第三次)重定向至http://127.0.0.1:8101/login/oauth2/code/myOauth2地址,并携带关键信息code(以及state),这个code正是测试服用于获取Access Token。

最后因为是访问测试服本身的资源,所以直接重定向回最初的http://127.0.0.1:8101/test/my?continue地址,并获得访问接口的数据,到这里,所有的流程已经走完。

 (3)Access Token

最后,我们尝试下用API工具,通过code获取Access Token。

还记得我们在授权服SecurityOAuth2ServerConfig配置中,填写了一个https://www.baidu.com回调地址吗?现在我们用参数自己拼接一个,让code码返回到https://www.baidu.com回调地址中,拼接地址如下:

http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=openid%20profile%20message.read%20message.write&redirect_uri=https://www.baidu.com

在浏览器中访问该地址并授权后,可以在Network中,找到一个请求是返回code码(或者直接看地址栏),这个code码,因为我们要求回调到百度上面,所以是还没有被使用的。

 我们打开API工具,填写一下参数:

  • 在Authorization中,选择Basic Auth,填写client_id和client_secret。
  • 在Params中,填写grant_type、code(上面获取的code)和redirect_uri。

 (注意,redirect_uri需要与授权服填写的地址一致,别在地址后面多加一些其他符号,比如/ )

填写好参数以后,选择post提交请求,发现拿到Access Token。

如果授权范围有填写openid,会默认获取,并且在获取Access Token的同时,还会有一个Id Token。这个是用于储存用户的相关信息,我们可以利用工具解析Id Token看看里面存放了什么信息。

 这里,后期我们自定义时,将一些需要用到的用户信息通过这个特性,来进行信息交换。这个后面再说。

以上,就是一个基础版本的Spring Authorization Server1.0(OAuth2.1)的基本内容。

四、后续

 下一章:实现授权服的前后分离(自定义登录页与授权页)。

 

OAuth2.1OAuth2.0的一个扩展协议,它增加了一些额外的安全措施,例如公共客户端的验证和授权服务器的证书绑定。在iOS上实现OAuth2.1,您可以使用第三方库或自己编写代码来处理授权流程和令牌管理。 以下是使用第三方库实现OAuth2.1的步骤: 1. 添加OAuth2.1依赖库到您的项目中。常用的库有Auth0、Okta和AppAuth。这些库提供了OAuth2.1的实现,并且已经集成了大多数授权服务器的配置信息。 2. 配置应用程序和授权服务器。您需要在应用程序和授权服务器之间设置正确的重定向URI和客户端ID。这些信息将在授权流程中使用。 3. 实现授权流程。使用第三方库提供的方法来执行授权流程。通常,您需要向授权服务器发送请求以获取授权代码,并将授权代码交换为访问令牌。一些库提供了UI控件来处理授权流程,使它们更容易集成到您的应用程序中。 4. 存储和管理访问令牌。一旦您获得了访问令牌,您需要存储它并在需要时使用它来访问受保护的资源。您可以使用Keychain或其他安全存储机制来保存令牌。 5. 处理令牌过期和刷新。访问令牌有一个有效期,在过期之前,您需要使用刷新令牌来获取新的访问令牌。您可以使用库提供的方法来处理令牌过期和刷新。 使用第三方库可以简化OAuth2.1的实现,但您需要确保库与您的授权服务器兼容,并且符合您的安全要求。如果您需要更高的安全性和控制权,您可以编写自己的代码来处理OAuth2.1
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值