一、分布式架构下的权限体系核心痛点
随着微服务架构的普及,单体应用中耦合的权限模块被彻底拆分,传统基于Session的权限方案面临无法逾越的瓶颈:
- 多服务重复开发权限逻辑,维护成本指数级上升,权限规则难以统一
- 跨服务、跨域场景下Session共享方案兼容性差,存在严重的安全隐患
- 第三方应用接入时,用户密码直接暴露给客户端,无法实现细粒度的权限管控
- 多端应用(Web、移动端、小程序)接入时,无法实现统一的身份认证与权限管理
统一认证授权体系正是为解决上述痛点而生,而OAuth2.0作为行业公认的开放授权标准,是构建分布式统一权限体系的核心基石。
二、核心概念底层逻辑与易混淆点辨析
2.1 两个核心概念的本质区分
很多开发者在落地时的核心误区,是混淆了认证与授权的边界,二者是完全独立的两个流程:
- 认证(Authentication) :验证用户身份的合法性,解决「你是谁」的问题,核心是身份校验
- 授权(Authorization) :验证用户对资源的访问权限,解决「你能做什么」的问题,核心是权限管控
2.2 OAuth2.0的核心设计与角色定义
OAuth2.0是基于令牌的开放授权协议框架,其核心设计思想是解耦认证与授权,让第三方应用无需获取用户账号密码,即可获得用户指定资源的有限访问权限。
协议定义了四个不可拆分的核心角色,所有流程均围绕这四个角色展开:
- 资源所有者(Resource Owner) :能够授予资源访问权限的实体,通常是终端用户
- 客户端(Client) :请求访问资源的应用,需经过资源所有者授权,才能访问对应资源
- 授权服务器(Authorization Server) :负责验证资源所有者身份,完成授权后发放令牌的核心服务
- 资源服务器(Resource Server) :存储受保护资源的服务,校验令牌合法性后,提供对应资源访问能力
2.3 OAuth2.0授权模式与适用场景
基于RFC6749规范与OAuth2.1最新安全标准,不同授权模式的安全等级与适用场景有明确边界,废弃高风险模式,仅保留生产环境可用的安全模式:
| 授权模式 | 核心流程 | 适用场景 | 安全等级 |
| 授权码模式+PKCE | 先获取授权码,再通过授权码换取令牌,PKCE防止授权码拦截 | 前端单页应用、移动端应用等公共客户端 | 极高 |
| 授权码模式 | 先获取授权码,再通过授权码换取令牌,全程不暴露用户密码 | 服务端渲染的Web应用、机密客户端 | 高 |
| 客户端模式 | 客户端直接以自身身份向授权服务器申请令牌,无用户参与 | 服务间通信、系统内部接口调用 | 中 |
❝注:OAuth2.1规范已正式废弃密码模式与简化模式,二者存在严重的密码泄露、令牌窃听风险,生产环境禁止使用。
授权码模式完整流程
2.4 高频易混淆概念辨析
- OAuth2.0 vs JWTOAuth2.0是授权协议框架,定义了授权的流程与规范;JWT是一种轻量级的令牌格式,定义了令牌的结构与编码方式。二者不是竞争关系,JWT可以作为OAuth2.0协议中令牌的载体。
- OAuth2.0 vs SSOSSO是单点登录的业务效果,即用户一次登录即可访问多个相互信任的应用系统;OAuth2.0是实现SSO的技术方案之一,CAS、SAML等协议也可实现SSO。
- 访问令牌(Access Token) vs 刷新令牌(Refresh Token)访问令牌是资源服务器校验访问权限的唯一凭证,生命周期短,通常15分钟,直接暴露在网络请求中;刷新令牌用于令牌过期后重新获取访问令牌,生命周期长,通常7天,仅在与授权服务器交互时使用,不暴露给资源服务器,极大降低令牌泄露风险。
三、分布式统一认证授权架构设计
3.1 整体架构分层设计
基于微服务架构的最佳实践,统一认证授权架构采用分层设计,实现职责解耦、高可用、可扩展的核心目标:
各层核心职责:
- 用户接入层:面向多端用户的入口,包括Web端、移动端、小程序、第三方应用等
- API网关层:全局流量入口,负责令牌透传、路由转发、限流熔断、跨域处理、统一日志等
- 授权服务中心:架构核心,负责用户身份认证、授权管理、令牌发放与校验、客户端管理等
- 资源服务集群:各业务微服务,即受保护的资源服务器,负责本地细粒度权限校验
- 数据存储层:存储用户信息、角色权限数据、OAuth2.0客户端数据、令牌数据等
3.2 权限模型设计
采用RBAC(基于角色的访问控制)模型作为核心权限模型,实现用户与权限的解耦,支持灵活的权限管控:
- 用户与角色是多对多关系
- 角色与权限是多对多关系
- 权限分为菜单权限、按钮权限、接口权限三类
- 支持数据权限的扩展,通过权限表达式实现细粒度数据管控
四、全链路架构落地实战
4.1 技术栈选型
所有组件均采用以下版本,基于JDK17构建,技术栈如下:
- 核心框架:Spring Boot 3.2.4
- 安全框架:Spring Security 6.2.3
- 授权服务:Spring Authorization Server 1.2.3
- 持久层框架:MyBatis Plus 3.5.6
- 数据库:MySQL 8.0
- 接口文档:SpringDoc OpenAPI 3 2.5.0
- 工具类:Spring Core、FastJSON2、Guava
- 项目管理:Maven
4.2 数据库表结构设计
以下SQL脚本基于MySQL 8.0编写,完整覆盖RBAC模型与OAuth2.0客户端存储需求:
CREATE DATABASE IF NOT EXISTS distributed_auth DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE distributed_auth;
-- 用户表
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(128) NOT NULL COMMENT '密码(BCrypt加密)',
real_name VARCHAR(32) DEFAULT NULL COMMENT '真实姓名',
mobile VARCHAR(11) DEFAULT NULL COMMENT '手机号',
email VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-启用',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username),
KEY idx_mobile (mobile),
KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户表';
-- 角色表
DROP TABLE IF EXISTS sys_role;
CREATE TABLE sys_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
role_code VARCHAR(64) NOT NULL COMMENT '角色编码',
role_name VARCHAR(32) NOT NULL COMMENT '角色名称',
role_desc VARCHAR(256) DEFAULT NULL COMMENT '角色描述',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-启用',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_role_code (role_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统角色表';
-- 权限表
DROP TABLE IF EXISTS sys_permission;
CREATE TABLE sys_permission (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父权限ID',
permission_code VARCHAR(128) NOT NULL COMMENT '权限编码',
permission_name VARCHAR(64) NOT NULL COMMENT '权限名称',
permission_type TINYINT NOT NULL COMMENT '权限类型 1-菜单 2-按钮 3-接口',
path VARCHAR(256) DEFAULT NULL COMMENT '路由路径',
url VARCHAR(256) DEFAULT NULL COMMENT '接口地址',
method VARCHAR(16) DEFAULT NULL COMMENT '请求方法',
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-启用',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
PRIMARY KEY (id),
UNIQUE KEY uk_permission_code (permission_code),
KEY idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统权限表';
-- 用户角色关联表
DROP TABLE IF EXISTS sys_user_role;
CREATE TABLE sys_user_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_user_role (user_id,role_id),
KEY idx_user_id (user_id),
KEY idx_role_id (role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';
-- 角色权限关联表
DROP TABLE IF EXISTS sys_role_permission;
CREATE TABLE sys_role_permission (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_role_permission (role_id,permission_id),
KEY idx_role_id (role_id),
KEY idx_permission_id (permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限关联表';
-- OAuth2.0客户端注册表(Spring Authorization Server官方标准结构)
DROP TABLE IF EXISTS oauth2_registered_client;
CREATE TABLE oauth2_registered_client (
id VARCHAR(100) NOT NULL COMMENT '主键ID',
client_id VARCHAR(100) NOT NULL COMMENT '客户端ID',
client_id_issued_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '客户端签发时间',
client_secret VARCHAR(200) DEFAULT NULL COMMENT '客户端密钥',
client_secret_expires_at TIMESTAMP NULL DEFAULT NULL COMMENT '客户端密钥过期时间',
client_name VARCHAR(200) NOT NULL COMMENT '客户端名称',
client_authentication_methods VARCHAR(1000) NOT NULL COMMENT '客户端认证方式',
authorization_grant_types VARCHAR(1000) NOT NULL COMMENT '授权类型',
redirect_uris VARCHAR(1000) DEFAULT NULL COMMENT '重定向地址',
post_logout_redirect_uris VARCHAR(1000) DEFAULT NULL COMMENT '登出重定向地址',
scopes VARCHAR(1000) NOT NULL COMMENT '授权范围',
client_settings VARCHAR(2000) NOT NULL COMMENT '客户端配置',
token_settings VARCHAR(2000) NOT NULL COMMENT '令牌配置',
PRIMARY KEY (id),
UNIQUE KEY uk_client_id (client_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OAuth2.0客户端注册表';
-- 初始化数据
-- 初始化管理员用户 用户名:admin 密码:Admin@123456
INSERT INTO sys_user (username, password, real_name, status) VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu', '系统管理员', 1);
-- 初始化管理员角色
INSERT INTO sys_role (role_code, role_name, role_desc) VALUES ('admin', '超级管理员', '系统最高权限角色');
-- 初始化用户角色关联
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);
-- 初始化接口权限
INSERT INTO sys_permission (parent_id, permission_code, permission_name, permission_type, url, method) VALUES (0, 'system:user:info', '用户信息查询', 3, '/api/user/info', 'GET');
-- 初始化角色权限关联
INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 1);
-- 初始化OAuth2.0客户端 客户端ID:demo-client 客户端密钥:demo-secret@123456
INSERT INTO oauth2_registered_client (id, client_id, client_secret, client_name, client_authentication_methods, authorization_grant_types, redirect_uris, scopes, client_settings, token_settings)
VALUES ('1', 'demo-client', '$2a$10$kU9Y8xVQw8aQZ7xX6zW5eO9L8K7J6H5G4F3D2S1A0S9D8F7G6H5J', '演示客户端', 'client_secret_basic', 'authorization_code,refresh_token,client_credentials', 'http://127.0.0.1:8080/login/oauth2/code/demo-client', 'openid,profile', '{"require-authorization-consent":true,"require-proof-key":false}', '{"access-token-time-to-live":900,"refresh-token-time-to-live":604800,"token-format":"jwt","id-token-signature-algorithm":"RS256"}');
4.3 授权服务器项目搭建
4.3.1 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth-server</name>
<description>分布式权限授权服务器</description>
<properties>
<java.version>17</java.version>
<spring-authorization-server.version>1.2.3</spring-authorization-server.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<springdoc.version>2.5.0</springdoc.version>
<guava.version>32.1.3-jre</guava.version>
<fastjson2.version>2.0.49</fastjson2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>${spring-authorization-server.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.3.2 配置文件application.yml
server:
port: 9000
spring:
application:
name: auth-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/distributed_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
security:
oauth2:
authorizationserver:
issuer: http://127.0.0.1:9000
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
4.3.3 核心实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统用户实体类
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String mobile;
private String email;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统角色实体类
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_role")
public class SysRole implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String roleCode;
private String roleName;
private String roleDesc;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统权限实体类
* @author ken
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_permission")
public class SysPermission implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String permissionCode;
private String permissionName;
private Integer permissionType;
private String path;
private String url;
private String method;
private Integer sort;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
4.3.4 Mapper层
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysPermission;
import com.jam.demo.entity.SysRole;
import com.jam.demo.entity.SysUser;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 系统用户Mapper接口
* @author ken
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 根据用户ID查询角色列表
* @param userId 用户ID
* @return 角色列表
*/
List<SysRole> selectRolesByUserId(@Param("userId") Long userId);
/**
* 根据用户ID查询权限编码列表
* @param userId 用户ID
* @return 权限编码列表
*/
List<String> selectPermissionCodesByUserId(@Param("userId") Long userId);
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysRole;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 系统角色Mapper接口
* @author ken
*/
public interface SysRoleMapper extends BaseMapper<SysRole> {
/**
* 根据权限ID查询角色列表
* @param permissionId 权限ID
* @return 角色列表
*/
List<SysRole> selectRolesByPermissionId(@Param("permissionId") Long permissionId);
}
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.SysPermission;
/**
* 系统权限Mapper接口
* @author ken
*/
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
}
4.3.5 Service层
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.SysUser;
/**
* 系统用户服务接口
* @author ken
*/
public interface SysUserService extends IService<SysUser> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户实体
*/
SysUser getUserByUsername(String username);
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.SysUser;
import com.jam.demo.mapper.SysUserMapper;
import com.jam.demo.service.SysUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 系统用户服务实现类
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private final SysUserMapper sysUserMapper;
@Override
public SysUser getUserByUsername(String username) {
if (!StringUtils.hasText(username)) {
return null;
}
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUsername, username)
.eq(SysUser::getStatus, 1);
return this.getOne(queryWrapper);
}
}
4.3.6 自定义用户信息加载服务
package com.jam.demo.security;
import com.jam.demo.entity.SysUser;
import com.jam.demo.mapper.SysUserMapper;
import com.jam.demo.service.SysUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义用户详情服务
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final SysUserService sysUserService;
private final SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!StringUtils.hasText(username)) {
throw new UsernameNotFoundException("用户名不能为空");
}
SysUser sysUser = sysUserService.getUserByUsername(username);
if (ObjectUtils.isEmpty(sysUser)) {
throw new UsernameNotFoundException("用户不存在");
}
List<String> permissionCodes = sysUserMapper.selectPermissionCodesByUserId(sysUser.getId());
List<SimpleGrantedAuthority> authorities = permissionCodes.stream()
.filter(StringUtils::hasText)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return User.withUsername(sysUser.getUsername())
.password(sysUser.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(sysUser.getStatus() != 1)
.credentialsExpired(false)
.disabled(sysUser.getStatus() != 1)
.build();
}
}
4.3.7 RSA密钥工具类
package com.jam.demo.util;
import lombok.extern.slf4j.Slf4j;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
/**
* RSA密钥工具类
* @author ken
*/
@Slf4j
public class RsaKeyUtil {
private RsaKeyUtil() {
}
/**
* 生成RSA密钥对
* @return RSA密钥对
* @throws NoSuchAlgorithmException 算法不存在异常
*/
public static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
/**
* 获取RSA公钥
* @param keyPair 密钥对
* @return RSA公钥
*/
public static RSAPublicKey getPublicKey(KeyPair keyPair) {
return (RSAPublicKey) keyPair.getPublic();
}
/**
* 获取RSA私钥
* @param keyPair 密钥对
* @return RSA私钥
*/
public static RSAPrivateKey getPrivateKey(KeyPair keyPair) {
return (RSAPrivateKey) keyPair.getPrivate();
}
}
4.3.8 授权服务器核心配置
package com.jam.demo.config;
import com.jam.demo.util.RsaKeyUtil;
import lombok.RequiredArgsConstructor;
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.jdbc.core.JdbcTemplate;
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.crypto.bcrypt.BCryptPasswordEncoder;
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.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
/**
* 授权服务器配置类
* @author ken
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthorizationServerConfig {
private final JdbcTemplate jdbcTemplate;
/**
* 授权服务器安全过滤链
* @param http HttpSecurity对象
* @return SecurityFilterChain对象
* @throws Exception 异常
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
http.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
).oauth2ResourceServer(resourceServer -> resourceServer
.jwt(Customizer.withDefaults())
);
return http.build();
}
/**
* 默认安全过滤链
* @param http HttpSecurity对象
* @return SecurityFilterChain对象
* @throws Exception 异常
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll()
.anyRequest().authenticated()
).formLogin(Customizer.withDefaults());
return http.build();
}
/**
* 密码编码器
* @return PasswordEncoder对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 客户端存储仓库
* @return RegisteredClientRepository对象
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
/**
* 授权服务
* @param registeredClientRepository 客户端存储仓库
* @return OAuth2AuthorizationService对象
*/
@Bean
public OAuth2AuthorizationService oAuth2AuthorizationService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 授权同意服务
* @param registeredClientRepository 客户端存储仓库
* @return OAuth2AuthorizationConsentService对象
*/
@Bean
public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* JWK源
* @return JWKSource对象
* @throws NoSuchAlgorithmException 算法不存在异常
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
KeyPair keyPair = RsaKeyUtil.generateRsaKeyPair();
RSAPublicKey publicKey = RsaKeyUtil.getPublicKey(keyPair);
RSAPrivateKey privateKey = RsaKeyUtil.getPrivateKey(keyPair);
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* JWT解码器
* @param jwkSource JWK源
* @return JwtDecoder对象
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* JWT生成器
* @param jwkSource JWK源
* @param jwtCustomizer JWT自定义器
* @return OAuth2TokenGenerator对象
*/
@Bean
public OAuth2TokenGenerator<?> jwtGenerator(JWKSource<SecurityContext> jwkSource, OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);
JwtGenerator generator = new JwtGenerator(jwtEncoder);
generator.setJwtCustomizer(jwtCustomizer);
return generator;
}
/**
* JWT自定义器,扩展JWT载荷
* @return OAuth2TokenCustomizer对象
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (context.getTokenType().getValue().equals("access_token")) {
context.getClaims().claim("client_id", context.getRegisteredClient().getClientId());
}
};
}
/**
* 授权服务器设置
* @return AuthorizationServerSettings对象
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
4.3.9 启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 授权服务器启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
4.4 资源服务器项目搭建
4.4.1 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>resource-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>resource-server</name>
<description>分布式权限资源服务器</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<springdoc.version>2.5.0</springdoc.version>
<guava.version>32.1.3-jre</guava.version>
<fastjson2.version>2.0.49</fastjson2.version>
</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-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.4.2 配置文件application.yml
server:
port: 8080
spring:
application:
name: resource-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/distributed_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:9000
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
4.4.3 资源服务器核心配置
package com.jam.demo.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* 资源服务器配置类
* @author ken
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class ResourceServerConfig {
/**
* 资源服务器安全过滤链
* @param http HttpSecurity对象
* @return SecurityFilterChain对象
* @throws Exception 异常
*/
@Bean
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
).oauth2ResourceServer(resourceServer -> resourceServer
.jwt(jwt -> jwt.jwtAuthenticationConverter(new CustomJwtAuthenticationConverter()))
).sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).csrf(csrf -> csrf.disable());
return http.build();
}
/**
* 密码编码器
* @return PasswordEncoder对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.4.4 自定义JWT认证转换器
package com.jam.demo.config;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 自定义JWT认证转换器
* @author ken
*/
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream.concat(
defaultGrantedAuthoritiesConverter.convert(jwt).stream(),
jwt.getClaimAsStringList("authorities").stream().map(SimpleGrantedAuthority::new)
).collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject());
}
}
4.4.5 用户信息接口
package com.jam.demo.controller;
import com.jam.demo.entity.SysUser;
import com.jam.demo.service.SysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户信息控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@Tag(name = "用户信息接口", description = "用户信息相关接口")
public class UserController {
private final SysUserService sysUserService;
/**
* 获取当前登录用户信息
* @param authentication 认证信息
* @return 用户实体
*/
@GetMapping("/info")
@Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息")
@PreAuthorize("hasAuthority('system:user:info')")
public SysUser getUserInfo(Authentication authentication) {
String username = authentication.getName();
return sysUserService.getUserByUsername(username);
}
}
4.4.6 启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 资源服务器启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class ResourceServerApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceServerApplication.class, args);
}
}
4.5 全流程调用示例
- 获取授权码浏览器访问地址:
http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=demo-client&scope=openid profile&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/demo-client
跳转至登录页面,输入用户名admin,密码Admin@123456,完成登录后确认授权,浏览器会重定向至配置的地址,携带授权码参数code。
- 通过授权码获取令牌使用Postman或curl发送POST请求:
curl --location --request POST 'http://127.0.0.1:9000/oauth2/token' \
--header 'Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXRAMTIzNDU2' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=上一步获取的授权码' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8080/login/oauth2/code/demo-client'
请求成功后,返回访问令牌与刷新令牌:
{
"access_token": "eyJraWQiOiI4N2YwYjE5OC0zYjUxLTRkYjgtOGYwOC0wYjE5OGM3YjUxNDQiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "MlU5aDdYb1k5aDdYb1k5aDdYb1k5aDdYb1k5aDdYb1k",
"scope": "openid profile",
"token_type": "Bearer",
"expires_in": 900
}
- 携带令牌访问资源接口
curl --location --request GET 'http://127.0.0.1:8080/api/user/info' \
--header 'Authorization: Bearer 上一步获取的access_token'
请求成功后,返回用户信息。
- 通过刷新令牌重新获取访问令牌
curl --location --request POST 'http://127.0.0.1:9000/oauth2/token' \
--header 'Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXRAMTIzNDU2' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=上一步获取的refresh_token'
五、生产环境最佳实践与安全加固
5.1 令牌安全管理
- 访问令牌必须设置短时效,建议15分钟以内,刷新令牌设置长时效,建议7天以内
- 刷新令牌必须实现一次性使用机制,使用后立即失效,防止重放攻击
- JWT必须使用非对称加密算法RS256,禁止使用对称加密算法HS256
- JWT载荷中禁止存放敏感信息,如用户密码、手机号等,仅存放必要的身份标识
- 令牌必须通过HTTPS传输,禁止在HTTP协议下传输,防止令牌被窃听
5.2 客户端安全管控
- 公共客户端(前端、移动端)必须使用授权码模式+PKCE,禁止使用客户端密钥
- 机密客户端(后端服务)的客户端密钥必须使用高强度随机字符串,定期轮换
- 严格限制redirect_uri的范围,禁止使用通配符,防止授权码被恶意拦截
- 客户端必须开启授权确认机制,防止用户被诱导授权
5.3 权限管控最佳实践
- 严格遵循最小权限原则,为每个客户端、每个用户分配最小必要的权限
- 采用RBAC+ABAC混合权限模型,实现细粒度的接口权限与数据权限管控
- 资源服务器必须做本地权限校验,不能完全依赖授权服务器的令牌校验
- 权限变更必须实时生效,支持动态权限刷新机制
5.4 安全防护与审计
- 对授权端点、令牌端点添加限流、防暴力破解机制,防止恶意攻击
- 所有认证、授权、令牌发放、资源访问操作必须记录完整的审计日志,便于追溯
- 开启CSRF防护,防止跨站请求伪造攻击
- 定期进行安全漏洞扫描与渗透测试,及时修复安全隐患
六、常见问题与踩坑指南
- 令牌校验失败核心原因包括:授权服务器与资源服务器的issuer配置不一致、公钥私钥不匹配、令牌已过期、签名算法不支持、系统时间不同步。解决方法:逐一校验配置项,确保issuer地址完全一致,使用非对称加密时公钥配置正确,同步服务器系统时间。
- 权限注解不生效核心原因包括:未开启@EnableMethodSecurity注解、用户权限未正确加载到SecurityContext中、AOP代理配置错误。解决方法:在配置类上添加@EnableMethodSecurity注解,校验JWT转换器中权限的加载逻辑,确保Spring AOP正常代理。
- 跨域配置不生效核心原因:Spring Security的过滤链优先级高于Spring MVC的跨域配置,仅配置WebMvcConfigurer的跨域规则无效。解决方法:在SecurityFilterChain中统一配置CORS规则,确保跨域配置在Spring Security中生效。
- 授权码模式重定向报错核心原因:客户端配置的redirect_uri与请求中的redirect_uri不完全一致,包括协议、域名、端口、路径的差异,或包含多余参数。解决方法:确保请求中的redirect_uri与数据库中配置的redirect_uri完全一致,禁止使用通配符。
- JWT载荷过大导致请求头溢出核心原因:JWT中存放了过多的权限信息、用户信息,导致请求头超出服务器的最大限制。解决方法:JWT中仅存放用户ID等必要标识,权限信息在资源服务器中通过用户ID实时查询,减少载荷体积。