# 引言

Spring Security 它是 Spring 家族中的一个安全框架,主要用于保护我们的应用程序,它提供了许多安全特性,如身份验证、授权、加密、会话管理、访问控制等。本文将介绍 Spring Security 的 6.3.1 版本的安全配置。

# Spring Security 简介

Spring Security 的最新版本是 6.3.1,本文将介绍 6.3.1 版本的安全配置。

  • Spring Security 的主要特性:
  • 认证与授权:提供多种认证方式和权限管理功能。
  • 攻击防护:防御常见的攻击,如跨站请求伪造 (CSRF) 、点击劫持等。
  • 灵活配置:通过 Java 配置、 XML 配置等多种方式进行灵活配置。
  • 集成支持:支持与多种身份验证协议(如 OAuth2JWT )和第三方服务的集成。

# Spring Security 依赖

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

# 环境准备

  • 工具和版本
    • JDK : 11 或更高版本
    • Spring Boot : 最新稳定版本
    • Maven/Gradle : 用于项目构建和依赖管理
    • IDE : 如 IntelliJ IDEAEclipse
  • 创建 Spring Boot 项目
    • 使用 Spring Initializr 创建一个新的 Spring Boot 项目。
    • 选择 "Spring Security" 作为依赖项。
    • 下载并导入项目到你的 IDE

# 安全配置

# 实现 UserDetails 接口

首先我们需要先配置自己的 UserBean , 将自己的 User 实体类实现 UserDetails 接口,并在 SecurityConfig 中配置 UserDetailsServiceUserDetails 接口提供了获取用户信息的方法,包括用户名、密码、权限等。

User.java
package com.fairiy.magic.magicbackend.bean;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.sql.Date;
import java.util.List;
/**
 * @Author: LightRain
 * @Description: User 实体类,实现 UserDeails 接口
 * @DateTime: 2023-04-01 16:37
 * @Version:1.0
 **/
@Data
public class User implements UserDetails {
    // id
    private int id;
    // 用户名
    private String username;
    // 密码
    private String password;
    // 头像地址
    private String avatar_address;
    // 性别
    private String gender;
    // 角色
    private String role;
    // 状态
    private String user_status;
    // 有效时间
    private Date valid_time;
    // 更新时间
    private Date update_time;
    // 创建时间
    private Date create_time;
    // 是否删除(0 - 未删,1 - 已删)
    private int is_deleted;
    private List<GrantedAuthority> authorities;
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

# 实现 UserDetailsService 接口

下面我们来实现登录权限的效验,来编写 SecurityServiceImpl 实现类,实现 UserDetailsService 接口中的 loadUserByUsername 方法,并在 SecurityConfig 中配置 AuthenticationManager

SecurityServiceImpl.java
package com.fairiy.magic.magicbackend.service;
import com.fairiy.magic.magicbackend.bean.User;
import com.fairiy.magic.magicbackend.mapper.UserMapper;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
/**
 * Created by LightRain on 2024/7/30 下午 23:25.
 * UserDetailsService 接口用于实现用户认证,使用 SpringSecurity 来处理授权请求
 */
@Service
public class SecurityServiceImpl implements UserDetailsService {
  /**
   * 注入 UserMapper
   */
  private final UserMapper userMapper;
  public SecurityServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;
  }
  @Override
  public UserDetails loadUserByUsername(String username) {
    User user = userMapper.getUserName(username);
    if (user != null && user.getUsername() != null && user.getPassword() != null) {
      return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
    }
    // 验证失败
    return new org.springframework.security.core.userdetails.User("t", "$2a$10$kiS2bbJHUiDINf466LuMae2kpoC0/iGvbmqNR7w7eiQhaEUlgw6Nq", AuthorityUtils.commaSeparatedStringToAuthorityList(""));
  }
}

# 配置 SecurityConfig 安全类

配置 SecurityConfig 类,这是 6.3.1 版本的最新配置格式,旧版方法均已被弃用,新版配置如下:

SecurityConfig.java
package com.fairiy.magic.magicbackend.config;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
/**
 * @Author: LightRain
 * @Description: Security 安全配置
 * @DateTime: 2024/7/30 下午 7:53
 * @Version:1.0
 **/
@EnableWebSecurity
@Configuration
public class SecurityConfig {
    /**
     * @Author: LightRain
     * @Date: 2024/7/30 下午 11:44
     * @Param: [httpSecurity]
     * @Return: org.springframework.security.web.SecurityFilterChain
     * @Description: SpringSecurity 配置
     * @Since 21
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 默认开启 CSRF 防护
        httpSecurity
                // 配置 CORS 从源策略
                .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
                // 开启表单登录
                .formLogin(formLogin -> {
                    // 登录页面,允许所有人访问
                    formLogin.loginPage("/login").permitAll();
                    // 登录成功后将请求转发到 successForwardUrl
                    formLogin.successForwardUrl("/login/success")
                            // 登录失败后转发到 failureForwardUrl
                            .failureForwardUrl("/login/failure").permitAll();
                })
                // 开启记住我
                .rememberMe(rememberMe -> rememberMe.tokenValiditySeconds(60 * 60 * 24 * 7))
                // 配置权限
                .authorizeHttpRequests(authorize -> {
                    // 配置白名单,允许访问 /public/**, 其他请求都需要认证
                    authorize.requestMatchers("/public/**", "/login/**").permitAll().anyRequest().authenticated();
                })
                // 配置 session 管理
                .sessionManagement(sessionManagement -> {
                    //session 失效时跳转的 url
                    sessionManagement.invalidSessionUrl("/session/invalid");
                })
                // 配置异常处理
                .exceptionHandling(exceptionHandling -> {
                    // 身份验证入口点
                    exceptionHandling.authenticationEntryPoint(new OAAuthenticationEntryPoint());
                    // 未授权时跳转的 url
//                    exceptionHandling.accessDeniedHandler(new OAAccessDeniedHandler());
                })
                // 配置退出登录
                .logout(logout -> {
                    // 退出登录 url
                    logout.logoutUrl("/logout")
                            // 退出成功后跳转的 url
                            .logoutSuccessUrl("/login")
                            // 是否清除 HttpSession
                            .invalidateHttpSession(true)
                            // 删除对应 cookie
                            .deleteCookies("JSESSIONID");
                });
        return httpSecurity.build();
    }
    /**
     * @Author: LightRain
     * @Date: 2024/7/30 下午 11:49
     * @Param: []
     * @Return: org.springframework.web.cors.CorsConfigurationSource
     * @Description: CORS 从源策略配置
     * @Since 21
     */
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域访问的站点
        corsConfiguration.setAllowedOrigins(List.of("http://localhost:5170","http://localhost:4500", "http://localhost:80"));
        // 允许跨域访问的 methods
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
        // 允许携带凭证
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setAllowedHeaders(List.of("*"));
        // 基于 url 的 cors 配置源
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对所有 URL 生效
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
    /**
     * 配置从源策略
     */
//    public CorsConfigurationSource corsConfigurationSource() {
//        CorsConfiguration corsConfiguration = new CorsConfiguration();
//        corsConfiguration.addAllowedOrigin("http://localhost:4500");
//        corsConfiguration.addAllowedMethod ("*"); // 允许所有方法
//        corsConfiguration.addAllowedHeader("Content-Type");
//        corsConfiguration.addAllowedHeader("Authorization");
//        corsConfiguration.addAllowedHeader("Access-Control-Allow-Origin");
//        corsConfiguration.addAllowedHeader("x-csrf-token");
//        corsConfiguration.setAllowCredentials(true);
//        // 基于 url 的 cors 配置源
//        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//        // 对所有 URL 生效
//        source.registerCorsConfiguration("/**", corsConfiguration);
//        return source;
//    }
    /**
     * @Author: LightRain
     * @Date: 2024/7/30 下午 11:50
     * @Param: []
     * @Return: org.springframework.security.crypto.password.PasswordEncoder
     * @Description: 配置密码加密器
     * @Since 21
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

# 实现身份验证入口点

实现 OA 身份验证入口点 OAAuthenticationEntryPoint ,用于处理身份验证失败的情况,如用户名或密码错误等。

OAAuthenticationEntryPoint.java
package com.fairiy.magic.magicbackend.config;
import com.fairiy.magic.magicbackend.bean.Return;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
import java.io.PrintWriter;
/**
 * @Author: LightRain
 * @Description: OA 身份验证入口点
 * @DateTime: 2024/7/30 下午 7:52
 * @Version:1.0
 **/
public class OAAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Return notAuthorized = Return.builder().code(401).message("尚未授权").status(Return.FAILED).build();
        out.write(new ObjectMapper().writeValueAsString(notAuthorized));
        out.flush();
        out.close();
    }
}

实现 Return 类,用于返回统一的返回信息。

Return.java
package com.fairiy.magic.magicbackend.bean;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
 * @Author: LightRain
 * @Description: 统一返回信息
 * @DateTime: 2023-03-29 12:55
 * @Version:1.0
 **/
@Getter
@Setter
@Builder
@ToString
public class Return {
  // 状态码
  private int code;
  // 返回信息
  private String message;
  // 返回数据
  private String status;
  // 成功
  public static final String SUCCESS = "SUCCESS";
  // 失败
  public static final String FAILED = "FAILED";
}

# 实现登录接口

实现登录接口,用于处理用户登录请求,并返回登录结果。

LoginController.java
package com.fairiy.magic.magicbackend.controller;
import com.fairiy.magic.magicbackend.bean.Return;
import com.fairiy.magic.magicbackend.service.LoginService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Author: LightRain
 * @Description: 登录控制器
 * @DateTime: 2024/7/30 下午 21:10
 * @Version:1.0
 **/
@ResponseBody
@RestController
@RequestMapping("/login")
public class LoginController {
    private final LoginService service;
    public LoginController(LoginService service) {
        this.service = service;
    }
    /**
     * @Author: LightRain
     * @Date: 2024/8/31 下午 4:48
     * @Param: [session, username, captcha]
     * @Return: com.fairiy.magic.magicbackend.bean.Return
     * @Description: 登录成功
     * @Since 21
     */
    
    @RequestMapping(value = "/success", method = {RequestMethod.GET, RequestMethod.POST})
    public Return loginSuccess(HttpSession session, String username, String captcha) {
       return service.loginSuccess(session, username, captcha);
    }
    /**
     * @Author: LightRain
     * @Date: 2024/8/31 下午 4:48
     * @Param: []
     * @Return: com.fairiy.magic.magicbackend.bean.Return
     * @Description: 登录失败
     * @Since 21
     */
    @RequestMapping(value = "/failure", method = {RequestMethod.GET, RequestMethod.POST})
    public Return loginFailure() {
        return Return.builder().code(401).message("授权失败").status(Return.FAILED).build();
    }
    /**
     * @Author: LightRain
     * @Date: 2024/8/31 下午 4:48
     * @Param: [request, response]
     * @Return: java.lang.String
     * @Description: 退出登录
     * @Since 21
     */
    @RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST})
    public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "redirect:/login";
    }
}
LoginService.java
package com.fairiy.magic.magicbackend.service;
import com.fairiy.magic.magicbackend.bean.Return;
import jakarta.servlet.http.HttpSession;
/**
 * Created by LightRain on 2024/07/30 23:30.
 * 登录服务接口
 */
public interface LoginService {
    Return loginSuccess(HttpSession session, String username, String captcha);
}
LoginServiceImpl.java
package com.fairiy.magic.magicbackend.service;
import com.fairiy.magic.magicbackend.bean.Return;
import com.fairiy.magic.magicbackend.mapper.UserMapper;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Service;
/**
 * @Author: LightRain
 * @Description: 登录服务实现类,用于处理登录逻辑,使用 SpringSecurity 来处理授权请求
 * @DateTime: 2024/7/30 下午 23:18
 * @Version:1.0
 **/
@Service
public class LoginServiceImpl implements LoginService {
  // 注入 UserMapper
  private final UserMapper userMapper;
  public LoginServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;
  }
  // 登录成功,可以在此添加验证码验证等逻辑
  @Override
  public Return loginSuccess(HttpSession session, String username, String captcha) {
    System.out.println("login success");
    session.setAttribute("User", userMapper.getUserName(username));
    return Return.builder().code(200).message("授权成功").status(Return.SUCCESS).build();
  }
}

# 实现 Token 认证

实现 Token 认证,用于处理用户登录请求,并返回登录结果。

package com.fairiy.magic.magicbackend.controller;
import cn.hutool.core.codec.Base64;
import com.fairiy.magic.magicbackend.bean.Register;
import com.fairiy.magic.magicbackend.bean.Return;
import com.fairiy.magic.magicbackend.service.PublicService;
import com.google.code.kaptcha.Producer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
 * @Author: LightRain
 * @Description: 不登录就可以访问的公开控制器
 * @DateTime: 2024/7/30 下午 8:13
 * @Version:1.0
 **/
@Controller
@RequestMapping("/public")
public class PublicController {
  private final PublicService publicService;
  private final Producer kaptchaProducer;
  public PublicController(PublicService publicService, Producer kaptchaProducer) {
    this.publicService = publicService;
    this.kaptchaProducer = kaptchaProducer;
  }
  /**
   * @Author: LightRain
   * @Date: 2024/8/21 下午 6:42
   * @Param: [request]
   * @Return: org.springframework.security.web.csrf.CsrfToken
   * @Description: 获取 csrfToken
   * @Since 21
   */
  @ResponseBody
  @GetMapping("/csrf-token")
  public CsrfToken csrfToken(HttpServletRequest request) {
    CsrfToken attribute = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
    System.out.println(attribute.getToken());
    return attribute;
  }
  /**
   * @Author: LightRain
   * @Date: 2024/8/21 下午 4:05
   * @Param: [request, activationCode]
   * @Return: com.fairiy.magic.magicbackend.bean.Return
   * @Description: 注册接口
   * @Since 21
   */
  @ResponseBody
  @RequestMapping("/register")
  public Return register(Register request,String activationCode) {
    return publicService.registerAccount(request,activationCode);
  }
  /**
   * @Author: LightRain
   * @Date: 2024/8/21 下午 6:42
   * @Param: [request, response]
   * @Return: com.fairiy.magic.magicbackend.bean.Return
   * @Description: 生成验证码,暂时弃用
   * @Since 21
   */
  @Deprecated
  @ResponseBody
  @GetMapping("/createKaptchaCodeImg")
  public Return createKaptchaCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
    request.getSession().removeAttribute("kaptchaCode");
    // 设置浏览器缓存机制
    response.setHeader("Cache-Control", "no-store, no-cache");
    // 设置返回响应类型
    response.setContentType("image/jpeg");
    // 生成验证码
    String code = kaptchaProducer.createText();
    // 保存验证码到 session
    request.getSession().setAttribute("kaptchaCode", code);
    // 生成图片验证码
    BufferedImage image = kaptchaProducer.createImage(code);
    // 转为 Base64
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    ImageIO.write(image, "png", stream);
    String imgString = "data:image/gif;base64," + Base64.encode(stream.toByteArray());
    stream.flush();
    stream.close();
    // 将数据存入并将其返回
    return Return.builder().code(200).message(imgString).status(Return.SUCCESS).build();
  }
}

# 实现 Session 管理

实现 Session 管理,用于处理用户登录请求,并返回登录结果。

application.yml
server:
  port: 5170
  servlet:
    session:
      timeout: 3600 #session Effective time is one hour 3600s
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/magic_journey?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
SessionController.java
package com.fairiy.magic.magicbackend.controller;
import com.fairiy.magic.magicbackend.bean.Return;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
 * @Author: LightRain
 * @Description: Session 控制器
 * @DateTime: 2024/7/30 下午 21:15
 * @Version:1.0
 **/
@Controller
public class SessionController {
    @ResponseBody
    @GetMapping("session/invalid")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String sessionInvalid(HttpSession session) {
        session.removeAttribute("User");
        return "session已失效,请重新认证";
    }
    @ResponseBody
    @RequestMapping("session/status")
    public Return sessionStatus(HttpSession session) {
        if (session.getAttribute("User") != null) {
            return Return.builder().code(200).message("授权成功").status(Return.SUCCESS).build();
        }
        return Return.builder().code(401).message("尚未授权").status(Return.FAILED).build();
    }
}

# 实现 MVC-CORS 配置

package com.fairiy.magic.magicbackend.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @Author: LightRain
 * @Description: Cors 从源策略配置
 * @DateTime: 2023-04-01 17:51
 * @Version:1.0
 **/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    /**
     * @Author: LightRain
     * @Date: 3/4/2023 下午 1:55
     * @Param: [registry]
     * @Return: void
     * @Description: 添加 Cors 从源策略映射
     * @since 17
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        /*
        addMapping ("/**"): 设置允许跨域的路径
        allowedOriginPatterns ("*"): 设置允许跨域请求的域名
        allowCredentials (true): 是否允许 cookie
        allowedMethods ("GET", "POST", "DELETE", "PUT"): 设置允许的请求方式
        allowedHeaders ("*"): 设置允许的 header 属性
        maxAge (3600): 跨域允许时间
         */
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}