客户端服务
整体流程
客户端前端 客户端后端 授权服务前端 授权服务后端 资源服务后端 请求/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";
}
}