前后端分离项目做了大半年,接口写了不少,但安全这块一直有点虚——登录接口裸奔、权限全靠前端按钮隐藏、稍微懂点技术的人就能直接往后台接口发请求。前阵子终于下定决心把SpringSecurity系统性地接进项目里,前后折腾了一周多,踩了十来个坑,总算是跑通了。这一章我就把这套方案的完整落地过程拆开讲清楚,包括为什么前后端分离下不能照着老写法抄、JWT这块怎么接、过滤器链怎么配、以及前后端对接时最容易翻车的地方。
适合正在做前后端分离项目、Spring Boot基础没问题、但Security一直停留在"会配个登录页"阶段的同学参考。文章以实战为主,原理只讲必须知道的,代码可以直接抄,但每个关键位置我都会解释为什么这么写。
1. SpringSecurity在前后端分离场景下的三个核心变化
1.1 传统配置为什么在前后端分离里"失灵"了
用SpringSecurity写过传统服务端渲染项目的同学应该都知道,最初的用法很固定:配置一个formLogin,写一个UserDetailsService,配好密码加密器,然后Security就接管了登录跳转、会话维持、csrf防护这一整套流程。浏览器访问受保护资源时没登录,Security会直接给你重定向到登录页,登录成功后跳回原来的页面,全程由服务端控制页面流转。
这套机制在前后端分离的架构下几乎没法用。前端是SPA,资源和接口完全分开部署,前端拿到的是一堆静态文件,服务端返回的应该是结构化数据,而不是重定向指令或者HTML页面。你试着把传统的配置搬过来就会发现,浏览器请求接口没带登录态时,收到的不是JSON格式的"请先登录",而是一个302跳转,前端根本没法处理。
再一个问题是Session机制。前后端分离后服务端天然要做水平扩展,多实例部署是常态,Session存在单机内存里意味着用户第一次请求落在A机器,第二次请求被负载均衡转发到B机器,登录态就丢了。要么引入Redis做共享Session,要么干脆换成无状态Token。这两个问题叠在一起,决定了前后端分离项目里的SpringSecurity必须做三件事:禁用Session、关闭Csrf、所有认证失败的响应改回JSON格式。
1.2 认证方式从Session换到Token的完整链路
换成Token之后,认证链路变成了这样:用户在登录页输入用户名密码,前端把数据POST到后端登录接口,后端校验用户名密码,校验通过后签一个Token字符串返回给前端,前端把Token存下来,之后每次请求都在Header里带上“Authorization: Bearer 一串token”,后端请求入口的过滤器解析Token、校验合法性,通过就把用户信息放进SecurityContext里,接口就可以直接用@PreAuthorize这类注解做权限控制了。
这个链路里,后端不保存任何登录状态,每次请求都是独立的,Token本身就是用户身份的凭证。只要Token设计得合理(签名校验、过期时间),就能做到和Session一样的效果,同时天然支持水平扩展——任何一台服务器都能独立校验Token,不需要共享存储。
1.3 这一整套改造改动范围到底有多大
明确一下改动范围。涉及的东西不算少:Spring Security的配置类要重写、要新增一个JWT工具类、要写一个Token解析过滤器、自定义的401/403响应处理器、用户实体和权限模型可能要调整、登录接口改造、前端所有请求要加Header,还要处理跨域。听着多,但每块代码量都不大,核心加起来也就几百行。后面几节我按顺序一步步展开,每块都给出可以直接复制的代码,以及对应的解释。
2. 认证方案选型:落地前必须想清楚的几个决策
2.1 JWT、OAuth2、Session方案怎么选
动手写配置之前,得先把方案定下来。很多初学者上来就问JWT怎么写,我建议你先想清楚自己的场景。如果你的项目就是单体的Spring Boot应用、前端是一个后台管理系统或者普通Web应用、没有第三方应用需要授权登录,那JWT是最轻量也最合适的选择。
OAuth2适合的是"开放平台"场景,比如你要给第三方开发者提供授权接口,或者用户可以用微信/QQ登录你的应用,这种情况才需要引入一套完整的授权服务器和资源服务器体系,复杂度明显高一个级别。很多教程把OAuth2和JWT混在一起讲,实际项目里做自用接口认证,直接用JWT就好,不需要把OAuth2那套客户端、授权码模式都拉进来。
至于Session方案,如果你明确知道服务端不会水平扩展、用户量就几千、也不打算把接口开放给非浏览器客户端,那老一套的Session机制真的可以继续用,没必要为了"前后端分离"强行上JWT。我做方案的时候对比过,Session方案实现成本大概是JWT的三分之一,省掉的功夫很可观。但考虑到后续要扩展接口给小程序用,最终还是选了JWT。
2.2 JWT工具类里必须有哪几个方法
定了JWT之后,工具类要写的方法也就四五个:生成Token、解析Token、校验Token是否有效、从Token里取用户名、从Token里取权限列表。其中校验方法内部就是解析Token、检查过期时间、再对比一下签名,逻辑不复杂。
有个容易踩的坑:JWT的payload只是Base64编码,不是加密,任何人拿到Token解码一下就能看到里面的内容。所以不要把密码、手机号这类敏感信息往payload里塞,只放用户名、用户ID、角色/权限列表这些非敏感信息就够了。密钥要单独配置在配置文件里,不能写死在代码中。
2.3 要不要引入Redis做Token黑名单
JWT有一个特性是"签发后没法主动失效",用户改密码了、后台把用户封了,已经签出去的Token在过期之前依然能用。这个问题要不要处理,完全取决于你的业务严不严格。
我做的时候先判断了一下:一个后台管理系统,用户量不大,Token过期时间设置成8小时,后台封号需求不频繁,所以没有做黑名单。如果你是做支付、交易这类强安全场景,可以引入Redis,把"需要被立即失效的Token"存进去,过滤器解析Token时先查一下黑名单,在不在里面。这个方案实现也很简单,但要注意黑名单的Key也得设置过期时间,时间等于Token剩余有效期,否则内存里会堆大量垃圾Key。
3. 核心代码落地:过滤器、配置类与登录接口
3.1 依赖引入与项目基础结构
先用Maven把依赖加了。这里我用的Spring Boot版本是2.7.x,对应的Spring Security是5.7.x,写法上已经用了新版的lambda配置风格。如果你用的是Spring Boot 3.x,Security也升到了6.x,大部分代码通用,只有少数配置方法名不同(比如antMatchers改成了requestMatchers),后面讲到的时候我会单独标注。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>注意jjwt拆成了三个包,api是编译期用的,impl和jackson是运行时用的,scope不能漏了。
3.2 JwtUtil工具类的完整写法
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secret); return Keys.hmacShaKeyFor(keyBytes); } public String generateToken(String username, Collection<? extends GrantedAuthority> authorities) { Map<String, Object> claims = new HashMap<>(); claims.put("authorities", authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); Date now = new Date(); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(now) .setExpiration(new Date(now.getTime() + expiration)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); } public String getUsernameFromToken(String token) { return parseToken(token).getSubject(); } public boolean validateToken(String token) { try { parseToken(token); return true; } catch (ExpiredJwtException e) { log.warn("Token已过期"); return false; } catch (JwtException | IllegalArgumentException e) { log.warn("Token解析失败"); return false; } } }配置项在application.yml里:
jwt: secret: "你的Base64编码密钥,至少256bit,可以用openssl rand -base64 64生成" expiration: 28800000expiration这里是毫秒,28800000就是8小时。密钥建议用工具生成一段随机Base64字符串,绝对不要用短字符串。
3.3 认证过滤器:Token校验与SecurityContext写入
这是整个改造的核心。写一个过滤器,继承OncePerRequestFilter,保证一个请求只执行一次。逻辑分三步:从Header取Token、解析Token拿到用户名、用户名不为空且当前SecurityContext里没有认证信息时,加载用户并把认证信息塞进去。
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); String token = null; String username = null; if (StringUtils.hasText(header) && header.startsWith("Bearer ")) { token = header.substring(7); } if (token != null && jwtUtil.validateToken(token)) { username = jwtUtil.getUsernameFromToken(token); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(request, response); } }注意这里有个顺序问题:必须先校验Token合法,再查数据库把UserDetails取出来。Query一下数据库是必要的,因为用户可能被删了、权限变了、被禁用了,这些信息在Token里是旧的,必须以数据库为准。有的教程直接拿Token里的用户名拼一个Authentication塞进去,那样做权限变更不会实时生效,而且用户被删除后Token依然能用,安全上有漏洞。
3.4 SecurityConfig配置类:Session关掉、白名单放行、异常处理
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private UserDetailsService userDetailsService; @Autowired private RestAuthenticationEntryPoint authenticationEntryPoint; @Autowired private RestAccessDeniedHandler accessDeniedHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .antMatchers("/api/auth/login", "/api/auth/register").permitAll() .antMatchers(HttpMethod.GET, "/api/articles/**").permitAll() .anyRequest().authenticated()) .exceptionHandling(ex -> ex .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .cors(); return http.build(); } }逐行解释几个关键配置。
csrf关闭:前后端分离项目用Token认证,Token在Header里,不再依赖Cookie,CSRF攻击的根基就不存在了,不关反而会拦截POST请求导致莫名其妙的403。
sessionManagement设置STATELESS:告诉Security别再创建Session了,也别从Session里读登录态。这一步一设,传统模式才算真正切换成无状态模式。
authorizeHttpRequests这一段是白名单配置。登录接口、注册接口必须放行,否则用户连登录都做不了。如果项目里有验证码接口、Swagger文档接口、静态资源路径,也都放这里。注意放行GET方法还可以精确控制到路径。
addFilterBefore:把自定义的JwtAuthenticationFilter放在UsernamePasswordAuthenticationFilter前面。这是Spring Security过滤器链的固定位置,保证Token认证在框架自带的用户名密码过滤器之前先执行。
cors():开启跨域配置,配合下面的CorsConfigurationSource。
3.5 登录接口与AuthenticationManager的使用
登录接口不能用传统的formLogin了,要自己写一个Controller接收JSON。核心是调用AuthenticationManager完成认证,认证成功就拿UserDetails生成Token返回前端。
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @PostMapping("/login") public Result<?> login(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword()) ); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); String token = jwtUtil.generateToken( userDetails.getUsername(), userDetails.getAuthorities()); return Result.success(new LoginResponse(token, userDetails.getUsername())); } }这里authenticationManager.authenticate会依次调用UserDetailsService.loadUserByUsername和PasswordEncoder.matches来完成校验。校验失败会抛出BadCredentialsException,配合全局异常处理器返回"用户名或密码错误"的JSON。
3.6 自定义401与403处理器
传统模式下Security默认会把未认证用户重定向到登录页,前后端分离项目里必须换成JSON输出。两个组件:一个是AuthenticationEntryPoint,管未登录(401);一个是AccessDeniedHandler,管已登录但没权限(403)。
@Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录状态已过期\"}"); } }@Component public class RestAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"没有权限访问该资源\"}"); } }这两个处理器一开始最容易被漏掉。我第一版没写,调试时发现接口返回的是HTML格式的"访问被拒绝",前端根本没法解析。加上之后,所有Security框架产生的异常都能以统一的JSON格式返回,前端拦截器只需要判断HTTP状态码是401还是403就行。
4. 用户信息与权限模型:从数据库到注解授权
4.1 用户表设计与UserDetailsService实现
Security本身不关心你的用户表长什么样,它只认UserDetails接口。所以第一步是让自己的用户实体实现UserDetails,或者单独写一个UserDetails实现类,从数据库查出用户后包装一下。
最省事的做法,用户实体直接实现UserDetails:
public class User implements UserDetails { private Long id; private String username; private String password; private String nickname; private String role; private boolean enabled; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return Collections.singletonList(new SimpleGrantedAuthority(role)); } // getUsername、getPassword、isEnabled 直接返回对应字段 // isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired 返回true }角色字段role我直接用了"ROLE_ADMIN"、"ROLE_USER"这种标准格式。注意Spring Security在hasRole("ADMIN")判断时,要求权限字符串必须以ROLE_前缀开头。如果你不用hasRole,而是用hasAuthority,那就不需要前缀,可以自定义成"sys:user:add"这种细粒度权限。
UserDetailsService实现类:
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return user; } }4.2 接口权限控制:@PreAuthorize注解的使用
SecurityConfig里已经加了@EnableGlobalMethodSecurity(prePostEnabled = true),这意味着可以在Controller方法上直接写权限注解。这是最常用的授权方式,比在SecurityConfig里逐个配URL更灵活。
@RestController @RequestMapping("/api/admin") public class AdminController { @GetMapping("/users") @PreAuthorize("hasRole('ADMIN')") public Result<?> listUsers() { return Result.success(userService.listAllUsers()); } @DeleteMapping("/users/{id}") @PreAuthorize("hasAuthority('sys:user:delete')") public Result<?> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return Result.success(); } }hasRole和hasAuthority的区别前面提到了,一个是ROLE_前缀,一个不带。建议统一用一种风格,别混着用,不然权限字符串维护起来很乱。
4.3 密码加密:为什么必须用BCrypt
密码存储绝对不能是明文,这一点在前后端分离项目里更要注意——因为注册接口是公开的,任何人都可以调。Spring Security自带的BCryptPasswordEncoder是目前最稳的选择,它每次生成的hash都不一样,同密码两次加密结果不同,这是因为内部自动混入了随机盐。所以校验时必须调用matches(rawPassword, encodedPassword),不能直接equals比较。
有个细节:用BCrypt时密码长度不能超过72字节,否则会抛异常。正常用户密码都不会超,但如果有做第三方系统同步数据的话要注意。
5. 前后端对接的完整细节
5.1 前端Token存储:localStorage还是Cookie
Token拿到手后放哪里,这个问题前端同学一定要想清楚。localStorage的优点是代码简单、读取方便,缺点是任何XSS脚本都能偷走。Cookie配合HttpOnly属性安全性更高,但容易受到CSRF影响,而且前后端分离下Cookie跨域处理更麻烦。
做内部管理系统,多数团队选localStorage,配合前端代码的XSS过滤基本够用。如果项目对安全性要求很高,可以调研一下Cookie+CSRF防护的成熟框架(如双击提交令牌等)。这事没有标准答案,但记住一点:无论放哪,都不要把Token放在URL参数里传递,会出现在日志里,泄露风险极大。
5.2 请求拦截器统一携带Token
前端在每个请求的Header上带Token,这一步用Axios拦截器最顺手:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });注意格式必须和服务端过滤器里预期的一致,Header名是Authorization,前面带Bearer加一个空格,然后才是Token字符串。很多对接问题都出在这里,前端只传了token,后端起头校验Bearer前缀匹配不上,直接返回401。
5.3 响应拦截器处理401和403
axios.interceptors.response.use( response => response, error => { if (error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } else if (error.response.status === 403) { // 提示"没有权限" console.error('无权访问该资源'); } return Promise.reject(error); } );有一件事要提前想好:Token过期后,前端是直接跳登录页还是走刷新Token流程。简单项目直接跳登录页最省事,用户体验差一点但实现成本最低。追求体验的项目可以加一个Refresh Token机制,登录时返回两个Token,访问Token过期后用刷新Token去换新Token,这个方案实现细节比较多,后面有机会可以单独开一章讲。
5.4 CORS配置的正确姿势
前后端分离必须解决跨域,否则浏览器会拦截所有非GET请求。在SecurityConfig里写一个CorsConfigurationSource的Bean:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(Collections.singletonList("http://localhost:5173")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Collections.singletonList("*")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }注意setAllowedOriginPatterns不要用setAllowedOrigins加通配符*,尤其是当allowCredentials为true时,通配符和credentials不能共存,Spring会直接报错。用setAllowedOriginPatterns可以写通配符,内部会处理兼容问题。
还有一个坑:开启了CORS之后,预检请求OPTIONS会先到达服务端,Security过滤器链必须让OPTIONS请求通过,否则预检直接被拦截,前端调接口也调不通。由于我们配置里放行了登录接口和查询接口,多数情况OPTIONS不会被打断,但如果遇到奇怪的405,可以先往白名单里加上antMatchers(HttpMethod.OPTIONS, "/**").permitAll()试一下。
6. 常见问题排查与避坑实录
6.1 过滤器没生效,所有请求都放行了
这种情况八成是SecurityConfig里的addFilterBefore写错位置,或者自定义过滤器没注册成Bean。我排坑时的经验:先在过滤器里加一行日志输出,如果请求进来完全没有日志,那就不是过滤器逻辑问题,而是整个过滤器根本没被Spring Security的过滤器链加载。检查两点:一是@Configuration是否被扫描到,二是过滤器是否加了@Component。
6.2 登录成功后访问接口还是401
这就很典型了,Token生成成功,前端也存了,但访问接口时没带Authorization头。先打开浏览器开发者工具看Network里的请求头,有没有Authorization: Bearer xxx。如果没有,问题在前端拦截器;如果有但后端还是401,拿Token去jwt.io手动解一下看是不是过期了。
6.3 接口返回401但响应体是HTML
自定义的AuthenticationEntryPoint没生效。这个Handler必须写@Component让Spring管理,并且在SecurityConfig里通过exceptionHandling配置。有些人把Handler写在SecurityConfig里用new创建,也能生效,但遇到了多次Spring代理问题导致不生效的情况,统一建议注册成Bean,由Spring容器管理。
6.4 密码加密后登录一直报错"用户名或密码错误"
注册时密码存库用的是new BCryptPasswordEncoder().encode(rawPassword),登录校验用的是authenticationManager.authenticate,框架内部会调用matches。这两个环节用的PasswordEncoder必须一模一样,我在SecurityConfig里定义了passwordEncoder这个Bean,确保全局只有一个实例。出现这个问题最大嫌疑是有两处地方手动new了PasswordEncoder,导致算法参数不一致。
6.5 权限注解不生效,hasRole('ADMIN')直接放行
@EnableGlobalMethodSecurity(prePostEnabled = true)没加,或者加在了一个没被Spring扫描的配置类上。权限注解依赖AOP代理,这个开关相当于开启注解权限校验的入口。没加这个注解时Controller上的@PreAuthorize会被静默忽略,接口所有人都能访问,这个问题肉眼很难发现。
6.6 退出登录别忘清理前端状态
无状态架构下,退出登录最直接的方式是前端删除本地Token,然后跳转到登录页。服务端不需要做任何事,Token下次请求就不会再携带了。如果想更严谨一点,服务端可以维护一个Token黑名单或者缩短Token有效期,但大多数内部系统删除LocalStorage就够了。有个细节:退出接口本身如果你做了登录保护,那请求退出的这个Token在服务端不校验也不行,所以退出登录接口通常建议放行,或者干脆不放行但对已删Token的访问不做拦截。
7. 一个简化的完整权限模型参考
最后给一个可以直接套用的最小权限模型。用户表、角色表、用户角色表三张表,用户通过角色获得权限,角色在UserDetails里以ROLE_前缀的GrantedAuthority返回。这个模型够覆盖后台管理系统的绝大多数需求。如果需要细粒度权限,在用户表和角色表之间再加一层权限表,然后把权限字符串作为hasAuthority的入参来用。
表结构:
create table sys_user ( id bigint primary key auto_increment, username varchar(50) unique not null, password varchar(100) not null, nickname varchar(50), enabled tinyint default 1 ); create table sys_role ( id bigint primary key auto_increment, role_code varchar(50) not null comment '例如 ROLE_ADMIN' ); create table sys_user_role ( user_id bigint not null, role_id bigint not null );对应的UserDetails实现把角色查出来之后,组装成SimpleGrantedAuthority集合。后续做菜单权限、按钮权限,都可以在此基础上扩展。实际开发中我第一次做的时候把权限模型设计成了五张表,加上部门和岗位,结果开发周期拉得很长。后来发现对于大多数业务系统,用户-角色两级就够用了,等确实有了细粒度权限要求再升级也不迟。
SpringSecurity这套东西,原理书上看十遍不如在项目里跑一遍。我这一周踩的坑基本上都是"配置顺序不对"和"组件没有交给Spring管理"这两类。如果你把上面这些代码完整跑通,再遇到的问题大概率都能在第六节找到方向。真排不掉的话,建议从过滤器链上打印日志开始查,Security的过滤器链是开放的,在哪一步丢了认证信息一目了然。