六、自定义认证filter替换掉默认的表单认证,改用json接收登录请求参数
说明
通过自定义认证filter,替换掉默认的表单认证,改用json接收登录请求参数。自定义响应处理方法,登录成功或失败后响应json数据
配置说明:
UsernamePasswordAuthenticationFilter: 以表单提交方式处理身份认证,提交用户名密码参数到POST /login完成登录认证。通过继承该类,重写attemptAuthentication方法以改写json参数接收方式和认证处理。
.addFilterAt(自定义Filter实例, UsernamePasswordAuthenticationFilter.class): 在HttpSecurity 配置自定义认证过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器。
这里还可以添加其他的过滤器,例如:.addFilterAfter() 、.addFilterBefore()
AuthenticationConfiguration:通过它可以获取到具体的认证处理器,本文中对应的认证方式为基于内存的认证方式。
SecurityConfig 配置
package com.example.uaa.config;
import com.example.uaa.filter.RestAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity(debug = false) // 开启调试模式,生产环境不要使用
@RequiredArgsConstructor
public class SecurityConfig {
private final ObjectMapper objectMapper;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((req) -> req
// 需要认证并拥有特定角色才能访问的URL
.antMatchers("/api/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
// 放行不需要认证的URL
.antMatchers("/authorize/**").permitAll()
.antMatchers("/webjars/**", "/error").permitAll()
// 放行通用的css、js等静态资源位置
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
// 除了以上规则,默认拦截所有URL请求,要求认证后才能访问(该语句必须要放在末尾)
.anyRequest().authenticated()
)
// 添加自定义登录过滤器,替换掉默认的UsernamePasswordAuthenticationFilter过滤器
.addFilterAt(restAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 以下表单登录,被restAuthenticationFilter配置替换:
// .formLogin(form-> form
// // 定义登陆页面
// .loginPage("/login").permitAll()
// // 登录成功后跳转的页面
// // .defaultSuccessUrl("/")
// // 登录成功后置处理器。会覆盖掉defaultSuccessUrl的配置
// .successHandler(((request, response, authentication) -> {
// ObjectMapper objectMapper = new ObjectMapper();
// response.setStatus(HttpStatus.OK.value());
// response.getWriter().println("LoginSuccess:"+
// objectMapper.writeValueAsString(authentication));
// }))
// // 登录失败后置处理器,不配置,默认跳转/login?error
// .failureHandler(((request, response, exception) -> {
// response.setStatus(HttpStatus.UNAUTHORIZED.value());
// response.getWriter().println("LoginFailure!");
// }))
// )
// 配置退出登录的路径,配置后使用POST请求。若不配置,默认为/logout,退出成功跳到/login
.logout(logout-> logout.logoutUrl("/perform_logout").logoutSuccessUrl("/login"))
.httpBasic(Customizer.withDefaults())
// 开启CSRF ,并配置不需要CSRF的路径
// .csrf(csrf->csrf.ignoringAntMatchers("/api/**"))
// .csrf()
// 禁用csrf
.csrf().disable()
.rememberMe(Customizer.withDefaults());
// ...
return http.build();
}
/**
* 设定认证方式 Authentication
* 代替了配置
* spring.security.user.name=user
* spring.security.user.password=12345
* spring.security.user.roles=ADMIN,USER
*
* 基于spring boot 2.7 后新的认证方式:
* https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
*/
/**
* 基于内存的认证 InMemoryUserDetailsManager
*/
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("123456"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private RestAuthenticationFilter restAuthenticationFilter() throws Exception {
RestAuthenticationFilter filter = new RestAuthenticationFilter(objectMapper);
// 登录成功后置处理器。
filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.getWriter().println("LoginSuccess: "+
objectMapper.writeValueAsString(authentication));
});
// 登录失败后置处理器
filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().println("LoginFailure!");
});
// 获得AuthenticationManager
filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
filter.setFilterProcessesUrl("/authorize/login");
return filter;
}
}
自定义认证过滤器 RestAuthenticationFilter.java
package com.example.uaa.filter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登录过滤器
*/
@RequiredArgsConstructor
public class RestAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持的请求方法: " + request.getMethod());
}
UsernamePasswordAuthenticationToken authRequest;
try {
var jsonNode = objectMapper.readTree(request.getInputStream());
String username = jsonNode.get("username").textValue();
String password = jsonNode.get("password").textValue();
authRequest = new UsernamePasswordAuthenticationToken(username, password);
} catch (IOException e) {
e.printStackTrace();
throw new BadCredentialsException("用户名或密码错误");
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

浙公网安备 33010602011771号