Springboot3+SpringSecurity6Oauth2+vue3前后端分离认证授权-客户端

客户端服务

整体流程

客户端前端 客户端后端 授权服务前端 授权服务后端 资源服务后端 请求/hello接口 无权限返回code=1001 跳转到登录页 请求登录/login接口 返回授权服务获取授权码页面地址 跳转到获取授权码页面 请求获取授权码/oauth2/authorize接口 无权限返回code=1001 跳转到登录页 请求登录/login接口验证用户密码 登录成功返回token 跳转回获取授权码页面 带token请求获取授权码/oauth2/authorize接口 返回授权码和客户端回调地址(带token) 跳转到客户端回调地址(带token) 请求回调/callback接口 带token请求获取access_token的/oauth2/token接口 返回access_token 返回access_token 跳转回最初始地址/ 带access_token请求/hello接口 带access_token请求/authentication接口 返回认证授权信息Authentication 带Authentication走接下来流程 返回/hello接口结果 客户端前端 客户端后端 授权服务前端 授权服务后端 资源服务后端

前端

技术栈

vue3+vite4+axios+pinia+naiveui

项目结构

代码

vite.config.ts

ts 复制代码
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    port: 3001,
    open: false,
    proxy: {
      '/api': {
        changeOrigin: true,
        target: "http://localhost:8083",
        rewrite: (p) => p.replace(/^\/api/, '')
      }
    }
  }
})

HomeView.vue

ts 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { useRoute, useRouter } from 'vue-router'
import { useDialog } from 'naive-ui'

const route = useRoute()
const router = useRouter()
const dialog = useDialog()

const h = ref('')

function init() {
  let accessToken = localStorage.getItem('access_token')
  if (accessToken && accessToken.length) {
    accessToken = 'Bearer ' + accessToken
  }
  axios.get('/api/hello', {
    headers: {
      Authorization: accessToken
    }
  })
  .then(r => {
    let data = r.data
    h.value = data
    if (data && data.code && data.code == 1001) {
      dialog.warning({
        title: '未登录',
        content: '去登录?' + data.msg,
        positiveText: '确定',
        negativeText: '取消',
        draggable: true,
        onPositiveClick: () => {
          router.push(`/login?back=${encodeURIComponent(route.fullPath)}`)
        },
        onNegativeClick: () => {
          // message.error('取消')
          console.log('取消')
        }
      })
    }
  })
  .catch(e => {
    console.error(e)
  })
}

init()
</script>

<template>
  <main>
    <p>{{ h }}</p>
  </main>
</template>

Login.vue

ts 复制代码
<script setup lang="ts">
import axios from 'axios'
import { useRoute } from 'vue-router'

const route = useRoute()

function handleValidateClick() {
  axios.get('/api/login', {
    params: {
      callback: route.query.back
    }
  })
    .then(r => {
      window.location.href = r.data
    })
    .catch(e => {
      console.error(e)
    })
}

handleValidateClick()
</script>

<template>
</template>

Callback.vue

ts 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
const t = ref('')

function handleValidateClick() {
  axios.get('/api/callback', {
    params: {
      code: route.query.code,
      token: route.query.token
    }
  })
    .then(r => {
      let data = r.data
      localStorage.setItem('access_token', data.access_token)
      t.value = data
      router.push(route.query.back)
    })
    .catch(e => {
      console.error(e)
    })
}

handleValidateClick()
</script>

<template>
  <main>
    <div>
      {{ t }}
    </div>
  </main>
</template>

后端

技术栈

springboot3

spring security6 oauth2

项目结构

代码

pom.xml

xml 复制代码
<?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>org.example</groupId>
        <artifactId>security</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>security-client</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>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>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
    </dependencies>

</project>

application.yml

yml 复制代码
logging:
  level:
    org.springframework.security: TRACE

server:
  port: 8083

spring:
  security:
    oauth2:
      client:
        registration:
          client1:
            provider: client1
            client-id: client1
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:3003/callback
            scope:
              - openid
              - profile
              - all
            client-name: client1
        provider:
          client1:
            issuer-uri: http://localhost:8081

SecurityConfig.java

java 复制代码
package org.example.client.config;

import com.alibaba.fastjson2.JSON;
import org.example.client.security.JwtTokenFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;

import java.util.HashMap;
import java.util.Map;

/**
 * Spring security配置
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 14:55 2025/6/23
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        		// 放在ExceptionTranslationFilter之后,自定义的filter中的异常才能被exceptionHandling中的自定义处理器处理
                .addFilterAfter(new JwtTokenFilter(), ExceptionTranslationFilter.class)
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/login", "/error", "/callback").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Client(Customizer.withDefaults())
                .oauth2Login(AbstractHttpConfigurer::disable)
                .exceptionHandling(e ->
                        e.authenticationEntryPoint((request, response, authException) -> {
                            logger.error("request: {}, error: ", request.getRequestURI(), authException);
                            Map<String, Object> responseData = new HashMap<>(4);
                            responseData.put("code", 1001);
                            responseData.put("msg", authException.getMessage());
                            response.setContentType("application/json;charset=utf-8");
                            response.setStatus(200);
                            response.getWriter().write(JSON.toJSONString(responseData));
                        }));
        return http.build();
    }
}

WebMvcConfig.java

java 复制代码
package org.example.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域配置
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 14:26 2025/7/14
 */
@Configuration
public class WebMvcConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(@NonNull CorsRegistry registry) {
                registry.addMapping("/**");
            }
        };
    }
}

TokenInvalidException.java

java 复制代码
package org.example.client.security;

import org.springframework.security.core.AuthenticationException;

import java.io.Serial;

/**
 * 自定义access_token异常
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 19:12 2025/7/19
 */
public class TokenInvalidException extends AuthenticationException {

    @Serial
    private static final long serialVersionUID = 3054997322961458614L;

    public TokenInvalidException(String msg) {
        super(msg);
    }
}

JwtTokenFilter.java

java 复制代码
package org.example.client.security;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;

/**
 * 通过jwt从资源服务获取用户验证授权信息
 * 客户端服务是从本地缓存获取,对应TokenFilter.class
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 11:36 2025/7/17
 */
public class JwtTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String url = "http://localhost:8082/authentication";
        String authorization = request.getHeader("Authorization");
        if (StringUtils.hasLength(authorization)) {
            HttpClient httpClient = HttpClient.newHttpClient();
            HttpRequest build = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .GET()
//                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Authorization", authorization)
                    .build();
            try {
                HttpResponse<String> send = httpClient.send(build, HttpResponse.BodyHandlers.ofString());
                String body = send.body();
                if (StringUtils.hasLength(body)) {
                    JSONObject jsonObject = JSON.parseObject(body);
                    Integer code = jsonObject.getInteger("code");
                    Authentication authentication = jsonObject.getObject("data", Authentication.class);
                    if (code == 200 && authentication != null) {
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    } else {
                        String msg = Optional.ofNullable(jsonObject.getString("msg")).orElse("Token invalid.");
                        throw new TokenInvalidException(msg);
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new TokenInvalidException("Token invalid.");
            }
        }
        filterChain.doFilter(request, response);
    }
}

ClientLoginController.java

java 复制代码
package org.example.client.controller;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

/**
 * 客户端接口
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 13:56 2025/7/14
 */
@RestController
public class ClientLoginController {

    @GetMapping("/login")
    public String login(@RequestParam("callback") String callback) {
        return "http://localhost:3001/code?client_id=client1&response_type=code&scope=all+openid" +
                "&redirect_uri=http://localhost:3003/callback&back=" + callback;
    }

    @GetMapping("/callback")
    public String callback(@RequestParam("code") String code, @RequestParam("token") String token) throws IOException, InterruptedException {
    	// 获取access_token
        String url = "http://localhost:8081/oauth2/token";
        // 构建请求参数
        String requestBody = "grant_type=authorization_code" +
                "&redirect_uri=http://localhost:3003/callback" +
                "&code=" + code;
        HttpClient httpClient = HttpClient.newHttpClient();
        HttpRequest build = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
                .header("token", token)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("client1:secret".getBytes()))
                .build();
        HttpResponse<String> send = httpClient.send(build, HttpResponse.BodyHandlers.ofString());
        return send.body();
    }
}

HelloController.java

java 复制代码
package org.example.client.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 客户端资源接口
 *
 * @author qiongying.huai
 * @version 1.0
 * @date 10:02 2025/6/24
 */
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}
相关推荐
华仔啊9 小时前
Java异常处理别再瞎搞了!阿里大神总结的 9 种最佳实践,让你的代码更健壮!
java·后端
杨杨杨大侠9 小时前
手搓责任链框架 5:执行流程
java·spring·github
Metaphor6929 小时前
Java 压缩 PDF 文件大小:告别臃肿,提升效率!
java·经验分享·pdf
m0_709788629 小时前
单片机点灯
java·前端·数据库
BYSJMG9 小时前
计算机大数据毕业设计选题:基于Spark+hadoop的全球香水市场趋势分析系统
大数据·vue.js·hadoop·python·spark·django·课程设计
野犬寒鸦9 小时前
力扣hot100:螺旋矩阵(边界压缩,方向模拟)(54)
java·数据结构·算法·leetcode
RedEric10 小时前
Vue加载速度优化,verder.js和element.js加载速度慢解决方法
前端·javascript·vue.js·前端性能优化
初始化10 小时前
JavaFx:生成布局 ViewBinding,告别 @FXML 注解
java·kotlin
leon_teacher10 小时前
ArkUI核心功能组件使用
android·java·开发语言·javascript·harmonyos·鸿蒙