SpringSecurity登录后自定义成功和失败处理器(源码级讲解)

一、简单叙述

上一节【SpringSecurity登陆原理(源码级讲解)】已经讲过了完整的登录流程,所以此处不再重复讲解,只是接着上一节继续讲解。登陆完后会给我们返回信息,这没毛病,但是问题来了,他跳转了页面,假设我们不想跳转页面,只想返回固定格式的JSON呢?这就需要我们自定义成功处理器了。失败的情况也雷同。

二、源码分析

(一)、接着上一节。AbstractAuthenticationProcessingFilter.doFilter()

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
    
    // 过滤器doFilter方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        /*
         * 判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理。
         */
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;

        try {
            // 很关键!!!调用了子类(UsernamePasswordAuthenticationFilter)的方法
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            // 最终认证成功后,会处理一些与session相关的方法(比如将认证信息存到session等操作)。
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            // 认证失败后的一些处理。
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        /*
         * 最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中
         * 并调用成功处理器做相应的操作。
         */
        successfulAuthentication(request, response, chain, authResult);
    }
}

PS:不难发现,我们成功/失败后会各自调用一个方法来处理。

(二)、成功处理器

protected void successfulAuthentication(HttpServletRequest request,
    HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
    }
    
    // 将当前的认证信息放到SecurityContextHolder中
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }
    // 调用成功处理器,可以自己实现AuthenticationSuccessHandler接口重写方法写自己的逻辑
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

最后一句话调用了AuthenticationSuccessHandler接口的onAuthenticationSuccess方法。这下简单了,我们可以自己写一个类去实现此接口。并配置为接口的处理器即可。

(三)、失败处理器

同理,调用了AuthenticationFailureHandler接口的onAuthenticationFailure方法。老规矩,我们依然是自己写一个类去实现此接口,并配置为此接口的处理器即可。

三、总结

这。。。好尴尬,没啥说的。因为很简单,只要摸清楚上一章节的东西,这章节一点压力都没有,看一眼就懂了。一步步分析我给出的源码即可。实在有问题留言给我,看到必回!

四、Demo

1、成功处理器

package com.chentongwei.security.browser.authentication;

import com.alibaba.fastjson.JSON;
import com.chentongwei.security.core.entity.SimpleResponse;
import com.chentongwei.security.core.enums.LoginType;
import com.chentongwei.security.core.properties.SecurityProperties;
import org.apache.commons.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 为什么不实现接口,而是继承SavedRequestAwareAuthenticationSuccessHandler类的方式?
 * 因为SavedRequestAwareAuthenticationSuccessHandler这个类记住了你上一次的请求路径,比如:
 * 你请求user.html。然后被拦截到了登录页,这时候你输入完用户名密码点击登录,会自动跳转到user.html,而不是主页面。
 *
 * 若是前后分离项目则实现接口即可,因为我弄的是通用的权限组件,所以选择了继承
 *
 * @author [email protected] 2018-03-26 14:09
 */
@Component("ctwAuthenticationSuccessHandler")
public class CtwAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        logger.info("登录成功!");
        if (ObjectUtils.equals(securityProperties.getBrowser().getLoginType(), LoginType.JSON)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(new SimpleResponse(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase(), authentication)));
        } else {
            // 会帮我们跳转到上一次请求的页面上
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

PS:我是根据配置loginType是否是JSON而定返回形式,JSON反回JSON,REDIRECT跳转页面。

2、失败处理器

package com.chentongwei.security.browser.authentication;

import com.alibaba.fastjson.JSON;
import com.chentongwei.security.core.entity.SimpleResponse;
import com.chentongwei.security.core.enums.LoginType;
import com.chentongwei.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * 自定义失败处理器
 *
 * @author [email protected] 2018-03-26 14:02
 */
@Component("ctwAuthenticationFailureHandler")
public class CtwAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("登录失败!");

        if (Objects.equals(securityProperties.getBrowser().getLoginType(), LoginType.JSON)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(new SimpleResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null)));
        } else {
            response.setContentType("text/html;charset=UTF-8");
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

PS:我是根据配置loginType是否是JSON而定返回形式,JSON反回JSON,REDIRECT跳转页面。

3、配置类

package com.chentongwei.security.core.authentication;

import com.chentongwei.security.core.constant.SecurityConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

/**
 * 封装处理请求成功/失败的操作
 *
 * @author [email protected] 2018-03-26 10:35
 */
public class AbstractChannelSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler ctwAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler ctwAuthenticationFailureHandler;

    protected void authenticationConfig(HttpSecurity http) throws Exception {
        // 表单登录
        http.formLogin()
            // 默认表单登录页
            .loginPage(SecurityConstant.DEFAULT_UNAUTHENTICATION_URL)
            // 登录接口
            .loginProcessingUrl(SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM)
            .successHandler(ctwAuthenticationSuccessHandler)
            .failureHandler(ctwAuthenticationFailureHandler);
    }
}

4、html登录页




    
    登录


标准登录页面

表单登录

用户名:
密码:

5、Service类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
 * @author [email protected] 2018-03-26 13:15
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("表单登录用户名:" + username);
        return buildUser(username);
    }

    private UserDetails buildUser(String username) {
        /**
         * passwordEncoder.encode这步骤应该放到注册接口去做,而这里只需要传一个从db查出来的pwd即可。
         *
         * passwordEncoder.encode("123456")每次打印出来都是不同的,虽然是同一个(123456)密码,
         * 但是他会随机生成一个盐(salt),他会把随机生成的盐混到加密的密码里。Springsecurity验证(matches方法)的时候会将利用此盐解析出pwd,进行匹配。
         * 这样的好处是:如果数据库里面有10个123456密码。但是被破解了1个,那么另外九个是安全的,因为db里存的串是不一样的。
         */
        String password = passwordEncoder.encode("123456");
        logger.info("数据库密码是:" + password);
        // 这个User不一定必须用SpringSecurity的,可以写一个自定义实现UserDetails接口的类,然后把是否锁定等判断逻辑写进去。
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

PS:具体完整的代码请看我的码云,已经全部提交到码云了。

五、广告

  • Demo源码已上传到码云,文章会定期更新。下面链接是我对Spring-Security进行的二次封装。使之变得零配置,高扩展。如果觉得对您有帮助,希望给个star,没帮助也可以看看框架思想。

    https://gitee.com/geekerdream/common-security

  • QQ群【Java初学者学习交流群】:458430385

  • 微信公众号【Java码农社区】

    SpringSecurity登录后自定义成功和失败处理器(源码级讲解)_第1张图片
    img
  • 今日头条号:编程界的小学生

你可能感兴趣的:(SpringSecurity登录后自定义成功和失败处理器(源码级讲解))