当前位置: 首页 > news >正文

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

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

<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.datah.value = dataif (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

<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

<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.datalocalStorage.setItem('access_token', data.access_token)t.value = datarouter.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 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

logging:level:org.springframework.security: TRACEserver:port: 8083spring:security:oauth2:client:registration:client1:provider: client1client-id: client1client-secret: secretauthorization-grant-type: authorization_coderedirect-uri: http://localhost:3003/callbackscope:- openid- profile- allclient-name: client1provider:client1:issuer-uri: http://localhost:8081

SecurityConfig.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());@Beanpublic 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

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 {@Beanpublic WebMvcConfigurer corsConfigurer() {return new WebMvcConfigurer() {@Overridepublic void addCorsMappings(@NonNull CorsRegistry registry) {registry.addMapping("/**");}};}
}

TokenInvalidException.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 {@Serialprivate static final long serialVersionUID = 3054997322961458614L;public TokenInvalidException(String msg) {super(msg);}
}

JwtTokenFilter.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 {@Overrideprotected 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

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

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";}
}
http://www.xdnf.cn/news/1440649.html

相关文章:

  • 【机器学习入门】5.4 线性回归模型的应用——从CO₂浓度预测学透实战全流程
  • 远程的 develop 比你本地的 develop 更新,Git 拒绝直接覆盖
  • 【55页PPT】旧QC七大手法培训精选讲义(附下载方式)
  • 深入解析Flowable工作流引擎:从原理到实践
  • 2 XSS
  • 深入掌握sed:Linux文本处理的流式编辑器利器
  • PHP如何解决使用国密SM4解密Base64数据错误问题?(基于lpilp/guomi)
  • 协议分析基础
  • 以技术共享点燃全球能源变革新引擎的智慧能源开源了
  • 低代码革命遇瓶颈?这个“套娃神技“才是破局关键!
  • 在Excel和WPS表格中隔多行插入一个空白行
  • 多场景对练数据的 Excel 横向导出方案(EasyExcel 动态表头实践)
  • 【XR硬件系列】Vivo Vision 与 Apple VisionPro 深度技术对比:MR 时代的轻量化革命与生态霸权
  • 单元测试数据库回滚问题
  • Android音频学习(十六)——CreateTrack
  • 资产管理还靠Excel?深度体验系统如何让企业高效数字化升级!
  • 自然语言处理深层语义分析中公理化体系的可行性、挑战与前沿进展
  • php:PHP 8 新特性深度解析与实战应用:提升开发效率的关键技巧
  • 为何 React JSX 循环需要使用 key
  • 一文弄懂C/C++不定参数底层原理
  • Zygote 进程启动流程
  • 视频判重需求:别为同一内容花两次钱!
  • 涨了一倍多的顺丰同城,还能继续做大即时零售基建的蛋糕吗?
  • HTML5 标题标签、段落、换行和水平线
  • 光谱相机的探测器类型
  • 相机在两个机械臂上安装方式比较
  • 字节跳动后端 一面凉经
  • 单片机:GPIO、按键、中断、定时器、蜂鸣器
  • 知微传感Dkam系列3D相机SDK例程篇:CSharp连接相机及保存数据
  • Debezium日常分享系列之:Debezium 3.3.0.Alpha2发布