SpringSecurity框架——认证流程介绍,实战代码

1 SpringSecurity过滤器链说明

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。 因为是过滤器链的工作形式,所以Security的工作流程是环环相扣的。下面对各个过滤器进行说明:

  • HeaderWriterFilter:用于将头信息加入响应中。
  • CsrfFilter:用于处理跨站请求伪造。
  • LogoutFilter:用于处理退出登录。
  • UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。更多的是对UsernamePasswordAuthenticationFilter进行定制化,下面就是定制化的演示
  • DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
  • BasicAuthenticationFilter:检测和处理 http basic 认证。
  • RequestCacheAwareFilter:用来处理请求的缓存。
  • SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
  • AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
  • SessionManagementFilter:管理 session 的过滤器
  • ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
  • FilterSecurityInterceptor:可以看做过滤器链的出口。
  • RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

2 自定义UsernamePasswordAuthenticationFilter过滤器

流程有点长又枯燥,耐心看

项目中大部分都需要对Security框架的类包含过滤器进行自定义,下面是UsernamePasswordAuthenticationFilter的定制化,来满足我们项目中的业务需要。
认证的方法流程是:attemptAuthentication() ——> authenticate() ——> retrieveUser() ——> loadUserByUsername() ——> additionalAuthenticationChecks () ——> onAuthenticationSuccess()/onAuthenticationFailure()
用一张图来关联:
在这里插入图片描述

自定义UsernamePasswordAuthenticationFilter过滤器

首先会经过第一个过滤器AbstractAuthenticationProcessingFilter它是UsernamePasswordAuthenticationFilter的父类,所以我们的自定义CustomAuthenticationFilter过滤器继承AbstractAuthenticationProcessingFilter类 就可以了,重写attemptAuthentication()方法,通常这里会在这里对验证码,请求方法类型等进行合法性校验,最后的返回值是Authentication类型的对象。

这里需要注意返回值是Authentication类型对象流程是:

  • 认证是通过AuthenticationManager的authenticate()函数实现的。

  • 也就是通过AuthenticationManager的实现类ProviderManager的authenticate()函数认证

  • ProviderManager的authenticate()函数会轮询ProviderManager的List<AuthenticationProvider> providers 成员变量下面会讲述如何把自定义AuthenticationProvider加入到ProviderManager的providers属性中加入自定义AuthenticationProvider

  • 如果该providers中如果有一个AuthenticationProvider的supports函数返回true,那么就会调用该AuthenticationProvider的authenticate函数认证,认证成功则整个认证过程结束。

  • 如果不成功,则继续使用下一个合适的AuthenticationProvider进行认证,只要有一个认证成功则为认证成功。

这里我们自定义了一个AuthenticationProvider类型对象(就是下面第二个自定义的CustomAuthenticationProvider),使用默认的AuthenticationManager对象调用authenticate(authToken)方法就会通过轮询的方式进入我们自定义的AuthenticationProvider对象(就是下面第二个自定义的CustomAuthenticationProvider)的authenticate()方法
注意点:注意这里的redissonClient的注入是以BeanFactoryUtil工厂对象创建的,这里不能直接在filter中使用@Autowired或者@Resource的方式进行注入,因为这里会在bean对象创建之前进行加载,所以bean对象注入会失败。

 @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        CustomAuthenticationToken authToken = null;
        if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
            try (InputStream is = request.getInputStream()) {
                AdminLoginRequest adminLoginRequest = JsonUtil.toObj(StreamUtils.copyToString(is, StandardCharsets.UTF_8), AdminLoginRequest.class);
          
                String loginName = adminLoginRequest.getLoginName().trim();
                String password = adminLoginRequest.getPassword().trim();
                String captcha = adminLoginRequest.getCaptcha().trim();

                String expectCaptcha = (String) BeanFactoryUtil.getBean(RedissonClient.class).getBucket(StrUtil.format("auth:captcha:admin:{}", uuid))
                        .getAndDelete();

                if (!captcha.equalsIgnoreCase(expectCaptcha)) {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    HttpContextUtil.write(response, Rest.error( "验证码错误"));
                    return null;
                }
                // 自定义的token会有一个属性: Boolean  Authenticated; 刚开始走到这里会创建一个token对象并将这个属性设为false,因为还没有经过认证方法authenticate() 
                authToken = new CustomAuthenticationToken(loginName, password);

                setDetails(request, authToken);

                Authentication authenticate = this.getAuthenticationManager().authenticate(authToken);
                return authenticate;
            } catch (IOException e) {
                throw ServiceExceptionUtil.error(BUSY, e, e.getMessage());
            }
        } else {
            throw ServiceExceptionUtil.fail(AuthenticationErrorCodeEnum.FORM_LOGIN_NOT_SUPPORT, "json");
        }

    }

自定义CustomAuthenticationProvider代理

自定义CustomAuthenticationProvider继承AuthenticationProvider类,重写authenticate()方法,这里通常用来校验所请求参数是否存在,随后调用retrieveUser()方法,该方法其实就是做了一层转发调用UserDetailsService().loadUserByUsername()做账号密码的校验,然后通过additionalAuthenticationChecks()方法用来做额外的校验,最后返回认证后的Authentication对象(就是authenticated属性为true的自定义AbstractAuthenticationToken对象)
注意点:

  • 这里需要注意,因为在解密的时候使用的是默认的密码校验器,所以在用户注册时的密码加密必须要使用passwordEncoder.encode()方法。当然也可以对密码校验器做自定义化自定义的密码校验器记得要在配置AuthenticationProvider的时候作为构造参数传入
  • authenticate()方法会也会返回一个token对象这个与上一次filter返回的对象不同的是,这里面指定了认证属性为true,并且包含authorities权限集合的属性;
  • 最后Security会将token转为Authentication对象(自定义的目的:父类引用指向子类对象)存储起来,后面校验权限的时候调用的就是token的authorities属性;

定制化的provider是通过实现AuthenticationProvider接口的supports方法适配具体处理哪个定制化的Authentication对象,源码如下:

    @Override
    public boolean supports(Class<?> authentication) {
    	// ProviderManager轮询provider时调用该方法,查询当前定制化的Authentication对象是该provider是想要处理的定制化authentication类 
        return (CustomUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
 @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(CustomAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only CustomAuthenticationToken is supported"))
        UserDetails user = this.userCache.getUserFromCache(authJson);
 
        if (user == null) {
            try {
            	// 调用retrieveUser 就是做了一层转发 这里会抓取抛出的异常,对异常进行重新抛出,目的是对应失败处理器的异常类型
                user = retrieveUser(authJson, (CustomAuthenticationToken) authentication);
            } catch (UsernameNotFoundException notFound) {
                logger.debug("Identity '" + loginName + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                } else {
                    throw notFound;
                }
            }

            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
        } catch (Exception exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                  // 查询到用户
                user = retrieveUser(authJson, (CustomAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                // 查询到用户后 检测存储的密码和提交的密码是否相同,否则抛出异常
                additionalAuthenticationChecks(user, (CustomAuthenticationToken) authentication);
             } else {
                throw exception;
            }
        }
        // 这里是返回了一个认证过的自定义token
        // 自定义的token会有一个属性: Boolean  Authenticated; 走到这里会创建一个token对象并将这个属性设为true
        return CustomAuthenticationToken(platform, organId, principalToReturn, authentication, user);
    }

	// 这里需要注意,在用户注册时的密码加密必须要使用passwordEncoder.encode()方法
	@Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  CustomAdminUsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }




 @Override
    protected final UserDetails retrieveUser(String authJson,
                                             CustomHealthUsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(authJson);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

自定义UserDetailsService和UserDetails类

这里需要对CustomDetailsService继承UserDetailsService和CustomDetails继承UserDetails进行自定义,service里面主要是重写loadUserByUsername()方法,对账号做校验,登录成功返回CustomDetails对象。CustomDetails里面根据业务需要存放一些登录信息,权限、账号体系、菜单集合等;

    @Override
    public UserDetails loadUserByUsername(String authJson) throws UsernameNotFoundException {

        HealthLoginRequest healthLoginRequest = JsonUtil.toObj(authJson, HealthLoginRequest.class);

        Identity identity = identityService.selectOneByProperty(Identity::getLoginName, healthLoginRequest.getLoginName());
        if (Objects.nonNull(identity)) {
            Long organId = identity.getOrganId();
           
            return new CustomDetails(organId, identity);
        } else {
            // 员工不存在
            throw new UsernameNotFoundException("员工名不存在");
        }
    }

自定义AbstractAuthenticationToken类

自定义第二个类CustomAuthenticationToken继承AbstractAuthenticationToken类,这里我们定义了一些认证属性:请求参数、权限集合、业务参数等

public class CustomAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private Long organId;

    private final Object principal;

    private Object credentials;
    
     public CustomAuthenticationToken(Long organId, Object principal, Object credentials) {
        super(null);
        this.organId = organId;
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    public CustomAuthenticationToken(Long organId, Object principal, Object credentials,
                                                           Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.organId = organId;
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

自定义WebSecurityConfigurerAdapter配置类

自定义后怎样启用则是需要WebSecurityConfigurerAdapter这个类来进行管理和配置
1.首先是自定义配置类 CustomWebSecurityConfig继承WebSecurityConfigurerAdapter 这个类里面可以完成上述流程图的所有配置。随后需要重写configure()方法,在里面配置我们的自定义登录。

 		http
 				// 表示对该路径下的所有接口进行认证管理,可以使用通配符*
 				.antMatcher("/custom/**")
 				// 需要登录请求检验
                .authorizeRequests()
                /* 这里是对一些路径进行放开,比如静态资源、登录和登出、验证码获取接口 不定长参数 */
                .antMatchers("/custom/login", "/health/logout")
                // 对所有请求校验权限
                .permitAll()
                // 需要进行认证
                .authenticated()
                // 对所有请求生效
                .anyRequest()
                // 权限鉴定方法指定
                .access("@customSecurityExpressionRoot.hasPermission(request, authentication)");

然后继续配置我们自定义的过滤器UsernamePasswordAuthenticationFilter,其中传入了一个AuthenticationManager对象的方法authenticationManagerBean(),这里使用的是父类的方法,没有对其作出自定义,authenticationDetailsSource对象也是Security框架中的bean对象,没有做自定义处理。

// 获取自定义认证过滤器对象 该方法在下面代码展示
UsernamePasswordAuthenticationFilter authenticationProcessingFilter = authenticationProcessingFilter();
// 
authenticationProcessingFilter.setAuthenticationManager(authenticationManagerBean());
authenticationProcessingFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
authenticationProcessingFilter.setFilterProcessesUrl("/custom/login");

// session 管理
ConcurrentSessionControlAuthenticationStrategy sessionControlAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
// 最大同时在线设备数为 1
sessionControlAuthenticationStrategy.setMaximumSessions(2);
// 登录时如果超过最大同时在线数则阻止本次登录
sessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(false);
authenticationProcessingFilter.setSessionAuthenticationStrategy(sessionControlAuthenticationStrategy);
// 将自定义的登录过滤器放在过滤器链中,和UsernamePasswordAuthenticationFilter的类型一样的位置,并不会覆盖传入的过滤器类
http.addFilterAt(authenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);

authenticationProcessingFilter() 代码

public UsernamePasswordAuthenticationFilter authenticationProcessingFilter() throws Exception {
        UsernamePasswordAuthenticationFilter authenticationFilter = new UsernamePasswordAuthenticationFilter("", HttpMethod.POST.name());
        // 指定登录成功处理器 这里自定义了登录成功处理器
        authenticationFilter.setAuthenticationSuccessHandler(successHandler);
        // 指定登录失败处理器 这里自定义了登录失败处理器
        authenticationFilter.setAuthenticationFailureHandler(failureHandler);
        return authenticationFilter;
    }

配置ProviderManager的providers属性, 加入自定义AuthenticationProvider方法:

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
    	// 这里启用我们自定义的authenticationProvider
        builder.authenticationProvider(authenticationProvider());
    }
    public AuthenticationProvider authenticationProvider() {
        CustomAuthenticationProvider provider = new CustomAuthenticationProvider();
        provider.setHideUserNotFoundExceptions(false);
        // 传入我们自定义的userDetailsService对象
        provider.setUserDetailsService(userDetailsService);
        // 传入默认的密码校验器 如果做了自定义,这里传入的是自定义的密码校验器对象
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }

认证成功处理器

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

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

        CustomHealthUserDetails userDetails;
        LoginResponse loginResponse = new LoginResponse();
        if (principal instanceof CustomHealthUserDetails) {
            loginResponse.setLoginName(userDetails.getUsername())
                    .setOrganId(userDetails.currentOrganId())

        HttpContextUtil.write(response, JsonUtil.defaultObjectMapper()
                .createObjectNode()
                .put("code", "00000")
                .put("msg", "认证成功")
                .putPOJO("data", loginResponse));
    }
}

认证失败处理器,返回对应异常类型的错误信息

@Component
@Primary
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        Rest<BaseResponse> rest = Rest.error(AuthenticationErrorCodeEnum.FAILURE);
        if (exception instanceof LockedException) {
            rest = Rest.error(SysManageErrorCodeEnum.CURRENT_ACCOUNT_IS_LOCKED);
        }else if (exception instanceof UsernameNotFoundException) {
            rest = Rest.error(AuthenticationErrorCodeEnum.USERNAME_NOT_FOUND);
        }else if (exception instanceof SmsCodeNotFoundException) {
            rest = Rest.error(SysManageErrorCodeEnum.SMS_CODE_NOT_FOUND);
        }else if (exception instanceof UserInfoNotFoundException) {
            rest = Rest.error(SysManageErrorCodeEnum.USER_INFO_NOT_FOUND);
        } else if (exception instanceof BadCredentialsException) {
            rest = Rest.error(AuthenticationErrorCodeEnum.BAD_CREDENTIALS);
        } else if (exception instanceof SessionAuthenticationException) {
            rest = Rest.error(AuthenticationErrorCodeEnum.LOGIN_ON_ANOTHER_DEVICE, "本次登录失败");
        }
        HttpContextUtil.write(response, rest);
    }
}

3 Security中使用到的设计模式

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值