请写一篇关于【Spring Security基础:快速搭建安全认证系统】的技术文章

爬虫不想爬
2026-01-04 20:45
阅读 1007

去年十月,北京的秋天冷得特别早。我裹着那件穿了三年的优衣库摇粒绒,在国贸站挤进10号线时,手机震动了一下——是老婆发来的消息:“妈今天又咳嗽了,药快吃完了。”

那一刻,我站在车厢中间,背包被挤得歪到一边,心里像被人攥了一把。房贷每月8200,房租3500,孩子奶粉一个月1200,老家父母医药费不定……我月薪22k,听起来体面,可扣完五险一金、税、花呗,月底能存下的钱,连给丈母娘买台制氧机都得分期。

那天晚上十一点半,我拖着身子回到家,打开电脑准备改一个紧急bug。突然收到HR的消息:“公司下周开始推行全员远程办公试点,你有兴趣吗?”

我盯着屏幕愣了三秒,手指有点抖。远程?回老家?省下3500房租?
我立刻拨通老婆电话:“喂,如果……我说如果,我能回老家工作,你愿意吗?”
电话那头沉默了几秒,然后她轻声说:“只要你在,哪儿都是家。”

一周后,我退了租,打包行李,带着笔记本和一颗忐忑的心回到了河南小县城。没有CBD,没有深夜加班的灯光,只有院子里那棵老槐树,和每天早上六点准时打鸣的公鸡。

但生活不会因为你换了地方就变得简单。新项目上线在即,我负责后端安全模块——用 Spring Security 搭建一套完整的认证授权系统。说实话,以前在公司都是“调包侠”,复制粘贴别人的配置,跑通就行。可现在一个人在家,没人帮你兜底,出问题只能自己扛。


一、从“Hello World”到“403 Forbidden”:我的安全初体验

项目是个内部管理系统,前端用 Vue 写的,后端是 Spring Boot。老板的要求很朴素:“登录要安全,不同角色能看到不同的菜单,别让人随便删数据就行。”

我一开始想偷懒,直接上 Spring Security 的默认配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .formLogin();
        return http.build();
    }
}

跑起来,浏览器自动跳转到 /login 页面——一个丑到爆的表单,连 CSS 都没加载。更糟的是,前端同事(其实就一个兼职的大学生)发来消息:“哥,我们用的是 JSON 登录,不是表单!”

我这才意识到:Spring Security 默认是面向传统 Web 应用的,而我们是前后端分离架构。前端发的是 POST /api/auth/login,带 JSON 体;后端要返回 token,而不是重定向。

那一刻,我坐在老家客厅的小书桌前,窗外是邻居家孩子的哭闹声,键盘上还沾着中午吃的胡辣汤油渍。我深吸一口气,打开官方文档,开始啃。


二、改造登录:从表单到 JSON + JWT

我决定用 JWT(JSON Web Token)做无状态认证。思路很清晰:

  1. 用户提交用户名密码(JSON)
  2. 后端验证,生成 JWT 返回
  3. 前端保存 token,后续请求带在 Header 里
  4. 后端拦截请求,解析 token,还原用户身份

关键在于自定义认证入口和成功/失败处理器

先写一个 JwtAuthenticationFilter

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = getTokenFromHeader(request);
        if (token != null && jwtUtil.validateToken(token)) {
            String username = jwtUtil.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken auth = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }

    private String getTokenFromHeader(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

然后重写登录逻辑:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(), 
                    request.getPassword()
                )
            );
            UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
            String token = jwtUtil.generateToken(user);
            return ResponseEntity.ok(new JwtResponse(token));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }
    }
}

最后,在 SecurityConfig 中禁用表单登录,加入自定义过滤器:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

搞定!本地测试通过。我兴奋地给前端发消息:“接口好了,试试!”
结果他回:“403 Forbidden。”

我懵了。查日志发现,CORS 跨域问题!前端在 localhost:8080,后端在 localhost:8081,浏览器拦了 OPTIONS 预检请求。

赶紧加 CORS 配置:

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOriginPatterns(Arrays.asList("*"));
    config.setAllowedMethods(Arrays.asList("*"));
    config.setAllowedHeaders(Arrays.asList("*"));
    config.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

再次测试,终于通了。那一刻,我靠在椅子上,看着屏幕上绿色的 200 OK,眼眶有点热。不是因为技术多难,而是在这座小县城的深夜,我一个人,搞定了本该团队协作的事


三、权限控制:不是所有按钮都能点

系统有三个角色:ADMIN、EDITOR、VIEWER。
需求:ADMIN 能删用户,EDITOR 只能编辑内容,VIEWER 只读。

Spring Security 的 @PreAuthorize 注解简直是神器:

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

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.ok().build();
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasAnyRole('ADMIN','EDITOR')")
    public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody UserUpdateDto dto) {
        userService.update(id, dto);
        return ResponseEntity.ok().build();
    }
}

但要注意:默认角色前缀是 ROLE_,所以数据库里存的角色名要是 ROLE_ADMIN,否则 hasRole('ADMIN') 会失效。这个坑我踩了整整两个小时,差点砸键盘。

前端那边也得配合:根据用户角色动态渲染菜单和按钮。比如 Vue 组件里:

<template>
  <div>
    <button v-if="hasPermission('ADMIN')">删除</button>
    <button v-if="hasPermission('ADMIN', 'EDITOR')">编辑</button>
  </div>
</template>

<script>
export default {
  methods: {
    hasPermission(...roles) {
      return roles.includes(this.currentUser.role);
    }
  }
}
</script>

安全不能只靠前端隐藏!必须前后端双重校验。曾有个实习生以为“按钮不显示=不能操作”,结果被人用 Postman 直接调删除接口,数据全没了。血的教训。


四、代码人生:在安全与生活之间找平衡

写这篇文的时候,已经是凌晨一点。孩子睡了,老婆在隔壁房间刷短视频。我泡了杯浓茶,回想这几个月。

远程办公省了房租,多了陪伴家人的时间,但也意味着孤独和责任加倍。没人盯着你打卡,但 bug 必须修;没人催进度,但上线日期不会推迟。

Spring Security 看似只是几行配置,背后却是对“信任边界”的思考:谁可以访问什么?如何证明你是你?怎样防止别人冒充你?
这不也像极了我们的生活吗?
在北漂时,我拼命证明自己“值得留在北京”;回老家后,又担心“会不会被时代抛弃”。
安全的本质,是建立可信的身份和可控的权限——无论是系统,还是人生。


五、给后来者的几点建议

  1. 别迷信默认配置:Spring Security 很强大,但默认行为可能不符合你的架构(比如前后端分离)。
  2. JWT 不是银弹:它适合无状态场景,但无法主动失效 token(除非加黑名单)。敏感操作建议用短期 token + 刷新机制。
  3. 权限最小化原则:给用户刚好够用的权限,别图省事给所有人 ADMIN。
  4. 永远不要只靠前端做权限控制:后端是最后的防线。
  5. 日志很重要:记录谁在什么时候尝试访问了什么,出事才能溯源。

结语:在代码与烟火气之间

上周五晚上,我陪孩子在院子里放烟花。他举着小手喊:“爸爸,你看!像不像你电脑上的绿色对勾?”

我笑了。是啊,200 OK,403 Forbidden,500 Internal Error……这些冰冷的状态码,构成了我谋生的工具,也成了孩子眼中的童话。

房贷还在还,技术也在学。但我不再焦虑了。
因为我知道,无论在北京的地铁里,还是在老家的书桌前,只要代码能跑,日子就能过

Spring Security 教会我的,不只是如何保护一个系统,更是如何在混乱的世界里,守住自己的边界——既不让外界轻易入侵,也不把自己锁死。

共勉。

评论 0

最热最新
暂无评论
爬虫不想爬Lv.1
0
影响力
0
文章
0
粉丝