一、分布式架构下的权限体系核心痛点

随着微服务架构的普及,单体应用中耦合的权限模块被彻底拆分,传统基于Session的权限方案面临无法逾越的瓶颈:

  • 多服务重复开发权限逻辑,维护成本指数级上升,权限规则难以统一

  • 跨服务、跨域场景下Session共享方案兼容性差,存在严重的安全隐患

  • 第三方应用接入时,用户密码直接暴露给客户端,无法实现细粒度的权限管控

  • 多端应用(Web、移动端、小程序)接入时,无法实现统一的身份认证与权限管理

统一认证授权体系正是为解决上述痛点而生,而OAuth2.0作为行业公认的开放授权标准,是构建分布式统一权限体系的核心基石。

二、核心概念底层逻辑与易混淆点辨析

2.1 两个核心概念的本质区分

很多开发者在落地时的核心误区,是混淆了认证与授权的边界,二者是完全独立的两个流程:

  • 认证(Authentication) :验证用户身份的合法性,解决「你是谁」的问题,核心是身份校验

  • 授权(Authorization) :验证用户对资源的访问权限,解决「你能做什么」的问题,核心是权限管控

2.2 OAuth2.0的核心设计与角色定义

OAuth2.0是基于令牌的开放授权协议框架,其核心设计思想是解耦认证与授权,让第三方应用无需获取用户账号密码,即可获得用户指定资源的有限访问权限

协议定义了四个不可拆分的核心角色,所有流程均围绕这四个角色展开:

  1. 资源所有者(Resource Owner) :能够授予资源访问权限的实体,通常是终端用户

  2. 客户端(Client) :请求访问资源的应用,需经过资源所有者授权,才能访问对应资源

  3. 授权服务器(Authorization Server) :负责验证资源所有者身份,完成授权后发放令牌的核心服务

  4. 资源服务器(Resource Server) :存储受保护资源的服务,校验令牌合法性后,提供对应资源访问能力

2.3 OAuth2.0授权模式与适用场景

基于RFC6749规范与OAuth2.1最新安全标准,不同授权模式的安全等级与适用场景有明确边界,废弃高风险模式,仅保留生产环境可用的安全模式:

授权模式 核心流程 适用场景 安全等级
授权码模式+PKCE 先获取授权码,再通过授权码换取令牌,PKCE防止授权码拦截 前端单页应用、移动端应用等公共客户端 极高
授权码模式 先获取授权码,再通过授权码换取令牌,全程不暴露用户密码 服务端渲染的Web应用、机密客户端
客户端模式 客户端直接以自身身份向授权服务器申请令牌,无用户参与 服务间通信、系统内部接口调用

注:OAuth2.1规范已正式废弃密码模式与简化模式,二者存在严重的密码泄露、令牌窃听风险,生产环境禁止使用。

授权码模式完整流程

2.4 高频易混淆概念辨析

  1. OAuth2.0 vs JWT OAuth2.0是授权协议框架,定义了授权的流程与规范;JWT是一种轻量级的令牌格式,定义了令牌的结构与编码方式。二者不是竞争关系,JWT可以作为OAuth2.0协议中令牌的载体。

  2. OAuth2.0 vs SSO SSO是单点登录的业务效果,即用户一次登录即可访问多个相互信任的应用系统;OAuth2.0是实现SSO的技术方案之一,CAS、SAML等协议也可实现SSO。

  3. 访问令牌(Access Token) vs 刷新令牌(Refresh Token) 访问令牌是资源服务器校验访问权限的唯一凭证,生命周期短,通常15分钟,直接暴露在网络请求中;刷新令牌用于令牌过期后重新获取访问令牌,生命周期长,通常7天,仅在与授权服务器交互时使用,不暴露给资源服务器,极大降低令牌泄露风险。

三、分布式统一认证授权架构设计

3.1 整体架构分层设计

基于微服务架构的最佳实践,统一认证授权架构采用分层设计,实现职责解耦、高可用、可扩展的核心目标:

各层核心职责:

  1. 用户接入层:面向多端用户的入口,包括Web端、移动端、小程序、第三方应用等

  2. API网关层:全局流量入口,负责令牌透传、路由转发、限流熔断、跨域处理、统一日志等

  3. 授权服务中心:架构核心,负责用户身份认证、授权管理、令牌发放与校验、客户端管理等

  4. 资源服务集群:各业务微服务,即受保护的资源服务器,负责本地细粒度权限校验

  5. 数据存储层:存储用户信息、角色权限数据、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 全流程调用示例

  1. 获取授权码 浏览器访问地址:

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

  1. 通过授权码获取令牌 使用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
}
  1. 携带令牌访问资源接口

curl --location --request GET 'http://127.0.0.1:8080/api/user/info' \
--header 'Authorization: Bearer 上一步获取的access_token'

请求成功后,返回用户信息。

  1. 通过刷新令牌重新获取访问令牌

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防护,防止跨站请求伪造攻击

  • 定期进行安全漏洞扫描与渗透测试,及时修复安全隐患

六、常见问题与踩坑指南

  1. 令牌校验失败 核心原因包括:授权服务器与资源服务器的issuer配置不一致、公钥私钥不匹配、令牌已过期、签名算法不支持、系统时间不同步。解决方法:逐一校验配置项,确保issuer地址完全一致,使用非对称加密时公钥配置正确,同步服务器系统时间。

  2. 权限注解不生效 核心原因包括:未开启@EnableMethodSecurity注解、用户权限未正确加载到SecurityContext中、AOP代理配置错误。解决方法:在配置类上添加@EnableMethodSecurity注解,校验JWT转换器中权限的加载逻辑,确保Spring AOP正常代理。

  3. 跨域配置不生效 核心原因:Spring Security的过滤链优先级高于Spring MVC的跨域配置,仅配置WebMvcConfigurer的跨域规则无效。解决方法:在SecurityFilterChain中统一配置CORS规则,确保跨域配置在Spring Security中生效。

  4. 授权码模式重定向报错 核心原因:客户端配置的redirect_uri与请求中的redirect_uri不完全一致,包括协议、域名、端口、路径的差异,或包含多余参数。解决方法:确保请求中的redirect_uri与数据库中配置的redirect_uri完全一致,禁止使用通配符。

  5. JWT载荷过大导致请求头溢出 核心原因:JWT中存放了过多的权限信息、用户信息,导致请求头超出服务器的最大限制。解决方法:JWT中仅存放用户ID等必要标识,权限信息在资源服务器中通过用户ID实时查询,减少载荷体积。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐