Skip to main content

概述

LangChat Pro 权限体系基于 Sa-Token 框架实现,采用经典的 RBAC(Role-Based Access Control) 模型,提供完整的认证授权、权限管理、数据权限和审计日志功能。

技术栈

认证框架

  • Sa-Token: 1.44.0 - 轻量级Java权限认证框架

安全加密

  • AES加密 - 密码加密存储

模块组成

┌─────────────────────────────────────────────────────────┐
│           langchat-auth (业务模块)                  │
│  - 用户管理                                         │
│  - 角色管理                                         │
│  - 菜单管理                                         │
│  - 部门管理                                         │
│  - 数据权限                                         │
│  - 操作日志                                         │
└────────────────────┬────────────────────────────────────┘

                     │ 依赖

┌─────────────────────────────────────────────────────────┐
│     langchat-common-auth (公共认证组件)              │
│  - Token配置                                        │
│  - 认证拦截器                                       │
│  - Token服务                                        │
└─────────────────────────────────────────────────────────┘

核心组件

1. langchat-common-auth 模块

TokenConfiguration

路径: langchat-common/langchat-common-auth/src/main/java/cn/langchat/common/auth/config/TokenConfiguration.java 职责: Sa-Token全局配置 配置说明:
@Configuration
public class TokenConfiguration {
    
    @Bean
    @Primary
    public SaTokenConfig getTokenConfig() {
        return new SaTokenConfig()
            .setIsPrint(false)              // 不打印Sa-Token日志
            .setTokenName("Authorization")   // Token名称
            .setTokenPrefix("Bearer")      // Token前缀
            .setTimeout(24 * 60 * 60)    // Token过期时间(24小时)
            .setTokenStyle("uuid")        // Token风格(UUID)
            .setIsLog(false)              // 不记录日志
            .setIsReadCookie(false);      // 不读取Cookie
    }
}
配置参数说明:
参数说明
tokenNameAuthorizationHTTP Header中的Token名称
tokenPrefixBearerToken前缀,用于识别Token类型
timeout86400秒Token有效时长(24小时)
tokenStyleuuidToken生成风格

AuthInterceptor

路径: langchat-common/langchat-common-auth/src/main/java/cn/langchat/common/auth/interceptor/AuthInterceptor.java 职责: 注册认证拦截器 实现代码:
@Component
public class AuthInterceptor implements WebMvcConfigurer {
    
    private final StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 验证码拦截器 - 仅对登录接口生效
        registry.addInterceptor(new CaptchaInterceptor(redisTemplate))
            .addPathPatterns("/auth/login");
        
        // Sa-Token认证拦截器 - 拦截所有请求
        registry.addInterceptor(new SaInterceptor())
            .addPathPatterns("/**");
    }
}
拦截器说明:
  1. CaptchaInterceptor: 对/auth/login接口进行验证码校验
  2. SaInterceptor: 对所有请求进行Token认证

2. langchat-auth 模块

AuthEndpoint

路径: langchat-auth/src/main/java/cn/langchat/auth/endpoint/AuthEndpoint.java 职责: 认证端点,处理登录、注册、登出等操作 核心接口:
接口方法说明
/auth/loginPOST用户登录
/auth/infoGET获取用户信息
/auth/update/infoPOST更新用户信息
/auth/update/passwordPOST修改密码
/auth/logoutDELETE用户登出
/auth/registerPOST用户注册
/auth/email/registerPOST邮箱注册
/auth/email/codeGET获取邮箱验证码
/auth/forgetPOST忘记密码

用户登录流程

@PostMapping("/login")
public R login(@RequestBody UserInfo user) {
    // 1. 参数验证
    if (StrUtil.isBlank(user.getUsername()) || StrUtil.isBlank(user.getPassword())) {
        throw new ServiceException("用户名或密码为空");
    }
    
    // 2. 查询用户
    AigcUser aigcUser = userService.findByName(user.getUsername());
    UserInfo userInfo = BeanUtil.copyProperties(aigcUser, UserInfo.class);
    
    // 3. 状态检查(管理员除外)
    if (!AuthUtil.isAdministrator(user.getUsername()) && !userInfo.getStatus()) {
        throw new ServiceException("该用户已经禁用,请联系管理员");
    }
    
    // 4. 密码验证
    String decryptPass = AuthUtil.decrypt(authProps.getSaltKey(), userInfo.getPassword());
    if (!decryptPass.equals(user.getPassword())) {
        throw new ServiceException("密码不正确");
    }
    
    // 5. Sa-Token登录
    StpUtil.login(userInfo.getId());
    
    // 6. 获取Token信息
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    
    // 7. 存储Session信息
    StpUtil.getSession()
        .set(CacheConst.AUTH_USER_INFO_KEY, userInfo)   // 用户信息
        .set(CacheConst.AUTH_TOKEN_INFO_KEY, tokenInfo);  // Token信息
    
    // 8. 记录登录日志
    OprLog oprLog = LogUtil.build(LogEnum.USER_LOGIN, HttpMethod.POST.name());
    oprLog.setUserId(userInfo.getId());
    oprLog.setUsername(userInfo.getUsername());
    aigcLogService.add(oprLog);
    
    return R.ok(new TokenInfo()
        .setToken(tokenInfo.tokenValue)
        .setExpiration(tokenInfo.tokenTimeout));
}
登录流程图:
┌─────────────┐
│   Client    │
└──────┬──────┘
       │ POST /auth/login
       │ {username, password}

┌─────────────────────────────────────────────────────┐
│           AuthEndpoint.login()              │
└────────────────┬────────────────────────────────┘

                 ├────────────────────────────────┐
                 │                          │
                 ▼                          ▼
         ┌──────────────┐          ┌──────────────┐
         │ 参数验证       │          │ 用户查询       │
         └──────┬───────┘          └──────┬───────┘
                │                         │
                │                         ▼
                │                  ┌──────────────┐
                │                  │  状态检查    │
                │                  └──────┬───────┘
                │                         │
                │                         ▼
                │                  ┌──────────────┐
                │                  │  密码验证    │
                │                  └──────┬───────┘
                │                         │
                └────────────────────────┤


                         ┌──────────────────┐
                         │ Sa-Token登录   │
                         └──────┬─────────┘


                         ┌──────────────────┐
                         │ 生成Token      │
                         └──────┬─────────┘


                         ┌──────────────────┐
                         │ 存储Session    │
                         └──────┬─────────┘


                         ┌──────────────────┐
                         │ 记录登录日志   │
                         └──────┬─────────┘


                         ┌──────────────────┐
                         │ 返回Token      │
                         └─────────────────┘

数据模型

核心实体

AigcUser(用户表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcUser.java
字段类型说明
idString主键
deptIdString部门ID
usernameString用户名
passwordString密码(AES加密)
realNameString真实姓名
sexString性别
emailString邮箱
avatarString头像URL
phoneString手机号
statusBoolean状态(true-有效,false-锁定)
creatorString创建人
createTimeLong创建时间

AigcRole(角色表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcRole.java
字段类型说明
idString主键
nameString角色名称
codeString角色别名(如:admin、user)
descriptionString描述
creatorString创建人
createTimeLong创建时间

AigcMenu(菜单/权限表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcMenu.java
字段类型说明
idString主键
nameString资源名称
parentIdString父级ID(树形结构)
pathString路由地址
permsString权限标识(如:user:list)
typeString菜单类型(button/button、menu/menu)
iconString菜单图标
componentString组件路径
orderNoInteger排序
isDisabledBoolean是否禁用
isExtBoolean是否外链
isKeepaliveBoolean是否缓存
isShowBoolean是否显示

AigcUserRole(用户角色关联表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcUserRole.java
字段类型说明
idString主键
userIdString用户ID
roleIdString角色ID

AigcRoleMenu(角色菜单关联表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcRoleMenu.java
字段类型说明
idString主键
roleIdString角色ID
menuIdString菜单ID

AigcDept(部门表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcDept.java
字段类型说明
idString主键
nameString部门名称
parentIdString父级ID
orderNoInteger排序
createTimeLong创建时间

AigcDataAccess(数据权限表)

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/AigcDataAccess.java
字段类型说明
idString主键
tableString数据表名
aliasString表别名
accessTypeInteger访问类型
userIdString用户ID
deptIdString部门ID
createTimeLong创建时间

RBAC权限模型

权限模型结构

用户(User)

  ├─→ 多对多


角色(Role)

  ├─→ 多对多


菜单/权限(Menu/Permission)

权限关联关系

// 用户 → 角色(多对多)
AigcUserRole: userId + roleId

// 角色 → 菜单(多对多)
AigcRoleMenu: roleId + menuId

// 最终:用户通过角色关联菜单权限

权限验证流程

┌─────────────┐
│   请求       │
└──────┬──────┘
       │ 携带Token

┌─────────────────────────────────────────────────────┐
│         SaInterceptor(拦截)              │
└────────────────┬────────────────────────────────┘

                 │ 验证Token有效性

         ┌──────────────────┐
         │  提取用户ID     │
         └──────┬──────────┘


         ┌──────────────────┐
         │ 查询用户角色    │
         └──────┬──────────┘


         ┌──────────────────┐
         │ 查询角色权限    │
         └──────┬──────────┘


         ┌──────────────────┐
         │  权限验证      │
         │  - 检查菜单权限 │
         │  - 检查按钮权限│
         └──────┬──────────┘


         ┌──────────────────┐
         │  允许/拒绝     │
         └─────────────────┘

工具类

AuthUtil

路径: langchat-auth/src/main/java/cn/langchat/auth/utils/AuthUtil.java 核心方法:
public class AuthUtil {
    
    // 判断是否为超级管理员
    public static boolean isAdministrator();
    
    // 获取超级管理员账号
    public static String getAdministrator();
    
    // 获取Request对象
    public static HttpServletRequest getRequest();
    
    // 获取Response对象
    public static HttpServletResponse getResponse();
    
    // 截取Token(去除Bearer前缀)
    public static String getToken(String token);
    
    // 获取用户信息
    public static UserInfo getUserInfo();
    
    // 获取用户名
    public static String getUsername();
    
    // 获取用户ID
    public static String getUserId();
    
    // 获取部门ID
    public static String getDeptId();
    
    // 获取角色ID集合
    public static List<String> getRoleIds();
    
    // 获取角色名称集合
    public static List<String> getRoleNames();
    
    // 获取权限集合
    public static List<String> getPermissionNames();
    
    // 密码加密(AES)
    public static String encode(String salt, String password);
    
    // 密码解密(AES)
    public static String decrypt(String salt, String password);
}
使用示例:
// 在任何业务代码中获取当前用户信息
String userId = AuthUtil.getUserId();
String username = AuthUtil.getUsername();
List<String> permissions = AuthUtil.getPermissionNames();

// 判断是否为管理员
if (AuthUtil.isAdministrator()) {
    // 管理员拥有所有权限
}

数据权限

数据权限表(AigcDataAccess)

字段说明:
字段说明
table数据表名(如:aigc_knowledge)
alias表别名(如:k)
accessType访问类型(1-全部,2-本部门,3-本部门及以下,4-仅本人)
userId用户ID
deptId部门ID

数据权限类型

类型说明SQL示例
1全部数据-
2本部门数据AND k.dept_id = #{deptId}
3本部门及以下数据AND k.dept_id IN (子部门ID列表)
4仅本人数据AND k.creator = #{userId}

数据权限拦截器

路径: langchat-auth/src/main/java/cn/langchat/auth/filter/KnowledgeAccessFilter.java 职责: 对知识库等资源进行数据权限过滤
@Component
public class KnowledgeAccessFilter {
    
    public void filter(AigcKnowledge query) {
        // 获取当前用户
        String userId = AuthUtil.getUserId();
        
        // 获取用户的数据权限配置
        List<AigcDataAccess> accessList = 
            dataAccessService.getByUserId(userId);
        
        // 应用数据权限
        for (AigcDataAccess access : accessList) {
            if ("aigc_knowledge".equals(access.getTable())) {
                applyDataPermission(query, access);
            }
        }
    }
}

操作日志

OprLog(操作日志)

路径: langchat-common/langchat-common-auth/src/main/java/cn/langchat/common/auth/entity/OprLog.java
字段类型说明
idString主键
userIdString用户ID
usernameString用户名
operationString操作类型
methodString请求方法
paramsString请求参数
timeLong执行耗时
ipString请求IP
statusBoolean执行状态
errorMsgString错误信息
createTimeLong创建时间

日志类型枚举

路径: langchat-auth/src/main/java/cn/langchat/auth/enums/LogEnum.java
public enum LogEnum {
    USER_LOGIN("用户登录"),
    USER_LOGOUT("用户登出"),
    USER_REGISTER("用户注册"),
    USER_UPDATE("用户更新"),
    USER_DELETE("用户删除"),
    ROLE_UPDATE("角色更新"),
    ROLE_DELETE("角色删除"),
    MENU_UPDATE("菜单更新"),
    // ... 更多日志类型
}

日志记录注解

路径: langchat-common/langchat-common-core/src/main/java/cn/langchat/common/core/annotation/ApiLog.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLog {
    
    String value() default "";
    
    String title() default "";
}
使用示例:
@ApiLog("更新用户信息")
@PostMapping("/update/info")
public R updateUserInfo(@RequestBody UserInfo user) {
    // 业务逻辑
}

密码加密

加密算法

使用 AES 加密算法对用户密码进行加密存储。

密码加密流程

原始密码: "password123"

AES加密(使用salt)

加密密码: "tQ66bemoIPntUhOOnhp4yI4z2xwCBZzhSVZZGpNRXSg="

存储到数据库

密码验证流程

用户输入: "password123"

从数据库读取加密密码

AES解密(使用salt)

解密后密码: "password123"

比对验证

验证成功/失败

密码加密示例

// 加密
String plainPassword = "password123";
String salt = "langchat-salt";
String encryptedPassword = AuthUtil.encode(salt, plainPassword);
// 结果: "tQ66bemoIPntUhOOnhp4yI4z2xwCBZzhSVZZGpNRXSg="

// 解密
String decryptedPassword = AuthUtil.decrypt(salt, encryptedPassword);
// 结果: "password123"

会话管理

Session存储结构

Sa-Token的Session存储用户相关信息:
StpUtil.getSession()
    .set(CacheConst.AUTH_USER_INFO_KEY, userInfo)   // UserInfo对象
    .set(CacheConst.AUTH_TOKEN_INFO_KEY, tokenInfo);  // SaTokenInfo对象

UserInfo 结构

路径: langchat-auth/src/main/java/cn/langchat/auth/entity/UserInfo.java
public class UserInfo {
    private String id;
    private String username;
    private String realName;
    private String email;
    private String phone;
    private String deptId;
    private Boolean status;
    
    // 关联信息(登录时填充)
    private List<String> roleIds;      // 角色ID列表
    private List<AigcRole> roles;       // 角色详情
    private List<AigcMenu> perms;      // 权限菜单
}

会话获取示例

// 获取用户信息
UserInfo userInfo = (UserInfo) StpUtil.getSession()
    .get(CacheConst.AUTH_USER_INFO_KEY);

// 使用AuthUtil获取
UserInfo userInfo = AuthUtil.getUserInfo();
String userId = AuthUtil.getUserId();
List<String> permissions = AuthUtil.getPermissionNames();

配置说明

application.yml 配置

langchat:
  auth:
    # 管理员账号
    administrator: langchat
    
    # 加密密钥
    salt-key: langchat-salt
    
    # 邮件配置
    email:
      host: smtp.exmail.qq.com
      port: 465
      from: [email protected]
      pass: email-password

sa-token:
  # Token配置(可在代码中通过TokenConfiguration覆盖)
  token-name: Authorization
  token-prefix: Bearer
  timeout: 86400
  token-style: uuid

权限验证流程

完整验证链路

┌─────────────────────────────────────────────────────────┐
│                  Client Request                     │
│         Header: Authorization: Bearer {token}        │
└────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│            SaInterceptor                        │
│         1. 提取Token                             │
│         2. 验证Token有效性                        │
│         3. 解析用户ID                            │
└────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│          StpUtil.getTokenInfo()                    │
│         获取Token信息和Session                     │
└────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│         AuthUtil.getUserInfo()                    │
│       从Session中获取UserInfo                     │
└────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│          权限判断(@RequiresPermission)         │
│         1. 获取用户权限列表                       │
│         2. 检查是否包含所需权限                  │
└────────────────────┬────────────────────────────────┘


         ┌───────────────────┐
         │   允许访问       │
         │   或             │
         │   拒绝访问       │
         └───────────────────┘

扩展指南

添加新的权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    
    String value();
    
    Logical logical() default Logical.AND;
}

添加新的数据权限类型

  1. AigcDataAccess 表中添加新记录
  2. 实现对应的数据权限拦截器
  3. 在MyBatis Plus的拦截器中应用

添加第三方认证

集成OAuth2.0、LDAP等第三方认证:
@Component
public class OAuth2AuthService {
    
    public R oauth2Login(String code) {
        // 1. 通过code获取access_token
        // 2. 通过access_token获取用户信息
        // 3. 查询或创建本地用户
        // 4. 使用Sa-Token登录
        StpUtil.login(userId);
        return R.ok();
    }
}

最佳实践

1. Token管理

  • Token过期时间: 根据业务需求设置,一般24小时较为合理
  • Token刷新: 实现Token自动刷新机制,提升用户体验
  • Token黑名单: 使用Redis维护Token黑名单,支持强制登出

2. 密码安全

  • 密码强度: 要求密码包含大小写字母、数字、特殊字符
  • 密码过期: 定期要求用户修改密码
  • 密码历史: 防止用户重复使用旧密码

3. 权限设计

  • 最小权限原则: 用户只拥有完成工作所需的最小权限
  • 角色分离: 区分管理员、普通用户、访客等角色
  • 权限分组: 将相关权限分组,便于管理

4. 日志审计

  • 关键操作记录: 记录登录、登出、敏感数据访问等操作
  • 日志保留: 根据合规要求保留日志一定时间
  • 日志分析: 定期分析日志,发现异常行为

5. 安全考虑

  • HTTPS: 生产环境必须使用HTTPS
  • 防XSS: 对输入进行转义和过滤
  • 防CSRF: 使用Token验证防止跨站请求伪造
  • 频率限制: 限制登录尝试次数,防止暴力破解

参考文档