Spring Boot整合Spring Authorization Server(四)——整合网关和资源服务,添加Redis,前后端分离

前后端分离Oauth2.0 - springsecurity + spring-authorization-server —序言 前后端分离的Oauth2.0实践-序言 阅读详情
AI摘要

本文详细介绍如何在Spring Boot项目中整合OAuth2实现前后端分离的权限管理,包括客户端和服务端的配置步骤。

前言

之前写了认证服务,实际生产中都是前后端分离的项目,现在来搭建客户端和资源服务器。

代码

客户端代码

1. 新建module

2. 添加TestController

/**
 * 测试接口
 *
 * @author lxq
 */
@RestController
public class TestController {

    @GetMapping("/test01")
    @PreAuthorize("hasAuthority('message.read')")
    public String test01() {
        Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        return "test01";
    }

    @GetMapping("/test02")
    @PreAuthorize("hasAuthority('SCOPE_message.write')")
    public String test02() {
        return "test02";
    }

    @GetMapping("/app")
    @PreAuthorize("hasAuthority('app')")
    public String app() {
        return "app";
    }

}

2. 添加application.yml

server:
  # 修改端口
  port: 8000

spring:
  security:
    oauth2:
      client:
        provider:
          # 认证提供者,自定义名称
          custom-issuer:
            # Token签发地址(认证服务地址)
            issuer-uri: http://c.example.com:8080
            # 获取用户信息的地址,默认的/userinfo端点需要IdToken获取,为避免麻烦自定一个用户信息接口
            user-info-uri: ${spring.security.oauth2.client.provider.custom-issuer.issuer-uri}/user
        registration:
          messaging-client-oidc:
            # oauth认证提供者配置,和上边配置的认证提供者关联起来
            provider: custom-issuer
            # 客户端名称,自定义
            client-name: message-client
            # 客户端id,从认证服务申请的客户端id
            client-id: messaging-client
            # 客户端秘钥
            client-secret: 123456
            # 客户端认证方式
            client-authentication-method: client_secret_basic
            # 获取Token使用的授权流程
            authorization-grant-type: authorization_code
            # 回调地址,这里设置为Spring Security Client默认实现使用code换取token的接口
            redirect-uri: http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc
            scope:
              - message.read
              - message.write

3. pom.xml

<dependencies>
        <!-- 引入bootstrap依赖,不引入这个依赖是无法使用bootstrap配置文件的 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</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-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

客户端的代码相对比较简单,spring boot 帮我们封装好了。

资源端代码

1. ResourceServerConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> {
                    // 下边一行是放行接口的配置,被放行的接口上不能有权限注解,e.g. @PreAuthorize,否则无效
                    // .requestMatchers("/test02").permitAll()
                    authorize.anyRequest().authenticated();
                })
                .oauth2ResourceServer(oauth2 -> {
                    oauth2
                            // 可在此处添加自定义解析设置
                            .jwt(Customizer.withDefaults())
                            // 添加未携带token和权限不足异常处理
                            .accessDeniedHandler(SecurityUtils::exceptionHandler)
                            .authenticationEntryPoint(SecurityUtils::exceptionHandler);
                });
        return http.build();
    }

    /**
     * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key
     * 添加自定义解析token配置,注入一个JwtAuthenticationConverter
     *
     * @return jwt解析器 JwtAuthenticationConverter
     */
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 设置解析权限信息的前缀,设置为空是去掉前缀
        grantedAuthoritiesConverter.setAuthorityPrefix("");
        // 设置权限信息在jwt claims中的key
        grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

}

2. SecurityUtils

public class SecurityUtils {

    private SecurityUtils() {
        // 禁止实例化工具类
        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
    }

    /**
     * 认证与鉴权失败回调
     *
     * @param request  当前请求
     * @param response 当前响应
     * @param e        具体的异常信息
     */
    public static void exceptionHandler(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        Map<String, String> parameters = getErrorParameter(request, response, e);
        String wwwAuthenticate = computeWwwAuthenticateHeaderValue(parameters);
        response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
        try {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write(JsonUtils.objectCovertToJson(parameters));
            response.getWriter().flush();
        } catch (IOException ex) {
            log.error("写回错误信息失败", e);
        }
    }

    /**
     * 获取异常信息map
     *
     * @param request  当前请求
     * @param response 当前响应
     * @param e        本次异常具体的异常实例
     * @return 异常信息map
     */
    private static Map<String, String> getErrorParameter(HttpServletRequest request, HttpServletResponse response, Throwable e) {
        Map<String, String> parameters = new LinkedHashMap<>();
        if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
            // 权限不足
            parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
            parameters.put("error_description",
                    "The request requires higher privileges than provided by the access token.");
            parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
            response.setStatus(HttpStatus.FORBIDDEN.value());
        }
        if (e instanceof OAuth2AuthenticationException authenticationException) {
            // jwt异常,e.g. jwt超过有效期、jwt无效等
            OAuth2Error error = authenticationException.getError();
            parameters.put("error", error.getErrorCode());
            if (StringUtils.hasText(error.getUri())) {
                parameters.put("error_uri", error.getUri());
            }
            if (StringUtils.hasText(error.getDescription())) {
                parameters.put("error_description", error.getDescription());
            }
            if (error instanceof BearerTokenError bearerTokenError) {
                if (StringUtils.hasText(bearerTokenError.getScope())) {
                    parameters.put("scope", bearerTokenError.getScope());
                }
                response.setStatus(bearerTokenError.getHttpStatus().value());
            }
        }
        if (e instanceof InsufficientAuthenticationException) {
            // 没有携带jwt访问接口,没有客户端认证信息
            parameters.put("error", BearerTokenErrorCodes.INVALID_TOKEN);
            parameters.put("error_description", "Not authorized.");
            parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }
        parameters.put("message", e.getMessage());
        return parameters;
    }

    /**
     * 生成放入请求头的错误信息
     *
     * @param parameters 参数
     * @return 字符串
     */
    public static String computeWwwAuthenticateHeaderValue(Map<String, String> parameters) {
        StringBuilder wwwAuthenticate = new StringBuilder();
        wwwAuthenticate.append("Bearer");
        if (!parameters.isEmpty()) {
            wwwAuthenticate.append(" ");
            int i = 0;
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
                if (i != parameters.size() - 1) {
                    wwwAuthenticate.append(", ");
                }
                i++;
            }
        }
        return wwwAuthenticate.toString();
    }
}

3. JsonUtils

@Slf4j
public class JsonUtils {

    private JsonUtils() {
        // 禁止实例化工具类
        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
    }

    private final static ObjectMapper MAPPER = new ObjectMapper();

    static {
        // 对象的所有字段全部列入,还是其他的选项,可以忽略null等
        MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
        // 取消默认的时间转换为timeStamp格式
        MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        // 设置Date类型的序列化及反序列化格式
        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 忽略空Bean转json的错误
        MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * json字符串转为对象
     *
     * @param json  json
     * @param clazz T类的class文件
     * @param <T>   泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, Class<T> clazz) {
        if (json == null || clazz == null) {
            return null;
        }
        try {
            return MAPPER.readValue(json, clazz);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * json字符串转为对象
     *
     * @param json json
     * @param type 对象在Jackson中的类型
     * @param <T>  泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, TypeReference<T> type) {
        if (json == null || type == null) {
            return null;
        }
        try {
            return MAPPER.readValue(json, type);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 将流中的数据转为java对象
     *
     * @param inputStream 输入流
     * @param clazz       类的class
     * @param <T>         泛型, 代表返回参数的类型
     * @return 返回对象 如果参数任意一个为 null则返回null
     */
    public static <T> T covertStreamToObject(InputStream inputStream, Class<T> clazz) {
        if (inputStream == null || clazz == null) {
            return null;
        }
        try {
            return MAPPER.readValue(inputStream, clazz);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * json字符串转为复杂类型List
     *
     * @param json            json
     * @param collectionClazz 集合的class
     * @param elementsClazz   集合中泛型的class
     * @param <T>             泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T jsonCovertToObject(String json, Class<?> collectionClazz, Class<?>... elementsClazz) {
        if (json == null || collectionClazz == null || elementsClazz == null) {
            return null;
        }
        try {
            JavaType javaType = MAPPER.getTypeFactory().constructParametricType(collectionClazz, elementsClazz);
            return MAPPER.readValue(json, javaType);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 对象转为json字符串
     *
     * @param o 将要转化的对象
     * @return 返回json字符串
     */
    public static String objectCovertToJson(Object o) {
        if (o == null) {
            return null;
        }
        try {
            return o instanceof String ? (String) o : MAPPER.writeValueAsString(o);
        } catch (IOException e) {
            log.error("json转换失败,原因:", e);
        }
        return null;
    }

    /**
     * 将对象转为另一个对象
     * 切记,两个对象结构要一致
     * 多用于Object转为具体的对象
     *
     * @param o               将要转化的对象
     * @param collectionClazz 集合的class
     * @param elementsClazz   集合中泛型的class
     * @param <T>             泛型, 代表返回参数的类型
     * @return 返回T的实例
     */
    public static <T> T objectCovertToObject(Object o, Class<?> collectionClazz, Class<?>... elementsClazz) {
        String json = objectCovertToJson(o);
        return jsonCovertToObject(json, collectionClazz, elementsClazz);
    }

}

4. TestController

@RestController
public class TestController {

    @GetMapping("/test01")
    @PreAuthorize("hasAuthority('SCOPE_message.read')")
    public String test01() {
        return "test01";
    }

    @GetMapping("/test02")
    @PreAuthorize("hasAuthority('SCOPE_message.write')")
    public String test02() {
        return "test02";
    }

    @GetMapping("/app")
    @PreAuthorize("hasAuthority('app')")
    public String app() {
        return "app";
    }

}

5. application.yml

server:
  # 设置资源服务器端口
  port: 8001

spring:
  security:
    oauth2:
      # 资源服务器配置
      resourceserver:
        jwt:
          # Jwt中claims的iss属性,也就是jwt的签发地址,即认证服务器的根路径
          # 资源服务器会进一步的配置,通过该地址获取公钥以解析jwt
          issuer-uri: http://c.example.com:8080

6. pom.xml

<dependencies>
        <!-- 引入bootstrap依赖,不引入这个依赖是无法使用bootstrap配置文件的 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

资源服务器要配置认证服务器的地址,当客户端携带token访问资源服务器会调用认证服务器解析token

认证服务代码

1. 修改AuthorizationConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {

    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    private final RedisSecurityContextRepository redisSecurityContextRepository;

    private final RedisOperator<String> redisOperator;

    /**
     * 生成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;
    }

    /**
     * 配置端点的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http
            , RegisteredClientRepository registeredClientRepository
            , AuthorizationServerSettings authorizationServerSettings) throws Exception {
        // OAuth2 security配置 默认的设置,忽略认证端点的csrf校验
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

       

        // 新建设备码converter和provider
        DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
                new DeviceClientAuthenticationConverter(
                        authorizationServerSettings.getDeviceAuthorizationEndpoint());
        DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
                new DeviceClientAuthenticationProvider(registeredClientRepository);

        // 使用redis存储、读取登录的认证信息
        http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 开启OpenID Connect 1.0协议相关端点
                .oidc(Customizer.withDefaults())
                // 设置自定义用户确认授权页
                .authorizationEndpoint(authorizationEndpoint -> {
                    System.out.println("process trace | authorizationEndpoint.consentPage(" + CUSTOM_CONSENT_PAGE_URI + ")");
                    authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI);
                })
                // 设置设备码用户验证url(自定义用户验证页)
                .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> {
                    deviceAuthorizationEndpoint.verificationUri("/activate");
                })
                // 设置验证设备码用户确认页面
                .deviceVerificationEndpoint(deviceVerificationEndpoint -> {
                    deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI);
                })
                // 客户端认证添加设备码的converter和provider
                .clientAuthentication(clientAuthentication -> {
                    clientAuthentication
                            .authenticationConverter(deviceClientAuthenticationConverter)
                            .authenticationProvider(deviceClientAuthenticationProvider);
                });
        http
                // 当未登录时访问认证端点时重定向至login页面
                .exceptionHandling((exceptions) -> {
                    System.out.println("process trace | exceptions.defaultAuthenticationEntryPointFor(/login)");
                    // 下面是旧的处理,上面是新的处理 使用自定义的未登录处理,并设置登录地址为前端的登录地址
                    exceptions.defaultAuthenticationEntryPointFor(
                            new LoginTargetAuthenticationEntryPoint("http://127.0.0.1:3000")
                            , new MediaTypeRequestMatcher(MediaType.TEXT_HTML));
                    /*exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login")
                            , new MediaTypeRequestMatcher(MediaType.TEXT_HTML));*/
                })
                // 处理使用access token访问用户信息端点和客户端注册端点
                .oauth2ResourceServer((resourceServer) -> {
                    System.out.println("process trace | resourceServer.jwt(Customizer.withDefaults())");
                    resourceServer.jwt(Customizer.withDefaults());
                });

        // 自定义短信认证登录转换器
        SmsCaptchaGrantAuthenticationConverter converter = new SmsCaptchaGrantAuthenticationConverter();
        // 自定义短信认证登录认证提供
        SmsCaptchaGrantAuthenticationProvider provider = new SmsCaptchaGrantAuthenticationProvider();
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 让认证服务器元数据中有自定义的认证方式
                .authorizationServerMetadataEndpoint(metadata -> {
                    metadata.authorizationServerMetadataCustomizer(customizer -> {
                        customizer.grantType(SecurityConstants.GRANT_TYPE_SMS_CODE);
                    });
                })
                // 添加自定义grant_type——短信认证登录
                .tokenEndpoint(tokenEndpoint -> tokenEndpoint
                        .accessTokenRequestConverter(converter)
                        .authenticationProvider(provider));

        DefaultSecurityFilterChain build = http.build();

        // 从框架中获取provider中所需的bean
        OAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
        // 以上三个bean在build()方法之后调用是因为调用build方法时框架会尝试获取这些类,
        // 如果获取不到则初始化一个实例放入SharedObject中,所以要在build方法调用之后获取
        // 在通过set方法设置进provider中,但是如果在build方法之后调用authenticationProvider(provider)
        // 框架会提示unsupported_grant_type,因为已经初始化完了,在添加就不会生效了
        provider.setTokenGenerator(tokenGenerator);
        provider.setAuthorizationService(authorizationService);
        provider.setAuthenticationManager(authenticationManager);

        return build;
    }

    /**
     * 配置认证相关的过滤器链
     *
     * @param http spring security核心配置类
     * @return 过滤器链
     * @throws Exception 抛出
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // 添加跨域过滤器
        http.addFilter(corsFilter());
        // 禁用 csrf 与 cors
        http.csrf(AbstractHttpConfigurer::disable);
        http.cors(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests((authorize) -> {
                    System.out.println("process trace | authorizeHttpRequests config");
                    authorize
                            .requestMatchers("/assets/**", "/webjars/**", "/login", "/getCaptcha"
                                    , "/getSmsCaptcha").permitAll()
                            .anyRequest().authenticated();
                })
                // 指定登录页面 这里一定不能注释掉 不然页面请求不到post的login
                .formLogin(formLogin -> {
                    System.out.println("process trace | formLogin.loginPage(\"/login\")");
                    formLogin.loginPage("/login")
                            // 登录成功和失败改为写回json,不重定向了
                            .successHandler(new LoginSuccessHandler())
                            .failureHandler(new LoginFailureHandler());
                });
        // 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的token
        http.oauth2ResourceServer((resourceServer) -> {
            resourceServer
                    .jwt(Customizer.withDefaults())
                    .accessDeniedHandler(SecurityUtils::exceptionHandler)
                    .authenticationEntryPoint(SecurityUtils::exceptionHandler);
        });

        http
                // 当未登录时访问认证端点时重定向至login页面
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginTargetAuthenticationEntryPoint("http://127.0.0.1:3000"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                );

        // 使用redis存储、读取登录的认证信息
        http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));

        // 在UsernamePasswordAuthenticationFilter拦截器之前添加验证码校验拦截器,并拦截POST的登录接口
        // http.addFilterBefore(new CaptchaAuthenticationFilter("/login"), UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }

    /**
     * 跨域过滤器配置
     *
     * @return CorsFilter
     */
    @Bean
    public CorsFilter corsFilter() {

        // 初始化cors配置对象
        CorsConfiguration configuration = new CorsConfiguration();

        // 设置跨域访问可以携带cookie 出现该问题是跨域问题,当allowCredentials为true时,allowingOrigins不能包含特殊值“ *”,
        // 因为无法在“ Access-Control-Allow-Origin”响应标头上设置。
        //处理方法是:首先找打你后端跨域设置文件,将方法中的addAllowedOrigin(““)替换成addAllowedOriginPattern(””)
        configuration.setAllowCredentials(true);
        // 设置允许跨域的域名,如果允许携带cookie的话,路径就不能写*号, *表示所有的域名都可以跨域访问
        configuration.addAllowedOriginPattern("http://127.0.0.1:8080");
        configuration.addAllowedOriginPattern("http://c.example.com:8080");
        configuration.addAllowedOriginPattern("http://127.0.0.1:3000");
        // 允许所有的请求方法 ==> GET POST PUT Delete
        configuration.addAllowedMethod("*");
        // 允许携带任何头信息
        configuration.addAllowedHeader("*");

        // 初始化cors配置源对象
        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();

        // 给配置源对象设置过滤的参数
        // 参数一: 过滤的路径 == > 所有的路径都要求校验是否跨域
        // 参数二: 配置类
        configurationSource.registerCorsConfiguration("/**", configuration);

        // 返回配置好的过滤器
        return new CorsFilter(configurationSource);
    }


    /**
     * 自定义jwt,将权限信息放至jwt中
     *
     * @return OAuth2TokenCustomizer的实例
     */
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
        return context -> {
            // 检查登录用户信息是不是UserDetails,排除掉没有用户参与的流程
            if (context.getPrincipal().getPrincipal() instanceof UserDetails user) {
                // 获取申请的scopes
                Set<String> scopes = context.getAuthorizedScopes();
                // 获取用户的权限
                Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
                // 提取权限并转为字符串
                Set<String> authoritySet = Optional.ofNullable(authorities).orElse(Collections.emptyList()).stream()
                        // 获取权限字符串
                        .map(GrantedAuthority::getAuthority)
                        // 去重
                        .collect(Collectors.toSet());

                // 合并scope与用户信息
                authoritySet.addAll(scopes);

                JwtClaimsSet.Builder claims = context.getClaims();
                // 将权限信息放入jwt的claims中(也可以生成一个以指定字符分割的字符串放入)
                claims.claim("authorities", authoritySet);
                // 放入其它自定内容
                // 角色、头像...
            }
        };
    }

    /**
     * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key
     *
     * @return jwt解析器 JwtAuthenticationConverter
     */
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 设置解析权限信息的前缀,设置为空是去掉前缀
        grantedAuthoritiesConverter.setAuthorityPrefix("");
        // 设置权限信息在jwt claims中的key
        grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

    /**
     * 配置密码解析器,使用BCrypt的方式对密码进行加密和验证
     *
     * @return BCryptPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置客户端Repository
     *
     * @param jdbcTemplate    db 数据源信息
     * @param passwordEncoder 密码解析器
     * @return 基于数据库的repository
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate
            , PasswordEncoder passwordEncoder) {
        System.out.println("password encode : " + passwordEncoder.encode("123456"));
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id
                .clientId("messaging-client")
                // 客户端秘钥,使用密码解析器加密
                .clientSecret(passwordEncoder.encode("123456"))
                // 客户端认证方式,基于请求头的认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 配置资源服务器使用该客户端获取授权时支持的方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // .authorizationGrantType(SecurityConstants.GRANT_TYPE_SMS_CODE)
                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
                // 这里每次启动是不会该数据库的,所以改了这个链接要相应的改一下数据库
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
                .redirectUri("https://www.baidu.com/")
                .redirectUri("http://127.0.0.1:8080/notify/oauth2/code")
                // 该客户端的授权范围,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();

        // 基于db存储客户端,还有一个基于内存的实现 InMemoryRegisteredClientRepository
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        // 初始化客户端
        RegisteredClient repositoryByClientId =
                registeredClientRepository.findByClientId(registeredClient.getClientId());
        if (repositoryByClientId == null) {
            registeredClientRepository.save(registeredClient);
        }

        // 设备码授权客户端
        RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("device-message-client")
                // 公共客户端
                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                // 设备码授权
                .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                .build();
        RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());
        if (byClientId == null) {
            registeredClientRepository.save(deviceClient);
        }

        // PKCE客户端
        RegisteredClient pkceClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("pkce-message-client")
                // 公共客户端
                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                // 设备码授权
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
                .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
                .clientSettings(ClientSettings.builder().requireProofKey(Boolean.TRUE).build())
                // 自定scope
                .scope("message.read")
                .scope("message.write")
                .build();
        RegisteredClient findPkceClient = registeredClientRepository.findByClientId(pkceClient.getClientId());
        if (findPkceClient == null) {
            registeredClientRepository.save(pkceClient);
        }
        return registeredClientRepository;
    }

    /**
     * 配置基于db的oauth2的授权管理服务
     *
     * @param jdbcTemplate               db数据源信息
     * @param registeredClientRepository 上边注入的客户端repository
     * @return JdbcOAuth2AuthorizationService
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                           RegisteredClientRepository registeredClientRepository) {
        // 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置基于db的授权确认管理服务
     *
     * @param jdbcTemplate               db数据源信息
     * @param registeredClientRepository 客户端repository
     * @return JdbcOAuth2AuthorizationConsentService
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
                                                                         RegisteredClientRepository registeredClientRepository) {
        // 基于db的授权确认管理服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationConsentService
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
     * 在配置JWKSource之前先从redis中尝试获取一下,获取不到就生成并存入redis;获取到直接解析并生成一个JWKSet
     *
     * @return JWKSource
     */
    @Bean
    @SneakyThrows
    public JWKSource<SecurityContext> jwkSource() {
        // 先从redis获取
        String jwkSetCache = redisOperator.get(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY);
        if (StringUtils.isEmpty(jwkSetCache)) {
            KeyPair keyPair = generateRsaKey();
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
            RSAKey rsaKey = new RSAKey.Builder(publicKey)
                    .privateKey(privateKey)
                    .keyID(UUID.randomUUID().toString())
                    .build();
            // 生成jws
            JWKSet jwkSet = new JWKSet(rsaKey);
            // 转为json字符串
            String jwkSetString = jwkSet.toString(Boolean.FALSE);
            // 存入redis
            redisOperator.set(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY, jwkSetString);
            return new ImmutableJWKSet<>(jwkSet);
        }
        // 解析存储的jws
        JWKSet jwkSet = JWKSet.parse(jwkSetCache);
        return new ImmutableJWKSet<>(jwkSet);
    }


    /**
     * 配置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()
                /*
                    设置token签发地址(http(s)://{ip}:{port}/context-path, http(s)://domain.com/context-path)
                    如果需要通过ip访问这里就是ip,如果是有域名映射就填域名,通过什么方式访问该服务这里就填什么
                    签发信息是调用资源服务器时能够使资源服务器识别的
                 */
                .issuer("http://c.example.com:8080")
                .build();
    }


    /**
     * 先暂时配置一个基于内存的用户,框架在用户认证时会默认调用
     * {@link UserDetailsService#loadUserByUsername(String)} 方法根据
     * 账号查询用户信息,一般是重写该方法实现自己的逻辑
     *
     * @param passwordEncoder 密码解析器
     * @return UserDetailsService
     */
    /*@Bean
    public UserDetailsService users(PasswordEncoder passwordEncoder) {
        UserDetails user = User.withUsername("admin1")
                .password(passwordEncoder.encode("123456"))
                .roles("admin", "normal", "unAuthentication")
                .authorities("app", "web", "/test2", "/test3")
                .build();
        return new InMemoryUserDetailsManager(user);
    }*/

}
  1. 去掉了内存获取用户信息改为mysql
  2. 修改签发地址
  3. 新增登录成功失败处理器
  4. session存入redis
  5. 自定义跳转前端登录页面(前后端分离)
  6. 新增跨域Filter
  7. 新增模拟短信登录

内容比较多,比较复杂,需要好好理解梳理!!!

测试

访问http://127.0.0.1:8000/app(客户端接口)跳转到认证登录页面

这个时候服务器不会跳转,因为现在有了成功失败处理器都只会返回json,需浏览器再调一下记得携带cookie

剩下的晚点更新!!!!

Spring Authorization Server基于Spring Session的前后端分离实现 在处理类内添加了前后端分离和不分离的适配,根据相关地址的配置自适应。在重定向至登录页面时将当前请求的请求路径挂在登录页面的target参数中,前端登录成功后直接跳转至target中挂的url。在包下添加类。/*** 重定向至登录处理*/@Slf4j/*** URL.*/@Override// 兼容设备码前后端分离// 如果是请求验证设备激活码(user_code)时未登录并且设备码验证页面是前后端分离的那种则写回json。 阅读详情

相关推荐

Spring Boot整合OAuth2:从授权协议到微服务安全实战

在现代应用开发中,授权是保障系统安全与用户体验的核心环节。OAuth2作为一种行业标准的授权协议,其核心原理在于通过令牌(Token)机制,允许第三方应用在用户授权后访问其资源,而无需获取用户密码,从而实现了认证与授权的安全解耦。这一机制的技术价值在于提供了安全、标准化的授权流程,极大地简化了多平台登录、API资源访问等场景的开发复杂度。其典型的应用场景包括第三方登录(如微信、GitHub登录)、微服务间的安全通信以及构建统一单点登录(SSO)系统。本文聚焦于如何在Spring Boot生态中高效整合OAu

weixin_34275734的博客 496

Spring Authorization Server入门 (十二) 实现授权码模式使用前后端分离的登录页面

今天的主题就是使用单独部署的登录页面替换认证服务器默认的登录页面(前后端分离时使用前端的登录页面),目前在网上能搜到的很多都是理论,没有很好的一个示例,我就按照我自己的想法写了一个实现,给大家提供一个思路,如果有什么问题或者更好的想法可以在评论区提出,谢谢。

云逸的博客 1万+

Spring Boot + Vue + Shiro 整合实战:构建企业级前后端分离应用

前后端分离架构是现代Web应用开发的主流模式,其核心在于通过RESTful API实现前后端职责解耦,后端专注于数据与业务逻辑,前端负责交互与展示。这种模式基于无状态通信原理,能有效支持并行开发与技术栈独立演进,其技术价值在于提升开发效率、增强系统可维护性与可扩展性。在实际工程实践中,该架构广泛应用于中后台管理系统、SaaS平台等需要清晰权限控制与高效协作的场景。本文聚焦于整合Spring Boot、Vue.js与Apache Shiro这一经典技术栈,深入探讨如何实现基于JWT的无状态认证与授权,并解决跨

weixin_33912453的博客 496

前后端分离Oauth2.0 - springsecurity + spring-authorization-server —授权码模式

前后端分离的Oauth2.0实践-授权码模式

qjyn1314的博客 1万+

微服务安全实战:基于Spring Cloud Alibaba与OAuth 2.0构建统一认证授权体系

在分布式微服务架构中,服务间安全认证与授权是保障系统安全的核心挑战。OAuth 2.0作为工业级授权框架,通过标准化流程实现资源访问的委托授权,其核心原理是分离认证与授权职责,引入授权服务器、资源服务器等角色。该技术价值在于实现认证逻辑集中化、资源服务无状态化,有效解决微服务场景下用户身份统一识别与权限管控难题。典型应用场景包括前端应用访问后端多服务、服务间内部调用等。本文以Spring Cloud Alibaba生态为基础,结合JWT(JSON Web Token)与Spring Authorizatio

weixin_30764771的博客 467

spring-authorization-server 授权码许可类型授权页面自定义JSON响应,应用前后端分离场景

spring-authorization-server 授权码许可类型授权页面自定义JSON响应,应用前后端分离场景

。。。。。。。 2672

Spring Boot 3 + Spring Authorization Server 构建企业级 SSO:多端互通、会话共享与

<think>我们根据要求,需要根据给定内容生成一篇≤150字的文章摘要。内容是关于Spring Authorization Server实现企业级SSO与OAuth2/OIDC的架构方案。摘要需提炼核心要点,控制在150字内。注意字数,简洁。 要点:传统Session不足,OAuth2+OIDC,Spring Authorization Server,架构:认证中心、网关、资源服务,令牌流转(授权码+PKCE, client credentials, JWT短效+refresh),会话管理(JWT验签+R

专注分享原创技术干货。大厂资深架构师,多年技术架构与技术管理经验,多年面试官经验。一对一技术指导培训,带你从小白到入门到架构设计到技术管理。关注我,免费领取学习资料。一对一免费面试指导,快速拿offer。 633

Spring Security OAuth2 单点登录与 JWT 资源服务器实战

单点登录(SSO)用于把多系统身份认证收敛到统一入口,常见协议是 OAuth2 与 OIDC。其原理是通过授权码模式在授权服务器完成认证,客户端以 code 换取 JWT 访问令牌,资源服务器只需校验签发者与签名即可放行请求。技术价值在于降低重复登录与账号维护成本,并让跨域、多终端应用共享身份。典型场景包括企业后台、微服务 API、前后端分离系统。落地时还需关注令牌有效期、刷新轮换、单点登出、Redis 会话共享及 JWK 轮换。基于 Spring Boot 3 与 Spring Security 6,Sp

weixin_33842304的博客 227

【springboot】万字操作指南 Spring Boot 2.x 升级到 3.x:微服务项目实战

摘要: 本文提供Spring Boot 2.x升级到3.x的实战指南,重点针对微服务项目。升级涉及JDK 17、Spring Framework 6、Jakarta EE等连锁变化,需系统规划版本矩阵(如Spring Boot 3.5.x + Spring Cloud 2025.0.x)。建议升级路线:先统一JDK 17环境,再处理依赖迁移(javax→jakarta)、Spring Security 6适配、OAuth2授权服务器改造等关键步骤。

Cjh20pan的博客 969

Spring Boot + Vue前后端分离实战:从接口联调到部署上线全解析

前后端分离架构已成为现代Web开发的主流模式,它通过将后端服务与前端界面解耦,显著提升了开发效率与系统可维护性。理解这一架构的核心原理,掌握RESTful API设计、跨域通信、无状态认证等关键技术,是开发者构建企业级应用的基础。本文从工程实践角度出发,系统梳理了基于Spring Boot与Vue技术栈的全栈开发链路,涵盖接口规范约定、CORS跨域解决方案、JWT登录认证、文件上传与静态资源映射、Nginx部署及经典问题排查等高频场景。无论是毕业设计、内部管理系统,还是希望系统掌握前后端分离流程的开发者,都

weixin_33889245的博客 316

东方财富网 数据访问接口及字段说明

东方财富网 数据访问接口和字段说明

雨中山草 2万+

SpringCloud + SpringBoot + OAuth2 + Spring Security + Redis 实现的微服务统一认证授权

本文基于SpringCloud生态构建了一套微服务统一认证授权系统,采用OAuth2协议实现分布式环境下的安全认证。系统包含认证中心、资源服务和网关三大核心组件:1. 认证中心基于Spring Authorization Server实现,支持多种授权模式并颁发JWT令牌;2. 资源服务通过解析JWT实现无状态认证;3. 网关统一处理外部请求并校验令牌。系统利用Redis存储客户端信息、刷新令牌和JWT黑名单,实现了令牌的高效管理。文章详细介绍了技术选型、架构设计、核心实现及安全增强措施,提供了一套完整的微

油墨香^-^的博客 1019

RDM协议控制端实战:从数据包构造到高效设备发现算法

本文深入探讨了RDM协议控制端的核心开发流程,涵盖从数据包构造、发送到响应解析的全过程。重点解析了如何利用二分搜寻算法高效实现网络设备发现,并分享了组包、解包中的关键代码实现与常见调试技巧,为舞台灯光与智能硬件领域的嵌入式开发提供实战指导。

week9的博客 457

前后端分离项目实战:Vue3+Spring Boot+JWT+Nginx部署全流程

前后端分离是现代Web开发中广泛采用的架构范式,核心在于将页面展示与业务逻辑通过HTTP+JSON解耦,使前端与后端各自独立演进、按需扩展。Vue3与Spring Boot是这一架构下的经典技术组合:Vite带来高效开发体验,Spring Boot提供稳定的接口服务,JWT无状态鉴权则让登录认证在分布式场景中天然可扩展。合理设计接口规范、统一响应结构并处理跨域代理,能显著提升前后端联调效率,缩短项目交付周期。这类架构适合中后台管理系统、全栈项目快速落地以及微服务化前的工程演练。从工程搭建、接口联调、权限控制

weixin_33894640的博客 515

【OpenArm|Control】openarm机械臂ROS2仿真控制

openarm_ros2 存储库包含用于 ros2_control 的包集合。 它抽象化了硬件控制,将手臂公开为接收位置、速度和扭矩命令并输出关节状态的接口。 openarm_bringup 包提供了通过包与 ROS2 控制框架集成的启动文件。该软件包提供启动文件和配置,用于启动硬件接口、加载控制器以及将物理臂连接到 ROS2 生态系统。启动后,您可以使用标准 ROS2 控制工具和接口来命令手臂并接收反馈。openarm_bringup bringup 包支持模拟硬件(用于模拟/测试)和通过硬件插件的真实

ai领域探索者,欢迎一起交流,目前从事具身智能行业。 2955

华为Atlas200DK实战指南:从零搭建AI开发环境到人像语义分割模型部署

本文提供了一份详尽的华为Atlas 200DK AI开发环境搭建与模型部署实战指南。文章采用合设环境方案,从SD卡准备、系统镜像烧录、网络配置到必备工具安装,逐步引导开发者完成环境搭建。核心部分详细讲解了如何利用华为ATC工具将TensorFlow模型转换为昇腾专用格式,并最终成功编译和运行人像语义分割模型,实现实时人像识别效果。

weixin_29309213的博客 355

Spring Authorization Server终极指南:5分钟构建企业级OAuth2授权服务器

Spring Authorization Server是Spring Security团队开发的官方OAuth2授权服务器实现,支持OAuth2.1和OpenID Connect 1.0规范,为微服务架构和现代应用提供专业级的身份认证与授权解决方案。无论您是构建单点登录系统还是保护API接口,Spring Authorization Server都能提供强大而灵活的支持。 ## 🚀 核心功能解

gitblog_00783的博客 412

matlab kwave模拟超声换能器学习笔记

kwave模拟超声换能器的学习笔记,从零开始造车轮

SolvingMaster的博客 3197

Spring Boot + Vue 前后端分离实战:高校自习室预约系统开发指南

前后端分离架构已成为现代Web应用开发的主流模式,其核心在于将用户界面与业务逻辑解耦,通过RESTful API进行数据交互。这种架构模式提升了开发效率、便于团队协作,并能实现前后端的独立部署与扩展。在技术实现上,后端通常采用Spring Boot框架,它通过自动配置、起步依赖等特性简化了Java应用的开发;前端则常选用Vue.js这类渐进式框架,通过组件化开发构建交互式界面。这种技术组合在构建资源管理系统、电商平台、后台管理系统等场景中具有广泛应用价值。本文聚焦于一个典型应用——高校自习室预约系统,详细阐

weixin_30301449的博客 471

《湖科大教书匠计算机网络》学习笔记总目录+思维导图

本篇文章是对《湖科大教书匠计算机网络》所有知识点的笔记总结归档和计算机网络的思维导图

weixin_73077810的博客 8229

【tips】在PyCharm中安装opencv图文版详细教程

pip如下图所示,左边的蓝色图标就是终端。点开终端后,键入命令,按下回车后就开始下载了。如果你更喜欢图形化操作,可以在 PyCharm 中直接安装:。点击右上角的 +按钮(添加包)。搜索或,点击。

zhousanguhe的博客 6740

选课管理系统开发实战:Spring Boot+Vue前后端分离全解析

前后端分离架构已成为现代Web开发的主流模式,它通过后端API与前端页面的解耦,显著提升开发效率与系统可维护性。在这一架构下,Spring Boot作为后端框架,配合Vue构建用户界面,MySQL存储业务数据,成为最常用的技术组合。选课管理系统是这类架构的典型实践场景,它涵盖了登录认证、角色权限、事务处理、并发扣减等关键工程问题。系统设计时需要重点关注数据库表结构与唯一约束,通过原子操作防止超卖,利用JWT实现无状态认证。从业务模型梳理、核心表设计、接口分层、前端路由守卫到联调部署,每个环节都与真实企业项目

weixin_34391445的博客 334

OpenClaw用户必看:每天0成本使用OpenClaw!NVIDIA NIM免费大模型接入教程

🚀 OpenClaw接入NVIDIA NIM免费API!glm5、minimax-m2.5等大量模型任选,0成本升级AI助手!💰

XiaoqiangClub的博客 5575

前后端分离开发实战:Spring Boot与Vue 3的RESTful API设计与实现

在前后端分离架构中,API接口作为软件组件间通信的基石,定义了明确的规则和协议。其核心原理是基于HTTP/HTTPS协议,通过标准方法(如GET、POST)和数据结构(如JSON)实现数据交换。这一设计模式的技术价值在于解耦前后端职责,支持跨平台复用,并提升开发效率。典型的应用场景包括构建Web应用、移动端服务以及微服务间的交互。本文聚焦于如何设计并实现一套健壮的RESTful API,通过Spring Boot后端与Vue 3前端的完整实战,详细解析了从接口定义、业务逻辑处理到安全配置的全过程。针对开发中

weixin_33866037的博客 437

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

微服务架构下基于Spring Security与JWT的REST API安全实战指南

在分布式系统与微服务架构中,REST API的安全保障是构建可靠应用的核心。其安全范式已从传统的Session-Cookie有状态模式,演进为以Token为中心的无状态认证机制。JWT(JSON Web Token)作为该机制的事实标准,通过数字签名确保信息完整性与来源可信,实现了安全上下文的跨服务安全传递。结合OAuth 2.0协议,不仅能支撑第三方授权登录,其密码模式与客户端凭证模式更是微服务内部服务间认证与授权的基石。这套技术方案的价值在于,它解耦了认证与业务逻辑,提升了系统的可伸缩性与可维护性。其典

weixin_34248023的博客 358

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Spring Security OAuth2搭建认证授权中心、资源服务中心、并结合网关校验的完整详细示例

使用Spring Security OAuth2搭建认证授权中心、资源服务中心,并结合网关校验的完整详细示例。

积跬步,至千里。 961

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

上一篇: Spring Boot整合Spring Authorization Server(三)——登录图形验证码
下一篇: 解决使用PowerShell不能运行cnpm
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值