☰
若依-代码学习01
2026/9/28 4:37:15 网站建设 项目流程

登录

问题

解释一下这三行代码

AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); recordLoginInfo(loginUser.getUserId());

为什么要记录两遍日志?

以及authentication.getPrincipal()这个代码的作用是?

解释

三者区别(一句话)

行写到哪目的

第 1 行

sys_logininfor(异步)

登录审计流水

第 2 行

内存对象

拿到当前登录用户

第 3 行

sys_user

更新该用户最近登录 IP/时间

之后才会执行tokenService.createToken(loginUser)生成 JWT 返回给前端。

token是uuid?

问题

我发现若依的token是随机uuid?

/** * 创建令牌 * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); setUserAgent(loginUser); refreshToken(loginUser); Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); return createToken(claims); }

解释

不完全是“随便发个 UUID 当令牌”。我先把createToken/ JWT / Redis 这几段串起来看。

对了一半:IdUtils.fastUUID()生成的是 Redis 会话键,不是最终返回给前端的那个 token。

两层“token”

名字实际是什么作用

String token = IdUtils.fastUUID()

随机 UUID

作为 Redis key 的一部分,关联整份LoginUser

return createToken(claims)

JWT 字符串

发给前端,Header 里带的那个

流程是:

1. 生成 uuid
2. Redis: login_tokens:{uuid} = LoginUser(权限、用户信息、过期时间等)
3. 把 uuid 放进 JWT 的 claims(login_user_key)
4. 用 secret 做 HS512 签名,得到真正的 JWT 返回前端

对应代码:

public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); // ① Redis 会话标识 loginUser.setToken(token); setUserAgent(loginUser); refreshToken(loginUser); // ② 写入 Redis Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); // ③ uuid 塞进 JWT claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); return createToken(claims); // ④ 返回签名后的 JWT }

请求进来时反过来:

解析 JWT → 取出里面的 uuid → 用 uuid 去 Redis 取 LoginUser

为什么这样设计

  • JWT 本身很“瘦”,不塞完整用户/权限(改权限可直接改 Redis,不用等 JWT 过期)。
  • UUID 只是会话 ID,随意生成没问题,安全靠:
    1. JWT 有secret签名,伪造不了;
    2. Redis 里有会话数据,删了就登出(强制下线也靠这个)。

所以:UUID 是会话钥匙;前端拿到的是包着这把钥匙的 JWT。

若依token校验拦截器?

问题

那当前项目是不是 都会有一个拦截器,如果没有token校验通过,就会直接阻拦?

解释

是的,思路对,但精确说:不是 MVC 拦截器,而是 Spring Security + JWT 过滤器。

两层配合

1.JwtAuthenticationTokenFilter(过滤器)

每个请求都会进这个 Filter:从 Header 取 JWT → 解析出 uuid → 去 Redis 取LoginUser→ 放进SecurityContext。

注意:它本身不会直接 return 拦截,没 token / token 无效时只是不往 SecurityContext 里塞用户,然后继续往下走。

2.SecurityConfig(真正决定拦不拦)

requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated();
  • 白名单(登录、注册、验证码、静态资源等):permitAll,不需要登录。
  • 其余全部:authenticated(),SecurityContext 里没有认证用户就拦截。

拦截后由AuthenticationEntryPointImpl返回 401,提示认证失败。

请求流程(简化)

请求进来
→ JwtFilter:有有效 token?有则写入 SecurityContext
→ Security 规则:
白名单 → 放行
其他 → 已认证?放行 : 401 拦住

补充

业务接口上还有@PreAuthorize等做权限校验(能登录 ≠ 有某个菜单/按钮权限)。那是第二道门,第一道是上面的登录态认证。

A01-菜单表的perms字段?

问题

我不太懂sys_menu表中的perms这个字段是干啥的。(都长这个样子)【system:role:list】

解释

perms是权限标识(permission string),用来做按钮/接口级鉴权,不是用来画菜单的。

它存什么

一段字符串,约定俗成写成:模块:功能:操作

例如:

  • system:user:list— 用户列表
  • system:user:add— 用户新增
  • system:user:edit— 用户修改

目录(M)一般为空;菜单(C)常带*:list;按钮(F)才是各类操作权限的主力。

它怎么用

登录后系统会按「用户 → 角色 → 菜单」查出该用户拥有的所有perms,放进权限集合。之后两处比对这个字符串:

  1. 后端接口:@PreAuthorize("@ss.hasPermi('system:user:add')")
    没有对应perms就调不通接口。
  2. 前端按钮:v-hasPermi="['system:user:add']"
    没有对应perms就隐藏「新增」等按钮。

两边写的字符串,必须和sys_menu.perms里配置的一致。

和菜单的关系(容易混)

概念作用

path/component

能不能看到、打开哪个页面

perms

能不能点某个按钮、调某个接口

所以:能进「用户管理」页,不等于能新增用户;新增还得角色勾了带system:user:add的那条按钮菜单。

一句话:perms= 权限开关的名字;角色勾了哪些菜单,用户就拿到哪些名字,前后端拿这些名字做鉴权。

A02-那为什么目录是空的?

问题

如果sys_menu表中的menu_type如果是M(即代表目录的时候),perms这个字段是空的?

解释

因为目录本身不对应任何可执行操作,只负责把菜单组织成树。

在若依里:

  • 目录M:只是分组(如「系统管理」),没有页面、没有接口,所以不需要perms,留空即可。
  • 菜单C:对应一个页面,常配xxx:list,用来控制「能不能进这个功能/查列表」。
  • 按钮F:对应具体操作(增删改导出等),perms才是真正被前后端拿去鉴权的。

权限实际是挂在菜单和按钮上的。角色勾选时,勾了某个页面下的按钮,就拿到那些perms;父级目录空着不影响——它既不参与@PreAuthorize,也不参与v-hasPermi。

简单说:目录只负责“归类显示”,不负责“能不能操作”,所以perms为空是正常设计。

以下是 目录和菜单的一一对应

树形结构代码-解析

代码

01-selectMenuTreeByUserId

@Override public List<SysMenu> selectMenuTreeByUserId(Long userId) { List<SysMenu> menus = null; if (SecurityUtils.isAdmin(userId)) { menus = menuMapper.selectMenuTreeAll(); } else { menus = menuMapper.selectMenuTreeByUserId(userId); } return getChildPerms(menus, MENU_ROOT_ID); }

02-getChildPerms

/** * 根据父节点的ID获取所有子节点 * * @param list 分类表 * @param parentId 传入的父节点ID * @return String */ public List<SysMenu> getChildPerms(List<SysMenu> list, long parentId) { List<SysMenu> returnList = new ArrayList<SysMenu>(); for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();) { SysMenu t = (SysMenu) iterator.next(); // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 if (t.getParentId() == parentId) { recursionFn(list, t); returnList.add(t); } } return returnList; }

03-recursionFn

/** * 递归列表 * 作用是:得到该父节点的直系子节点列表 * @param list 分类表 * @param t 子节点 */ private void recursionFn(List<SysMenu> list, SysMenu t) { // 得到该父节点的直系子节点列表 List<SysMenu> childList = getChildList(list, t); t.setChildren(childList); // 判断孩子有没有孩子 for (SysMenu tChild : childList) { if (hasChild(list, tChild)) { recursionFn(list, tChild); } } }

问题

你能否举个具体的实际例子,然后我看一下排序的整个过程

解释

好,用一组具体菜单数据把整条链路走一遍。这里的「排序过程」指的是递归建树时一步步怎么挂节点(getChildList本身不按orderNum排序,顺序取决于查库时列表的先后)。


1. 初始扁平列表list

假设从数据库查出来是这样(已按order_num排好):

序号menuIdmenuNameparentId

0

1

系统管理

0

1

100

用户管理

1

2

101

角色管理

1

3

1001

用户查询

100

4

1002

用户新增

100

5

2

系统监控

0

6

200

在线用户

2

调用:getChildPerms(list, 0)—— 找根节点(parentId == 0)。


2.getChildPerms:先找顶级节点

遍历list,parentId == 0的有两个:

  • 系统管理(1)→ 调用recursionFn(list, 系统管理),再加入结果
  • 系统监控(2)→ 调用recursionFn(list, 系统监控),再加入结果

3. 对「系统管理」做recursionFn(逐步展开)

第 1 层:t = 系统管理(1)

getChildList → parentId == 1 的节点

→ [用户管理(100), 角色管理(101)]

setChildren → 系统管理.children = [用户管理, 角色管理]

接着遍历这两个子节点:


第 2 层-A:tChild = 用户管理(100)

hasChild→ 有(1001、1002)→ 再递归:

getChildList → parentId == 100

→ [用户查询(1001), 用户新增(1002)]

setChildren → 用户管理.children = [用户查询, 用户新增]

再看叶子:

  • 用户查询:hasChild= false → 停
  • 用户新增:hasChild= false → 停

第 2 层-B:tChild = 角色管理(101)

hasChild→ 没有子节点 → 不递归,children保持空/未再挂


此时「系统管理」这棵子树已经成型:

系统管理(1)

├── 用户管理(100)

│ ├── 用户查询(1001)

│ └── 用户新增(1002)

└── 角色管理(101)


4. 对「系统监控」做recursionFn

getChildList → parentId == 2

→ [在线用户(200)]

setChildren → 系统监控.children = [在线用户]

在线用户:hasChild = false → 停

系统监控(2)

└── 在线用户(200)


5. 最终返回结果

[

系统管理(1)

├── 用户管理(100)

│ ├── 用户查询(1001)

│ └── 用户新增(1002)

└── 角色管理(101),

系统监控(2)

└── 在线用户(200)

]


6. 调用顺序小结(像栈一样)

getChildPerms(parentId=0)

│

├─ recursionFn(系统管理)

│ ├─ setChildren([用户管理, 角色管理])

│ ├─ recursionFn(用户管理)

│ │ ├─ setChildren([用户查询, 用户新增])

│ │ ├─ 用户查询 无子 → 停

│ │ └─ 用户新增 无子 → 停

│ └─ 角色管理 无子 → 停

│

└─ recursionFn(系统监控)

├─ setChildren([在线用户])

└─ 在线用户 无子 → 停

核心就三步循环:找儿子 → 挂到 children → 儿子还有孙子就继续递归。

如果你想看的是orderNum怎么影响最终菜单显示顺序,可以说一下,我再按 SQL/order_num那条线补一版。

全局安全规则SecurityConfig

代码

/** * spring security配置 * * @author ruoyi */ @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) @Configuration public class SecurityConfig { /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 允许匿名访问的地址 */ @Autowired private PermitAllUrlProperties permitAllUrl; /** * 身份验证实现 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Bean protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity // CSRF禁用,因为不使用session .csrf(csrf -> csrf.disable()) // 禁用HTTP响应标头 .headers((headersCustomizer) -> { headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); }) // 认证失败处理类 .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) // 基于token,所以不需要session .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 注解标记允许匿名访问的url .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() // 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); }) // 添加Logout filter .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) // 添加JWT filter .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) // 添加CORS filter .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) .addFilterBefore(corsFilter, LogoutFilter.class) .build(); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } }

问题

讲些一下这个SecurityConfig类需要学习的东西呗。跟接口没关系的吧

解释

对,它本身不是业务接口类(不是 Controller),而是 Spring Security 的全局安全规则配置。
但它会拦所有 HTTP 请求(含/login、业务 API),所以学若依权限体系时绕不开。


这类要学什么

1. 它在整条链路里的位置

请求进来后大致是:

CORS→JWT Filter→Security 鉴权规则→ Controller

SecurityConfig负责定规则 + 挂过滤器,不写具体业务逻辑。

2. 四个核心 Bean / 组件

组件作用

SecurityFilterChain

主配置:CSRF、Session、谁能访问、挂哪些 Filter

AuthenticationManager

登录时做用户名密码认证(给登录服务用)

BCryptPasswordEncoder

密码加密/校验

注入的 Handler/Filter

失败处理、退出、JWT 解析、匿名 URL

3. 为什么这样配(若依常用套路)

  • 关 CSRF:前后端分离 + JWT,不用 Cookie Session,CSRF 意义不大
  • STATELESS:不建 Session,认证信息靠 Token
  • permitAllvsauthenticated:白名单匿名;其余必须登录
  • @EnableMethodSecurity:Controller 上可用@PreAuthorize做按钮/接口级权限

4. 过滤器顺序(很重要)

CorsFilter → JwtAuthenticationTokenFilter → UsernamePasswordAuthenticationFilter

先跨域,再解析 Token 塞进 SecurityContext,后面才能判断“已登录”。

5. 和接口的真正关系

  • 写业务接口:去*Controller
  • 决定“这个 URL 要不要登录 / 匿名”:看这里的authorizeHttpRequests,以及@Anonymous扫出来的permitAllUrl
  • 决定“登录后有没有某个权限”:方法上的@PreAuthorize("hasPermi('xxx')"),由@EnableMethodSecurity打开

所以:不是接口代码,但是接口的门禁配置。


建议学习顺序

  1. 本类:filterChain每一段在干什么
  2. JwtAuthenticationTokenFilter:Token 怎么解析、怎么放进上下文
  3. SysLoginService+AuthenticationManager:登录怎么拿到 Token
  4. @PreAuthorize/ 权限注解:接口级授权

如果你愿意,我可以按这个顺序把JwtAuthenticationTokenFilter也拆开讲一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询