从零搭建一个安全认证系统:我在Spring Security上的实战经验分享

一行代码半杯茶
2025-06-17 09:21
阅读 547

大家好,我是一个有着多年Java后端开发经验的工程师。今天想和大家分享一次在项目中使用 Spring Security 实现用户认证与权限控制的经历。这个过程不仅让我重新梳理了 Spring Security 的核心机制,也让我意识到安全设计在项目初期就应该被重视。

这篇文章会结合我亲身经历的一个实际项目——某金融类管理平台的安全体系构建,来详细讲解如何快速上手 Spring Security,并实现一个基础但实用的安全认证系统。


项目背景与我的困惑

项目背景与我的困惑

去年年初,我们公司接了一个内部管理系统重构的需求。这个系统是为公司多个业务部门服务的后台平台,涉及客户信息、账户审核、权限分配等敏感操作。原本的系统虽然功能齐全,但在安全性方面存在诸多漏洞:

  • 用户登录没有加密
  • 所有接口都不需要鉴权,可以随意访问
  • 没有任何 RBAC(基于角色的权限控制)机制

这些问题让我们技术团队感到非常不安。作为项目的负责人之一,我决定趁这次重构的机会,把系统的安全架构好好设计一下。

当时我们选择了 Spring Boot + Spring Security 的方案。一方面是因为我们的主技术栈就是 Java,另一方面,Spring Security 作为老牌的安全框架,在社区和文档支持上都很成熟。但说实话,刚接手的时候我对它的理解还停留在“加个依赖就能用”的阶段,真正要深入去做,才发现这事儿并不简单。


遇到的挑战

遇到的挑战

1. 权限模型怎么设计?

我们要支持的角色包括:普通用户、业务管理员、风控审核员、超级管理员等。不同角色能操作的模块和资源各不相同。

一开始我打算直接通过角色名来判断是否有权限访问某个接口。比如:

if (user.getRole().equals("ADMIN")) {
    // do something
}

这种方式写起来快,但很快就被测试同学打回来了:“你这么写,如果以后加新角色怎么办?改一堆代码吗?”后来我才明白,权限系统的设计必须具备良好的扩展性和可配置性。

2. 如何统一处理登录流程?

原有的登录逻辑是裸写的 Controller 接收用户名和密码,然后调数据库比对。这样的方式显然不符合安全规范,也无法复用 Spring Security 提供的强大功能,例如自动防爆破、并发限制、Token 登录等等。

我们需要一种既能满足登录流程标准化,又能灵活扩展的方式。

3. 多种认证方式并存的问题

随着项目上线时间临近,产品提出后续可能会支持短信验证码、OAuth 登录等功能。这让我更头疼了:怎么让 Spring Security 支持这些未来的扩展需求?难道每次都要重写整个安全配置?


解决思路与实现方案

解决思路与实现方案

我花了几天时间研究 Spring Security 的官方文档,并结合几个实际案例进行实践。最终,我们采用了以下这套结构清晰、可扩展的认证授权体系。

核心组件分工明确

我们将整个安全模块拆分为以下几个部分:

  • AuthenticationManager:负责身份验证的整体调度
  • UserDetailsService:自定义用户加载逻辑
  • PasswordEncoder:统一加密策略
  • FilterChain:用于拦截请求,执行鉴权逻辑
  • JwtUtils:用于 Token 的生成和校验(后面我们会用 JWT)

分层解耦设计

为了方便后期接入其他认证方式(如 OAuth、短信验证码),我们抽象出 AuthenticationProvider 层,这样不同的登录方式都可以注册进系统中,统一交给 Spring Security 处理。

比如,我们可以先支持账号密码登录,未来再添加 SmsCodeAuthenticationProvider

统一的权限表达方式:基于 MethodSecurityExpression

我们没有直接用 .antMatchers() 来配置 URL 级别权限,而是启用了 @PreAuthorize 注解,通过 SpEL 表达式在方法级别上做权限控制。

这样做的好处是:

  • 权限与业务逻辑绑定紧密,修改时不易遗漏
  • 更适合前后端分离下的接口级权限粒度
  • 易于配合 AOP 做审计日志等操作

数据库设计优化

我们在用户表的基础上,增加了三张关系表:

users (
    id BIGINT PRIMARY KEY,
    username VARCHAR,
    password VARCHAR,
    enabled BOOLEAN
)

roles (
    id BIGINT PRIMARY KEY,
    name VARCHAR
)

user_roles (
    user_id BIGINT,
    role_id BIGINT
)

并在查询时通过 LEFT JOIN 把用户的权限一并查出来,组装成 Spring Security 要求的 GrantedAuthority 对象。


关键代码实现与配置示例

关键代码实现与配置示例

下面我挑几个关键代码片段,让大家直观感受下 Spring Security 的使用方式。

自定义 UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }

        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            authorities
        );
    }
}

安全配置类 SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

使用注解控制权限

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('ADMIN') or #id == authentication.principal.id")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        // ...
    }
}

这段代码的意思是:要么是管理员,要么是你自己,才能访问这个接口。

是不是比 .antMatchers() 的方式更有针对性?


开发过程中踩过的一些坑

1. 忘记启用 @EnableGlobalMethodSecurity 注解

刚开始我只用了 @PreAuthorize,但是发现不起作用,调试了半天才发现是因为没有加上那个注解。建议新手一定要记得启用!

2. 密码加密问题导致登录失败

一开始我没有设置 PasswordEncoder,结果登录一直失败。后来才知道 Spring Security 在比较密码时默认会调用 PasswordEncoder.matches() 方法,如果没有指定,它就会抛异常或者始终不匹配。

3. CORS 和 CSRF 冲突

我们在前端引入了 Vue,并跨域访问后端接口。起初没关掉 CSRF,结果每次 POST 请求都被拦下来了。这个问题折腾了好久,最后才想到去安全配置里关闭 CSRF。


上线后的效果与收益

项目上线后,整体效果还是挺不错的。

  • 用户登录更加安全,所有密码都通过 BCrypt 加密,避免明文存储
  • 权限控制更精细化,可以自由组合角色和菜单项
  • 可扩展性强,后面我们只花半天就接入了短信验证码登录
  • 运维反馈良好,日志中能看到完整的登录行为记录,便于审计排查

更重要的是,我们在后续迭代过程中也没有因为权限问题返工太多。相比之前那种“哪里出错修哪里”的模式,现在整套安全体系已经形成了一个相对稳定的内核。


我的经验总结

如果你也在考虑使用 Spring Security 构建认证授权系统,以下是几点我真心建议的 Tips:

✅ 提前规划好权限模型

权限不是靠写 if 判断来控制的,而应该从数据结构、接口设计、甚至 UI 级别统一规划。推荐使用 RBAC 或者 ABAC 结构,越早搭好底座,越不容易翻车。

✅ 尽量用注解代替 URL 匹配

URL 变化大、维护成本高。使用 @PreAuthorize 更加灵活,特别是在 API 很多的情况下。

✅ 认证流程要预留好扩展口子

不要一开始就把自己局限在账号密码登录这种单一方式里。提前设计好 Provider 的注册机制,未来对接第三方登录就会轻松很多。

✅ 用 JWT 替代 Session(前提是有移动端需求)

如果是前后端分离项目,Session 不太友好。JWT 是一个更现代的选择,而且兼容性更好。当然,要注意 Token 过期、续签、黑名单等机制。


最后一点感悟

其实,做安全这件事情说难也不难,说容易却也有门槛。Spring Security 为我们提供了很好的轮子,但能不能用得好,还是要看对整个认证和授权体系的理解深度。

在我参与过的多个项目中,只有这一次是从头开始就把安全架构做好了的。事后看来,虽然前期花了不少时间调研和设计,但从长远来看是值得的。

如果你还在用裸写的 Controller 做登录,不妨停下来想想:你的用户数据真的足够安全吗?你的系统有没有可能被人绕过接口权限访问?

希望这篇文章能给大家一些启发,少走点弯路。

下次我会分享我们是如何用 Redis 实现 Token 黑名单机制的,有兴趣的朋友可以关注一下。

共勉!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝