文章目录
- 1. AbstractAuthenticationProcessingFilter 过滤器
- 2. UsernamePasswordAuthenticationFilter 过滤器
- 3. 自定义过滤器实现 Json 格式登录
SpringSecurity 中默认的是表单登录格式,即用户在表单中输入用户名和密码进行登录,登录参数的提取是在 UsernamePasswordAuthenticationFilter 过滤器中完成的,UsernamePasswordAuthenticationFilter 是AbstractAuthenticationProcessingFilter 针对使用用户名和密码进行身份认证而定制化的一个过滤器。其添加是在调用http.formLogin() 时作用,默认的登录请求 pattern 为 “/login”,并且为 POST 请求。当我们登录的时候,也就是匹配到loginProcessingUrl,这个过滤器就会委托认证管理器 authenticationManager 来验证登录。
UsernamePasswordAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter
1. AbstractAuthenticationProcessingFilter 过滤器
AbstractAuthenticationProcessingFilter 是一个抽象类,主要的功能是身份认证。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都继承了 AbstractAuthenticationProcessingFilter ,并重写了方法 attemptAuthentication 进行身份认证。
AbstractAuthenticationProcessingFilter 源码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
//事件发布管理器
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
// 认证管理器,定义了SpringSecurity如何进行认证操作
// 认证成功后会返回一个Authentication对象,这个对象会被设置到SecurityContextHolder中
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
// 如果用户开启了类似“记住我”之类的免密码登录,RememberMeServices来进行管理。
private RememberMeServices rememberMeServices = new NullRememberMeServices();
// 请求匹配器,定义了match()方法,匹配请求HttpServletRequest是否符合定义的规则
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
// 会话验证管理
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean allowSessionCreation = true;
// 用户登录成功的后续处理
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
// 用户登录失败的后续处理
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
// 判断是否需要认证
if (!this.requiresAuthentication(request, response)) {
// 如果不需要认证,继续执行下一个过滤器
chain.doFilter(request, response);
} else {
Authentication authResult;
try {
// 认证处理,该方法需要子类去重写
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 身份认证成功,保存session
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
// 认证失败的处理逻辑
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
// 认证失败的处理逻辑
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 认证成功的处理逻辑
this.successfulAuthentication(request, response, chain, authResult);
}
}
// 判断该filter是否需要处理该次请求,即请求的路径和该filter配置的要处理的url是否匹配
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return this.requiresAuthenticationRequestMatcher.matches(request);
}
// 这个方法的目的很明确,就是需要子类提供身份认证的具体实现。
// 子类根据 HttpServletRequest 等信息进行身份认证,并返回 Authentication 对象、 null、异常
// 分别表示认证成功返回的身份认证信息、需要其他 Filter 继续进行身份认证、认证失败。
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
// 认证成功的处理逻辑
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 将认证成功的用户信息保存到 SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
// 处理记住我逻辑
this.rememberMeServices.loginSuccess(request, response, authResult);
// 发布时间,即发布认证成功消息,供其他的bean接收和处理
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 认证成功后后续处理
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
// 认证失败的处理逻辑
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
2. UsernamePasswordAuthenticationFilter 过滤器
1、页面输入用户名和密码;
2、登录请求被UsernamePasswordAuthenticationFilter过滤器拦截,开始进行登录认证;
3、UsernamePasswordAuthenticationFilter过滤器会获取用户表单输入的用户名和棉麻,并创建一个未认证的UsernamePasswordAuthenticationToken,然后调用认证管理器AuthenticationManager完成认证功能;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
// 表单提交的username的name属性值
private String usernameParameter = "username";
// 表单提交的password的name属性值
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
// 默认处理/login请求且为Post方式
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 获取用户名
String username = this.obtainUsername(request);
//获取密码
String password = this.obtainPassword(request);
// 参数校验
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 将用户的用户名和密码封装成 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
// 使用AuthenticationManager认证管理器完成认证功能
return this.getAuthenticationManager().authenticate(authRequest);
}
}
// 从request中获取用户提交的密码
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
// 从request中获取用户提交的用户名
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
4、AuthenticationManager不直接进行登录验证,而是使用DaoAuthenticationProvider进行验证,DaoAuthenticationProvider调用UserDetailsService(自定义)对象的loadUserByUsername方法获取用户信息,然后对获得的对象进行一系列的检查(自定义),包括预检查、附加检查、后检查。预检查检查用户是否被冻结、是否启用、是否过期。附加检查进行用户名密码验证。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
}
5、登陆成功后将验证过的用户信息存储在SecurityContext中,然后调用登录成功处理器(自定义)。
// 认证成功的处理逻辑
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 将认证成功的用户信息保存到 SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authResult);
// 处理记住我逻辑
this.rememberMeServices.loginSuccess(request, response, authResult);
// 发布时间,即发布认证成功消息,供其他的bean接收和处理
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 认证成功后后续处理
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
3. 自定义过滤器实现 Json 格式登录
在实际项目中,我们可能通过 json 格式来传递参数,这就需要我们自定义登录过滤器链来实现。登录参数的提取是在UsernamePasswordAuthenticationFilter过滤器中完成的,如果要使用 Json 格式登录,只要模仿UsernamePasswordAuthenticationFilter过滤器自定义自己的过滤器,再将自定义的额过滤器放到UsernamePasswordAuthenticationFilter过滤器所在的位置即可。
定义一个LoginFilter 继承自UsernamePasswordAuthenticationFilter:
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private ObjectMapper objectMapper;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 确保进入过滤器中的请求时Post请求
if(!request.getMethod().equals("POST")){
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)
|| request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)){
Map<String,String> userInfo = new HashMap<>();
try {
// 将输入流转为Map对象
userInfo = objectMapper.readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
// 从Map对象中分别提取用户名和密码
// 构造成UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest
= new UsernamePasswordAuthenticationToken(username,password);
setDetails(request,authRequest);
// 调用AuthenticationManager的 authticate()方法完成认证操作
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
//如果不是json格式,那么按照父类的表单登录逻辑处理
return super.attemptAuthentication(request,response);
}
}
LoginFilter 定义完后,需要将其添加到SpringSecurity过滤器链中,代码如下:
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("zhansan")
.password("{noop}123")
.roles("admin");
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManagerBean();
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
// 设置认证管理器
loginFilter.setAuthenticationManager(authenticationManagerBean());
// 自定义认证成功的处理逻辑,以json格式写回浏览器
loginFilter.setAuthenticationSuccessHandler(((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString(authentication));
}));
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
使用Postman测试 json 格式登录:
{"username":"zhangsan","password":"123"}