若依框架 SpringSecurity :基础配置源码全拆解(附图解)
本文详细解析了基于RBAC模型的Spring Security权限控制实现,重点介绍了若依框架中的安全配置机制。主要内容包括:1)RBAC核心概念(用户-角色-权限多对多关系);2)方法级权限注解(@Secured/@PreAuthorize)及Spring EL表达式应用;3)关键Bean的注入过程(AuthenticationManager/BCryptPasswordEncoder/Secu
一、前置知识: RBAC
- RBAC(Role-Based Access Control,基于角色的访问控制)是目前最主流的权限设计模型,核心是给用户分配角色,给角色配置权限,用户通过角色间接拥有权限,而非直接给用户绑定权限,实现用户 - 角色 - 权限的解耦。
-
用户(User):系统的操作主体(如登录的管理员、普通用户);
-
角色(Role):权限的集合(如管理员、运营、普通会员,一个角色包含多个权限);
-
权限(Permission):系统的操作权限(如菜单访问、按钮点击、接口调用,如
user:add、order:query)。 - 核心逻辑:用户 ↔ 角色(多对多,一个用户可拥有多个角色)、角色 ↔ 权限(多对多,一个角色可配置多个权限),用户最终的权限是其所有角色的权限合集。
二、配置类解读

配置类在framework下的config包中的SecurityConfig
三、注解解析
3.1 配置类上的注解
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig
@Configuration- 这是 Spring 框架的核心注解,用来标记这个类是一个配置类。Spring 会自动扫描并加载该类(注册Bean),将其内部定义的
@Bean方法生成的对象纳入 Spring 容器管理。
- 这是 Spring 框架的核心注解,用来标记这个类是一个配置类。Spring 会自动扫描并加载该类(注册Bean),将其内部定义的
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)- 作用是开启方法级别的安全校验。
p rePostEnabled = true:启用@PreAuthorize、@PostAuthorize等注解,支持在方法执行前 / 后进行权限判断。securedEnabled = true:启用@Secured注解,这是一种更简洁的权限校验方式
- 作用是开启方法级别的安全校验。
3.2 方法权限注解对比
| 特性 | @Secured | @PreAuthorize | @PostAuthorize |
|---|---|---|---|
| 核心作用 | 方法执行前校验用户角色 / 权限 | 方法执行前进行复杂权限校验 | 方法执行后校验返回值或权限 |
| 开启方式 | @EnableMethodSecurity(securedEnabled = true) |
@EnableMethodSecurity(prePostEnabled = true) |
@EnableMethodSecurity(prePostEnabled = true) |
| Spring EL 支持 | ❌ 不支持,仅能做简单角色匹配 | ✅ 完全支持,可写复杂逻辑 | ✅ 支持,可校验返回值(returnObject) |
| 典型语法 | @Secured({"ROLE_ADMIN"}) |
@PreAuthorize("hasAuthority('admin') and hasIpAddress('192.168.1.0/24')") |
@PostAuthorize("returnObject.userId == authentication.principal.id") |
| 执行时机 | 方法执行前 | 方法执行前 | 方法执行后、返回结果前 |
| 适用场景 | 简单的角色校验场景 | 复杂的权限、IP、参数校验 | 需要校验方法返回值的场景(如确保用户只能查看自己的数据) |
| 灵活性 | 低 | 高 | 高 |
3.3 Spring EL
- Spring EL本质是一个返回布尔值(true/false)的判断语句
- 引用内置对象,获取 Spring Security 上下文里的预设数据(当前登录用户、认证信息等),作为权限判断依据直接书写内置对象名称,通过「
.」链式访问属性(底层自动调用 getter 方法)。 -
调用内置方法,所有 Spring Security 权限相关的 EL 内置方法,都定义在
SecurityExpressionRoot,直接书写预设方法名,括号内传入对应参数(通常是权限 / 角色字符串). -
引用方法参数,获取当前被注解方法的入参,做参数合法性校验,或关联用户信息确保资源访问的安全性(如用户只能操作自己的数据),通过「
#+ 参数名」直接引用方法入参,支持继续通过「.」访问参数对象的属性。 -
校验返回值(仅
@PostAuthorize专属),方法执行完成后,校验返回结果的合法性,只有符合条件才返回结果,否则拦截(防止敏感数据泄露),通过专属关键字returnObject引用方法返回值,再配合运算符或内置方法做判断。
//1. 引用内置对象
// 引用 authentication 对象,判断当前用户是否已认证(未登录用户会被拦截)
@PreAuthorize("authentication.isAuthenticated()")
public List<User> getUserList() {
// 业务逻辑
}
//2. 调用内置方法
// 校验当前用户是否拥有 "user:query" 权限
@PreAuthorize("hasAuthority('user:query')")
public List<User> queryUser() {
// 业务逻辑
}
//3. 引用方法参数
// 校验入参 id 是否大于 0(参数合法性校验)
@PreAuthorize("#id > 0")
public User getUserById(Long id) {
// 业务逻辑
}
//4. 校验返回值(仅 @PostAuthorize 专属)
// 校验返回的订单数据,所属用户 ID 与当前登录用户 ID 一致(只能查看自己的订单)
@PostAuthorize("returnObject.userId == authentication.principal.userId")
public Order getOrderById(Long orderId) {
// 业务逻辑:从数据库查询订单并返回
return orderMapper.selectById(orderId);
}
注解的使用在用户登录篇讲解
四、Bean 的注入
4.1 AuthenticationManager的注入
/**
* 身份验证实现
*/
@Bean
public AuthenticationManager authenticationManager()
{
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return new ProviderManager(daoAuthenticationProvider);
}
-
AuthenticationManager- 它是 Spring Security 的认证入口,负责接收认证请求(比如用户名 + 密码),并委托给对应的
AuthenticationProvider去完成实际的认证逻辑。 - 代码中创建了
ProviderManager(AuthenticationManager的默认实现类)
- 它是 Spring Security 的认证入口,负责接收认证请求(比如用户名 + 密码),并委托给对应的
-
UserDetailsService- 它是 Spring Security 中负责加载用户信息的核心接口,由我们自己实现,放在用户登录篇重点解析。
- BCryptPasswordEncoder
- 是 Spring Security 提供的一个密码加密工具类,它使用 BCrypt 哈希算法,每次加密同一个密码都会生成不同的结果。
4.2 BCryptPasswordEncoder的注入
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
密码的加密使用在用户登录篇讲解,现在只是了解使用这个加密方式
4.3 SecurityFilterChain的注入
/**
* 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.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/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();
}
Spring Security 的核心底层就是一条「过滤器链(Filter Chain)」
注入了SecurityFilterChain,在默认情况下,所有请求都会经过Spring Security过滤器链(引入依赖也可以开启过滤器链,不过注入的方式可以自定义规则)。
- 设置白名单(白名单是登录后不管什么用户都可以访问)。
- 设置忽略名单(忽略名单是不需要登录也可以访问的,白名单和忽略名单都不需要权限认证)。
- 添加自定义过滤器。
五、白名单的设置
白名单的设置我们定位到下面这一句代码
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
我们再根据这个方法追踪到PermitAllUrlProperties
/**
* 设置Anonymous注解允许匿名访问的url
*
* @author ruoyi
*/
@Configuration
public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware
5.1 PermitAllUrlProperties接口的实现
-
ApplicationContextAware接口-
让当前 Bean 能够获取到 Spring 的
ApplicationContext容器实例。 -
下面接口实现需要用到容器,所以实现了这个接口。
-
-
InitializingBean接口-
这个接口是 Spring 提供的初始化回调接口,当 Bean 的所有属性都被 Spring 容器注入完成后,会自动调用afterPropertiesSet(),可以在这里做一些依赖属性的初始化操作,比如读取配置、初始化连接池等,确保在 Bean 完全准备好后再执行。
-
5.2 获取白名单
@Override
public void afterPropertiesSet()
{
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(info -> {
HandlerMethod handlerMethod = map.get(info);
// 获取方法上边的注解 替代path variable 为 *
Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns())
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
// 获取类上边的注解, 替代path variable 为 *
Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPatternsCondition().getPatterns())
.forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
});
}
RequestMappingHandlerMapping是 Spring MVC 用来管理所有@RequestMapping接口的核心类,通过getHandlerMethods()获取系统中所有接口的映射关系,拿到一个包含所有接口 URL 和对应处理方法的集合。AnnotationUtils.findAnnotation检查当前接口方法是否标记了@Anonymous,如果存在,就获取该接口的 URL 路径,并用正则把路径变量(如/user/{id})替换成通配符*,变成/user/*,把处理后的 URL 加入到白名单列表urls中。
最后想要调用url就直接
public List<String> getUrls()
{
return urls;
}
5.3 @Anonymous
这个注解是若依自己定义的,并不是Spring Security提供的。
/**
* 匿名访问不鉴权注解
*
* @author ruoyi
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous
{
}
思考:这是不是AOP?
- 标准 AOP(比如
@Around、@Before)是请求到达时,针对单个接口方法动态拦截(运行时、每次请求都可能触发)。 - 扫描
@Anonymous的逻辑是项目启动时一次性执行(初始化阶段、只执行一次),目的是收集数据,而非拦截请求。
5.4 放行白名单
-
requests.antMatchers(url).permitAll()就是放行的操作。
六、忽略名单的设置
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
这里将静态资源和登录相关操作设置为忽略名单。
七、log out filter
// 添加Logout filter
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
追踪到LogoutSuccessHandlerImpl
/**
* 自定义退出处理类 返回成功
*
* @author ruoyi
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Autowired
private TokenService tokenService;
/**
* 退出处理
*
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
// 记录用户退出日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
}
}
-
获取用户信息从请求中通过
tokenService拿到当前登录的LoginUser对象。 -
校验用户有效性如果用户信息不为空,才执行后续操作。
-
清理用户缓存通过
tokenService删除该用户在 Redis 中的登录缓存记录,使其令牌失效。 -
记录退出日志异步记录用户的登出行为到日志中,不影响主流程响应速度。
-
返回成功响应向前端返回一个登出成功的 JSON 响应。
7.1 @Configuration
@Configuration底层包含@Component,所以它能让LogoutSuccessHandlerImpl被自动注册为 Bean,从而可以被 Spring Security 的配置类注入和使用。严格来说,这个类是一个业务处理类而非配置类,用@Component或@Component的衍生注解(如@Service)会更符合语义。- RuoYi 框架在处理这类安全相关的处理器(如登录、登出)时,习惯用
@Configuration来标记,目的是让这些类能被 Spring 容器扫描并管理,同时也和框架内其他配置类的风格保持统一。
7.2 getLoginUser
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
private String getTokenKey(String uuid)
{
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
- 提取 Token:从
HttpServletRequest中获取 JWT 令牌,判空无效则直接返回null。 - 解析 Token:验证并解析 Token 得到
Claims,从中提取用户唯一标识uuid。 - 查询 Redis:用
uuid生成 Redis 键,从缓存中取出对应的LoginUser对象并返回。 - 异常兜底:解析或查缓存出错时,记录错误日志,最终返回
null。 - uuid的存入是在用户登录时操作的,我们放在用户登录篇来讲。
7.3 LoginUser
/**
* 登录用户身份权限
*
* @author ruoyi
*/
public class LoginUser implements UserDetails
{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
@JSONField(serialize = false)
@Override
public String getPassword()
{
return user.getPassword();
}
@Override
public String getUsername()
{
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired()
{
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked()
{
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled()
{
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return null;
}
//构造方法和getter,setter省略
}
我们在4.1中提到了AuthenticationManager,需要setUserDetailsService,若依中用UserDetailsServiceImpl实现了UserDetailsService接口,重写了loadUserByUsername方法,返回值就是UserDetails,而LoginUser实现了UserDetails接口。
- Spring Security 会用它的
getPassword()、getUsername()来完成账号密码校验。 - 认证通过后,这个对象会被存入
SecurityContext,作为后续请求的 “身份凭证”。 - 具体的细节我们放在用户登录篇来说,这里先清楚UserDetails的作用。
7.4 delLoginUser
/**
* 删除用户身份信息
*/
public void delLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
登出删除redis中的key。
思考:为什么要先去redis中获取user信息,再根据user信息再去删除对应的删除的缓存,不能一步到位直接删吗?
- 第一步user信息是为了保证redis中确实有这个用户,这个登出的操作是有效的,因为我们进行删除后都会进行日志记录,如果这个用户本身不存在,虽然不会报错,但是日志中会多无用信息。
八、JWT filter
// 添加JWT filter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
每一次请求前都要校验Jwt令牌,所以继承了OncePerRequestFilter。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
- 获取user的过程和log out一样,这里不再重复说明了。
- 验证 Token 有效性。
- 构建 Spring Security 认证对象,存入安全上下文。
- 放行请求,继续执行后续过滤器链。
8.1 校验令牌的有效性
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser 登录信息
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY)
{
refreshToken(loginUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
- 令牌是有有效期的,如果用户还没有登出,这时令牌就已经失效了,这是不行的,所以在每次发出请求时,都会进行jwt有效期的检验,如果时间不够了,就在redis中重新进行刷新。
8.2 Spring Security 安全上下文
- Spring Security 的
SecurityContextHolder默认是基于ThreadLocal实现的,它的生命周期只在当前请求线程内有效。请求结束后,线程被回收,上下文也会被清理,所以下一次请求必须重新设置。每一次发送请求就是一个线程,就是一个ThreadLocal,就有一个安全上下文,就要把用户信息放进去。 - 这样我们在后面的service层或者其他地方想要用到我们的用户信息,就可以直接从threadlocal里面直接获取了。
- 接下来我们扒一下源码看看是不是基于threadlocal实现的。
打开SecurityContextHolder。
private static SecurityContextHolderStrategy strategy;
private static void initialize() {
initializeStrategy();
++initializeCount;
}
private static void initializeStrategy() {
if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
Assert.state(strategy != null, "When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy");
} else {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL";
}
if (strategyName.equals("MODE_THREADLOCAL")) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
} else if (strategyName.equals("MODE_GLOBAL")) {
strategy = new GlobalSecurityContextHolderStrategy();
} else {
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
}
点开后面strategy后面具体的实现。
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal();
//省略
}
底层确实用到了ThreadLocal。
九、图解

十、后续内容
10.1 用户登录篇
若依框架 SpringSecurity :用户登录源码全拆解(附图解)-CSDN博客
10.2 动态菜单篇
更多推荐


所有评论(0)