从0开始Spring Boot - 7: 集成Spring Security

260 阅读6分钟

Spring Security 是 Spring 生态中用于身份验证和授权的强大框架。

常用的身份校验方案

特性Basic AuthenticationToken AuthenticationOAuth Authentication
原理客户端在每个请求的 Authorization 头中发送 Base64 编码的用户名和密码。客户端在登录后获取一个令牌(Token),后续请求在 Authorization 头中携带该令牌。客户端通过授权服务器获取访问令牌(Access Token),并使用该令牌访问资源服务器。
安全性较低。密码以 Base64 编码传输,容易被拦截和解码。建议与 HTTPS 结合使用。较高。令牌通常是加密的,且可以设置过期时间。支持 HTTPS 进一步增强安全性。高。支持多种授权流程(如授权码模式),令牌有较短的生命周期,支持刷新令牌。
复杂度简单。易于实现,适合小型应用或内部系统。中等。需要实现令牌的生成、验证和管理。高。需要实现授权服务器、资源服务器和客户端之间的复杂交互。
性能较高。每个请求都需要验证用户名和密码,可能增加服务器负载。较高。令牌验证通常比密码验证更快,但需要额外的令牌管理开销。较低。涉及多次网络请求(如获取令牌、刷新令牌),可能增加延迟。
适用场景适合简单的内部系统或测试环境。适合需要较高安全性的 Web 应用和 API。适合需要第三方授权的分布式系统或开放平台。
扩展性差。难以扩展,不支持复杂的授权需求。较好。支持自定义令牌和权限管理。优秀。支持多种授权流程和第三方集成。
令牌管理无。每次请求都需要发送用户名和密码。需要管理令牌的生成、存储、验证和过期。需要管理访问令牌、刷新令牌和授权码的生命周期。
跨域支持有限。需要额外的配置支持跨域请求。较好。令牌可以跨域使用,适合分布式系统。优秀。支持跨域和第三方授权。
示例协议HTTP Basic AuthJWT (JSON Web Token)OAuth 2.0
支持刷新JWT (JSON Web Token)可通过refresh token实现OAuth 2.0中有 refrsh token机制

用户(User)-角色(Role)-权限(Permission) 的权限管理方案

自行google或者AI一下,这里贴一个demo

-- 用户表
CREATE TABLE users (
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    email VARCHAR(100) UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 角色表
CREATE TABLE roles (
    role_id INT AUTO_INCREMENT PRIMARY KEY,
    role_name VARCHAR(50) NOT NULL UNIQUE,
    description TEXT
);

-- 权限表
CREATE TABLE permissions (
    permission_id INT AUTO_INCREMENT PRIMARY KEY,
    permission_name VARCHAR(50) NOT NULL UNIQUE,
    description TEXT
);

-- 用户角色关联表
CREATE TABLE user_roles (
    user_id INT,
    role_id INT,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE
);

-- 角色权限关联表
CREATE TABLE role_permissions (
    role_id INT,
    permission_id INT,
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE
);

JWT

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它通常用于身份验证和授权,特别是在分布式系统和微服务架构中。


1. JWT 的结构

JWT 由三部分组成,用 . 分隔:

  1. Header: 包含令牌类型(如 JWT)和签名算法(如 HMAC SHA256)。
  2. Payload: 包含声明(claims),如用户信息、角色、权限等。
  3. Signature: 用于验证令牌的完整性和真实性。

示例 JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

decode工具地址

image.png

2. JWT 的工作流程

  1. 用户登录: 客户端发送用户名和密码到服务器。
  2. 生成 JWT: 服务器验证用户信息后,生成 JWT 并返回给客户端。
  3. 客户端存储 JWT: 客户端将 JWT 存储在本地(如 localStorage 或 cookie)。
  4. 发送 JWT: 客户端在后续请求的 Authorization 头中携带 JWT。
  5. 验证 JWT: 服务器验证 JWT 的签名和有效期,并提取用户信息。

例如用postman 在 head中的Authorization中添加 jwt image.png

SpringBoot 集成 Spring Security

以下是在 Spring Boot 项目中集成 Spring Security 的最佳实践,涵盖基本配置、身份验证、授权、密码加密、CSRF 防护等内容。

1. 添加依赖

在 pom.xml 中添加 Spring Security 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 基本配置

创建一个 Spring Security 配置类,由于版本之间差异较大,很多方法已经Deprecated了,这里的springSecurity 是 6.3.3

Spring Security 6.3.3 及之后

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private static final Logger log = LoggerFactory. getLogger(SecurityConfig.class);

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        log.info("!-- securityFilterChain--");
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                // 不拦截特定的 URL,这里以 /public/** 为例
                                .requestMatchers("/public/**").permitAll()
                                .requestMatchers("/login").permitAll()
                                // 其他请求需要认证
                                .requestMatchers("/user/**").authenticated()
                                .requestMatchers("/admin/**").hasRole("admin")
                                .anyRequest().authenticated()
                )
                .formLogin(formLogin ->
                        formLogin.loginPage("/login").permitAll()
                )
                .logout(logout ->
                        logout.permitAll()
                )
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling
                                .accessDeniedHandler(accessDeniedHandler())
                );

        return http.build();
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            log.info("!--- access deny ");
            response.sendRedirect("/access-denied");
        };
    }

}

3. 用户详情服务

实现 UserDetailsService 接口,用于加载用户信息。


import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;


@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库或其他存储中加载用户信息
        if ("admin".equals(username)) {
            return User.withUsername("admin")
                    .password(passwordEncoder().encode("admin123"))
                    .roles("ADMIN")
                    .build();
        } else if ("user".equals(username)) {
            return User.withUsername("user")
                    .password(passwordEncoder().encode("user123"))
                    .roles("USER")
                    .build();
        } else {
            throw new UsernameNotFoundException("User not found");
        }
    }

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

4. 密码加密

使用 BCryptPasswordEncoder 对密码进行加密。

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

在注册或更新用户时,确保密码经过加密后存储:

String rawPassword = "user123";
String encodedPassword = passwordEncoder.encode(rawPassword);

5. CSRF 防护

Spring Security 默认启用 CSRF 防护。对于 REST API,可以禁用 CSRF 防护:

@Configuration
@EnableWebSecurity
public class CsrfSecurityConfig {
 
  	@Bean
  	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  		http
  			.csrf((csrf) -> csrf. disable());
  		return http. build();
  	}
}

6. JWT 集成(可选)

对于基于令牌的身份验证,可以集成 JWT(JSON Web Token)。

添加 JWT 依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.12.6</version>
</dependency>

JWT 工具类

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtil {

    private String SECRET_KEY = "jakeSecret";

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().
                claims(claims).
                subject(subject).
                issuedAt(new Date(System.currentTimeMillis())).
                expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)).signWith(this.getSigningKey())
                .compact();
    }

    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(this.SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    ...
    ...
    // 验证token.. 
    // 验证 有效期,.. 
    // 是否刷新,生成新token等.. 
}

** 如果需要全局验证登陆,可以添加 JWT 过滤器 **

@Component
public class JwtFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 从请求头中获取 JWT
        String token = request.getHeader("Authorization");

        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去掉 "Bearer " 前缀

            // 验证 JWT
            if (jwtUtil.validateToken(token)) {
                // 如果 JWT 有效,将用户信息放入请求中
                Claims claims = jwtUtil.parseToken(token);
                request.setAttribute("username", claims.getSubject());
            } else {
                // 如果 JWT 无效,返回 401 未授权
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        } else {
            // 如果没有 JWT,返回 401 未授权
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // 放行请求
        filterChain.doFilter(request, response);
    }
}

7. 测试

启动应用后,访问受保护的端点(如 /admin),系统会要求登录。登录后,可以访问授权资源。


8. 总结

以上是 Spring Boot 集成 Spring Security 的基础应用、其他可根据实际需求,可以进一步扩展和优化配置。