15个SpringSecurity实战技巧解析(附代码示例)

SpringBoot+SpringSecurity+JWT整合 SpringSecurity入门简单案例适合新手学习 阅读详情

1. 从零到一:快速搭建你的第一个安全防护

如果你刚开始接触Spring Security,可能会觉得它有点复杂,配置项多得让人眼花缭乱。别担心,我刚开始用的时候也是这种感觉,总觉得这玩意儿是不是有点“杀鸡用牛刀”了。但用久了才发现,它把很多复杂的安全问题都封装好了,我们只需要按需配置就行。今天我就从一个最简单的例子开始,带你一步步搭建一个具备基础认证授权的Web应用。

首先,我们得创建一个Spring Boot项目。现在用IDEA或者Spring Initializr都很方便,记得勾选上 Spring Web 和 Spring Security 这两个依赖。项目创建好后,你会发现什么都不用做,直接启动,访问任何一个接口,浏览器都会弹出一个登录框。这就是Spring Security的默认行为——它已经为你开启了一个最基本的安全防护,所有请求都需要认证,并且提供了一个默认用户(用户名是 user,密码会在控制台打印出来)。

这个默认行为虽然方便,但离我们实际项目需求差得远。我们通常需要自定义用户、定义哪些页面需要登录、哪些可以公开访问。这就得靠我们自己的配置类了。创建一个 SecurityConfig 类,继承 WebSecurityConfigurerAdapter(注意:在Spring Security 5.7+版本,官方推荐使用基于组件的配置,但为了理解原理,我们先从经典的适配器模式开始)。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home", "/css/**", "/js/**").permitAll() // 允许所有人访问的路径
                .antMatchers("/user/**").hasRole("USER") // 需要USER角色
                .antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色
                .anyRequest().authenticated() // 其他所有请求都需要认证
                .and()
            .formLogin()
                .loginPage("/login") // 自定义登录页路径
                .permitAll() // 登录页本身需要允许所有人访问
                .and()
            .logout()
                .permitAll(); // 允许所有人登出
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        // 先在内存中创建两个用户,方便测试
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("zhangsan")
                .password("123456")
                .roles("USER")
                .build();
        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("lisi")
                .password("admin123")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}

这段代码干了这么几件事:首先,我们通过 authorizeRequests() 开始定义访问规则。antMatchers() 用来匹配URL路径,permitAll() 表示完全放行,hasRole() 表示需要特定角色。这里我特意把静态资源(如CSS、JS)也放行了,不然你的登录页可能没有样式。然后,我们配置了表单登录,指定了登录页的路径是 /login,并且这个页面本身也要允许未登录用户访问,否则就死循环了。最后,配置了登出功能。

这里有个踩过的坑:User.withDefaultPasswordEncoder() 这个方法在开发测试时很方便,但它用的是不安全的 PlaintextPasswordEncoder,绝对不要在生产环境使用!我们只是为了快速演示。生产环境的密码加密我们后面会详细讲。

启动项目,访问 /admin/dashboard,你会被重定向到 /login 页面(如果你自己没写这个页面,Spring Security会提供一个非常简陋的默认页面)。用 lisi/admin123 登录,就能成功访问了。如果用 zhangsan/123456 登录,则会看到403禁止访问的错误页,因为张三没有ADMIN角色。你看,基本的角色访问控制(RBAC)就这么搭起来了,是不是比想象中简单?

2. 深入认证核心:自定义你的用户来源

刚才的例子我们把用户信息硬编码在内存里,这显然不现实。真实项目中,用户数据肯定存在数据库里。Spring Security通过 UserDetailsService 这个接口来加载用户信息,我们只需要实现它,从数据库查出用户,然后组装成 UserDetails 对象返回就行。

假设我们有一个简单的用户表,有用户名、密码和角色字段。首先,我们得创建对应的实体和Repository(这里用JPA举例)。

@Entity
public class SysUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role; // 简单起见,一个用户一个角色,如 "ROLE_ADMIN"
    // 省略 getter/setter
}

public interface UserRepository extends JpaRepository<SysUser, Long> {
    SysUser findByUsername(String username);
}

接下来,就是实现核心的 UserDetailsService。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名从数据库查询用户
        SysUser sysUser = userRepository.findByUsername(username);
        if (sysUser == null) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }

        // 2. 将数据库中的角色字符串转换为Spring Security需要的GrantedAuthority集合
        // 注意:如果使用hasRole,这里角色名需要带`ROLE_`前缀。如果数据库里存的是`ADMIN`,这里要加上。
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(sysUser.getRole()));

        // 3. 构建并返回UserDetails对象
        return new User(
                sysUser.getUsername(),
                sysUser.getPassword(), // 数据库里存的应该是加密后的密码
                authorities
        );
    }
}

实现完成后,我们需要在配置类里告诉Spring Security使用我们自定义的 UserDetailsService,并且配置一个密码编码器(PasswordEncoder)来校验密码。修改之前的 SecurityConfig:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希算法,这是目前最推荐的方式
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService) // 设置自定义的UserDetailsService
            .passwordEncoder(passwordEncoder()); // 设置密码编码器
    }

    // ... 其他的HttpSecurity配置保持不变
}

这里有几个关键点和我踩过的坑:

  1. 密码编码器必须匹配:你数据库里存的密码,必须是使用同一个 PasswordEncoder 加密后的结果。比如你注册用户时用 BCryptPasswordEncoder.encode("rawPassword") 加密密码再存库,这里校验时才能对上。如果对不上,登录就会失败。
  2. 角色前缀:注意我在 CustomUserDetailsService 里的注释。hasRole("ADMIN") 在检查时,实际上会去查找名为 ROLE_ADMIN 的权限。所以,要么你在数据库里就存 ROLE_ADMIN,要么在组装 GrantedAuthority 时手动加上 ROLE_ 前缀。而 hasAuthority() 则直接检查权限字符串,不需要加前缀。这个区别很容易搞混,导致配置了角色却总没权限。
  3. AuthenticationManagerBuilder:这里是配置认证管理的地方。我们通过 userDetailsService() 指定用户来源,通过 passwordEncoder() 指定密码校验器。这样,当用户登录时,Spring Security就会自动调用我们的 CustomUserDetailsService 加载用户,并用 BCryptPasswordEncoder 来比对前端传来的密码和数据库存储的哈希值。

3. 玩转授权:方法级与URL级权限控制

权限控制分两个层面:一个是Web请求层面(URL),另一个是方法调用层面。我们之前用 .antMatchers().hasRole() 就是在做URL级别的控制。这很直观,适合保护整个页面或API接口。但有时候,我们需要更细粒度的控制,比如同一个 UserController 的 getUserInfo 方法所有登录用户都能访问,而 deleteUser 方法只有管理员才能调用。这时候就需要方法级安全控制。

首先,要在配置类上加上 @EnableGlobalMethodSecurity 注解来启用它。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ... 其他配置
}
  • prePostEnabled = true:启用Spring EL表达式注解,如 @PreAuthorize。
  • securedEnabled = true:启用 @Secured 注解。

技巧一:使用 @Secured 注解 这个注解比较简单,直接指定角色名。

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

    @GetMapping("/info")
    @Secured("ROLE_USER") // 需要USER角色
    public String getUserInfo() {
        return "用户信息";
    }

    @DeleteMapping("/{id}")
    @Secured("ROLE_ADMIN") // 需要ADMIN角色
    public String deleteUser(@PathVariable Long id) {
        return "删除用户成功";
    }
}

技巧二:使用更强大的 @PreAuthorize 注解 这个注解支持Spring EL表达式,功能强大得多,可以实现复杂的逻辑判断。

@Service
public class OrderService {

    // 只有管理员,或者订单的所有者本人才能查看订单详情
    @PreAuthorize("hasRole('ADMIN') or #order.user.username == authentication.name")
    public Order getOrderDetail(Order order) {
        // ... 业务逻辑
    }

    // 检查用户是否有某个特定的权限(而非角色)
    @PreAuthorize("hasAuthority('ORDER_CREATE')")
    public Order createOrder(Order order) {
        // ... 业务逻辑
    }
}

看到没?在表达式里,authentication.name 可以直接拿到当前登录用户的用户名,#order 可以引用方法的参数。这让我们可以实现基于“数据”的权限控制,非常灵活。

技巧三:结合URL配置,实现双重防护 在实际项目中,我通常会将两者结合。在 SecurityConfig 中配置粗粒度的URL规则作为第一道防线,然后在关键的业务方法上使用 @PreAuthorize 做细粒度的校验。这样即使有人绕过了前端页面直接调用API,我们的方法级安全依然能起作用。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN") // URL层:用户相关API需要登录
            .antMatchers("/api/admin/**").hasRole("ADMIN") // URL层:管理后台API需要管理员
            .anyRequest().authenticated();
    // ... 其他配置
}

然后在 UserController 的删除方法上加 @PreAuthorize("hasRole('ADMIN')")。这样,即使一个普通USER用户通过某种手段构造了删除请求,也会在进入方法前被拦截。

4. 无状态API的守护神:JWT集成实战

现在前后端分离是主流,API通常是无状态(Stateless)的,传统的Session-Cookie机制就不太合适了。JWT(JSON Web Token)成了最流行的解决方案。Spring Security集成JWT,核心思路就是自定义一个过滤器(Filter),放在认证链条里,让它来解析请求头中的Token,并设置安全上下文。

第一步:引入依赖和工具类 首先在 pom.xml 中加入JWT库的依赖,比如Java JWT (jjwt)。

<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>

然后写一个JWT工具类,负责生成、解析、验证Token。

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String jwtSecret;
    @Value("${jwt.expiration}")
    private long jwtExpirationInMs;

    // 生成Token
    public String generateToken(Authentication authentication) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(authentication.getName())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    // 从Token中获取用户名
    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(jwtSecret)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    // 验证Token
    public boolean validateToken(String authToken) {
        try {
            Jwts.parserBuilder().setSigningKey(jwtSecret).build().parseClaimsJws(authToken);
            return true;
        } catch (Exception ex) {
            // 可以记录日志,Token过期、格式错误等
        }
        return false;
    }
}

第二步:创建JWT认证过滤器 这是最核心的一步。这个过滤器会检查每个请求的 Authorization 头,如果有合法的JWT,就手动创建一个 Authentication 对象并放到 SecurityContextHolder 里。

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;
    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            // 1. 从请求头中获取JWT
            String jwt = getJwtFromRequest(request);

            // 2. 验证JWT的有效性
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                // 3. 从JWT中解析出用户名
                String username = tokenProvider.getUsernameFromJWT(jwt);
                // 4. 根据用户名加载用户信息,生成Authentication对象
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                // 5. 将Authentication设置到安全上下文中
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("无法在请求中设置用户认证", ex);
        }

        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }

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

第三步:修改安全配置,接入过滤器 我们需要修改 SecurityConfig,告诉Spring Security使用无状态会话,并把我们的JWT过滤器加到过滤器链中合适的位置(通常是在用户名密码认证过滤器之前)。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .cors().and() // 处理跨域,API项目通常需要
        .csrf().disable() // 无状态API通常禁用CSRF
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
            .and()
        .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll() // 登录注册接口放行
            .anyRequest().authenticated() // 其他所有API都需要认证
            .and()
        // 将JWT过滤器加到UsernamePasswordAuthenticationFilter之前
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

第四步:创建认证接口 最后,我们需要提供一个接口让用户用用户名密码换取JWT。

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

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        // 1. 使用Spring Security的AuthenticationManager进行认证
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );

        // 2. 认证成功,将Authentication存入安全上下文(可选,但有时需要)
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 3. 生成JWT
        String jwt = tokenProvider.generateToken(authentication);

        // 4. 返回Token
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }
}

这样一套下来,一个基于Spring Security + JWT的无状态认证授权系统就搭建完成了。前端在登录后拿到Token,后续请求都在Header里带上 Authorization: Bearer <token>,后端就能识别用户身份并完成权限校验。实测下来非常稳定,也是目前业界最主流的方案之一。

5. 密码安全与加密策略演进

密码安全是系统的生命线。Spring Security在密码处理上给了我们很大的灵活性和很强的工具。最开始我们用了不安全的 withDefaultPasswordEncoder,后来提到了 BCryptPasswordEncoder,但实际生产环境更复杂:我们可能有老系统迁移过来的用MD5加密的密码,新用户又想用更安全的BCrypt,怎么办?Spring Security提供了一个“密码编码器委派”的完美解决方案——DelegatingPasswordEncoder。

你可以把它理解成一个“路由器”或者“分发器”。它根据密码字符串的前缀(ID)来决定用哪个具体的编码器去校验。比如,密码存成 {bcrypt}$2a$10$...,它看到 {bcrypt} 前缀,就会用 BCryptPasswordEncoder 去校验;存成 {MD5}5f4dcc3b5aa765d61d8327deb882cf99,就会用 Md4PasswordEncoder 去校验。

如何配置一个兼容多算法的DelegatingPasswordEncoder?

@Bean
public PasswordEncoder passwordEncoder() {
    // 定义支持的编码器ID和对应的实例
    String idForEncode = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("sha256", new StandardPasswordEncoder()); // 已弃用,仅作示例

    // 创建DelegatingPasswordEncoder,指定默认的编码器ID
    DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
    // 设置一个默认的编码器,用于处理没有ID前缀的密码(比如老系统的遗留密码)
    passwordEncoder.setDefaultPasswordEncoderForMatches(new MessageDigestPasswordEncoder("MD5"));
    return passwordEncoder;
}

在实际业务中如何使用?

  1. 新用户注册/修改密码:直接调用 passwordEncoder.encode(rawPassword)。由于我们默认编码器ID是 bcrypt,它生成的密码会自动带上前缀 {bcrypt},后面跟着BCrypt哈希值。
  2. 老用户登录:假设老用户密码在数据库里是纯MD5哈希值(没有前缀)。当用户登录时,DelegatingPasswordEncoder 发现密码字符串没有 {id} 格式的前缀,就会 fallback 到我们设置的 defaultPasswordEncoderForMatches,也就是MD5编码器去进行匹配。匹配成功后,你可以(也应该)在此时用新的编码器(如BCrypt)重新加密密码并更新数据库,这样下次用户登录就用上新算法了,实现了平滑升级。
@Service
public class AuthService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserRepository userRepository;

    public boolean checkAndUpgradePassword(String rawPassword, User user) {
        // 1. 匹配密码
        if (passwordEncoder.matches(rawPassword, user.getPassword())) {
            // 2. 判断当前存储的密码是否需要升级(检查是否有旧算法前缀或无前缀)
            if (passwordEncoder.upgradeEncoding(user.getPassword())) {
                // 3. 用新算法重新加密并保存
                String newEncodedPassword = passwordEncoder.encode(rawPassword);
                user.setPassword(newEncodedPassword);
                userRepository.save(user);
            }
            return true;
        }
        return false;
    }
}

这个策略非常优雅地解决了历史遗留密码问题和加密算法迭代的难题。我在一个用户量百万级的系统迁移中就用了这个方法,用户无感知地完成了从MD5到BCrypt的升级,整个过程平稳顺利。所以,强烈建议你在任何新项目开始时就直接使用 DelegatingPasswordEncoder,为未来留好扩展性。

6. 应对现代Web安全威胁:CSRF与CORS配置

在开发Web应用,特别是前后端分离的应用时,有两个“C”开头的安全问题是绕不开的:CSRF(跨站请求伪造)和CORS(跨域资源共享)。Spring Security对两者都提供了开箱即用的支持,但配置时需要根据你的架构做出正确选择。

关于CSRF:什么时候该开,什么时候该关? CSRF攻击简单说就是诱骗用户浏览器向已认证的网站发起非本意的请求。Spring Security默认是开启CSRF防护的,它的机制是在Session中存放一个CSRF Token,要求所有状态修改的请求(POST, PUT, DELETE等)都必须携带这个Token。

  • 传统服务端渲染(SSR)应用:应该开启CSRF。因为你的表单由服务端生成,可以很方便地将CSRF Token嵌入到表单的隐藏域中(Thymeleaf、JSP等模板引擎自动支持),或者放到Meta标签里供JS读取。这是最经典的防护模式。
  • 无状态API(如纯JWT方案):通常选择关闭CSRF。原因有二:第一,CSRF防护依赖于Session,而无状态API没有Session。第二,CSRF攻击的本质是借助浏览器的Cookie自动携带机制,而我们的JWT通常是放在请求头(Authorization)里的,标准的CSRF攻击无法构造出自定义请求头。因此,风险较低。这也是为什么我们在JWT配置示例中禁用了CSRF。

如何为传统应用配置CSRF? 其实默认就是开启的,你几乎不需要额外配置。只需要确保你的表单提交包含了Token。

<!-- 在Thymeleaf模板中 -->
<form th:action="@{/logout}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <button type="submit">退出登录</button>
</form>

如果你的前端是JavaScript框架(如React, Vue),你需要从服务端获取这个Token(通常可以通过一个接口返回,或者放在页面的Meta标签里),然后在发起Ajax请求时,将其作为一个Header(通常是 X-CSRF-TOKEN)或者参数带上。

关于CORS:前后端分离的必备配置 CORS是浏览器为了安全而实施的一种同源策略扩展。当前端应用(http://localhost:3000)访问后端API(http://localhost:8080)时,浏览器会先发一个“预检请求”(OPTIONS)来询问服务器是否允许跨域。Spring Security提供了两种方式配置CORS:

方式一:在Spring Security配置中集成(推荐) 这种方式将CORS过滤器整合进了Security的过滤器链,能和安全策略更好地协同。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .cors() // 启用CORS配置
            .and()
        // ... 其他安全配置
}

// 定义一个CorsConfigurationSource Bean
@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://your-production-app.com")); // 允许的源
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的方法
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type", "X-CSRF-TOKEN")); // 允许的请求头
    configuration.setAllowCredentials(true); // 允许携带凭证(如Cookie),如果前端需要,此项很重要
    configuration.setMaxAge(3600L); // 预检请求缓存时间(秒)

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration); // 对所有路径生效
    return source;
}

方式二:使用Spring MVC的 @CrossOrigin 注解 你可以在Controller类或方法上直接加 @CrossOrigin 注解。这种方式更细粒度,但不如全局配置方便统一管理。注意:如果同时使用了Spring Security的CORS配置和 @CrossOrigin,Spring Security的配置优先级更高。

我踩过的一个大坑:当 setAllowCredentials(true)(允许携带Cookie等凭证)时,setAllowedOrigins 不能包含通配符 *,必须明确指定域名列表。否则浏览器会报错。这是W3C规范的要求。所以,在生产环境,务必显式列出可信的源地址。

7. 记住我、会话管理与并发控制

“记住我”(Remember-Me)功能对于改善用户体验很重要,用户关闭浏览器再打开,一段时间内无需重新登录。Spring Security提供了两种实现方式:基于简单哈希的Token和基于持久化存储的Token。

基于简单哈希的Token(默认) 这种方式将用户名、过期时间和密码(的哈希)组合起来生成一个Token,发送给浏览器保存在Cookie中。它的优点是无需数据库存储,但缺点是如果用户修改了密码,之前所有的“记住我”Token都会失效,而且服务端无法主动让某个Token失效。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .rememberMe()
            .key("myAppRememberMeKey") // 必须设置一个密钥,用于增强Token安全性
            .tokenValiditySeconds(86400 * 30); // 记住我30天
}

基于持久化存储的Token(更安全,推荐) 这种方式会在数据库里存一张表(persistent_logins),记录Token序列号、用户名和过期时间。每次校验都查库,安全性更高,并且可以做到让用户“在所有设备上退出登录”这样的功能。

首先,你需要建表(Spring Security有默认的建表SQL,但最好自己控制)。

CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

然后配置使用 JdbcTokenRepositoryImpl。

@Autowired
private DataSource dataSource;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .rememberMe()
            .tokenRepository(persistentTokenRepository())
            .tokenValiditySeconds(86400 * 30);
}

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);
    // 第一次启动时可以设置此项来创建表,之后要注释掉,否则会报表已存在的错误
    // tokenRepository.setCreateTableOnStartup(true);
    return tokenRepository;
}

会话管理与并发控制 有时我们需要限制同一个账号同时在线的会话数量,防止账号被共享或盗用。Spring Security可以很方便地实现。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 需要时创建Session
            .maximumSessions(1) // 每个用户最多允许1个活跃会话
            .maxSessionsPreventsLogin(false) // false表示新登录会踢掉旧的,true表示阻止新登录
            .expiredUrl("/login?expired") // 会话过期后被踢下线的用户跳转的地址
            .and()
            .invalidSessionUrl("/login?invalid"); // Session失效跳转地址
}

这个配置表示一个账号只能在一个地方登录。如果张三在电脑A登录了,又在手机B登录,那么电脑A的会话会立即失效,下次请求会被重定向到 /login?expired。maxSessionsPreventsLogin(true) 则更严格,手机B的登录会被直接拒绝,适合对安全性要求极高的场景。

要实现会话并发控制,你需要将Session存储到共享介质中,比如Redis,并配置 SessionRegistry。这里涉及到Spring Session的集成,篇幅所限不展开,但思路是清晰的:通过 HttpSessionEventPublisher 发布会话事件,让 SessionRegistry 能够追踪所有活跃会话。

8. 异常处理与自定义响应

Spring Security在认证授权过程中抛出的异常(如 AccessDeniedException, AuthenticationException)默认会跳转到特定的错误页,这对于API项目来说很不友好,我们需要返回结构化的JSON错误信息。

自定义认证失败(登录失败)处理 当用户登录失败(用户名密码错误、账户被锁定等),我们可以实现 AuthenticationFailureHandler。

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        Map<String, Object> data = new HashMap<>();
        data.put("timestamp", new Date());
        data.put("status", HttpStatus.UNAUTHORIZED.value());
        data.put("error", "Unauthorized");
        data.put("message", exception.getMessage()); // 谨慎返回具体信息,可能暴露系统细节
        data.put("path", request.getRequestURI());

        // 更细粒度的错误信息
        if (exception instanceof BadCredentialsException) {
            data.put("message", "用户名或密码错误");
        } else if (exception instanceof LockedException) {
            data.put("message", "账户已被锁定,请联系管理员");
        }

        ObjectMapper mapper = new ObjectMapper();
        response.getWriter().write(mapper.writeValueAsString(data));
    }
}

然后在表单登录配置中指定这个失败处理器。

.formLogin()
    .loginPage("/login")
    .successHandler(authenticationSuccessHandler) // 也可以自定义成功处理器
    .failureHandler(customAuthenticationFailureHandler) // 使用自定义失败处理器

自定义权限不足(访问被拒绝)处理 当已认证用户访问没有权限的资源时,会抛出 AccessDeniedException,我们需要实现 AccessDeniedHandler。

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");

        Map<String, Object> data = new HashMap<>();
        data.put("timestamp", new Date());
        data.put("status", HttpStatus.FORBIDDEN.value());
        data.put("error", "Forbidden");
        data.put("message", "抱歉,您没有权限访问此资源");
        data.put("path", request.getRequestURI());

        ObjectMapper mapper = new ObjectMapper();
        response.getWriter().write(mapper.writeValueAsString(data));
    }
}

在 HttpSecurity 配置中指定它。

.exceptionHandling()
    .accessDeniedHandler(customAccessDeniedHandler)

自定义未认证(未登录)处理 当未认证用户访问受保护资源时,默认行为是重定向到登录页。对于API,我们需要返回401状态码和JSON。这可以通过实现 AuthenticationEntryPoint 来实现,逻辑和 AccessDeniedHandler 类似,设置状态码为 HttpStatus.UNAUTHORIZED。

.exceptionHandling()
    .authenticationEntryPoint(customAuthenticationEntryPoint) // 处理未认证
    .accessDeniedHandler(customAccessDeniedHandler) // 处理权限不足

通过自定义这些处理器,你的API就能返回前端期望的、统一的错误响应格式了,这对于前后端联调至关重要。我在项目中通常会定义一个通用的 Result 包装类,在成功和失败时都返回相同结构的数据,让前端处理起来非常方便。

SpringSecurity6从入门到上天系列第五篇:详解SpringSecurity6中的Security Filters的作用和原理以及默认的15个核心Security Filters 本文是SpringSecurity6从入门到上天系列第五篇,详细介绍了SpringSecurity6中的Security Filters的作用和原理以及默认的15个核心Security Filters 阅读详情

相关推荐

SpringSecurity与JWT构建RBAC权限认证实战

在现代分布式系统中,认证授权是保障服务安全的核心机制。JWT(JSON Web Token)作为一种无状态的令牌技术,通过数字签名实现安全的身份验证,相比传统Session方案更适应微服务架构。其核心原理是将用户信息编码为JSON对象,通过Header.Payload.Signature三段式结构确保数据完整性。结合SpringSecurity框架,可以快速实现基于角色的访问控制(RBAC),典型应用场景包括前后端分离系统的权限管理、跨服务调用的身份传递等。实战中需特别注意Token过期策略、权限拦截顺序等

weixin_33748818的博客 413

SpringSecurity实战

为什么写这篇博客呢,因为网络上关于SpringSecurity的内容不好找,官网很乱,所以在参考了官方文档,和b站三更草堂后,总结了一份文稿,也方便自己复习废话不多说,真是开始Spring Security是一个安全框架,众所周知要做好一个系统,首要考虑的就是系统的安全问题。Spring Security隶属Spring,有SpringBoot之后,容易集成了。

Autum_one的博客 1070

SpringBoot2.6.15 SpringSecurity配置

/ CSRF 禁用,因为不使用 session。// 基于spring-boot 2.6.15,SpringSecurity配置类。// 用于鉴定用户是否可以访问被保护的资源。// 用于设置受保护的资源信息数据源。// 授予任何请求允许无条件访问。// 部分允许无条件访问。// 其他需要鉴权认证。

phone13144830339的博客 673

SpringSecurity

SpringSecurity1、前言2、Spring Security2.1、Spring Security简介2.1.1、安全框架2.1.2、常用安全框架2.1.3、关于Spring Security2.2、Spring Security入门2.3、UserDetailsService2.4、PasswordEncoder密码解析器2.5、自定义登录逻辑2.6、自定义登录页面2.7、自定义错误页面2.8、自定义用户名和密码参数名 1、前言 以前对于安全框架不太熟悉,无论是Shiro还是Spring Sec

qq_42640067的博客 9675

SpringSecurity6 | 核心过滤器

大家好,我是Leo哥🫣🫣🫣,上一节我们通过源码剖析以及图文分析,了解了关于委派筛选器代理和过滤器链代理的原理和作用。这节课我们接着学习SpringSecurity的过滤器,了解SpringSecurity中都有哪些核心过滤器。好了,话不多说让我们开始吧😎😎😎。以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。

分享思想,留下痕迹。 1万+

KITTI数据集下载及解析

KITTI数据集下载及解析 版本 更新时间 更新内容 作者 1 V 1.0 xxx 完成主体内容 W. Xiao 2 文章目录KITTI Dataset1 简介1.1 数据采集平台1.2 坐标系2 数据解析2.1 image文件2.2 velodyne文件2.3 calib文件2.4 label文件3 KITTI可视...

u013086672的博客 5万+

15、SpringBoot:SpringSecurity

文章目录14.1、安全简介14.2、实战环境搭建14.3、认识SpringSecurity14.4、认证和授权14.5、权限控制和注销14.6、记住我14.7、定制登录页14.8、完整配置代码 14.1、安全简介 在Web开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面:应用的基本框架已经确定,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本框架已经确

cxycxytony的博客 201

舵机PWM信号转互斥继电器开关信号

-----------------本文由“麦粒电子”撰写,并提供相应产品服务。---------------- 叙述 上次的PWM转继电器开关方案得到大家的不少好评(博客链接),但是发现有些玩家需要一路PWM同时去控制两路继电器开关通断,并且两者是互斥的,好去控制两种不同的负载。所以就继续升级了如下的模块。 正常的控制部件顺序这样:遥控器按钮拨动,遥控器接收机相应通道PWM值变化,PWM转开关模块检测PWM值发生变化从而驱动相应机械继电器触点吸合释放。本模块的两个继电器动作是互斥的,也就是不会同时吸合。

Mai_LiElectronic的博客 1635

SpringSecurity + JWT实战(前后端分离)

先来聊一聊什么是SpringSecurity ,在上一篇文章中已经聊过了,大家可以去看看接下来我们聊聊什么是JWTJson web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且的,特别适用于。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

As_Yua的博客 3359

高效数据迁移:ICCAP到ADS的数据导入与导出实战指南

本文提供了从ICCAP到ADS进行高效、准确数据迁移的实战指南。针对射频与微波电路设计中的关键挑战,详细阐述了迁移前的战略规划、ICCAP数据的精细化导出(包括脚本化批处理)、ADS数据的多种导入方法(如Data Items、DML模型库及编程化API),并强调了数据完整性与准确性验证的重要性,确保模型参数和仿真结果在平台间可靠传递。

weixin_29291185的博客 133

Rsoft仿真实战:长周期光纤光栅参数优化与模式耦合分析

本文详细介绍了使用Rsoft软件进行长周期光纤光栅(LPFG)参数优化与模式耦合分析的实战方法。通过Rsoft仿真,可以高效优化光栅周期、包层厚度等关键参数,深入分析纤芯模与包层模的耦合特性,为光纤通信和传感应用提供可靠的设计依据。

h0i1j2k3l的博客 1115

SpringSecurity默认的15个过滤器链

SpringSecurity是针对Spring项目安全的一款开源的框架,可以与SpringBoot无缝集成,其注重于身份验证以及权限认证,可以通过少量的配置,从而实现强大的安全管理。避免了之前使用拦截器(比拦截器好玩多了)以及过滤器结合大量的原生代码,这种方式太过臃肿且过于繁琐。SpringSecuriyt包含两部分:认证:判断用户是否是系统合法用户过程;授权:判断系统内用户可以访问或具有访问哪些资源权限。使用方法:在已有的项目pom.xml文件中导入SpringSecurity的依赖。

qq_74736349的博客 550

《全连接到卷积神经网络:图像识别的演变历程》

这篇文章会从传统全连接层面临的困境说起,揭示卷积神经网络(CNN)成为图像识别核心动力的原因,探究其背后蕴含的设计思想与技术创新。但是回顾历史,CNN的出现提醒我们:技术的突破往往是由对自然规律的模仿和重新构建得来的。也许,当机器真的学会“像人一样看世界”的时候,它们的“眼睛”早就超过生物视觉的限制了。例如,用5×5的核处理224×224的输入时,只要25个参数(而不是全连接那种百万级的)。* 局部性:网络的底层只需留意局部的地方(像边缘、纹理这样的地方),不用管那些全局像素之间复杂的关系。

2401_82434295的博客 675

springsecurity登录认证授权示例

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。​ 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。​ 一般Web应用的需要进行认证和授权。​ 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户​ 授权:经过认证后判断当前用户是否有权限进行某个操作。

m0_67587248的博客 443

HyperLynx(十八)DDR(一)DDR简介和DDR的数据仿真

LVTTL电平为单端信号,而SSTL电平本质上是差分对,其实现机制是将信号与参考电平Vref组成差分对进行比较,如图所示,VIH和VIL各有一个交流参数AC和直流参数DC,信号第一次越过AC门限的时刻,作为时序计算的参考点,此后只要信号保持DC门限内,逻辑状态保持不变,这种设计减小了噪声、振铃等对于信号质量的影响。经过调整后,控制器在发送颗粒1的DQS后,延时1ns再发送颗粒2的DQS,则颗粒1在1ns后收到对齐的DQS和CLK,颗粒2在2ns后收到对齐的DQS和CLK,从而满足了时序关系。

小幽余生不加糖 7680

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的远距离停车位检测系统(深度学习代码+UI界面+训练数据集)

本文详细介绍了基于YOLOv5/YOLOv6/YOLOv7/YOLOv8深度学习模型的远距离停车位检测系统的完整实现方案。该系统能够准确识别200米范围内的停车位状态,为智能停车场管理系统、自动驾驶车辆提供关键的视觉感知能力。文章涵盖算法原理、数据集构建、模型训练、系统实现及优化策略,并提供完整的代码实现和UI界面设计。关键词:YOLO系列,深度学习,停车位检测,计算机视觉,智能交通系统pythonimport cv2import os"""创建数据存储目录""""""定时采集图像"""

YOLO 186

SpringSecurity-15-解决跨域访问

SpringSecurity-15-解决跨域访问什么是跨域 跨域是一种浏览器同源安全策略,是一种约定,是浏览器单方面限制脚本跨域访问,它会限制同一个域的JavaScript脚本和另一个域的内容进行交互,同源指的是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。同源政策的目的是为了保护用户信息的安全,防止恶意网站窃取数据一些人认为资源跨域时无法请求访问,实际是请求可以正常发起(部分浏览器存在特例),后端服务也正常处理了,但是在返回的时候被浏览器进行了拦截,导致响应内容不可使用

zhoubangbang1的博客 1315

21、使用 FastHenry 进行片上电感器的模拟与参数预测

本文介绍了使用 FastHenry 对片上电感器进行模拟和参数预测的方法。讨论了 FastHenry 的适用场景、线圈几何形状的选择与离散化方法,以及如何考虑模拟中的寄生效应。同时对比了 FastHenry 和 Magnet 在不同基板电阻率下的模拟精度,并提出了优化电感器设计的策略,为高效准确地预测电感参数提供了技术指导。

y9z0a1b的博客 296

SpringBoot第 15 讲:SpringSecurity

SpringSecurity用户认证和用户授权

分享日常工作技术 495

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

SpringSecurity系列教程汇总

因为优快云没有分类归纳博客的功能,所以特写本帖汇总Spring Security 5.x系列教程,方便大家查阅! Spring Security系列教程01--Spring Security系列教程简介 Spring Security系列教程02--初识Spring Security Spring Security系列教程03--创建SpringSecurity项目 Spring Security系列教程04--实现HTTP基本认证 Spring Security系列教程05--实现Form表单

一一哥 7584

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

SpringBoot整合SpringSecurity时需要注意的问题

前一段时间整合了一下SpringBoot+SpringSecurity+JWT,整合时JWT还好说,但是SpringSecurity出现的问题比较多,下面上源码图 这里SpringSecurity中设定死了登录时的用户名和密码的key 为username和password, 还有表单的提交路径和请求方式都固定了,但是表单提交时的路径可以通过 这里修改,文章末尾会提供此处代码 /** @De...

qq_39554240的博客 1556

SpringSecurity与JWT整合(含案例代码)

参考文章: https://www.bilibili.com/video/BV15a411A7kP?p=1 https://www.bilibili.com/video/BV1d7411y7Uu 1. 简介    SpringSecurity在B站上讲的真的很糟糕,特别是与JWT的整合,看完视频后,依照老师的思路,自己整合了一下,现在讲自己的成果与大家分享,无需使用SpringCloud,只用了SpringBoot即可!在这里也推荐一下第二个b站的视频,他讲的SpringSecurity会稍微好一点,对代

lendsomething的博客 1654

SpringSecurity学习

三更老师的springSecurity学习笔记

sisi13543911673的博客 1264

百度甩出SpringSecurity笔记,源码实战齐下

Spring Security Spring Security想必大家也在网上也看到过许多博客讨论,对于它的概念我就不再细说了。由于Spring Security是Spring生态系统中的一员,它伴随着整个Spring生态系统不断修正、升级,所以使用Spring Security 能最大程度减少企业系统安全控制编写大量重复代码的工作。 但据有关数据表示,现在大多数开发人员对于Spring Security仅仅停留在会用的阶段,对于其核心原理却不知其所以然。所以今天互联网雷锋(小编我)就把前段时间从百度一

wdj0311的博客 152

SpringSecurity常用过滤器介绍

  本文我们来介绍下SpringSecurity中常用的过滤器及其加载的过程。 一、常用的过滤器   常用的过滤器有15个,分别如下: 1.org.springframework.security.web.context.SecurityContextPersistenceFilter   首当其冲的一个过滤器,非常重要 主要是使用SecurityContextRepository在session...

波波烤鸭的博客 5123
上一篇: LabVIEW按钮机械动作的实战应用场景解析
下一篇: 利用Docker Buildx实现x86_64到ARM64的高效跨架构镜像构建实践
随身带U盘
博客等级 码龄4年 30粉丝 1526原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值