SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)

上篇文章已经大致把SpringSecurity的设计思路捋出来了,这次就开始着手改造一下。

总体思路

大体的设计思路和之前比较一致,只是在配置方面做了调整,重新梳理如下:

  1. 构建一个特定的Token类,例如PasswordAuthenticationToken,这个类需要继承AbstractAuthenticationToken,在需要做认证的地方把他new出来;
  2. 构建认证处理器类PasswordAuthenticationProvider类,实现AuthenticationProvider接口,并重写其中的authenticate()supports()方法;
  3. 构造PasswordAuthenticationFilter类继承自AbstractAuthenticationProcessingFilter并重写其中的attemptAuthentication()方法,同时重写AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,AuthenticationManager authenticationManager)方法用于注入AuthenticationManager
  4. 构造PasswordAuthenticationConfigurer类,继承自AbstractHttpConfigurer并重写configure方法,他可是提供了一个非常宝贵的HttpSecurity入参。
  5. 构造相应的成功及异常处理器。

结果

思路清楚了就直接上代码吧,关键位置都做了注释,一遍看不懂就多看几遍。先看下改造后的整体结构。
SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)_第1张图片

先来PasswordAuthenticationToken

/**
 * 基于用户名(手机号)、密码、验证码登录的认证实体
 */
public class PasswordAuthenticationToken extends AbstractAuthenticationToken {

    private final String loginId;
    private final String captchaId;
    private final String captchaValue;
    private final LoginUserPojo principal;
    private final String credentials;

    /**
     * 登录验证
     *
     * @param loginId      用户名或手机号
     * @param credentials  MD5+SM3密码
     * @param captchaId    图形验证码id
     * @param captchaValue 输入的图形验证码值
     */
    public PasswordAuthenticationToken(String loginId, String credentials, String captchaId, String captchaValue) {
        super(null);
        this.loginId = loginId;
        this.credentials = credentials;
        this.captchaId = captchaId;
        this.captchaValue = captchaValue;
        this.principal = null;
        this.setAuthenticated(false);
    }

    /**
     * 授权信息
     *
     * @param principal   LoginUserPojo
     * @param credentials token
     * @param authorities 角色清单
     */
    public PasswordAuthenticationToken(LoginUserPojo principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        this.loginId = null;
        this.captchaId = null;
        this.captchaValue = null;
        this.setAuthenticated(true);
    }

    public String getLoginId() {
        return loginId;
    }

    public String getCaptchaId() {
        return captchaId;
    }

    public String getCaptchaValue() {
        return captchaValue;
    }

    @Override
    public LoginUserPojo getPrincipal() {
        return principal;
    }

    @Override
    public String getCredentials() {
        return credentials;
    }
}

PasswordAuthenticationProvider

/**
 * 基于用户名(手机号)、密码、验证码的认证处理器
 */
public class PasswordAuthenticationProvider implements AuthenticationProvider {

    private static final String IMG_CAPTCHA_REDIS_PREFIX = "SK:CAPTCHA:IMG:";

    private final UserDetailServiceImpl userDetailService;
    private final RedisCacheUtil redisCacheUtil;

    public PasswordAuthenticationProvider(UserDetailServiceImpl userDetailService, RedisCacheUtil redisCacheUtil) {
        this.userDetailService = userDetailService;
        this.redisCacheUtil = redisCacheUtil;
    }

    /**
     * 验证主逻辑
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        PasswordAuthenticationToken authToken = (PasswordAuthenticationToken) authentication;

        // 验证码校验
        if (!checkImgCaptcha(authToken.getCaptchaId(), authToken.getCaptchaValue())) {
            throw new BadCaptchaException("验证码有误或已过期,请重新输入");
        }

        // 密码校验
        LoginUserPojo userDetails = (LoginUserPojo) userDetailService.loadUserByUsername(authToken.getLoginId());
        if (!new BCryptPasswordEncoder().matches(authToken.getCredentials(), userDetails.getPassword())) {
            throw new BadCredentialsException("用户名或密码错误,请重新输入");
        }

        // 用户状态校验
        if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {
            throw new LockedException("用户已禁用,请联系管理员启用");
        }

        return new PasswordAuthenticationToken(userDetails, authToken.getCredentials(), userDetails.getAuthorities());
    }

    /**
     * 当类型为PasswordAuthenticationToken的认证实体进入时才走此Provider
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    /**
     * 校验验证码正确与否,验证完成后删除当前码值
     *
     * @param id    验证码对应的id
     * @param value 用户输入的验证码结果
     * @return true or false
     */
    private boolean checkImgCaptcha(String id, String value) {
        if (StringUtils.isBlank(id) || StringUtils.isBlank(value)) {
            return false;
        }

        CaptchaCodePojo captchaCode = redisCacheUtil.getObject(IMG_CAPTCHA_REDIS_PREFIX + id);
        redisCacheUtil.deleteObject(IMG_CAPTCHA_REDIS_PREFIX + id);

        return !Objects.isNull(captchaCode) && value.equals(captchaCode.getResult());
    }
}

PasswordAuthenticationConfigurer

/**
 * 基于用户名(手机号)、密码、验证码的登录拦截器配置类
 */
public class PasswordAuthenticationConfigurer extends AbstractHttpConfigurer<PasswordAuthenticationConfigurer, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) {
        // 拦截 POST /login 请求
        RequestMatcher matcher = new AntPathRequestMatcher("/login", "POST", true);

        UserDetailServiceImpl userDetailService = builder.getSharedObject(ApplicationContext.class).getBean(UserDetailServiceImpl.class);
        RedisCacheUtil redisCacheUtil = builder.getSharedObject(ApplicationContext.class).getBean(RedisCacheUtil.class);
        AuthenticationManager localAuthManager = builder.getSharedObject(AuthenticationManager.class);

        PasswordAuthenticationFilter filter = new PasswordAuthenticationFilter(matcher, localAuthManager);
        filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(userDetailService));
        filter.setAuthenticationFailureHandler(new LoginFailHandler());

        // 务必注意这里与配置类中声明的先后顺序
        builder.authenticationProvider(new PasswordAuthenticationProvider(userDetailService, redisCacheUtil))
                .addFilterBefore(filter, AuthenticationTokenFilter.class);
    }
}

PasswordAuthenticationFilter

/**
 * 用户名密码登录拦截器
 */
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public PasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
        super(requiresAuthenticationRequestMatcher, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        JSONObject params = HttpRequestUtil.getBodyJson(request);
        Authentication authentication = new PasswordAuthenticationToken(
                params.getString("loginKey"), params.getString("password"), params.getString("id"), params.getString("value")
        );

        return this.getAuthenticationManager().authenticate(authentication);
    }
}

Token拦截器配置类TokenAuthenticationConfigurer

/**
 * Token拦截器配置类
 */
public class TokenAuthenticationConfigurer extends AbstractHttpConfigurer<TokenAuthenticationConfigurer, HttpSecurity> {
    @Override
    public void configure(HttpSecurity builder) {
        RedisCacheUtil redisCacheUtil = builder.getSharedObject(ApplicationContext.class).getBean(RedisCacheUtil.class);
        builder.addFilterBefore(new AuthenticationTokenFilter(redisCacheUtil), UsernamePasswordAuthenticationFilter.class);
    }
}

Token认证拦截器AuthenticationTokenFilter

/**
 * Token认证拦截器
 */
public class AuthenticationTokenFilter extends OncePerRequestFilter {

    private final RedisCacheUtil redisCacheUtil;

    public AuthenticationTokenFilter(RedisCacheUtil redisCacheUtil) {
        this.redisCacheUtil = redisCacheUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = JwtTokenUtil.getToken(request);
        if (Objects.nonNull(token) && JwtTokenUtil.checkToken(token) && redisCacheUtil.hasKey(TokenConstant.TOKEN_REDIS_PREFIX + token)) {
            // 从redis中获取数据
            LoginUserPojo userPojo = redisCacheUtil.getObject(TokenConstant.TOKEN_REDIS_PREFIX + token);

            // 写入上下文
            PasswordAuthenticationToken authenticationToken = new PasswordAuthenticationToken(userPojo, token, userPojo.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

            // 刷新ttl
            redisCacheUtil.setExpire(TokenConstant.TOKEN_REDIS_PREFIX + token, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);
        }
        filterChain.doFilter(request, response);
    }
}

LoginFailHandler

/**
 * 密码认证失败处理器
 */
public class LoginFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        JSONObject res = new JSONObject();
        res.put("success", false);
        res.put("msg", e.getMessage());

        response.setStatus(HttpStatus.SC_FORBIDDEN);
        response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().print(res.toJSONString());
    }
}

LoginSuccessHandler

/**
 * 登录成功后处理器
 */
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    private final UserDetailServiceImpl userDetailsService;

    public LoginSuccessHandler(UserDetailServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        LoginUserPojo loginUserPojo = (LoginUserPojo) authentication.getPrincipal();

        // 更新登陆时间
        userDetailsService.updateLoginTime(loginUserPojo.getUserId());

        // 构建token并缓存
        String token = userDetailsService.buildToken(loginUserPojo);

        JSONObject res = new JSONObject();
        res.put("success", true);
        res.put("msg", "OK");
        res.put("data", token);

        response.setStatus(HttpStatus.SC_OK);
        response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write(res.toString());
    }
}

UserDetailServiceImpl

/**
 * SpringSecurity登录处理类
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Map<String, Object> userData = sysUserMapper.getByUserNameOrUserPhone(username);
        if (MapUtils.isEmpty(userData)) {
            throw new UsernameNotFoundException("用户名或密码错误,请重新输入");
        }

        // 封装基础数据
        LoginUserPojo userPojo = new LoginUserPojo();
        userPojo.setUserId(MapUtils.getString(userData, "user_id"));
        userPojo.setUserName(MapUtils.getString(userData, "user_name"));
        userPojo.setUserPhone(MapUtils.getString(userData, "user_phone"));
        userPojo.setNickName(MapUtils.getString(userData, "nick_name"));
        userPojo.setPassword(MapUtils.getString(userData, "password"));
        userPojo.setUserStatus(MapUtils.getIntValue(userData, "user_status"));
        userPojo.setLastLogin(MapUtils.getString(userData, "last_login"));
        userPojo.setLastUpdatePassword(MapUtils.getString(userData, "last_update_password"));
        userPojo.setCreateUser(MapUtils.getString(userData, "create_user"));
        userPojo.setCreateTime(MapUtils.getString(userData, "create_time"));
        userPojo.setUpdateUser(MapUtils.getString(userData, "update_user"));
        userPojo.setUpdateTime(MapUtils.getString(userData, "update_time"));

        // 封装角色信息
        if (StringUtils.isNotBlank(MapUtils.getString(userData, "role_code"))) {
            List<UserGrantedAuthority> grantedAuthorityList = new ArrayList<>();
            String[] roleCodes = MapUtils.getString(userData, "role_code").split(",");
            String[] roleNames = MapUtils.getString(userData, "role_name").split(",");
            String[] roleTypes = MapUtils.getString(userData, "role_type").split(",");
            for (int i = 0; i < roleCodes.length; i++) {
                grantedAuthorityList.add(new UserGrantedAuthority(roleCodes[i], roleNames[i], Integer.valueOf(roleTypes[i])));
            }
            userPojo.setAuthorities(grantedAuthorityList);
        }

        return userPojo;
    }

    /**
     * 更新用户登录时间
     *
     * @param userId 用户ID
     */
    public void updateLoginTime(String userId) {
        SysUserEntity userEntity = new SysUserEntity();
        userEntity.setUserId(userId);
        userEntity.setLastLogin(DateTimeUtil.getCurrentDate("yyyy-MM-dd HH:mm:ss"));
        sysUserMapper.updateByUserId(userEntity);
    }

    /**
     * 根据用户信息构造token并写入redis
     *
     * @param loginUserPojo LoginUserPojo
     * @return token
     */
    public String buildToken(LoginUserPojo loginUserPojo) {
        JSONObject user = new JSONObject();
        user.put("userId", loginUserPojo.getUserId());
        user.put("userName", loginUserPojo.getUserName());
        user.put("roleCode", loginUserPojo.getAuthorities().stream().map(UserGrantedAuthority::getRoleCode).collect(Collectors.joining(",")));

        // 生成token
        String token = JwtTokenUtil.createJwtToken(user);
        redisCacheUtil.setObject(TokenConstant.TOKEN_REDIS_PREFIX + token, loginUserPojo, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);
        return token;
    }
}

还有最后的Security配置类SpringSecurityConfig

/**
 * SpringSecurity配置类
 */
@EnableWebSecurity
@PropertySource("classpath:authfilter.properties")
public class SpringSecurityConfig {

    @Value("${exclude_urls}")
    private String excludeUrls;

    @Autowired
    private AuthenticationLogoutHandler logoutHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .csrf().disable()
                .formLogin().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeHttpRequests().antMatchers(StringUtils.split(excludeUrls, ",")).permitAll()
                .anyRequest().authenticated().and()
                .exceptionHandling().authenticationEntryPoint(new AuthenticationFailHandler()).and()
                .logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).and()
                // 务必注意这里的先后顺序,否则会报NULL异常
                .apply(new TokenAuthenticationConfigurer()).and()
                .apply(new PasswordAuthenticationConfigurer()).and()
                .apply(new OAuthAuthenticationConfigurer()).and()
                .build();
    }
}

经过测试完美执行,自定义Provider可以仅在Local作用域中生效,不干扰全局AuthenticationManager
SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)_第2张图片

另外我想尽可能避免将所有类都标记为@Component,所以部分业务依赖项我是通过上下文ApplicationContext获取的,而这个实例正好也在httpSecuritysharedObject中。


后记

当我完成所有修改后已经过了三天多了,这段时间Security简直就是我的梦魇。
SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)_第3张图片

但我也和大多数人一样,看之前骂spring一个过滤器的事设计的这么复杂干什么,看完之后才发现给我上了一课:什么叫设计模式,什么叫开闭原则。跟各位写框架的大佬比,我还是太年轻了。

不过倒也发现了一些问题,比如在他推荐使用SecurityFilterChain的时候,javadoc里面却仍然是传统方式的例子。满心欢喜的以为能水一个issue呢结果已经被人修复好了。。。。。
SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(四)_第4张图片

附用到的一些比较有价值的资料(还是推荐spring官方文档,要一字一句的读才能体会到设计者的思路):
一文搞懂SpringSecurity+JWT前后端分离
避坑指南(三):Spring Security Oauth2框架如何初始化AuthenticationManager
Spring Security 实战干货:你不了解的Spring Security架构
Spring Security 实战干货:WebSecurityConfigurerAdapter即将被移除
SpringSecurity之ProviderManager是如何产生的,如何与UsernamePasswordAuthenticationFilter光联在一起
Spring Security小教程 Vol 3. 身份验证的入口-AbstractAuthenticationProcessingFilter
Spring Security without the WebSecurityConfigurerAdapter
ProviderManager

你可能感兴趣的:(SpringSecurity,Java,java,SpringSecurity)