Spring Boot OAuth2.0应用

news2024/11/28 10:38:38

本文展示Spring Boot中,新版本OAuth2.0的简单实现,版本信息:

spring-boot 2.7.10
spring-security-oauth2-authorization-server 0.4.0
spring-security-oauth2-client 5.7.7
spring-boot-starter-oauth2-resource-server 2.7.10

展示三个服务,分别为

  • 授权服务:作为认证中心,用于向客户端发放授权码code与令牌token
  • 资源服务:保存客户端要访问的资源
  • 客户端:向授权服务发起请求,获取授权码code,再用code换取token,最后使用token访问资源


授权服务搭建

总体预览

授权服务预览

pom

<?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>

    <groupId>org.example</groupId>
    <artifactId>OAuth2-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
    </parent>

    <dependencies>

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

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-authorization-server</artifactId>
            <version>0.4.0</version>
        </dependency>

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

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.26</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

配置类

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
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.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.*;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@EnableWebSecurity
@Configuration
public class OAuth2Config {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private DataSource dataSource;

    @Bean
    UserDetailsManager userDetailsManager() {
        return new JdbcUserDetailsManager(dataSource);
    }

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

        // 定义授权服务配置
        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();

        // 获取授权服务器相关的请求端点
        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();

        http
                .authorizeHttpRequests(authorize -> authorize
                        // 配置放行的请求,/register用于客户端及用户的注册
                        // /register/*代表放行api下的单层路径,/register/**代表其下的所有子路径
                        .antMatchers("/register/**", "/login").permitAll()
                        // 其他任何请求都需要认证
                        .anyRequest().authenticated()
                )

                //配置登录页,用于授权请求未认证时进行用户登录授权
                .formLogin()
                .and()

                // 忽略掉相关端点的CSRF(跨站请求伪造攻击防御): 对授权端点的访问不被CSRF拦截
                .csrf(
                        csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)
                                //如果是post请求,即便上面已经放行,还是会被csrf过滤器拦截,所以针对post请求,如果开启了csrf防护,需要再配置放行
                                .ignoringAntMatchers("/register/**")
                )


                // 使用BearerTokenAuthenticationFilter对AccessToken及idToken进行解析验证
                // idToken是开启OIDC时,授权服务连同AccessToken(就是访问资源需要的token)一起返回给客户端的,用于客户端验证用户身份,结合此处配置使用BearerTokenAuthenticationFilter来验证idToken
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)

                // 应用授权服务器的配置,使其生效
                .apply(configurer);


        configurer
                //开启oidc,客户端会对资源所有者进行身份认证,确保用户身份的真实性、防止身份伪造、增强安全性。
                // 开启后,除了访问令牌access_token,还会多一个用户身份认证的idToken
                .oidc(Customizer.withDefaults())

                //配置用何种方式保存注册的客户端信息,默认为内存保存,这里配置为数据库保存,表名为'oauth2_registered_client'
                //保存客户端注册信息,主要用于后续各种认证时对比客户端是否有效
                .registeredClientRepository(registeredClientRepository())

                //配置用何种方式保存OAuth2客户端的授权请求的信息。这包括授权码、访问令牌、刷新令牌等。
                // 默认为内存保存,这里配置为数据库保存,表名为'oauth2_authorization'
                //授权信息会作为认证依据,在后续请求token时被读取,不存在授权信息则不给客户端生成token
                .authorizationService(authorizationService())

                //配置用何种方式存储用户对客户端请求的授权同意(consent)信息。
                // 默认为内存保存,这里配置为数据库保存,表名为'oauth2_authorization_consent'
                //请求code时检查是客户端否已授权,未授权不予code
                .authorizationConsentService(authorizationConsentService())

                /**
                 * OAuth2AuthorizationService 与 OAuth2AuthorizationConsentService区别:
                 *  oauth2_authorization 主要与令牌管理相关,负责存储令牌及其生命周期信息。
                 *  oauth2_authorization_consent 主要用于管理用户授权同意的记录,确保用户的授权选择被正确记录和遵守。
                 * */
            
            
                //配置OAuth2认证各项端点的http访问路径,如获取授权码的、获取token的、验证token的等等
                .authorizationServerSettings(authorizationServerSettings());

        return http.build();
    }


    /**
     * 注册客户端应用的保存方式, 对应 oauth2_registered_client 表
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    /**
     * 令牌的发放记录, 对应 oauth2_authorization 表
     */
//  @Bean 这里的bean注解放开,就可以不用在上面的OAuth2AuthorizationServerConfigurer中配置了
    public OAuth2AuthorizationService authorizationService() {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository());
    }

    /**
     * 把资源拥有者授权确认操作保存到数据库, 对应 oauth2_authorization_consent 表
     */
//   @Bean 这里的bean注解放开,就可以不用在上面的OAuth2AuthorizationServerConfigurer中配置了
    public OAuth2AuthorizationConsentService authorizationConsentService() {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository());
    }


    /**
     * AuthorizationServerS 的相关配置
     */
//   @Bean 这里的bean注解放开,就可以不用在上面的OAuth2AuthorizationServerConfigurer中配置了
    public AuthorizationServerSettings authorizationServerSettings(){
        //使用默认配置
        return AuthorizationServerSettings.builder().build();
    }


    /**
     * token的配置项:过期时间、是否复用refreshToken刷新令牌等等
     * */
    @Bean
    public TokenSettings clientTokenSettings(){

        return TokenSettings.builder()
                // 令牌存活时间:2小时
                .accessTokenTimeToLive(Duration.ofHours(2))
                // 令牌可以刷新,重新获取
                .reuseRefreshTokens(true)
                // 刷新时间:30天(30天内当令牌过期时,可以用刷新令牌重新申请新令牌,不需要再认证)
                .refreshTokenTimeToLive(Duration.ofDays(30))
                .build();
    }

    /**
     *  针对 OAuth 2.0 客户端的各种设置
     * */
    @Bean
    public ClientSettings clientSettings(){

        return ClientSettings.builder()
                // 是否需要用户授权确认
                .requireAuthorizationConsent(true)
                //指定使用client_secret_jwt认证方式时的签名算法
                .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)

                //如果为true,当客户端使用授权码时,服务器会强制要求提供 PKCE 参数(code verifier 和 code challenge)
                //.requireProofKey(true)

                //为 OAuth 2.0 客户端配置一个 JWKS 的 URL 地址, 当其他服务需要验证该客户端的 JWT 时,它们可以访问这个 URL 获取用于验证的公钥。
                //.jwkSetUrl("")

                .build();
    }


    /**
     * 用于授权服务生成token令牌的JWT设置,如下代码使用非对称加密
     * 资源服务会通过issuer获取此配置来对token进行验证,验证通过则客户端可以访问资源
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() throws Exception {
        // 生成RSA密钥对
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();

        // 构建JWK
        RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())//公钥
                .privateKey(keyPair.getPrivate())//私钥
                // keyID是用来唯一标识密钥的,keyID可以帮助服务器区分不同的密钥。如果有多个密钥存在,服务器可以根据JWT中提供的kid值快速找到用于签名该JWT的密钥
                .keyID(UUID.randomUUID().toString())
                .build();

        // 构建JWKSet
        JWKSet jwkSet = new JWKSet(rsaKey);

        // 返回JWKSource实例
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }


    /**
     * JWT token 解码配置
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 密码编码器,用于对密码进行加密
     * 比如使用bcrypt对客户端密钥编码后,在数据库中其值大致为如下格式,多了一个{bcrypt}前缀:
     *  {bcrypt}$2a$10$BaExfIkMtKtdqMVfkxlAR.fWlRDoJrmTOEz4oM4jZ3fxkio9IMYJS
     *
     * */
    @Bean
    public PasswordEncoder passwordEncoder() {

        Map<String,PasswordEncoder> encoders = new HashMap<>();
        //使用bcrypt进行密码编码(这是目前最常用和推荐的密码加密方法)。
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        //基于 PBKDF2 算法进行密码编码
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        //基于 SCrypt 算法进行密码编码
        encoders.put("scrypt", new SCryptPasswordEncoder());
        //基于 SHA-256 算法进行密码编码
        encoders.put("sha256", new StandardPasswordEncoder());

        //DelegatingPasswordEncoder 是一个委托的密码编码器,它可以根据密码存储时的前缀标识符来选择不同的密码编码器。
        //DelegatingPasswordEncoder 以 "bcrypt" 为默认的编码器,同时允许使用其他定义在 encoders Map 中的编码器。
        PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);

        return passwordEncoder;
    }


}

yaml文件

server:
  port: 8080


spring:
  datasource:
    url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: wangziyu123
    driver-class-name: com.mysql.cj.jdbc.Driver

controller

主要用于向授权服务注册客户端与用户,保存到数据库中

import com.oauth.entity.ClientDemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
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.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("register")
public class RegisterController {
    
    //用于把用户存储到数据库
    @Autowired
    private UserDetailsManager userDetailsManager;
	
    //客户端token配置
    @Autowired
    private TokenSettings clientTokenSettings;

    //客户端配置
    @Autowired
    private ClientSettings clientSettings;
    
    //注册客户端的存储库,已通过配置类指定为数据库存储
    @Autowired
    private RegisteredClientRepository registeredClientRepository;

    //密码编码器
    @Autowired
    private PasswordEncoder passwordEncoder;

	//用户注册方法
    @GetMapping("user")
    public String addUser(String userName,String password,String role) {
        UserDetails userDetails = User.builder()
                .username(userName)
                //密码在数据库中存储为:{bcrypt}$2a$10$z******* 这样的格式,会加上{bcrypt}的前缀,后续OAuth2需要根据前缀做相关处理
                .password(passwordEncoder.encode(password))
                .roles(role)
                .build();

        userDetailsManager.createUser(userDetails);
        return "用户注册成功";
    }


	//客户端注册方法
    @PostMapping("client")
    public String addClient(@RequestBody ClientDemo client) {

        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                 
                //客户端ID和密钥
                .clientId(client.getId())
                .clientSecret(passwordEncoder.encode(client.getSecret()))
               
                //客户端获取token时的认证方式,这里指定使用client_secret_basic方式,即请求头的Authentication: Basic Auth
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                
                // 回调地址:授权码模式下,授权服务器会携带code向当前客户端的如下地址进行重定向。只能使用IP或域名,不能使用 localhost
                .redirectUri(client.getRedirectUri())

                // 授权范围(当前客户端的授权范围)
                .scopes(scopes -> scopes.addAll(client.getScopes()))

                //配置支持多种授权模式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)//授权码模式
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)// 刷新令牌
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)//客户端凭证模式
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)//密码模式

                // OIDC 支持, 用于客户端对用户(资源所有者)的身份认证
                .scope(OidcScopes.OPENID) //OIDC 并不是授权码模式的必需部分,但如果客户端请求包含 openid scope,就必须启用 OIDC 支持。
                .scope(OidcScopes.PROFILE)

                //token配置项
                .tokenSettings(clientTokenSettings)
                // 客户端配置项
                .clientSettings(clientSettings)

                .build();

        registeredClientRepository.save(registeredClient);
        return "客户端注册成功";
    }

    /**
     * 展示授权码的回调
     * */
    @GetMapping("returnCode")
    public String getCode(@RequestParam String code){
        return code;
    }
}

entity

import lombok.Data;

import java.util.List;

//注册客户端实体类
@Data
public class ClientDemo {

    private String id;
    private String secret;
    private String redirectUri;
    private List<String> scopes;
}

主启动类

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

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

用到的数据表

security框架中的表,如下语句为mysql数据库执行,注意yaml配置的数据库要与实际建表的数据库一致

authorities表

CREATE TABLE `authorities` (
  `username` varchar(50) NOT NULL,
  `authority` varchar(50) NOT NULL,
  UNIQUE KEY `ix_auth_username` (`username`,`authority`),
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

users表

CREATE TABLE `users` (
  `username` varchar(50) NOT NULL,
  `password` varchar(500) NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

oauth2_authorization表

CREATE TABLE `oauth2_authorization` (
  `id` varchar(100) NOT NULL,
  `registered_client_id` varchar(100) NOT NULL,
  `principal_name` varchar(200) NOT NULL,
  `authorization_grant_type` varchar(100) NOT NULL,
  `authorized_scopes` varchar(1000) DEFAULT NULL,
  `attributes` blob,
  `state` varchar(500) DEFAULT NULL,
  `authorization_code_value` blob,
  `authorization_code_issued_at` timestamp NULL DEFAULT NULL,
  `authorization_code_expires_at` timestamp NULL DEFAULT NULL,
  `authorization_code_metadata` blob,
  `access_token_value` blob,
  `access_token_issued_at` timestamp NULL DEFAULT NULL,
  `access_token_expires_at` timestamp NULL DEFAULT NULL,
  `access_token_metadata` blob,
  `access_token_type` varchar(100) DEFAULT NULL,
  `access_token_scopes` varchar(1000) DEFAULT NULL,
  `oidc_id_token_value` blob,
  `oidc_id_token_issued_at` timestamp NULL DEFAULT NULL,
  `oidc_id_token_expires_at` timestamp NULL DEFAULT NULL,
  `oidc_id_token_metadata` blob,
  `refresh_token_value` blob,
  `refresh_token_issued_at` timestamp NULL DEFAULT NULL,
  `refresh_token_expires_at` timestamp NULL DEFAULT NULL,
  `refresh_token_metadata` blob,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

oauth2_authorization_consent表

CREATE TABLE `oauth2_authorization_consent` (
  `registered_client_id` varchar(100) NOT NULL,
  `principal_name` varchar(200) NOT NULL,
  `authorities` varchar(1000) NOT NULL,
  PRIMARY KEY (`registered_client_id`,`principal_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

oauth2_registered_client表

CREATE TABLE `oauth2_registered_client` (
  `id` varchar(100) NOT NULL,
  `client_id` varchar(100) NOT NULL,
  `client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `client_secret` varchar(200) DEFAULT NULL,
  `client_secret_expires_at` timestamp NULL DEFAULT NULL,
  `client_name` varchar(200) NOT NULL,
  `client_authentication_methods` varchar(1000) NOT NULL,
  `authorization_grant_types` varchar(1000) NOT NULL,
  `redirect_uris` varchar(1000) DEFAULT NULL,
  `scopes` varchar(1000) NOT NULL,
  `client_settings` varchar(2000) NOT NULL,
  `token_settings` varchar(2000) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

spring-security-oauth2-authorization-server下,可以看到对应sql

数据表



资源服务搭建

总体预览

资源服务预览

pom

<?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>

    <groupId>org.example</groupId>
    <artifactId>OAuth2-test-resource</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
    </parent>

    <dependencies>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>

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

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

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置类

import org.springframework.context.annotation.Bean;
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.web.SecurityFilterChain;


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

        http
                //所有请求都需要验证
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                //开启jwt,用于令牌解析
                .oauth2ResourceServer(resourceServer -> resourceServer.jwt());

        return http.build();
    }

}

yaml

server:
  port: 8081

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          #项目启动初始化时,在JwtDecoderProviderConfigurationUtils的getConfiguration方法处,发起http://127.0.0.1:8080/.well-known/openid-configuration请求,向授权服务获取元数据端点信息,
          #此项为必须配置,资源服务会根据此地址获取的信息来对token进行验证
          issuer-uri: http://127.0.0.1:8080

          #如果配置了issuer-uri,此项可以不配置。因为通过issuer-uri配置的值,在JwtDecoders的withProviderConfiguration方法中自动获取为http://127.0.0.1:8080/oauth2/jwks
          #jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks

controller

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessagesController {

	@GetMapping("/read/resource")
	@PreAuthorize("hasAuthority('SCOPE_read')")//限制访问资源所需要的权限
	public String getResource1(){
		return "已成功获取资源";
	}

}

主启动类

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

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

}


请求测试

第一步注册用户与客户端

注册用户

三个参数分别是用户名、密码、权限

http://127.0.0.1:8080/register/user?userName=wzy&password=wzy&role=ADMIN

执行:

用户注册成功

查看用户表

用户表



注册客户端

http://127.0.0.1:8080/register/client

请求体json

  • id:客户端id
  • secret:客户端密钥
  • redirectUri:重定向地址,授权服务会将授权码通过此地址返回给客户端
  • scopes:客户端的权限范围
{
	"id": "test-client",
	"secret": "FjKNY8p2&Xw9Lqe$GH7Rd3Bt*5mZ4Pv#CV2sE6J!n",
	"redirectUri": "http://127.0.0.1:8080/register/returnCode",
	"scopes": ["read", "write"]
}

执行请求

客户端注册成功

查看数据库表oauth2_registered_client中是否注册上:

客户端注册表

第二步获取授权码

oauth2/authorize路径为security默认的授权码请求路径

http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=test-client&scope=read&redirect_uri=http://127.0.0.1:8080/register/returnCode

其他参数解释:

  • response_type:表示客户端请求授权码
  • client_id:客户端id
  • scope:客户端权限范围
  • redirect_uri:客户端重定向地址,要与与注册时一致

这些参数会由授权服务获取,来进行授权认证。

浏览器输入上面地址回车,自动跳转如下页面,输入注册的用户名密码点击Sign in

登录页

上面登录后,会再跳转到下面的授权页面,勾选要授予的权限read,然后Submit

授权页面


然后会获得授权码:

授权码

第三步换取token

请求地址

http://127.0.0.1:8080/oauth2/token

需要的参数

  • grant_type:授权模式,此处使用授权码模式,值固定为authorization_code
  • code:上一步返回的授权码
  • redirect_uri:重定向地址,与注册客户端时保持一致
  • client_id:客户端id,表单中可去除参数,不是必须
  • client_secret:客户端密钥,表单中可去除参数,不是必须

token请求请求体

请求头Auth处为必填,因为授权服务要验证客户端身份,类型选Bacis AuthUsername为客户端id,Password为客户端密钥(未加密的)

token请求header

如果你用的是postman,要按如下方式填写:

image-20240821201659492


实际请求中,Auth对应请求头的'Authorization: Basic dGVzdC1jbGllbnQ6RmpLTlk4cDImWHc5THFlJEdIN1JkM0J0KjVtWjRQdiNDVjJzRTZKIW4=',Basic后面是客户端id与密钥经过编码后的值,下面是实际请求展示:

curl --location --request POST 'http://127.0.0.1:8080/oauth2/token' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Authorization: Basic dGVzdC1jbGllbnQ6RmpLTlk4cDImWHc5THFlJEdIN1JkM0J0KjVtWjRQdiNDVjJzRTZKIW4=' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1:8080' \
--header 'Connection: keep-alive' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=ORM8bkef2X1fhvCmrIqzXSwzYwxD-RbD4yzcotRVW36iaLJJMiLLoCe7kbRCWtmMVGCB7ESJAqkBUbSC_zoUL5KXEX63f4Mc1MVTLe_DS-PKpvAwqzYb7Hv1qQ1ftLeZ' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8080/register/returnCode'

点击发送后,返回的access_token即为令牌token

得到令牌

需要注意的是:授权码是一次性的,换取token后,原授权码就会失效,再获取token要使用新的授权码


最后获取资源

地址为资源服务的controller地址

http://127.0.0.1:8081/read/resource

参数如下,token处填的就是上面获取的access_token,发送后返回已成功获取资源,即为成功

请求资源

如果是postman测试工具,按照如下填写:

image-20240821202154474



客户端

关于客户端,其实在上面的请求测试中,我们已经模拟了客户端的操作。

在实际开发中,也存在许多客户端的变体形式,可能是前后端分离的前端项目,也有可能是单独的后端微服务程序,这里以单独的Spring Boot后端程序展示OAuth2 Clinet的使用。

Spring Boot同样提供了OAuth2的客户端集成,在授权码模式下,使用spring-boot-starter-oauth2-client结合@RegisteredOAuth2AuthorizedClient注解,客户端可以自动实现授权码的请求及令牌的获取,而不需要上面的手动请求操作。

总体预览

因为SpringBoot-OAuth2的请求缓存默认使用session实现,本文演示又使用了三个不同端口的服务,所以结合spring-session-data-redis实现会话管理,来达到自动请求的目的。

客户端预览

pom

<?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>

    <groupId>org.example</groupId>
    <artifactId>OAuth2-test-client</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
    </parent>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>

        <!-- 用于客户端向资源服务发起请求 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <!-- 使用redis管理会话,实现不同服务的session共享 -->
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        
    </dependencies>

</project>

yaml

server:
  port: 8082

spring:
  security:
    oauth2:
      client:
        registration:
          #除了重定向地址,此处的客户端各项配置要与授权服务注册客户端的RegisterController中一致
          test-client:
            provider: oauth2server #这里的值可以自定义,需要和下面的issuer-uri上面的一致
            client-id: test-client  #客户端id
            client-secret: FjKNY8p2&Xw9Lqe$GH7Rd3Bt*5mZ4Pv#CV2sE6J!n  #客户端密钥
            client-authentication-method: client_secret_basic  #客户端认证方式
            authorization-grant-type: authorization_code   #客户端支持的授权模式

            #重定向地址,格式在下方注释,这里要修改为:'客户端ip:port/login/oauth2/code/客户端id值'
            redirect-uri: "http://127.0.0.1:8082/login/oauth2/code/test-client"
            #redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"

            scope: read,openid,profile,write #权限

        provider:
          # 配置服务提供地址
          oauth2server:
            # issuer-uri 用于客户端向授权服务获取jwks信息
            issuer-uri: http://127.0.0.1:8080

  #共享session使用redis配置
  redis:
    host: 127.0.0.1
    port: 6379

config

package com.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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;


@EnableWebSecurity
@Configuration
public class OAuth2ClientConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()//所有请求都需要认证
                )

                //security默认情况下使用的oauth2Login配置
                .oauth2Login(Customizer.withDefaults())

                //security默认情况下使用的oauth2Client配置
                .oauth2Client(Customizer.withDefaults())

                //总是开启session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

        return http.build();
    }

}

controller

使用@RegisteredOAuth2AuthorizedClient注解作用:

  1. 当客户端请求资源并经过授权服务的认证后,客户端默认会将认证通过信息保存在内存中;
  2. @RegisteredOAuth2AuthorizedClient会使用Spring MVC的请求参数解析器,将保存的认证信息转为Controller的方法参数OAuth2AuthorizedClient对象;
  3. 最后在Controller中,可以直接从转换的OAuth2AuthorizedClient中取出token,再请求资源

示例如下:

package com.controller;

import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/getResource")
public class ClientController {

    @GetMapping("/getToken")
    public String getToken(@RegisteredOAuth2AuthorizedClient("test-client") OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        return oAuth2AuthorizedClient.getAccessToken().getTokenValue();
    }

    @GetMapping("/read")
    public String getServerARes1(@RegisteredOAuth2AuthorizedClient("test-client") OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        //向资源服务发起请求,获取资源
        return getServer("http://127.0.0.1:8081/read/resource", oAuth2AuthorizedClient);
    }

    @GetMapping("/write")
    public String getServerARes2(@RegisteredOAuth2AuthorizedClient("test-client") OAuth2AuthorizedClient oAuth2AuthorizedClient) {
        //向资源服务发起请求,获取资源
        return getServer("http://127.0.0.1:8081/write/resource", oAuth2AuthorizedClient);
    }


    /**
     * 获取token,请求资源服务
     */
    private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) {

        // 获取 access_token
        String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();

        // 发起请求
        Mono<String> stringMono = WebClient.builder()
                .defaultHeader("Authorization", "Bearer " + tokenValue) 
                .build()
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);

        return stringMono.block();
    }
}

上面多了个write请求,将资源服务项目示例的Controller改为如下即可

package com.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessagesController {

	@GetMapping("/read/resource")
	@PreAuthorize("hasAuthority('SCOPE_read')")//限制访问资源所需要的权限
	public String getResource1(){
		return "已成功获取资源";
	}

	@GetMapping("/write/resource")
	@PreAuthorize("hasAuthority('SCOPE_write')")//限制访问资源所需要的权限
	public String getResource2(){
		return "已成功获取资源2";
	}

}

主启动类

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

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

数据表修改

需要将oauth2_registered_client表中,注册客户端的重定向地址改为yaml中配置的地址,即:http://127.0.0.1:8082/login/oauth2/code/test-client

如果不使用此地址,授权服务无法将授权码自动返回给客户端

数据库改重定向地址

为了展示新的效果,清空oauth2_authorizationoauth2_authorization_consent两张表的数据

客户端测试

先启动redis,然后是授权服务,最后启动资源服务与客户端。

浏览器发起如下请求,来请求资源:

http://127.0.0.1:8082/getResource/read

127.0.0.1:8082/getResource/read是客户端向资源服务请求资源的地址,发起请求后,因为没有权限,会被客户端过滤器拦截(第一次请求还没有保存认证信息在上下文中)。

拦截后,客户端会向授权服务发起授权码请求,然后授权服务会要求用户登录授权,所以返回的是127.0.0.1:8080/login的登录页面:

登录页123

进行登录后,来到授权页面:

123

下面是图中授权页面的地址栏信息,包含客户端id、权限范围、state、重定向地址等参数:

http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=test-client&scope=read%20openid%20profile%20write&state=M43A6Pvs7ce-a98FigCQSsvPWkp4Rhs3bgr9xQLdKsE%3D&redirect_uri=http://127.0.0.1:8082/login/oauth2/code/test-client&nonce=Y3U-NfewZBtI6P24GtRRwHuzvUx-3ZfVGh6a_jh9Rys

然后勾选三个权限,并点击submit提交。

提交后,会直接跳转到资源。授权码的获取、交换令牌、携带令牌访问资源的过程,已经由框架为我们自动实现

读资源


在发起另一个资源的访问请求:

http://127.0.0.1:8082/getResource/write

因为已经登录授权过,所以第二个资源会直接返回:

写资源

通过请求测试可以发现,进行登录授权后,浏览器直接访问到了资源,而无需再手动进行授权码及token部分的操作,实现了客户端自动获取资源的效果。

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

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

相关文章

汽车服务管理系统 _od8kr

TOC springboot580汽车服务管理系统 _od8kr--论文 系统概述 该系统由个人管理员和员工管理&#xff0c;用户三部分组成。其中&#xff1a;用户进入系统首页可以实现首页&#xff0c;热销汽车&#xff0c;汽车配件&#xff0c;汽车资讯&#xff0c;后台管理&#xff0c;在线客…

TCP端口范围

ip_local_port_range sysctl -a | grep ip_local_port_range | head 默认情况下&#xff0c;net.ipv4.ip_local_port_range的默认值为32768-60999。这意味着本地应用程序可以使用的端口号范围为32768到60999。 sysctl -a | grep net.ipv4.ip_local_reserved_ports |head …

光伏检测气象站:实时监测:高效管理

随着全球对可再生能源需求的日益增长&#xff0c;光伏发电作为清洁能源的重要组成部分&#xff0c;其重要性日益凸显。然而&#xff0c;光伏发电的效率与稳定性受气象条件影响显著&#xff0c;如光照强度、温度、湿度、风速等因素均能直接影响光伏板的发电效率。因此&#xff0…

宠物空气净化器是智商税吗?希喂、范罗士热门产品真实性能测试

宠物空气净化器作为宠物领域的新产品&#xff0c;凭借自身独特的功能受到铲屎官们的喜爱&#xff0c;越来越多的商家关注到这个市场。然而&#xff0c;市面上品牌逐渐增多&#xff0c;质量却参差不齐&#xff0c;一些不良商家以次充好&#xff0c;容易让消费者陷入消费陷阱。 …

Codeforces Round 961 D. Cases 【SOS DP、思维】

D. Cases 题意 有一个长度为 n n n 且仅由前 c c c 个大写字母组成的字符串&#xff0c;问最少选取多少种字母为每个单词的结尾&#xff0c;使得每个单词长度不超过 k k k 思路 首先注意到最后一个字母一定要选择&#xff0c;接下来我们给出一个断言&#xff1a;如果一个…

Fx - day3 - 沙盒/更改集/互联更改集/配置包

Fxiaoke - day3 - 沙盒/更改集/互联更改集/配置包 学习目标&#xff1a;熟悉 沙盒&#xff0c;更改集&#xff0c;配置包&#xff0c;互联更改集 的概念以及使用场景 0、前言 沙盒理解 很多时候我们可能需要一个沙盒环境&#xff0c;什么是沙盒环境&#xff1f; 沙盒环境&…

如何打造Java SpringBoot私房菜定制上门服务系统,实现个性化餐饮体验?

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

惠海H4312 dcdc同步整流降压恒压IC 30V 40V转3.3V/5V/12V小体积大电流单片机供电

1.产品描述 H4312是一种内置30V耐压MOS&#xff0c;并且能够实现精确恒压以及恒流的同步降压型 DC-DC 转换器: 支持 3.1A 持续输出电流输出电压可调&#xff0c;最大可支持 100%占空比;通过调节FB 端口的分压电阻&#xff0c;可以输出2.5V到 24V的稳定电压。 H4312 采用高端…

【脏数据 bug 解决】ValueError: mean must have 1 elements if it is an iterable, got 3

问题描述&#xff1a; 在训练模型的过程中&#xff0c;出现 clip_image_processor 无法处理数据的问题&#xff0c;说明数据集中很可能出现了脏数据。本文使用的数据为 LAION-Aesthetics-V2-6.5plus&#xff0c;从 https://dagshub.com/DagsHub-Datasets/LAION-Aesthetics-V2-…

21、springboot3 vue3开发平台-前端-自定义树形穿梭框,用于角色权限分配

文章目录 1. 使用原因2. 实现3. 使用 1. 使用原因 elemenutplus 有穿梭框&#xff0c;但是不支持树状数据的操作&#xff0c;所以这里自定义树状穿梭框&#xff0c;用于菜单权限分配&#xff0c; 如下&#xff1a; 2. 实现 这里主要是将菜单列表树解构后添加修改组合再恢复…

STM32H7双路CAN踩坑记录

STM32H7双路CAN踩坑记录 目录 STM32H7双路CAN踩坑记录1 问题描述2 原因分析3 解决办法4 CAN配置参考代码 1 问题描述 STM32的CAN1和CAN2无法同时使用。 注&#xff1a;MCU使用的是STM32H743&#xff0c;其他型号不确定是否一样&#xff0c;本文只以STM32H743举例说明。 2 原因…

odoo17 网站内容存在哪了

odoo17 网站内容存在哪了 查数据库内容&#xff0c;却没找到 没理解这些内容到底存在了哪里呢

图文详解ThreadLocal:原理、结构与内存泄漏解析

目录 一.什么是ThreadLocal 二.ThreadLocal的内部结构 三.ThreadLocal带来的内存泄露问题 ▐ key强引用 ▐ key弱引用 总结 一.什么是ThreadLocal 在Java中&#xff0c;ThreadLocal 类提供了一种方式&#xff0c;使得每个线程可以独立地持有自己的变量副本&#xff0c;而…

「黑神话:悟空」狂销 15 亿!高清游戏录制神器助你称霸

短短一天时间 《黑神话&#xff1a;悟空》在Steam上已售出超过300万份 加上wegame、epic和ps平台 目前总销量超过450万份&#xff0c;总销售额超过15亿元。 根据Steam平台实时数据 8月20日晚间20点30分 该平台《黑神话&#xff1a;悟空》同时在线玩家人数突破200万 达到2…

手机怎么把百度网盘里的文件打印出来?

在日常生活中&#xff0c;我们常常需要打印各种文档&#xff0c;比如合同、报告或是学习资料。有时这些文件存储在网盘中&#xff0c;比如百度网盘&#xff0c;这时候如果能够直接从网盘中打印出来&#xff0c;将会极大地提高效率。今天&#xff0c;就让我们来了解一下如何使用…

PL3366C 用0.1+的芯片做过认证5V1A电源

PL3366C是一款原边反激式5W开关电源芯片恒流/恒压电源。PL3366C高度集成了功率开关&#xff0c;无需光耦以及次级控制电路&#xff0c;PL3366C的复合模式的应用使芯片能够实现低静态功耗、低音频噪音、高效率。满载时PL3366C工作在PFM模式&#xff0c;随着负载降低&#xff0c;…

I2C学习:上拉电阻选取

一&#xff0e;I2C简介 I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。I2C总线在使用时&#xff0c;需要接上拉电阻&#xff0c;这是因为I2C接口是开漏输出&#xff0c;如图1所示。 图1 I2C开漏输出 I2C有5种速度模式&#xff1a;标准&#xff08;100KHz&am…

日志文件的理解

前言 说实在话我一直对于日志不太理解&#xff0c;感觉这词说的这么高大上&#xff0c;不好理解&#xff0c;甚至还有点畏惧这个东西&#xff0c;所以专门去研究了下&#xff0c;最后发现这家伙不就是输出信息嘛&#xff0c;就像C语言中printf输出的信息&#xff0c;C中cout输…

【电子通识】开关上的“|”和“0”到底哪个是开?哪个是关?

有的电器、灯具和插座上带有电源开关&#xff0c;开关上会出现“|”和“O”两个符号。如下所示船型开关上就有“|”和“0”。 也有开关用ON/OFF代表开闭。 如果只看符号判断“|”和“O”到底代表什么含义呢&#xff1f;你又能分清哪个是电路连通&#xff0c;哪个是电路断开…

05.震动控制继电器开关

首先先知道控制器的原理 通过继电器来控制电路&#xff0c;比如智能插座&#xff0c;比如 220V 的灯&#xff0c;比如我们项目不带开关的傻瓜式报警器 当设置继电器为低电平触发时&#xff0c; STC89C52RC 的 IO 输出 低电平&#xff0c;就会导致 COM口和NO口闭合 &#xff0c…