一、前置知识: RBAC

  • RBAC(Role-Based Access Control,基于角色的访问控制)是目前最主流的权限设计模型,核心是给用户分配角色,给角色配置权限,用户通过角色间接拥有权限,而非直接给用户绑定权限,实现用户 - 角色 - 权限的解耦。
  • 用户(User):系统的操作主体(如登录的管理员、普通用户);

  • 角色(Role):权限的集合(如管理员、运营、普通会员,一个角色包含多个权限);

  • 权限(Permission):系统的操作权限(如菜单访问、按钮点击、接口调用,如user:addorder:query)。

  • 核心逻辑:用户 ↔ 角色(多对多,一个用户可拥有多个角色)、角色 ↔ 权限(多对多,一个角色可配置多个权限),用户最终的权限是其所有角色的权限合集。

二、配置类解读

配置类在framework下的config包中的SecurityConfig

三、注解解析

3.1 配置类上的注解

@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig
  • @Configuration
    • 这是 Spring 框架的核心注解,用来标记这个类是一个配置类。Spring 会自动扫描并加载该类(注册Bean),将其内部定义的@Bean方法生成的对象纳入 Spring 容器管理。
  • @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 去完成实际的认证逻辑。
    • 代码中创建了 ProviderManagerAuthenticationManager 的默认实现类)
  • 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 动态菜单篇

若依框架 SpringSecurity :动态菜单源码全拆解(附图解)-CSDN博客

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐