spring security验证流程:

  • spring security的登录验证是由UsernamePasswordAuthenticationFilter这个过滤器完成,在该类的父类AbstractAuthenticationProcessingFilter(585行)中有一个AuthenticationManager接口属性,验证工作主要是通过这个AuthenticationManager接口的实例来完成的。默认情况下,spring security会把ProviderManager类的实例注入到该属性。而自定义AbstractAuthenticationProcessingFilter的情况下,一般会在配置文件:,起别名,然后注入该自定义类的某个属性中,看上面那个配置文件就知道了
  • UsernamePasswordAuthenticationFilter的验证过程如下:
    • 首先过滤器会调用自身的attemptAuthentication方法(646行),该方法规定请求必须为post,并从request中取出authentication,authentication是在SecurityContextPersistenceFilter过滤器中通过捕获用户提交的登录表单中的内容生成的一个Authentication接口实例。
    • attemptAuthentication方法拿到authentication对象后,在方法中又调用ProviderManager类的authenticate方法并传入authentication对象(由于只能验证用户名、密码的简单信息,所以经常重写以实现复杂的用户验证,并返回包含当前用户所有信息的authentication),验证用户是否能登录,并处理定义登录成败重定向的页面
    • 也就是说attemptAuthentication方法会调用类中的List providers集合中各个AuthenticationProvider接口实现类中的authenticate(Authentication authentication)进行验证
    • provider的实现类在验证用户时,会调用UserDetailsService的实现类的loadUserByUsername方法获取用户信息。
  • 由此可见,真正的验证逻辑是由各个AuthenticationProvider接口实现类来完成的。DaoAuthenticationProvider类是默认情况下会被注入AuthenticationProvider的接口实现类。

源码分析

  • AbstractAuthenticationProcessingFilter

    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;
    
            /*
             * 1、判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理。
             */
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
    
                return;
            }
    
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }
    
            Authentication authResult;
    
            /*
             * 2、调用子类(UsernamePasswordAuthenticationFilter)attemptAuthentication方法
             */
            try {
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
    
                /*
                 * 3、最终认证成功后,会处理一些与session相关的方法
                 * (比如将认证信息存到session等操作)。
                 */ 
                sessionStrategy.onAuthentication(authResult, request, response);
            }
    
            /*
             * 3、认证失败后的一些处理。
             */ 
            catch (InternalAuthenticationServiceException failed) {
                logger.error(
                        "An internal error occurred while trying to authenticate the user.",
                        failed);
                unsuccessfulAuthentication(request, response, failed);
                return;
            }
            catch (AuthenticationException failed) {
                unsuccessfulAuthentication(request, response, failed);
                return;
            }
            /*
             * 4、认证成功,继续下个请求
             */
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
    
            /*
             * 5、最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中
             * 并调用认证成功处理器AuthenticationSuccessHandler接口实现类的
             * onAuthenticationSuccess()。
             */
            successfulAuthentication(request, response, chain, authResult);
        }
    }
    
  • UsernamePasswordAuthenticaionFilter,以上AbstractAuthenticationProcessingFilter的子类,主要是重写attemptAuthentication(),规定请求必须为post。

    public class UsernamePasswordAuthenticationFilter 
            extends AbstractAuthenticationProcessingFilter {
    
        //实现父类的方法
        public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
    
            // 认证请求的方式必须为POST
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            // 获取表单参数
            String username = obtainUsername(request);
            String password = obtainPassword(request);
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            /*
             * 封装进Authentication实现类UsernamePasswordAuthenticationToken
             * public UsernamePasswordAuthenticationToken(Object principal, Object credentials)    {
                    super((Collection)null); //设置权限为null
                    this.principal = principal;
                    this.credentials = credentials;
                    this.setAuthenticated(false);
                }
             */
            UsernamePasswordAuthenticationToken authRequest = 
                new UsernamePasswordAuthenticationToken(
                    username, password); //一般来说principle是User对象较常见
            setDetails(request, authRequest); //Allow subclasses to set the "details" property
    
            //调用AuthenticationManager管理下的AuthenticationProvider接口实现类的authenticate()
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    
  • AuthenticationManager:AuthenticationManager仅仅是一个接口,默认实现类是ProviderManager,ProviderManager不是自己直接对请求进行验证,而是将其委派给一个AuthenticationProvider列表。例如我们要同时使用jdbc认证和ldap认证,那么就可以用这个列表。列表中的每一个AuthenticationProvider将会被一次查询是否需要通过器进行验证。每个provider的验证结果只有两种情况:抛出ProviderNotFoundException异常或者完全填充一个Authentication对象,并存储在SecurityContext中。
  • ProviderManager

    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
    
        Class toTest = authentication.getClass();
        Object lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        // 拿到全部的provider
        Iterator e = this.getProviders().iterator();
        // 遍历provider
        while(e.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)e.next();
            // 调用provider的supports()校验是否支持当前token
            if(provider.supports(toTest)) {
                if(debug) {
                    logger.debug("Authentication attempt using " 
                        + provider.getClass().getName());
                }
    
                try {
                    /*
                     * 找到后直接break,并由当前provider来进行校验工作
                     * 调用AuthenticationProvider实现类的authenticate()
                     */
                    result = provider.authenticate(authentication);
                    if(result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }
    
        // 若没有一个支持,则尝试交给父类来执行
        if(result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }
    ..........................
    }
    
  • DaoAuthenticationProvider:AuthenticationProvider默认实现类,实现了父类主要方法authenticate()要用到的查找用户方法retrieveUser()

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
        //重现用户的方法
        protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication) 
                throws AuthenticationException {
    
            UserDetails loadedUser;
            try {
                /*
                 * 调用UserDetailsService接口的loadUserByUsername方法。
                 */
                loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            } catch (UsernameNotFoundException var6) {
                if(authentication.getCredentials() != null) {
                    String presentedPassword = authentication.getCredentials().toString();
                    this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
                }
                throw var6;
            } catch (Exception var7) {
                throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
            }
    
            if(loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        }
    }
    
  • AbstractUserDetailsAuthenticationProvider:以上DaoAuthenticationProvider的父类,定义了authenticate(),调用子类retrieveUser()查找用户all信息,可以从db查找,并进行用户验证。

    public abstract class AbstractUserDetailsAuthenticationProvider 
            implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    
        // 用户验证方法
        public Authentication authenticate(Authentication authentication) 
                throws AuthenticationException {
    
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider
                .onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
    
            String username = authentication.getPrincipal() == null?
                "NONE_PROVIDED":authentication.getName();
            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);
            if(user == null) {
                cacheWasUsed = false;
    
                try {
                    // 调用子类retrieveUser()
                    user = this.retrieveUser(username,
                        (UsernamePasswordAuthenticationToken)authentication);
                } catch (UsernameNotFoundException var6) {
                    this.logger.debug("User \'" + username + "\' not found");
                    if(this.hideUserNotFoundExceptions) {
                        throw new BadCredentialsException(this.messages
                            .getMessage("AbstractUserDetailsAuthenticationProvider
                                .badCredentials", "Bad credentials"));
                    }
                    throw var6;
                }
    
                Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
            }
    
            try {
                /*
                 * 前检查由DefaultPreAuthenticationChecks类实现
                 * (主要判断当前用户是否锁定,有效,过期)
                 */
                this.preAuthenticationChecks.check(user);
                // 子类具体实现
                this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
            } catch (AuthenticationException var7) {
                if(!cacheWasUsed) {
                    throw var7;
                }
                cacheWasUsed = false;
                user = this.retrieveUser(username,
                    (UsernamePasswordAuthenticationToken)authentication);
                this.preAuthenticationChecks.check(user);
                this.additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken)authentication);
            }
            // 检测用户密码是否过期
            this.postAuthenticationChecks.check(user);
            if(!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }
    
            Object principalToReturn = user;
            if(this.forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }
    
            //进行授权成功,主要是将认证信息的一块内容放到Authentication对象中返回
            return this.createSuccessAuthentication(principalToReturn, authentication, user);
        }
    
        // 成功授权
        protected Authentication createSuccessAuthentication(Object principal,
                Authentication authentication, UserDetails user) {
    
            /*
             * 回调UsernamePasswordAuthenticationToken的构造器,这里调用的是授权成功的构造器
             * public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
                    Collection<? extends GrantedAuthority> authorities) {
                    // 不再是null,而是传来的权限,由UserDetailsService类返回,可以从db查
                    super(authorities);
                    this.principal = principal;
                    this.credentials = credentials;
                    // 这里是true,不再是false。
                    super.setAuthenticated(true);
                }
            UsernamePasswordAuthenticationToken result = 
                new UsernamePasswordAuthenticationToken(principal, 
                    authentication.getCredentials(), 
                    this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
            // 将认证信息的一块内容放到details
            result.setDetails(authentication.getDetails());
            return result;
        }
    }
    
  • 用户验证走完后回到起点,AbstractAuthenticationProcessingFilter.doFilter(),执行session存储和认证成功处理器。
  • 总结:类之间的调用顺序
    • UsernamePasswordAuthenticationFilter
      • Authentication
      • AuthenticationManager
        • AuthenticationProvider
          • UserDetailsService
    • UsernamePasswordAuthenticationFilter回到起点进行后续操作
  • 用户验证调用链

    自定义用户验证

  • 我们要做的工作主要是实现几个接口:用户信息UserDetails、用户信息获取服务UserDetailsService(自定义用户获取方式是从数据库还是别的地方)、而需要自定义验证方式(不只用户名、密码,增加验证码等)的时候还要实现验证工具AuthenticationProvider。
  • 否则默认使用authenticate方法来验证,只能验证username和密码,不够灵活
  • 不是通过java注册spring security的话要添加以下配置

    <!--还要在http中添加-->
    <security:custom-filter ref="passcard_filter" after="SECURITY_CONTEXT_FILTER"/>
    
    <!--注入authentication-provider-->
    <security:authentication-manager alias="authenticationManager"> //起别名,方便被下面filter的bean注入
        <!-- 注意,这里仅仅是系统默认的认证机制,请在正式系统中明确知道其功能再使用 -->
        <security:authentication-provider ref="acocunt_defaultAnthentiactionProvider"/> //默认的
        <security:authentication-provider ref="registrationService"/> //注册的
        <security:authentication-provider ref="enrollmentService"/> //不知干嘛的
        <security:authentication-provider ref="userService"/> //登录的
    </security:authentication-manager>    
    <!--注入自定义的PasscardAuthenticationProcessingFilter,获取登录时提交的表单-->
    <bean id="passcard_filter" 
        class="cn.edu.jszg.cert.security.PasscardAuthenticationProcessingFilter">
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="useVerifyCode" value="true"/>
        <property name="failurePage" value="/portal/home/auth/"></property>
        <property name="config" ref="webAppConfig"/>
        <property name="userLogService" ref="userLogService" />
        <property name="certLoginUrl" value="${cert.login.url}"/>
    </bean>
    
  • 自定义Authentication(包含着当前登录用户的信息)实现类PassCardAuthenticationToken(默认实现类为UsernamePasswordAuthenticationToken),用于保存authentication中用得到的参数,主要是重写以下方法

    /**
     * 凭证,用户密码
     */
    @Override
    public Object getCredentials() {
        return password;
    }
    
    /**
     * 当事人,登录名 用户Id
     */
    @Override
    public Object getPrincipal() {
        return userID;
    }
    
  • Authentication类包含
    • principle:其实就是实现了UserDetails的User类,它包含User类的所有属性,User中的字段和表单匹配的就会被赋值
    • details:包含remoteAddress(ip地址),sessionId
  • Authentication来源

    Authentication auth=SecurityContextHolder.getContext().getAuthentication();
    
  • spring security获取(所有)登录用户的信息
  • 需要UserDetails实现类User增加一些支持authenticate()的方法

    /*首先增加一些支持authenticate()的方法*/
    /**
     * 返回用户所属权限
     */
    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return this.accesses;
    }
    
    @Override
    public Object getCredentials() {
        return null;
    }
    @Override
    public Object getDetails() {
        return null;
    }
    /**
     * 登录名称
     */
    @Override
    public Object getPrincipal() {
        return loginName;
    }
    /**
     * 是否已认证
     */
    @Override
    public boolean isAuthenticated() {
        return this.authenticated;
    }
    /**
     * 设置是否已认证字段
     */
    @Override
    public void setAuthenticated(boolean isAuthenticated)
            throws IllegalArgumentException {
        this.authenticated=isAuthenticated;
    }
    
  • 创建AuthenticationProvider实现类,重写authenticate()。如果只是简单的用户名、密码比对的话,那有用户服务就够了,不需要这一步。

    @Component
    public class MyAuthenticationProvider implements AuthenticationProvider {
    
        @Autowired
        private MyUserDetailsService userService; //如果重定义了可以用这个
    
        /**
         * 自定义的用户验证:判断该用户是否能登录,是则返回包含所有信息的user对象,否则返回null
         */
        @SuppressWarnings("unchecked")
        @Override
        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
    
            //自定义类,继承自Authentication相同父类,用于保存authentication中用得到的参数
            PassCardAuthenticationToken token=(PassCardAuthenticationToken)authentication;
    
            if(token.getUserID()!=null&&token.getPassword()!=null){ //否则直接返回null,登录失败
    
                /* userID:当前用户名作id
                 * 根据用户id查询数据库找到整个user信息
                 * 这里进行逻辑认证操作,可以获取token中的属性来自定义验证逻辑
                 */
                User user=(User)this.getDao().executeQueryUnique("User.loadByLoginName", 
                                                QueryCmdType.QUERY_NAME, token.getUserID());
                /*当然最好是用重定义的UserDetailsService
                User user=userService.loadUserByUsername(username)*/
    
                String password=token.getPassword(); //用户登录上传的密码
                if(this.passwordEncoder!=null){ //将用户提交的密码加密后再与数据库比对
                    //在第2点那里加了自定义加密器之后这里就不需要再加密
                    password=this.passwordEncoder.encodePassword(password, null);
                }
    
                if(!password.equalsIgnoreCase(user.getPassword())){
                    token.setErrCode("2");
                    return null;
                }
    
                //如果开启了额外的验证方式的话,进行验证,否则直接判为登录成功
                //不过验证码之类的一般是用前端js当场解决,不会传到后台来解决
                if( token.isEnablePasscard() && usePassCard ){ //token中激活密码卡且系统使用密码卡
                     int position1=((token.getRow1()-1)*7)+token.getColumn1();
                    int position2=((token.getRow2()-1)*7)+token.getColumn2();
    
                    if(user.getPassCardId()==null){
                        token.setErrCode("10");
                        return null;
                    }
                    PassCard passcard=this.passCardDao.findById(user.getPassCardId(), false);
    
                    if(passcard==null||passcard.getStatus()==PassCardHelper.STATUS_CANCEL ){
                        token.setErrCode("10");
                        return null;
                    }
                    if(passcard.getConfusedContent()==null || passcard.getConfusedContent().length()<7*7*32 ){
                        token.setErrCode("10");
                        return null;
                    }
    
                    String content=passcard.getConfusedContent();
                    int perLen=content.length()/49;
                    String str1=content.substring((position1-1)*perLen, position1*perLen);
                    String str2=content.substring((position2-1)*perLen, position2*perLen);
                    String inputStr1=token.getCard1();
                    String inputStr2=token.getCard2();
                    if(this.passwordEncoder!=null){
                        inputStr1 = md5.getMD5ofStr(md5.getMD5ofStr(inputStr1));
                        inputStr2 = md5.getMD5ofStr(md5.getMD5ofStr(inputStr2));
                    }
    
                    if((!str1.equalsIgnoreCase(inputStr1))||(!str2.equalsIgnoreCase(inputStr2))){
                        token.setErrCode("10");
                        return null;
                    }
                }
    
                //可登陆情况
                user.setLastIp(token.getIp());
                user.setLastLogin(new Date());
                this.getDao().saveOrUpdate(user); //更新用户的最近登录时间和ip地址        
                user.setAuthenticated(true); //设置为已认证,表示可登陆
    
                /*
                 * 导入当前用户角色,并且把权限set到User中,
                 * 用于spring验证用户权限(getAuthorities方法)
                 */
                List<UserRole> userRoles=(List<UserRole>)this.getDao().
                                            executeQueryList("UserRole.listRoleByUserID", 
                                                QueryCmdType.QUERY_NAME, -1, -1, user.getId());
                Set<GrantedAuthority> accesses=new HashSet<GrantedAuthority>();
                for(UserRole ur:userRoles){
                    accesses.add(ur.getRole());                
                }
                user.getOrg().getOrgName();
                if(user.getOrg().getCertTypes()!=null) user.getOrg().getCertTypes().size();//延迟载入一下
                user.setAccesses(accesses); //导入角色
                return user;
            }
            return null;
        }
    
        /**
         * 如果此处验证不通过,是不会执行authentication方法的
         */
        @Override
        public boolean supports(Class<? extends Object> authentication) {
            // AuthenticationProvider内置实现类只支持UsernamePasswordAuthenticationToken
            return authentication.equals(PassCardAuthenticationToken.class);
        }
    }
    
  • 定义AbstractAuthenticationProcessingFilter实现类,重写attempAuthentication(),用于获取在登录时提交的参数,否则默认只获取j_username,j_password

    import java.io.IOException;
    import java.util.Date;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.apache.log4j.Logger;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
    import org.springframework.util.StringUtils;
    
    import cn.edu.jszg.cert.user.UserLog;
    import cn.edu.jszg.cert.user.UserLogService;
    import cn.edu.jszg.cert.web.WebApplicationConfiguration;
    import cn.edu.jszg.cert.web.controller.portal.auth.RemoteDataValidator;
    
    import com.google.code.kaptcha.servlet.KaptchaServlet;
    
    public class PasscardAuthenticationProcessingFilter extends
            AbstractAuthenticationProcessingFilter {
    
        private String successPage = "/home/admin/index";
        private String failurePage = "/public/adminLoginEntry";
        private boolean forward = false;
        private boolean useVerifyCode=true;
        private String certLoginUrl;
    
        static Logger logger = Logger.getLogger(PasscardAuthenticationProcessingFilter.class);
    
        private WebApplicationConfiguration config;
        private UserLogService userLogService;    
    
        public void setConfig(WebApplicationConfiguration config) {
            this.config = config;
        }
    
        /**
         * 实现AbstractAuthenticationProcessingFilter的有参构造
         * 没记错的话,相当于该filter的访问路径 
         */
        protected PasscardAuthenticationProcessingFilter() {
            super("/adminLoginCheck"); //那表单提交应该是提交到这里
        }
    
        public void setUseVerifyCode(boolean useVerifyCode) {
            this.useVerifyCode = useVerifyCode;
        }
    
        public void setUserLogService(UserLogService userLogService) {
            this.userLogService = userLogService;
        }
    
        public boolean validate(HttpServletRequest request) {
            String userId = request.getParameter("username");
            String md2 = request.getParameter("m");
            String l = request.getParameter("l");
            if (userId == null || md2 == null || l == null) {
                return false;
            }
            long longTime = Long.parseLong(l);
            if (longTime < new Date().getTime()) { //获取时间秒制
                return false;
            }
    
            try {
                String md1 = RemoteDataValidator.genExamMd5Digest(userId, longTime);
                if (md1.equals(md2))
                    return true;
            } catch (Exception e) {            
                //e.printStackTrace();
            }
    
            return false;
        }
    
        /**
         * 可以通过request获取页面传递过来的参数,并且set到相应的token中
         */
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException,
                IOException, ServletException {
    
    //        logger.warn("-----------------start证书登录用户----------");
            HttpSession s = request.getSession(true);
            //Authentication子类,自动获取表单信息
            PassCardAuthenticationToken token = new PassCardAuthenticationToken();
    
            String verifyCode = request.getParameter("verifyCode");
            String userID = request.getParameter("username");
            //....此处省略获取参数,并且验证、赋值的逻辑
            Authentication auth = null;
    
            try {
                //此处调用getAuthenticationManager的authenticate方法,验证用户是否能登录
                auth = this.getAuthenticationManager().authenticate(token); //当supports方法返回true时执行authenticate方法
    
                //此处为登录失败后,相应的处理逻辑
                if (auth == null || !auth.isAuthenticated()) {
                    s.setAttribute("__login_error", token.getErrCode());
                } else  {
                    s.removeAttribute("__login_error");
                    s.removeAttribute("__login_username");
                    s.removeAttribute("__cert_userid");
                    if( token.isEnablePasscard()) {
                        s.removeAttribute("__passcard_row1");
                        s.removeAttribute("__passcard_row2");
                        s.removeAttribute("__passcard_column1");
                        s.removeAttribute("__passcard_column2");
                    }
                }
            } catch (AuthenticationException e) {
                s.setAttribute("__login_error", token.getErrCode());
                throw e;
            }
            return auth;
        }
    
        public void setSuccessPage(String successPage) {
            this.successPage = successPage;
        }
    
        public void setFailurePage(String failurePage) {
            this.failurePage = failurePage;
        }
    
        public void setForward(boolean forward) {
            this.forward = forward;
        }
    
        public void setCertLoginUrl(String certLoginUrl) {
            this.certLoginUrl = certLoginUrl;
        }
    
        @Override
        public void afterPropertiesSet() {
            super.afterPropertiesSet();
            /*
            *该处理器实现了AuthenticationSuccessHandler, AuthenticationFailureHandler
            *用于处理登录成功或者失败后,跳转的界面
            */
            AuthenticationResultHandler handler = new AuthenticationResultHandler();
            handler.setForward(forward);
            handler.setLoginFailurePage(failurePage);
            handler.setLoginSuccessPage(successPage);
            handler.setCertLoginUrl(certLoginUrl);
            //设置父类中的处理器
            this.setAuthenticationSuccessHandler(handler);
            this.setAuthenticationFailureHandler(handler);
    
        }
    }
    

    代码实例

  • D:/ideaprojects/thz/thz-parent/thz-manager-web
  • 自定义认证成功处理器

  • 详解过滤链版
  • 简单版
  • 我们要自定义的类的被调用链:AbstractAuthenticationProcessingFilter.doFilter(调用自己的successfulAuthentication(调用AuthenticationSuccessHandler接口的onAuthenticationFailure()))
  • 我们要做的就是自定义AuthenticationSuccessHandler实现类,重写onAuthenticationFailure(),来处理登录成功后要做的事情,比如保存用户到session、更新用户最近登录时间到数据库等。

    import com.thz.pojo.User;
    import com.thz.service.UserService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.WebAuthenticationDetails;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Date;
    
    /**
     * @Author haien
     * @Description 自定义认证成功处理器
     *              为什么不实现接口而是继承SavedRequestAwareAuthenticationSuccessHandler类,
     *              因为这个类记住了上次请求路径,如果你是请求其他页面被拦截到登录页的,
     *              这时候输入用户名和密码点击登录,还会自动跳转到该页面,而不是默认主页。
     * @Date 2019/1/31
     **/
    @Component
    public class MyAuthenticationSuccessHandler
            extends SavedRequestAwareAuthenticationSuccessHandler {
        private static final Logger logger=LoggerFactory
                .getLogger(MyAuthenticationSuccessHandler.class);
    
        @Resource
        private UserService userService;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication)
                throws ServletException, IOException {
    
            logger.info("----登录成功,username="+request.getParameter("username")+"----");
    
            //获取当前用户
            User user=(User)authentication.getPrincipal();
            //获取authentication中的details中的remoteAddress
            WebAuthenticationDetails wauth=(WebAuthenticationDetails)authentication.getDetails();
            user=userService.findUserByName(user.getUsername());
            user.setLoginTime(new Date());
            user.setLoginIp(wauth.getRemoteAddress());
            //存入session
            request.getSession().setAttribute("user",user);
            //更新数据库
            userService.updateUser(user);
    
            //执行父类重定向到原访问路径的方法
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }
    

    自定义认证失败处理器

  • 默认处理器就只能跳转登录失败页面,自定义的话可以将用户提交的用户名保存到request,带回登录页面。

    @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("登录失败!");
    
            //根据配置loginType是否是json而定返回形式,JSON返回json
            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)));
            } 
            //REDIRECT跳转前面定义的页面:.failureUrl("/login.jsp?error=1")
            else {
                //存值带回
                String username=request.getParameter("username");
                request.setAttribute("username",username);
    
                response.setContentType("text/html;charset=UTF-8");
                //设置登录失败后要重定向的url
                super.setDefaultFailureUrl("/login.jsp?error=1&username="+username);
                //重定向,request丢失
                super.onAuthenticationFailure(request, response, exception); 
    
                /*不然就自己转发,request就不会丢失
                request.getRequestDispatcher("/login?error=1")
                        .forward(request, response);*/
            }
    
        }
    }
    
  • 或者能确定值返回json的话,直接往客户端写json字符串

    @Component
    public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        private static final Logger logger=LoggerFactory
                .getLogger(MyAuthenticationSuccessHandler.class);
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request
                , HttpServletResponse response, AuthenticationException exception)
                throws IOException, ServletException {
    
            logger.info("----登录失败----");
    
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json");
            JSONObject result=new JSONObject();
            result.put("message","用户名或密码错误");
    
            PrintWriter printWriter=null;
            String jsonString="";
            try { //必须捕获,不能抛出,因为要保证资源被关闭
                printWriter=response.getWriter();
                jsonString = JSONObject.toJSONString(result);
                printWriter.write(jsonString);
                printWriter.flush();
            } catch (IOException e) {
                logger.error("Get response writer failed!",e); //不用再抛出,因为抛出了就要再生成一次报文,但既然getWrite都出错了生成的报文又怎么打印到前端呢
            }finally {
                if(null!=printWriter)
                    printWriter.close();
            }
        }
    }
    

    代码实例

  • thz-manager-web/config