信息发布→ 登录 注册 退出

spring security 自定义Provider 如何实现多种认证

发布时间:2026-01-11

点击量:
目录
  • security内部认证流程是这样的
    • 1、 Controller
    • 2、spring security
    • 3、调用匹配的provider内部认证逻辑
    • 4、UserDetailsService
    • 5、继续走spring security内部逻辑
    • 6、所有调用完毕就会
  • 1、基础配置-SecurityConfig
    • 2、基础配置-自定义AuthenticationToken
      • 3、基础配置-自定义provider
        • 4、Controller发起身份认证
          • 5、service查询数据库中用户对象
            • 6、service返回的LoginUser
              • 7、另一套用户controller登录认证方法
                • 8、另一套用户service

                  我的系统里有两种用户,对应数据库两张表,所以必须自定义provider 和 AuthenticationToken,这样才能走到匹配自定义的UserDetailsService。

                  必须自定义原因在于,security内部是遍历prodvider,根据其support 方法判断是否匹配Controller提交的token,然后走provider注入的认证service方法。

                  security内部认证流程是这样的

                  1、 Controller

                  用用户名和密码构造AuthenticationToken 并提交给 authenticationManager,

                  authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

                  2、spring security

                  会遍历自定义和内置provider,根据provider的support方法判断入参Token所匹配provider

                  public boolean supports(Class<?> authentication) {
                     return (EcStaffUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
                  }

                  3、调用匹配的provider内部认证逻辑

                  过程中会调用UserDetailsService.loadUserByUsername,这个service可以在SecurityConfig中配置注入到provider

                  4、UserDetailsService

                  需要我们自己查询数据库中用户对象,返回对象UserDetails,

                  我返回的是LoginUser ( implements UserDetails ),这样把数据库查出来用户对象加进去,方便前台Controller使用

                  @Override
                  public UserDetails loadUserByUsername(String username) //查询数据库

                  5、继续走spring security内部逻辑

                  包括判断密码是否匹配等,如果密码不匹配或帐号过期等spring会上抛异常到Controller

                  6、所有调用完毕就会

                  回到Controller的方法,并返回authentication。对于异常需要自己捕获,详情可参见后面的代码。

                  authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
                  LoginUser loginUser = (LoginUser) authentication.getPrincipal();

                  说明:

                  大部分人是在流程最前面使用filter实现各种校验,而我的项目全部是前后端分离,所以我的filter只校验token有效性,我把各种非空校验放在controller。

                  1、基础配置-SecurityConfig

                      @Autowired
                      @Qualifier("userDetailsServiceImpl")
                      private UserDetailsService userDetailsService;
                      
                      @Autowired
                      @Qualifier("ecStaffDetailsServiceImpl")
                      private UserDetailsService ecStaffDetailsServiceImpl;
                   
                      /**
                       * token认证过滤器
                       */
                      @Autowired
                      private JwtAuthenticationTokenFilter authenticationTokenFilter;
                      
                      /**
                       * 解决 无法直接注入 AuthenticationManager
                       *
                       * @return
                       * @throws Exception
                       */
                      @Bean
                      @Override
                      public AuthenticationManager authenticationManagerBean() throws Exception
                      {
                          return super.authenticationManagerBean();
                      }
                   
                      /**
                       * anyRequest          |   匹配所有请求路径
                       * access              |   SpringEl表达式结果为true时可以访问
                       * anonymous           |   匿名可以访问
                       * denyAll             |   用户不能访问
                       * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
                       * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
                       * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
                       * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
                       * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
                       * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
                       * permitAll           |   用户可以任意访问
                       * rememberMe          |   允许通过remember-me登录的用户访问
                       * authenticated       |   用户登录后可访问
                       */
                      @Override
                      protected void configure(HttpSecurity httpSecurity) throws Exception
                      {
                          httpSecurity
                                  // CRSF禁用,因为不使用session
                                  .csrf().disable()
                                  // 认证失败处理类
                                  .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                                  // 基于token,所以不需要session
                                  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                                  // 过滤请求
                                  .authorizeRequests()
                                  // 对于登录login 验证码captchaImage 允许匿名访问
                                  .antMatchers("/login", "/captchaImage", "/store-api/ecommerce/login/**").anonymous()
                                  .antMatchers(
                                          HttpMethod.GET,
                                          "/*.html",
                                          "/**/*.html",
                                          "/**/*.css",
                                          "/**/*.js"
                                  ).permitAll()
                                  .antMatchers("/profile/**").anonymous()
                                  .antMatchers("/common/download**").anonymous()
                                  .antMatchers("/common/download/resource**").anonymous()
                                  .antMatchers("/swagger-ui.html").anonymous()
                                  .antMatchers("/swagger-resources/**").anonymous()
                                  .antMatchers("/webjars/**").anonymous()
                                  .antMatchers("/*/api-docs").anonymous()
                                  .antMatchers("/druid/**").anonymous()
                                  // 除上面外的所有请求全部需要鉴权认证
                                  .anyRequest().authenticated()
                                  .and()
                                  .headers().frameOptions().disable();
                          httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
                          // 添加JWT filter
                          httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
                      } 
                      
                      /**
                       * 强散列哈希加密实现
                       */
                      @Bean
                      public BCryptPasswordEncoder bCryptPasswordEncoder()
                      {
                          return new BCryptPasswordEncoder();
                      }
                   
                      /**
                       * 身份认证接口
                       */
                      @Override
                      protected void configure(AuthenticationManagerBuilder auth) throws Exception
                      {
                  		//自定义provider及service,一套身份认证
                          auth.authenticationProvider(getEcStaffUsernamePasswordAuthenticationProvider())
                  		//使用系统自带provider,及自定义service,另一套认证
                              .userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
                      }    
                  	 /**
                       * 自定义provider,注入自定义service
                       */
                      public EcStaffUsernamePasswordAuthenticationProvider getEcStaffUsernamePasswordAuthenticationProvider() {
                          EcStaffUsernamePasswordAuthenticationProvider provider = new EcStaffUsernamePasswordAuthenticationProvider();
                          provider.setPasswordEncoder(bCryptPasswordEncoder());
                          provider.setUserDetailsService(ecStaffDetailsServiceImpl);
                          return provider;
                      }

                  2、基础配置-自定义AuthenticationToken

                  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
                  public class EcStaffUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken{
                      public EcStaffUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
                          super(principal, credentials);
                      }
                      private static final long serialVersionUID = 8665690993060353849L;   
                  }

                  3、基础配置-自定义provider

                  import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 
                  import com.ruoyi.framework.security.authToken.EcStaffUsernamePasswordAuthenticationToken;
                  public class EcStaffUsernamePasswordAuthenticationProvider extends DaoAuthenticationProvider{
                      public boolean supports(Class<?> authentication) {
                          return (EcStaffUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
                      }
                  }

                  4、Controller发起身份认证

                          // 用户验证
                          Authentication authentication = null;
                          try
                          {
                              // 该方法会去调用EcStaffDetailsServiceImpl.loadUserByUsername
                              // 因为这个自定token只被自定provider的support所支持
                              // 所以才会provider中注入的EcStaffDetailsServiceImpl,在security配置文件注入的
                              authentication = authenticationManager.authenticate(new EcStaffUsernamePasswordAuthenticationToken(username, password));
                          }
                          catch (Exception e)
                          {
                              if (e instanceof BadCredentialsException)
                              {        
                                  //密码不匹配,需自定义返回前台消息
                                  throw new UserPasswordNotMatchException();
                              }
                              else
                              {
                                  throw new CustomException(e.getMessage());
                              }
                          }        
                          //登录成功
                          LoginUser loginUser = (LoginUser) authentication.getPrincipal();

                  5、service查询数据库中用户对象

                  import java.util.HashSet;
                  import java.util.Set; 
                  import org.slf4j.Logger;
                  import org.slf4j.LoggerFactory;
                  import org.springframework.beans.factory.annotation.Autowired;
                  import org.springframework.security.core.userdetails.UserDetails;
                  import org.springframework.security.core.userdetails.UserDetailsService;
                  import org.springframework.stereotype.Service;
                   
                  import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
                  import com.ruoyi.common.constant.Constants;
                  import com.ruoyi.common.exception.BaseException;
                  import com.ruoyi.common.utils.MessageUtils;
                  import com.ruoyi.common.utils.StringUtils;
                  import com.ruoyi.ecommerce.constant.StaffStatusConstant;
                  import com.ruoyi.ecommerce.domain.EcStaff;
                  import com.ruoyi.ecommerce.service.IEcStaffService;
                  import com.ruoyi.framework.security.LoginUser;
                   
                  /**
                   * 用户验证处理
                   */
                  @Service
                  public class EcStaffDetailsServiceImpl implements UserDetailsService
                  {
                      private static final Logger log = LoggerFactory.getLogger(EcStaffDetailsServiceImpl.class); 
                      @Autowired
                      private IEcStaffService ecStaffService; 
                      @Autowired
                      private SysPermissionService permissionService; 
                      @Override
                      public UserDetails loadUserByUsername(String username)
                      {
                          QueryWrapper<EcStaff> queryWrapper = new QueryWrapper<>();
                          queryWrapper.eq("phone", username);
                          EcStaff user = ecStaffService.getOne(queryWrapper);
                          
                          if (StringUtils.isNull(user))
                          {
                              log.info("登录用户:{} 不存在.", username);
                              throw new BaseException(MessageUtils.message("user.not.exists"));
                          }
                          else if (Constants.DELETED.equals(user.getDeleted()))
                          {
                              log.info("登录用户:{} 已被删除.", username);
                              throw new BaseException(MessageUtils.message("user.password.delete"));
                          }
                          return createLoginUser(user);
                      }
                   
                      /**
                       * 查询用户权限
                       * @param user
                       * @return
                       */
                      public UserDetails createLoginUser(EcStaff user)
                      {
                          return new LoginUser(user, permissionService.getMenuPermission(user));        
                      }
                  }

                  6、service返回的LoginUser

                  因为有两种用户sysuser和ecstaff,为了基于这个LoginUser统一提供getUsername方法,让他们继承或实现统一BaseUser,

                  可以不统一封装因为LoginUser构造方法入参是object , 即LoginUser(Object user, Set<String> permissions)

                  import java.util.Collection;
                  import java.util.Set; 
                  import org.springframework.security.core.GrantedAuthority;
                  import org.springframework.security.core.userdetails.UserDetails; 
                  import com.fasterxml.jackson.annotation.JsonIgnore;
                  import com.ruoyi.ecommerce.domain.BaseUser;
                   
                  /**
                   * 登录用户身份权限
                   * 
                   * @author ruoyi
                   */
                  public class LoginUser implements UserDetails
                  {
                      private static final long serialVersionUID = 1L;
                   
                      /**
                       * 用户唯一标识
                       */
                      private String token;
                   
                      /**
                       * 登陆时间
                       */
                      private Long loginTime;
                   
                      /**
                       * 过期时间
                       */
                      private Long expireTime;
                   
                      /**
                       * 登录IP地址
                       */
                      private String ipaddr;
                   
                      /**
                       * 登录地点
                       */
                      private String loginLocation;
                   
                      /**
                       * 浏览器类型
                       */
                      private String browser;
                   
                      /**
                       * 操作系统
                       */
                      private String os;
                   
                      /**
                       * 权限列表
                       */
                      private Set<String> permissions;
                   
                      /**
                       * 用户信息
                       */
                      private Object user;
                      /**
                       * 用户的class
                       */
                      private Class userClass;
                   
                      public String getToken()
                      {
                          return token;
                      }
                   
                      public void setToken(String token)
                      {
                          this.token = token;
                      }
                   
                      public LoginUser()
                      {
                      }
                   
                      public LoginUser(Object user, Set<String> permissions)
                      {
                          this.userClass = user.getClass();
                          this.user = user;
                          this.permissions = permissions;
                      }
                   
                      @JsonIgnore
                      @Override
                      public String getPassword()
                      {
                          return ((BaseUser)user).getPassword();
                      }
                   
                      @Override
                      public String getUsername()
                      {
                          return ((BaseUser)user).getUserName();
                      }
                   
                      /**
                       * 账户是否未过期,过期无法验证
                       */
                      @JsonIgnore
                      @Override
                      public boolean isAccountNonExpired()
                      {
                          return true;
                      }
                   
                      /**
                       * 指定用户是否解锁,锁定的用户无法进行身份验证
                       * 
                       * @return
                       */
                      @JsonIgnore
                      @Override
                      public boolean isAccountNonLocked()
                      {
                          return true;
                      }
                   
                      /**
                       * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
                       * 
                       * @return
                       */
                      @JsonIgnore
                      @Override
                      public boolean isCredentialsNonExpired()
                      {
                          return true;
                      }
                   
                      /**
                       * 是否可用 ,禁用的用户不能身份验证
                       * 
                       * @return
                       */
                      @JsonIgnore
                      @Override
                      public boolean isEnabled()
                      {
                          return true;
                      }
                   
                      public Long getLoginTime()
                      {
                          return loginTime;
                      }
                   
                      public void setLoginTime(Long loginTime)
                      {
                          this.loginTime = loginTime;
                      }
                   
                      public String getIpaddr()
                      {
                          return ipaddr;
                      }
                   
                      public void setIpaddr(String ipaddr)
                      {
                          this.ipaddr = ipaddr;
                      }
                   
                      public String getLoginLocation()
                      {
                          return loginLocation;
                      }
                   
                      public void setLoginLocation(String loginLocation)
                      {
                          this.loginLocation = loginLocation;
                      }
                   
                      public String getBrowser()
                      {
                          return browser;
                      }
                   
                      public void setBrowser(String browser)
                      {
                          this.browser = browser;
                      }
                   
                      public String getOs()
                      {
                          return os;
                      }
                   
                      public void setOs(String os)
                      {
                          this.os = os;
                      }
                   
                      public Long getExpireTime()
                      {
                          return expireTime;
                      }
                   
                      public void setExpireTime(Long expireTime)
                      {
                          this.expireTime = expireTime;
                      }
                   
                      public Set<String> getPermissions()
                      {
                          return permissions;
                      }
                   
                      public void setPermissions(Set<String> permissions)
                      {
                          this.permissions = permissions;
                      }
                   
                      public Object getUser()
                      {
                          return user;
                      }
                   
                      public void setUser(Object user)
                      {
                          this.user = user;
                      }
                   
                      public Class getUserClass() {
                          return userClass;
                      }
                   
                      public void setUserClass(Class userClass) {
                          this.userClass = userClass;
                      }
                   
                      @Override
                      public Collection<? extends GrantedAuthority> getAuthorities()
                      {
                          return null;
                      }
                  }

                  7、另一套用户controller登录认证方法

                  注意这里换了security提供的AuthToken,这个token会调用security内部的DaoAuthenticationProvider进行认证

                          // 用户验证
                          Authentication authentication = null;
                          try
                          {
                              // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
                              // 该方式使用的security内置token会使用内置DaoAuthenticationProvider认证
                              // UserDetailsServiceImpl是在security config中配置的
                              authentication = authenticationManager
                                      .authenticate(new UsernamePasswordAuthenticationToken(username, password));
                          }
                          catch (Exception e)
                          {
                              if (e instanceof BadCredentialsException)
                              {               
                                  throw new UserPasswordNotMatchException();
                              }
                              else
                              {                
                                  throw new CustomException(e.getMessage());
                              }
                          }       
                          LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 该方法会去调用

                  8、另一套用户service

                  可参照上述service写,查询另一张用户表即可,返回UserDetails

                  以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

                  在线客服
                  服务热线

                  服务热线

                  4008888355

                  微信咨询
                  二维码
                  返回顶部
                  ×二维码

                  截屏,微信识别二维码

                  打开微信

                  微信号已复制,请打开微信添加咨询详情!