Spring Security基础:我是如何在项目中快速搭建安全认证系统的
开头:为什么我要写这篇文章?

作为一名在互联网公司工作的后端开发工程师,我经常遇到这样的情况:在一个新的Spring Boot项目开始时,老板或产品经理总是一脸严肃地说,“这个系统需要登录、权限控制,用户分为管理员和普通用户”。这时候,作为一个熟悉Java生态的开发者,我的第一反应就是——用Spring Security来实现认证授权功能。
不过,说起来容易做起来难。很多刚接触Spring Security的同学常常会觉得“这玩意儿太复杂了”,配置项太多、类名都看不懂;而一些经验尚浅的团队可能会选择自己手写一套鉴权逻辑,结果越往后越不好维护。
今天我想结合我们最近做的一个内部管理系统项目,分享一下我是如何用Spring Security快速搭建出安全认证系统的,包括我在实际工作中踩过的坑、解决的问题,以及一些实用的小技巧。
项目背景和挑战

我们做的系统是一个面向企业内部员工使用的运营支持平台,主要功能包括查看业务数据、导出报表、审批流程等。项目是基于Spring Boot 2.7 + MyBatis Plus + MySQL构建的,前端使用Vue3 + Element Plus。
这个系统一开始只是一个简单的CRUD应用,但随着需求变化,老板突然决定要加入“多角色权限管理”、“单点登录支持”、“接口级别的权限控制”这些特性,并且希望能在两周内上线。
这时候问题来了:
- 我们团队有新人,对Spring Security不太熟;
- 原来的代码里没有考虑权限控制;
- 不同接口对应的角色权限不同,怎么统一处理?
- 登录成功后的Token要怎么生成和验证?
- 还有,以后如果接入SSO或者LDAP怎么办?
面对这些问题,我和组长商量了一下,最终决定采用Spring Security + JWT + Role-based Access Control(RBAC)模型的方式来设计整个安全体系。
解决方案详解

第一步:引入Spring Security依赖
我们在pom.xml中添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
虽然Spring Security默认会拦截所有请求并强制登录,但我们可以通过配置来自定义自己的行为。
第二步:自定义登录流程
我们的系统采用JWT作为Token机制,所以传统的Form Login并不适用。因此我们需要自定义一个Filter来接管登录请求。
这里我写了两个类:
JwtAuthenticationFilter: 拦截/login接口,进行用户名密码校验JwtAuthorizationFilter: 在每个请求进入Controller之前校验Token是否合法
示例:登录过滤器
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserLoginRequest login = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequest.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((UserDetails) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
response.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
// 可以返回更多用户信息
}
}
这个类做的事情其实很简单:获取用户的登录信息,进行身份验证,然后生成Token,通过响应头返回给客户端。
示例:鉴权过滤器
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && validateToken(token)) {
String username = extractUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader(HEADER_STRING);
if (header != null && header.startsWith(TOKEN_PREFIX)) {
return header.replace(TOKEN_PREFIX, "");
}
return null;
}
}
这段代码的作用是在每次请求到来时解析Token,验证合法性,并将用户信息绑定到当前线程上下文中,这样后面的Controller就可以通过@AuthenticationPrincipal或SecurityContextHolder来获取当前登录用户的信息。
第三步:配置SecurityConfig
接下来就是在配置类中把上面的Filter注册进去,并设置URL的访问权限。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilterBefore(new JwtAuthorizationFilter(), UsernamePasswordAuthenticationToken.class)
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
这一段看起来简单,但实际上有很多可以优化的地方,比如:
- 角色前缀问题:默认情况下Spring Security要求ROLE_开头,如果不加这个前缀会匹配不到权限;
- URL路径设计:建议按模块划分URL路径,比如
/api/admin/*表示管理员相关接口; - 异常处理:Token过期或签名错误需要统一拦截处理,而不是直接抛异常导致500错误;
后面我会详细讲这些细节。
第四步:数据库设计与RBAC模型
为了支持更灵活的权限控制,我们设计了基于RBAC模型的表结构:
sys_user: 用户表sys_role: 角色表sys_permission: 权限表(例如:user:view、report:export)sys_user_role: 用户与角色关系表sys_role_permission: 角色与权限关系表
这套结构的好处在于:
- 用户可以通过角色间接获得权限;
- 后续扩展新权限时,只需新增Permission记录即可;
- 权限可以细化到接口级别,非常灵活。
对应的,在Spring Security中我们可以将权限映射为GrantedAuthority对象,传入Authentication对象中,从而实现细粒度的权限控制。
第五步:接口级别的权限控制
除了在URL路径上做权限限制外,我们还用了Spring Security的注解方式来做更细粒度的控制。
例如在Controller方法上加上:
@PreAuthorize("hasAuthority('user:view')")
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
这种写法可以让权限控制更加直观,同时也便于后期维护。
当然,为了使用这个功能,你还需要在配置类上加上:
@EnableGlobalMethodSecurity(prePostEnabled = true)
遇到的坑和解决方案
说实话,在这个过程中也踩了不少坑,下面列出几个比较典型的:
1. Token失效后前端不知道,还在继续请求
这个问题很常见。前端拿到Token之后,通常会存到localStorage或Vuex中,当Token失效后继续请求API,后台返回401,前端却无法感知。
解决方案:我们在Response中加了一个自定义字段,比如:
{
"code": 401,
"message": "未授权,请重新登录",
"data": null
}
前端可以在axios拦截器中统一处理这类状态码,提示用户重新登录。
2. 使用内存存储用户信息不适合生产环境
我们最开始为了快速开发,用了Spring Security的In-Memory User Details Service:
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(User.withDefaultPasswordEncoder()
.username("admin").password("123456").roles("ADMIN").build());
}
这种方式在测试阶段没问题,但一旦上线,肯定要用数据库中的真实数据。
于是我们后来改成了实现UserDetailsService接口的方式:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) {
SysUser user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(getRolesAndPermissions(user)));
}
private String[] getRolesAndPermissions(SysUser user) {
List<String> rolesAndPerms = new ArrayList<>();
for (SysRole role : user.getRoles()) {
rolesAndPerms.add(role.getCode()); // ROLE_ADMIN
for (SysPermission perm : role.getPermissions()) {
rolesAndPerms.add(perm.getCode());
}
}
return rolesAndPerms.toArray(new String[0]);
}
}
这样,用户信息就来自数据库了,后续也方便扩展。
3. JWT密钥硬编码不安全
我们最开始把JWT的加密Key写死在代码里:
private static final String SECRET_KEY = "my-secret-key";
这显然不够安全,因为别人拿到源码就能知道密钥,从而伪造Token。
我们后来的做法是:
- 将密钥通过配置中心注入,避免明文暴露;
- 设置密钥轮换机制,定期更新(这部分还没做完,后续规划中);
4. 登录失败没有提示具体原因
最开始我们只是扔了一个AuthenticationException出去,前端只能看到"Bad credentials",但这对我们排查问题帮助不大。
后来,我们做了统一的异常处理类:
@ControllerAdvice
public class AuthExceptionAdvice {
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<?> handleAuthenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("认证失败");
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDeniedException() {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("权限不足");
}
}
这样不管是密码错误还是权限不足,都能返回合适的提示。
实施效果与收益
这套安全机制上线后,整体运行稳定,基本达到了预期目标:
- 用户登录后能正常获取Token;
- 不同角色的用户只能访问被授权的接口;
- 系统安全性得到了保障;
- 为将来接入SSO打下了良好基础;
- 新人更容易理解项目的权限结构,减少沟通成本;
从运维的角度来看:
- 接口级权限控制让我们更方便地监控和审计谁访问了什么资源;
- 统一的Token校验机制降低了重复代码量;
- 数据库权限模型让权限分配变得可视化和可配置化;
一些小建议和注意事项
如果你正在准备使用Spring Security来实现认证授权功能,我有几点建议可以分享给你:

1. 不要一开始就追求完美,先跑通再优化
特别是新手,不要想着一次性完成所有功能。先把登录流程打通,确保Token能正确生成、解析,再逐步加上权限控制和RBAC模型。
2. 理清楚Filter、Authentication、UserDetailsService之间的关系
Spring Security的Filter链非常强大,但也复杂。你可以把它想象成一连串关卡,每道关卡负责处理不同的任务。搞明白各个Filter的作用和调用顺序,会让你少走很多弯路。
3. 把权限抽象为字符串,而不是硬编码在代码里
权限最好是可配置的,比如存在数据库里,这样修改起来不需要改代码。而且Spring Security也推荐用这种方式来做权限控制。
4. 给前端提供良好的错误提示和状态码
不要让前端同学在控制台看到一堆403 Forbidden但不知道具体原因。尽量返回明确的提示语句,比如“您无权访问该接口”。
5. 日志要详细,方便排错
我们当时就在日志中加了很多TraceId、用户名、访问路径等信息,方便定位权限问题。例如:
[TRACE-ID: 12345] [USER: admin] [PATH: /api/admin/delete-user] [PERMISSION: 'user:delete'] -> SUCCESS
总结

通过这次项目实践,我对Spring Security的理解更加深入,也意识到它远比我们平时用到的要强大得多。它不仅仅是用来做登录和权限控制的工具,更是一个完整的安全框架。
在这个过程中,我也收获了很多教训:
- 认证和授权不是一时兴起的事情,要早做设计;
- 安全性不是最后一刻才考虑的事情,而是从架构层面就要兼顾;
- 一个好的权限模型,能让你在日后维护中省去大量麻烦;
最后送大家一句话:安全不是银弹,但Spring Security是你值得信赖的好伙伴。希望这篇分享对你有所帮助,如果你有任何问题或想法,欢迎留言交流!

评论 0