• 作者:老汪软件技巧
  • 发表时间:2024-10-01 11:03
  • 浏览量:

Spring Security 是一个强大且可扩展的框架,用于保护 Java 应用程序,尤其是基于 Spring 的应用。它提供了身份验证(验证用户身份)、授权(管理用户权限)和防护机制(如 CSRF 保护和防止会话劫持)等功能。

Spring Security 允许开发者通过灵活的配置实现安全控制,确保应用程序的数据和资源安全。通过与其他 Spring 生态系统的无缝集成,Spring Security 成为构建安全应用的理想选择。

核心概念

1、准备工作1.1 引入依赖

当我们引入 security 依赖后,访问需要授权的 url 时,会重定向到 login 页面(security 自己创建的),login 页面需要账号密码,账号默认是 user, 密码是随机的字符串,在spring项目的输出信息中

一般我们会创建一个 SecurityConfig 类,来管理我们所有与 security 相关的配置。(我们讲的是 security 5.7 版本之后的配置方法,之前的方法跟现在不太一样)

@Configuration
@EnableWebSecurity		// 该注解启用 Spring Security 的 web 安全功能。
public class SecurityConfig {
}

下面的都要写到 SecurityConfig 类中

1.2 用户认证的配置

基于内存的用户认证

通过 createUser , manager 把用户配置的账号密码添加到spring的内存中, InMemoryUserDetailsManager 类中有一个 loadUserByUsername 的方法通过账号(username)从内存中获取我们配置的账号密码,之后调用其他方法来判断前端用户输入的密码和内存中的密码是否匹配。

@Bean
public UserDetailsService userDetailsService() {
	// 创建基于内存的用户信息管理器
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
	
    manager.createUser(
	    // 创建UserDetails对象,用于管理用户名、用户密码、用户角色、用户权限等内容
	    User.withDefaultPasswordEncoder().username("user").password("user123").roles("USER").build()
    ); 		
    // 如果自己配置的有账号密码, 那么上面讲的 user 和 随机字符串 的默认密码就不能用了
    return manager;
}

当我们点进 InMemoryUserDetailsManager 中 可以发现它实现了 UserDetailsManager 和 UserDetailsPasswordService 接口,其中 UserDetailsManager 接口继承的 UserDetailsService 接口中就有 loadUserByUsername 方法

基于数据库的用户认证

上面讲到,spring security 是通过 loadUserByUsername 方法来获取 User 并用这个 User 来判断用户输入的密码是否正确。所以我们只需要继承 UserDetailsService 接口并重写 loadUserByUsername 方法即可

下面的样例我用的 mybatis-plus 来查询数据库中的 user, 然后通过当前查询到的 user 返回特定的 UserDetails 对象

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("username", username);	// 这里不止可以用username,你可以自定义,主要根据你自己写的查询逻辑
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);	// UserDetailsImpl 是我们实现的类
    }
}

UserDetailsImpl 是实现了 UserDetails 接口的类。UserDetails 接口是 Spring Security 身份验证机制的基础,通过实现该接口,开发者可以定义自己的用户模型,并提供用户相关的信息,以便进行身份验证和权限检查。

@Data
@AllArgsConstructor
@NoArgsConstructor	// 这三个注解可以帮我们自动生成 get、set、有参、无参构造函数
public class UserDetailsImpl implements UserDetails {
    private User user;	// 通过有参构造函数填充赋值的
    @Override
    public Collectionextends GrantedAuthority> getAuthorities() {
        return List.of();
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    @Override
    public String getUsername() {
        return user.getUsername();
    }
    @Override
    public boolean isAccountNonExpired() {  // 检查账户是否 没过期。
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {   // 检查账户是否 没有被锁定。
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {  //检查凭据(密码)是否 没过期。
        return true;
    }
    @Override
    public boolean isEnabled() {    // 检查账户是否启用。
        return true;
    }
    
    // 这个方法是 @Data注解 会自动帮我们生成,用来获取 loadUserByUsername 中最后我们返回的创建UserDetailsImpl对象时传入的User。
    // 如果你的字段包含 username和password 的话可以用强制类型转换, 把 UserDetailsImpl 转换成 User。如果不能强制类型转换的话就需要用到这个方法了
	public User getUser() {	
	        return user;	
	    }
	}

1.3 基本的配置

下面这个是 security 的默认配置。我们可以修改并把它加到spring容器中,完成我们特定的需求。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            // 开启授权保护
            .authorizeHttpRequests(authorize -> authorize
                    // 不需要认证的地址有哪些
                    .requestMatchers("/blog/**", "/public/**", "/about").permitAll()	// ** 通配符
                    // 对所有请求开启授权保护
                    .anyRequest().
                    // 已认证的请求会被自动授权
                    authenticated()
            )
            // 使用默认的登陆登出页面进行授权登陆
            .formLogin(Customizer.withDefaults())
            // 启用“记住我”功能的。允许用户在关闭浏览器后,仍然保持登录状态,直到他们主动注销或超出设定的过期时间。
            .rememberMe(Customizer.withDefaults());
    // 关闭 csrf CSRF(跨站请求伪造)是一种网络攻击,攻击者通过欺骗已登录用户,诱使他们在不知情的情况下向受信任的网站发送请求。
    http.csrf(csrf -> csrf.disable());
    return http.build();
}

关于上面放行路径写法的一些细节问题:如果在 application.properities 中配置的有 server.servlet.context-path=/api 前缀的话,在放行路径中不需要写 /api。如果 @RequestMapping(value = "/test/") 中写的是 /test/, 那么放行路径必须也写成 /test/, (/test)是不行的,反之亦然。如果 @RequestMapping(value = "/test") 链接 /test 后面要加查询字符的话(/test?type=0),不要写成 @RequestMapping(value = "/test/")

上面都是一些细节问题,但是遇到 bug 的时候不容易发现。(笔者初学时找了一个小时,.. .. .)

下面这个是我常用的配置,配置了jwt,加密的类,过滤器启用 jwt,而不是session

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(CsrfConfigurer::disable) // 基于token,不需要csrf
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 基于token,不需要session
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/login/",  "/getPicCheckCode").permitAll() // 放行api
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

2、加密

Spring Security 提供了多种加密和安全机制来保护用户的敏感信息,尤其是在用户身份验证和密码管理方面。(我们只讲默认的 BCryptPasswordEncoder)

1. 密码加密的重要性

在应用程序中,用户密码是最敏感的数据之一。为了防止密码泄露,即使数据库被攻击者获取,密码也应以加密形式存储。加密可以保护用户的隐私,并在一定程度上增加安全性。

2. Spring Security 的加密机制2.1 PasswordEncoder 接口

Spring Security 提供了 PasswordEncoder 接口,用于定义密码的加密和验证方法。主要有以下几种实现:

2.2 BCrypt 算法

BCrypt 是一种安全的密码哈希算法,具有以下特点:

3. 使用 PasswordEncoder3.1 配置 PasswordEncoder

可以在 Spring Security 的配置类中定义 PasswordEncoder bean,例如:

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 也可用有参构造,取值范围是 4 到 31,默认值为 10。数值越大,加密计算越复杂
        return new BCryptPasswordEncoder();	
    }
}

3.2 加密密码

在注册用户或更新密码时,可以使用 PasswordEncoder 来加密密码:

@Autowired
private PasswordEncoder passwordEncoder;
public void registerUser(String username, String rawPassword) {
    String encryptedPassword = passwordEncoder.encode(rawPassword);
    // 保存用户信息到数据库,包括加密后的密码
}

3.3 验证密码

在用户登录时,可以使用 matches 方法验证输入的密码与存储的加密密码是否匹配:

public boolean login(String username, String rawPassword) {
    // 从数据库中获取用户信息,包括加密后的密码
    String storedEncryptedPassword = // 从数据库获取;
    return passwordEncoder.matches(rawPassword, storedEncryptedPassword);
}

3、前后端分离1. 用户认证流程1.1 用户登录

后端:

2. 使用 JWT 进行用户认证2.1 前端存储 JWT2.2 发送受保护请求

fetch('/api/protected-endpoint', {
    method: 'GET',
    headers: {
        'Authorization': `Bearer ${token}`
    }
});

2.3 后端解析 JWT3. 退出登录

由于 JWT 是无状态的,后端不需要记录会话状态。用户可以通过前端操作(例如,删除存储的 JWT)来“退出登录”。可以实现一个注销接口,用于前端执行相关逻辑。

4. 保护敏感信息4. 实现 jwt4.1 OncePerRequestFilter

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
        // 其他配置...
        return http.build();
    }
}

4.2 生成 JWT

基于我们上面引入的三个 JWT 相关的依赖,写JwtUtil 类

@Component
public class JwtUtil {
    public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;  // 有效期14天
    public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .id(uuid)
                .subject(subject)
                .issuer("sg")
                .issuedAt(now)
                .signWith(secretKey)
                .expiration(expDate);
    }
    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
    }
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(jwt)
                .getPayload();
    }
}

1. 类注解与常量2. UUID 生成

public static String getUUID() {
    return UUID.randomUUID().toString().replaceAll("-", "");
}

3. 创建 JWT

public static String createJWT(String subject) {
    JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
    return builder.compact();
}

4. JWT 构建器

_接入方式_接入技术

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
    //...
}

5. 生成密钥

public static SecretKey generalKey() {
    byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
    return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}

6. 解析 JWT

public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(jwt)
            .getBody();
}

4.3 应用 JWT

先加一个依赖 JetBrains Java Annotations, 下面的代码会用到其中的一个注解 @NotNull。

这个 JwtAuthenticationTokenFilter 类是一个实现了 OncePerRequestFilter 接口自定义的 Spring Security 过滤器。

import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;
    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        token = token.substring(7);
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        User user = userMapper.selectById(Integer.parseInt(userid));
        if (user == null) {
            throw new RuntimeException("用户名未登录");
        }
        UserDetailsImpl loginUser = new UserDetailsImpl(user);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);
        // 如果是有效的jwt,那么设置该用户为认证后的用户
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

1. 类注解与继承2. 依赖注入

@Autowired
private UserMapper userMapper;

3 获取 JWT

String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
    filterChain.doFilter(request, response);
    return;
}

4 解析 JWT

token = token.substring(7); // 去掉 "Bearer " 前缀
String userid;
try {
    Claims claims = JwtUtil.parseJWT(token);
    userid = claims.getSubject();
} catch (Exception e) {
    throw new RuntimeException(e);
}

5 查询用户信息

User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
    throw new RuntimeException("用户名未登录");
}

6 设置安全上下文

UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

4. 继续过滤链

filterChain.doFilter(request, response);

4.4 注册自定义的 JwtAuthenticationTokenFilter 过滤器

把我们的过滤器应用到 spring security 中

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

5、常用操作5.1 配置 AuthenticationManager

将 AuthenticationManager 对象添加到spring容器中.(添加到我们创建的 SecurityConfig 配置类中)

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
    return authConfig.getAuthenticationManager();
}

5.2 验证当前用户

@Autowired
private AuthenticationManager authenticationManager;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// 从数据库中对比查找,如果找到了会返回一个带有认证的封装后的用户,否则会报错,自动处理。(这里我们假设我们配置的security是基于数据库查找的)
Authentication authenticate = authenticationManager.authenticate(authenticationToken);	
// 获取认证后的用户
User user = (User ) authenticate.getPrincipal();	// 如果强制类型转换报错的话,可以用我们实现的 `UserDetailsImpl` 类中的 getUser 方法了
String jwt = JwtUtil.createJWT(user.getId().toString());

5.3 获取当前用户的认证信息

以下是获取当前用户认证信息的具体步骤:

// SecurityContextHolder 是一个存储安全上下文的工具类,提供了一个全局访问点,用于获取当前请求的安全上下文。
// SecurityContext 是当前线程的安全上下文,包含了当前用户的认证信息(即 Authentication 对象)。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
User user = (User ) authenticate.getPrincipal();
// 另一种获取 User 的方法
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal();
User user = userDetails.getUser();

5.4 对比 authenticationManager 和 SecurityContext 获取 Authentication

SecurityContextHolder.getContext().getAuthentication():

authenticationManager.authenticate(authenticationToken):

5.5 Authentication 接口

Authentication 接口包含以下重要方法:

6、授权

Spring Security 的授权机制用于控制用户对应用程序资源的访问权限。授权通常是基于用户角色或权限的,以下是对 Spring Security 授权的详细讲解。

1. 授权的基本概念2. 授权的主要组成部分2.1 权限与角色2.2 GrantedAuthority 接口3. 授权方式

Spring Security 支持多种授权方式,主要包括:

3.1 基于角色的授权

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")  // 只有 ADMIN 角色可以访问
        .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")  // USER 和 ADMIN 角色可以访问
        .anyRequest().authenticated();  // 其他请求需要认证
}

3.2 基于权限的授权

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/edit/**").hasAuthority("EDIT_PRIVILEGE")  // 仅具有 EDIT_PRIVILEGE 权限的用户可以访问
        .anyRequest().authenticated();
}

4. 自定义授权逻辑

如果需要更复杂的授权逻辑,可以实现自定义的 AccessDecisionVoter 或 AccessDecisionManager。

4.1 AccessDecisionVoter4.2 AccessDecisionManager7、其他自定义行为

以下接口和类用于处理不同的安全事件,提供了自定义处理的能力:

1. AuthenticationSuccessHandler

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    }
}
// 让我们实现的类生效
http.formLogin(form ->
                form.successHandler(new MyAuthenticationSuccessHandler()));

2. AuthenticationFailureHandler

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
    }
}
// 让我们实现的类生效
http.formLogin(form ->
        form.failureHandler(new MyAuthenticationFailureHandler()));

3. LogoutSuccessHandler

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    }
}
// 让我们实现的类生效
http.logout(logout -> {
               logout.logoutSuccessHandler(new MyLogoutSuccessHandler());
           });

4. AuthenticationEntryPoint

// 重写 AuthenticationEntryPoint 接口
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    }
}
// 让我们重写的类生效
http.exceptionHandling(exception -> {
    exception.authenticationEntryPoint(new MyAuthenticationEntryPoint());
});

5. SessionInformationExpiredStrategy

// 重写 SessionInformationExpiredStrategy 接口
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
    }
}
// 让我们重写的类生效
http.sessionManagement(session -> {
    session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});

6. AccessDeniedHandler

// 重写 AccessDeniedHandler 接口
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, 
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
    }
}
// 在安全配置中注册自定义的 AccessDeniedHandler
http.exceptionHandling(exception -> {
            exception.accessDeniedHandler(new MyAccessDeniedHandler());
        });
});

~~文章到这里就结束了~~~