当前位置: 首页 > 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:8081",rewrite: (p) => p.replace(/^\/api/, '')}}}
})

Code.vue

<script setup lang="ts">
import axios from 'axios'
import { useRoute, useRouter } from 'vue-router'const route = useRoute()
const router = useRouter()function init() {const token = route.query.tokenaxios.get('/api/oauth2/authorize', {params: route.query,headers: {'token': token}}).then(r => {let data = r.dataif (data && data.code && data.code == 1001) {router.push(`/login?back=${encodeURIComponent(route.fullPath)}`)}// 返回的客户端回调地址let location = r.headers.get('Location')if (location) {location += '&token=' + token + '&back=' + route.query.backwindow.location.href = location}}).catch(e => {console.error(e)})
}init()
</script><template>
</template>

Login.vue

<script setup lang="ts">
import { NForm, NFormItem, NButton, NInput } from 'naive-ui'
import { ref } from 'vue'
import axios from 'axios'
import { useRoute, useRouter } from 'vue-router'const route = useRoute()
const router = useRouter()
const formValue = ref({"username": '', "password": ''})
const m = ref('')function handleValidateClick(e: MouseEvent) {axios.post('/api/login', formValue.value).then(r => {let data = r.dataif (data && data.code && data.code == 1001) {m.value = data.msg} else {let back = route.query.back + '&token=' + datarouter.push(back)}}).catch(e => {console.error(e)})
}
</script><template><main><div><n-form inline :label-width="80" :model="formValue"><n-form-item label="姓名"><n-input v-model:value="formValue.username" placeholder="输入姓名" /></n-form-item><n-form-item label="年龄"><n-input v-model:value="formValue.password" placeholder="输入年龄" /></n-form-item><n-form-item><n-button attr-type="button" @click="handleValidateClick">验证</n-button></n-form-item></n-form></div><div>{{ m }}</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-server</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.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId></dependency></dependencies></project>

application.yml

logging:level:org.springframework.security: TRACEserver:port: 8081

AuthorizationServerConfig.java

package org.example.server.config;import com.alibaba.fastjson2.JSON;
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.example.server.security.MapSecurityContextRepository;
import org.example.server.security.OkAuthenticationSuccessHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
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.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.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
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;/*** 授权服务配置* * @author qiongying.huai* @version 1.0* @date 11:15 2025/6/23*/
@Configuration
public class AuthorizationServerConfig {private final Logger logger = LoggerFactory.getLogger(getClass());@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServer = OAuth2AuthorizationServerConfigurer.authorizationServer();http// 只匹配 OAuth2 授权服务器的相关端点.securityMatcher(authorizationServer.getEndpointsMatcher())// 启用 OpenID.with(authorizationServer, c -> c.oidc(Customizer.withDefaults()).authorizationEndpoint(a -> a.authorizationResponseHandler(new OkAuthenticationSuccessHandler()))).authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()).securityContext(c -> c.securityContextRepository(securityContextRepository())).exceptionHandling(exceptions -> exceptions.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));})).oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));return http.build();}@Beanpublic SecurityContextRepository securityContextRepository() {return new MapSecurityContextRepository();}@Beanpublic RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("client1").clientSecret(passwordEncoder.encode("secret")).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).redirectUri("http://localhost:3003/callback").scope("all").scope("user_info").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE)// 客户端设置,设置用户需要确认授权,设置false后不需要确认.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())// 设置accessToken有效期.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofSeconds(10)).build()).build();return new InMemoryRegisteredClientRepository(client);}@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}@Beanpublic JWKSource<SecurityContext> jwkSource() {RSAKey rsaKey = generateRsa();JWKSet jwkSet = new JWKSet(rsaKey);return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);}@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {// 会有60s的时间偏差: org.springframework.security.oauth2.jwt.JwtTimestampValidator.DEFAULT_MAX_CLOCK_SKEWreturn OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}private RSAKey generateRsa() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();return new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();}private KeyPair generateRsaKey() {try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);return keyPairGenerator.generateKeyPair();} catch (NoSuchAlgorithmException e) {throw new IllegalStateException(e);}}
}

SecurityConfig.java

package org.example.server.config;import com.alibaba.fastjson2.JSON;
import org.example.server.security.TokenFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.access.ExceptionTranslationFilter;import java.util.HashMap;
import java.util.Map;/*** Spring security配置** @author qiongying.huai* @version 1.0* @date 11:23 2025/6/23*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {private final Logger logger = LoggerFactory.getLogger(getClass());private final SecurityContextRepository securityContextRepository;public SecurityConfig(SecurityContextRepository securityContextRepository) {this.securityContextRepository = securityContextRepository;}@Beanpublic SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/error", "/login").permitAll().anyRequest().authenticated())// 放在ExceptionTranslationFilter之后,自定义的filter中的异常才能被exceptionHandling中的自定义处理器处理.addFilterAfter(new TokenFilter(securityContextRepository), ExceptionTranslationFilter.class).formLogin(AbstractHttpConfigurer::disable)// 前后端分离的,需要关闭CSRF保护,不然自定义的login接口403.csrf(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();}@Beanpublic UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {UserDetails user = User.withUsername("user").password(passwordEncoder.encode("password"))// 不需要加ROLE_前缀.roles("USER").build();return new InMemoryUserDetailsManager(user);}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}
}

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("/**");}};}
}

MapSecurityContextHolder.java

package org.example.server.security;import org.springframework.security.core.context.SecurityContext;
import org.springframework.util.StringUtils;import java.util.HashMap;
import java.util.Map;/*** 简单的token本地缓存** @author qiongying.huai* @version 1.0* @date 16:20 2025/7/19*/
public class MapSecurityContextHolder {private static final Map<String, SecurityContext> CONTEXT_MAP = new HashMap<>();private MapSecurityContextHolder() {}public static SecurityContext getContext(String key) {return CONTEXT_MAP.get(key);}public static void addContext(String key, SecurityContext context) {CONTEXT_MAP.put(key, context);}public static boolean containsKey(String key) {if (!StringUtils.hasLength(key)) {return false;}return CONTEXT_MAP.containsKey(key);}
}

MapSecurityContextRepository.java

package org.example.server.security;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.StringUtils;/*** 安全上下文存储** @author qiongying.huai* @version 1.0* @date 17:57 2025/7/16*/
public class MapSecurityContextRepository implements SecurityContextRepository {@Overridepublic SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {String token = requestResponseHolder.getRequest().getHeader("token");return MapSecurityContextHolder.getContext(token);}@Overridepublic void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {String token = request.getHeader("token");if (StringUtils.hasLength(token)) {MapSecurityContextHolder.addContext(token, context);}}@Overridepublic boolean containsContext(HttpServletRequest request) {return MapSecurityContextHolder.containsKey(request.getHeader("token"));}
}

OkAuthenticationSuccessHandler.java

package org.example.server.security;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;import java.io.IOException;
import java.nio.charset.StandardCharsets;/*** 让获取授权码接口返回200而不是302重定向* 因为跳转到不到域的地址重定向会有跨域问题,返回地址让前端跳转** @author qiongying.huai* @version 1.0* @date 10:11 2025/7/17* @see OAuth2AuthorizationEndpointFilter#sendAuthorizationResponse*/
public class OkAuthenticationSuccessHandler implements AuthenticationSuccessHandler {private final RedirectStrategy redirectStrategy;public OkAuthenticationSuccessHandler() {DefaultRedirectStrategy strategy = new DefaultRedirectStrategy();strategy.setStatusCode(HttpStatus.OK);redirectStrategy = strategy;}@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri()).queryParam(OAuth2ParameterNames.CODE,authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {uriBuilder.queryParam(OAuth2ParameterNames.STATE,UriUtils.encode(authorizationCodeRequestAuthentication.getState(), StandardCharsets.UTF_8));}// build(true) -> Components are explicitly encodedString redirectUri = uriBuilder.build(true).toUriString();this.redirectStrategy.sendRedirect(request, response, redirectUri);}
}

TokenFilter.java

package org.example.server.security;import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.DeferredSecurityContext;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;/*** 从token的本地缓存中拿安全上下文** @author qiongying.huai* @version 1.0* @date 11:05 2025/7/16*/
public class TokenFilter extends OncePerRequestFilter {private final SecurityContextRepository securityContextRepository;public TokenFilter(SecurityContextRepository securityContextRepository) {this.securityContextRepository = securityContextRepository;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (StringUtils.hasLength(token)) {DeferredSecurityContext deferredSecurityContext = securityContextRepository.loadDeferredContext(request);if (deferredSecurityContext != null) {SecurityContext securityContext = deferredSecurityContext.get();if (securityContext != null) {SecurityContextHolder.setContext(securityContext);}}}filterChain.doFilter(request, response);}
}

ServerLoginController.java

package org.example.server.controller;import org.example.server.security.MapSecurityContextHolder;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.util.Map;
import java.util.UUID;/*** 登录接口** @author qiongying.huai* @version 1.0* @date 13:46 2025/7/14*/
@RestController
public class ServerLoginController {private final AuthenticationManager authenticationManager;public ServerLoginController(AuthenticationManager authenticationManager) {this.authenticationManager = authenticationManager;}@PostMapping("/login")public String login(@RequestBody Map<String, String> params) {String username = params.get("username");String password = params.get("password");UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);Authentication authenticate = authenticationManager.authenticate(token);if (!authenticate.isAuthenticated()) {throw new RuntimeException("登录失败");}String id = UUID.randomUUID().toString().replaceFirst("-", "");SecurityContext context = SecurityContextHolder.getContext();context.setAuthentication(authenticate);MapSecurityContextHolder.addContext(id, context);return id;}
}
http://www.xdnf.cn/news/1448209.html

相关文章:

  • FastVLM:高效视觉编码助力视觉语言模型突破高分辨率效率瓶颈
  • LeNet-5:卷积神经网络的奠基之作
  • 0903 C++类的运算符重载、静态成员与继承
  • 前端-安装VueCLI
  • 【ARM嵌入式汇编基础】-数据处理指令(三)
  • OpenHarmony Ability“全家桶”彻底拆解:从UIAbility到ExtensionAbility一文说清楚
  • LeetCode 1537.最大得分
  • 残差连接的概念与作用
  • 蓝桥杯算法之基础知识(6)
  • Netty从0到1系列之Channel
  • 【 线段树】P12347 [蓝桥杯 2025 省 A 第二场] 栈与乘积|普及+
  • 基于 HTML、CSS 和 JavaScript 的智能图像灰度直方图分析系统
  • HTML全屏功能实现汇总
  • npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR!
  • 求单源最短路(Dijkstra 算法-迪杰斯特拉算法,SPFA)
  • 【Unity基础】两个关于UGUI中Text对非英文字体支持的问题
  • SpringAI应用开发面试全流程:技术原理、架构优化与企业场景解析
  • 复写零(双指针)
  • JavaScript学习最后一章节(小练习)
  • 如何解决虚拟机网络连接问题:配置固定 IP 篇
  • Spring Authorization Server 1.5.2 使用YML配置的方式,最常用法总结
  • 【算法--链表】141.环形链表(通俗讲解链表中是否有环)
  • 分布式AI算力系统番外篇-----超体的现世《星核》
  • 强化学习中的模仿学习是什么?
  • 相关性分析与常用相关系数
  • react的 hooks 是如何存储的
  • HTML第七课:发展史
  • Streamlit 数据看板模板:非前端选手快速搭建 Python 数据可视化交互看板的实用工具
  • 如何画时序图、流程图
  • android集成unity后动态导入 assetsBundle