SpringSecurity的认证全过程源码梳理,自定义认证逻辑

369 阅读6分钟
   springSecurity = 认证(authentication) + 授权(authorization)

🥐 过滤器链

SpringSecurity对sevlet的安全支持底层其实就是基于servlet过滤器

SpringSecurity默认会配置过滤器链,在servlet应用中,过滤器的执行顺序是根据web.xml配置的先后顺序执行的,而Security可以在配置类中配置。

🎉认证(Authentication) 关于认证的几个核心类:

AuthenticationManager
  AuthenticationProvider
  UsernamePasswordAuthenticationFilter
  UsernamePasswordAuthenticationToken
  UserDetailsService
  UserDetail

认证的流程: 用户发送认证请求,过滤器链执行到: UsernamePasswordAuthenticationFilter,该类继承了AbstractAuthenticationProcessingFilter,拦截请求后,执行dofilter()方法。 注意看源码:这个过滤器是匹配的/login的url,所以只会过滤登录请求,只有访问/login才会触发。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
 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 {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                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);
        }
    }
}
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";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        //过滤该url匹配(看出这个过滤器是过滤登录请求的)
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //表示认证请求只能够是Post请求
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            //从请求中获取参数username,password
            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);
            //记录这个UsernamePasswordAuthenticationToken
            this.setDetails(request, authRequest);
            //然后将UsernamePasswordAuthenticationToken交给AuthenticationManager管理
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}
      //建立一个未认证的UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authRequest
      = new UsernamePasswordAuthenticationToken(username, password);

可以看到根据请求参数中的username和password会新建一个Token,看下UsernamePasswordAuthenticationToken的源码:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    private final Object principal;
    private Object credentials;
    //两个参数的构造函数
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
      //调用自己的方法,设置未认证
        this.setAuthenticated(false);
    }
    //三个参数的构造函数
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        //调用父类的方法,设置为已认证
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

通过源码可以看到,UsernamepasswordAuthentication new的Token是未认证的,并且principal,credentials分别对应username,password.继续回到

 //记录这个UsernamePasswordAuthenticationToken
            this.setDetails(request, authRequest);

该步骤就是记录这个登录请求的未认证Token.

  //然后将UsernamePasswordAuthenticationToken交给AuthenticationManager管理
            return this.getAuthenticationManager().authenticate(authRequest);

将这个未认证的Token交给当前的AuthenticationManager处理。而manager管理了一系列的AuthenticationProvider。 AuthenticationManager AuthenticationManager管理一系列AuthenticationProvider 看看AuthenticationProvider

public interface AuthenticationProvider {
    //真正的认证处理,返回一个已经认证类。
    Authentication authenticate(Authentication var1) throws AuthenticationException;
    //能否处理相应的Token类。
    boolean supports(Class<?> var1);
}

下面是AuthenticationManager的一个实现类部分代码,可以看到manager先遍历自身管理的provider,

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
      //获取provider集合迭代
        Iterator var8 = this.getProviders().iterator();
      //遍历provider集合
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
          //判断该provider是否能够处理该Token类型登录请求传递过来的就是我们之前说的UsernamePasswordAuthenticationToken)。
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                  //真正的认证逻辑,返回的是一个已认证类,
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (AuthenticationException var14) {
                    lastException = var14;
                }
            }
        }

provider处理之后,如果成功返回了一个已经认证的..Token,那么就会将这个认证信息记录到session中。 Security登录认证UsernamePassword...Token默认的Provider是DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider. 先看看父类AbstractUserDetailsAuthenticationProvider的关键代码

//认证逻辑
 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
      //从..Token中获取用户名信息
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
            //关键代码,根据用户名认证。DaoAuthenticationProvider实现
                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 {
            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();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

DaoAuthenticationProvider关键代码:

  /**返回了一个UserDetails对象,
  *  用户查询认证
  **/
 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
          //userDetailsSerivce(可以类比为UserDao)和userDetail(类比为UserDto)
            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 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"));
            }
        }
    }

😊 整个登录认证的过程就梳理完成了,通过阅读源码我们可以知道自己可以自定义登录认证的逻辑: 1.我们只需要编写一个AuthenticationProvider实现类, supports(Class clazz)方法去匹配我们要处理的认证类(即UsernamePasswordAuthenticationToken), authenticate(authentication), 2.在实现类中注入UserDetailService,所以我们可以自定义一个UserDetailService进行注入,UserDetail也相应重写。 3.Security配置类中配置生效该实现类即可实现自定义登录认证。 下面是示例代码🐱‍🚀: 编写一个AuthenticationProvider实现类:

@Component
@Slf4j
public class MyUserProvider implements AuthenticationProvider {
    //注入自定义的UserService(实现UserDetailsService)
    @Autowired
    private UserService userService;
    @Autowired
    private PasswordEncoder passwordEncoder;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String pwd = (String) authentication.getCredentials();
        UserDetails userDetails = userService.loadUserByUsername(username);
        if (!userDetails.isEnabled()){
            throw new UsernameNotFoundException("该账户已被停用");
        }
        if (!passwordEncoder.matches(pwd,userDetails.getPassword())){
            throw new UsernameNotFoundException("账户或者密码错误");
        }
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
        return usernamePasswordAuthenticationToken;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

自定义UserService

@Component
@Slf4j
public class UserService implements UserDetailsService {
    @Autowired
    private UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetails userDto = (UserDetails) userDao.findUserDtoByUsername(username);
        if (userDto == null){
            throw new UsernameNotFoundException("账户不存在");
        }
        if (!userDto.isAccountNonLocked()){
            throw new UsernameNotFoundException("账户被锁定,无法使用");
        }
        if (userDto.getAuthorities() == null || userDto.getAuthorities().isEmpty()){
            log.error("用户:"+userDto.getUsername()+"没有角色可以查询");
            throw  new UsernameNotFoundException("服务器内部错误");
        }
        return userDto;

    }
}

自定义封装的UseDto,实现UserDetail

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDto implements UserDetails {
    private String flag;
    private String status;
    private String password;
    private String username;
    private String role;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        if (role != null && !StringUtils.isEmpty(role))
        authorities.add(new SimpleGrantedAuthority(role));
        return authorities;
    }

    @Override
    public String getPassword() {
        return password ;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return "Y".equals(flag)?true:false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账户是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return "0".equals(status)?true:false;
    }
}

最后在Security配置类中启用生效:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserProvider myUserProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**")
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .successForwardUrl("/login/success");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(myUserProvider);
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}