Spring Security的maven依赖(version由spring-boot-starter-parent管理):
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>5.7.0</version>
</dependency>
1、创建 Security 配置类
Security 配置类通过bean方法来提供Spring Security的相关配置工作。
如下所示的SecurityConfig为Security配置类,属于新的配置方式,老的配置方式是配置类需要从WebSecurityConfigurerAdapter类继承,然后重写父类的一些方法。新的配置方式是在Spring Security 5.7版本及以上才能使用(对应SpringBoot版本需要2.7.0及以上)。
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter; //JWT认证过滤器,详见下面对其的解释
@Bean //提供Spring Security的认证管理器,Controller中会通过依赖注入来使用它
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
//提供用户信息来源:Spring Security会自动发现并使用UserDetailsService的实现customUserDetailsService来作为用户信息来源,如果像下面这样
//编写一个Bean方法来手动提供用户信息来源的话一般为测试中方式
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password(passwordEncoder().encode("password")).roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean //提供认证管理器中的密码编码器
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //使用BCrypt算法来提供密码加密(存储时)和密码验证(登录时)
}
@Bean //提供Web安全定制器,在下面配置的Spring Security过滤器链生效之前进行底层定制
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> {
web.debug(true); // 开启Spring Security 的调试日志输出(如果想知道一个请求究竟经过了哪些安全过滤器,或者安全配置为何没有生效,启用调试模式非常有帮助)
web.ignoring().requestMatchers("/css/**", "/js/**", "/images/**") //设置指定的路径不进行过滤(比如静态资源、对外公开访问的API如login)
.requestMatchers("/favicon.ico")
.requestMatchers("/public/api/**");};
}
//配置Spring Security过滤器链
//Spring Security默认会对所有请求进行认证,如下所示可以配置单个或多个过滤器来指定需要进行认证的路径
//在过滤器中可以设置认证的方式(如JWT认证、表单认证)、设置认证失败处理、设置Session相关等
//@Order用来指定过滤器的优先级,数字小的先匹配,当请求被第一个过滤器捕获后,后面的过滤器不会再捕获该请求
//需要JWT认证的路径:现代SPA项目一般使用JWT进行认证
@Bean
@Order(1)
public SecurityFilterChain securedApiFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/test/**", "/api/**") // 指定该过滤器链只对 /test 和 /api 开头的路径生效.
.authorizeHttpRequests( //开始配置请求的授权规则
authz -> authz
.anyRequest() //匹配所有类型的请求(如GET、POST等)
.authenticated() //指定这些请求都需要认证
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //指定进行JWT认证
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 告诉Spring Security不要自动创建Session,即无状态:JWT认证相当于是使用token来维护状态,所以无需使用Session来保持状态
.csrf(csrf -> csrf.disable()) //禁用CSRF保护:基于表单的认证(Session 认证)使用CSRF Token来杜绝CSRF,而JWT认证使用请求头携带token而不是使用cookie来携带SessionID,所以不需要CSRF保护
.exceptionHandling(exception -> exception//认证失败(请求无token、token无效或过期、权限不足等)异常处理
.authenticationEntryPoint((request, response, authException) -> { //请求无token、token无效或过期
//返回需要认证的应答
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"error\": \"需要认证访问\"}"); })
.accessDeniedHandler((request, response, accessDeniedException) -> { //权限不足
//返回权限不足应答
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("{\"error\": \"权限不足\"}"); })
);
return http.build();
}
//需要表单认证的路径:MPA项目多使用表单认证
//在Spring Security中,在配置了.formLogin()后,框架会自动处理登录请求(POST方式),比如如下所示,当用户请求了/auth登录接口后,
//调用配置的UserDetailsService来加载用户信息并进行密码比对等验证工作(从请求中提取出默认字段名为 username和 password的参数),
//认证成功会重定向到用户最初想访问的页面或配置的成功页面,认证失败重定向到配置的登录失败页面。
//.formLogin()后默认也会开启Session 会话来维持用户的登录状态,当用户未认证访问页面的时候,会重定向到配置的登录页。
@Bean
@Order(2)
public SecurityFilterChain formFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/**") //指定该过滤器对所有路径生效
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login").permitAll() //Spring Security可能不会自动放行下面通过.loginPage()配置的GET类型的登录页路径,所以这里对登录页显示放行
.anyRequest().authenticated())
.formLogin( //启用表单认证
form -> form
.loginPage("/login") //登录页
.loginProcessingUrl("/auth") //登录接口路径
.defaultSuccessUrl("/home") // 登录成功后跳转到用户最初访问的页面或home
.failureUrl("/my-login?error") // 登录失败后的跳转页面
.permitAll() //对 /auth 和 /login 放行,不进行认证
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)); // 有状态
return http.build();
}
//无需进行认证的路径(公共资源如图片、CSS、JS这些静态资源,错误页等)
@Bean
@Order(3)
public SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/public/**", "/error") //仅过滤public开头的路径和/error路径
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); //被过滤的请求都不需要进行认证
return http.build();
}
}
.securityMatcher()用来设置过滤器链级别的匹配,即决定是否进入该过滤器链,.requestMatchers()用来进一步控制授权规则级别的匹配,即进入过滤链后再次决定访问权限,如下所示:
java
@Bean
public SecurityFilterChain testFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/**") // 所有请求都进入这个过滤器链,也可以不显示调用该方法,因为默认即为过滤所有请求
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll() // public开头路径不进行认证
.requestMatchers("/api/**").authenticated() // API需要认证
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin开头路径不仅需要认证还需要有管理员权限
.anyRequest().authenticated() // 其他所有路径需要认证
);
return http.build();
}
2、认证行为设置和认证过滤
下面为配置类中认证过滤器成员的类型,其用来设置认证的具体动作,比如这里是进行JWT认证:
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { //继承自OncePerRequestFilter保证了它在一个请求的生命周期内只会被执行一次
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
private final PathMatcher pathMatcher = new AntPathMatcher();
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
//设置对过滤的请求进行JWT验证
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { //判断是否带JWT认证头
String token = authorizationHeader.substring(7); //提取到的JWT令牌
if (jwtUtil.validateToken(token)) { //令牌验证通过
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//创建Authentication认证信息对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将认证信息对象设置到安全上下文中,相当于告诉Spring Security:"这个用户已成功登录",Spring Security会保存该用户信息以便后续使用
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response); //无论令牌是否存在或验证是否通过,最后必须放行请求
}
private static final String[] INCLUDE_PATHS = { "/login", "/public1/*", "/public2/**", "/public3/*.html", };//login(不包含子路径)、/public1及其一级子路径、/public2及其所有子路径、public3下所有后缀名为html的路径
//返回true表示不对当前请求进行上面doFilterInternal方法中设置的认证,返回false表示进行验证,不重写该方法的话默认会对所有请求进行doFilterInternal设置的认证
//对于登录、公开资源这些不需要认证的路径可以直接跳过上面doFilterInternal方法中设置的认证流程,以提高效率
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String requestPath = request.getServletPath();
return !Arrays.stream(INCLUDE_PATHS)
.anyMatch(pattern -> pathMatcher.match(pattern, requestPath));
}
}
上面的shouldNotFilter内仅仅是设置不进行认证动作以提高性能,对于不需要进行认证的路径,还是需要在Security配置类中的FilterChainBean过滤器方法中来设置。对于完全不需要进行认证的路径(如静态资源、公开api),一般是直接在Security配置类中的WebSecurityCustomizerBean方法来进行一次性配置,其配置的路径完全绕过整个 Spring Security 过滤器链。对于不需要进行认证但需要在过滤器里进行相关配置,比如对于登录页面,提交登录信息的 POST请求通常需要 CSRF Token 保护,可以在过滤器类的shouldNotFilter内设置不进行认证动作,然后在FilterChainBean过滤器方法中设置相关的配置。
3、实现UserDetailsService
UserDetailsService是用来给Security提供用户名、密码等用户信息的。Security使用这些信息来验证登录请求,以及提供SecurityContextHolder、@AuthenticationPrincipal等方式来获得这些用户信息(详见下面4)。
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
//提供用户信息给 Spring Security使用
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getByName(username); //获得用户信息
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
//返回包含用户名、密码、权限的用户信息,返回类型为Spring Security 的 UserDetails格式
return org.springframework.security.core.userdetails.User.builder()
.username(user.getName())
.password(user.getPassword())
.authorities("USER") //用户角色/权限
.build();
}
}
4、修改登录接口
在登录接口中可以使用Spring Security来验证用户登录:
java
@Controller
public class UserController {
@Autowired
ObjectMapper objectMapper;
@Autowired
private JwtUtil jwtUtil;
@PostMapping(value = "/login", produces = "application/json;charset=UTF-8")
public ResponseEntity<String> login(@RequestBody LoginRequest requestBodyData) {
String username = requestBodyData.getUsername();
String password = requestBodyData.getPassword();
Map<String, String> response = new HashMap<>();
try {
// 使用 Spring Security 进行用户名密码认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password) //封装用户名密码作为认证请求的载体
);
// 认证成功,生成 token后发送给客户端
String token = jwtUtil.generateToken(username);
response.put("token", token);
return ResponseEntity.ok(objectMapper.writeValueAsString(response));
} catch (AuthenticationException e) {
response.put("error", "用户名或密码错误");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(objectMapper.writeValueAsString(response));
} catch (Exception e) {
response.put("error", "登录失败");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(objectMapper.writeValueAsString(response));
}
}
......
}
5、Security自动处理认证
用户登录成功后,用户访问需要认证(登录)的接口的时候,因为请求中携带了token,所以Spring Security会自动处理认证,只有认证成功才会进入该请求的Controller处理方法,认证失败的话,不进入Controller方法,直接触发在配置类中配置的认证失败异常处理。在Controller接口中可以直接使用Spring Security相关方法来获得用户信息,因为代码能执行到Controller方法的话说明请求已经认证成功:
java
@GetMapping("/test")
public ResponseEntity<String> test() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //获取当前线程的安全上下文
String username = authentication.getName(); //从认证对象中获得用户名
Map<String, String> response = new HashMap<>();
response.put("message", "已认证成功");
response.put("username", username);
return ResponseEntity.ok(objectMapper.writeValueAsString(response));
}
除了使用SecurityContextHolder外,还有多种方式可以获得用户信息,如下所示。
下面的Controller方法中都没有对参数值为null做判断处理,因为只有请求通过了Spring Security认证或者请求的是无需认证的公开路径(请求不会被 Spring Security过滤器链过滤)或者请求的路径授权规则被设置为为.permit()即无需认证,对应的Controller方法才会执行,如果请求没有被认证通过(比如请求无携带token、token过期、无权限等),会触发过滤器链中设置的异常处理。为了防止在无需进行认证路径的Controller方法中误添加了 Principal 参数或 @AuthenticationPrincipal参数注解,还是最好添加参数是否为null的判断。
java
@RestController
public class UserController {
// 方式1:通过 Principal 对象获得用户的简单信息(Principal是Java SE中接口,可以像HttpServletRequest那样直接作为Controller方法中的参数来使用)
@GetMapping("/user/principal")
public String getUserByPrincipal(Principal principal) {
return "当前用户: " + principal.getName();
}
// 方式2:通过 Authentication 对象(Authentication继承自Principal)
@GetMapping("/user/auth")
public String getUserByAuthentication(Authentication authentication) {
return "当前用户: " + authentication.getName() +
", 权限: " + authentication.getAuthorities();
}
// 方式3:通过 @AuthenticationPrincipal 注解获得详细的用户信息到UserDetails参数
@GetMapping("/user/details")
public String getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {
return "用户名: " + userDetails.getUsername() +
", 账号未过期: " + userDetails.isAccountNonExpired();
}
// 方式4:通过@AuthenticationPrincipal获得详细的用户信息到自定义用户对象CustomUserDetails
@GetMapping("/user/custom")
public String getCustomUser(@AuthenticationPrincipal CustomUserDetails user) {
return "用户ID: " + user.getId() +
", 用户名: " + user.getUsername() +
", 邮箱: " + user.getEmail();
}
}