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

目录
  • 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

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

(0)

相关推荐

  • 详解Spring Security 中的四种权限控制方式

    Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑.一句话,你想怎么玩都可以! 今天松哥来和大家介绍一下 Spring Security 中四种常见的权限控制方式. 表达式控制 URL 路径权限 表达式控制方法权限 使用过滤注解 动态权限 四种方式,我们分别来看.  1.表达式控制 URL 路径权

  • 详解spring security 配置多个AuthenticationProvider

    前言 发现很少关于spring security的文章,基本都是入门级的,配个UserServiceDetails或者配个路由控制就完事了,而且很多还是xml配置,国内通病...so,本文里的配置都是java配置,不涉及xml配置,事实上我也不会xml配置 spring security的大体介绍 spring security本身如果只是说配置,还是很简单易懂的(我也不知道网上说spring security难,难在哪里),简单不需要特别的功能,一个WebSecurityConfigurerA

  • 详解spring security四种实现方式

    spring security实现方式大致可以分为这几种: 1.配置文件实现,只需要在配置文件中指定拦截的url所需要权限.配置userDetailsService指定用户名.密码.对应权限,就可以实现. 2.实现UserDetailsService,loadUserByUsername(String userName)方法,根据userName来实现自己的业务逻辑返回UserDetails的实现类,需要自定义User类实现UserDetails,比较重要的方法是getAuthorities()

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

    目录 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.servi

  • spring security自定义认证登录的全过程记录

    spring security使用分类: 如何使用spring security,相信百度过的都知道,总共有四种用法,从简到深为: 1.不用数据库,全部数据写在配置文件,这个也是官方文档里面的demo: 2.使用数据库,根据spring security默认实现代码设计数据库,也就是说数据库已经固定了,这种方法不灵活,而且那个数据库设计得很简陋,实用性差: 3.spring security和Acegi不同,它不能修改默认filter了,但支持插入filter,所以根据这个,我们可以插入自己的f

  • Spring Security 自定义短信登录认证的实现

    自定义登录filter 上篇文章我们说到,对于用户的登录,security通过定义一个filter拦截login路径来实现的,所以我们要实现自定义登录,需要自己定义一个filter,继承AbstractAuthenticationProcessingFilter,从request中提取到手机号和验证码,然后提交给AuthenticationManager: public class SmsAuthenticationFilter extends AbstractAuthenticationPro

  • Spring Security自定义认证逻辑实例详解

    目录 前言 分析问题 自定义 Authentication 自定义 Filter 自定义 Provider 自定义认证成功/失败后的 Handler 配置自定义认证的逻辑 测试 总结 前言 这篇文章的内容基于对Spring Security 认证流程的理解,如果你不了解,可以读一下这篇文章:Spring Security 认证流程 . 分析问题 以下是 Spring Security 内置的用户名/密码认证的流程图,我们可以从这里入手: 根据上图,我们可以照猫画虎,自定义一个认证流程,比如手机短

  • Spring Security自定义认证器的实现代码

    目录 Authentication AuthenticationProvider SecurityConfigurerAdapter UserDetailsService TokenFilter 登录过程 在了解过Security的认证器后,如果想自定义登陆,只要实现AuthenticationProvider还有对应的Authentication就可以了 Authentication 首先要创建一个自定义的Authentication,Security提供了一个Authentication的子

  • Spring Security自定义登录页面认证过程常用配置

    目录 一.自定义登录页面 1.编写登录页面 2.修改配置类 3.编写控制器 二. 认证过程其他常用配置 1.失败跳转 1.1编写页面 1.2修改表单配置 1.3添加控制器方法 1.4设置fail.html不需要认证 2.设置请求账户和密码的参数名 2.1源码简介 2.2修改配置 2.3修改页面 3.自定义登录成功处理器 3.1源码分析 3.2代码实现 4.自定义登录失败处理器 4.1源码分析 4.2代码实现 一.自定义登录页面 虽然Spring Security给我们提供了登录页面,但是对于实际

  • Spring security 自定义过滤器实现Json参数传递并兼容表单参数(实例代码)

    依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId&g

  • Springboot+Spring Security实现前后端分离登录认证及权限控制的示例代码

    目录 前言 本文主要的功能 一.准备工作 1.统一错误码枚举 2.统一json返回体 3.返回体构造工具 4.pom 5.配置文件 二.数据库表设计 初始化表数据语句 三.Spring Security核心配置:WebSecurityConfig 四.用户登录认证逻辑:UserDetailsService 1.创建自定义UserDetailsService 2.准备service和dao层方法 五.用户密码加密 六.屏蔽Spring Security默认重定向登录页面以实现前后端分离功能 1.实

  • spring security自定义决策管理器

    首先介绍下Spring的决策管理器,其接口为AccessDecisionManager,抽象类为AbstractAccessDecisionManager.而我们要自定义决策管理器的话一般是继承抽象类而不去直接实现接口. 在Spring中引入了投票器(AccessDecisionVoter)的概念,有无权限访问的最终觉得权是由投票器来决定的,最常见的投票器为RoleVoter,在RoleVoter中定义了权限的前缀,先看下Spring在RoleVoter中是怎么处理授权的. public int

  • spring security 5.x实现兼容多种密码的加密方式

    前言 本文主要给大家介绍了关于spring security 5.x实现兼容多种密码的加密方式,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1.spring security PasswordEncoder spring security 5不需要配置密码的加密方式,而是用户密码加前缀的方式表明加密方式,如: {MD5}88e2d8cd1e92fd5544c8621508cd706b代表使用的是MD5加密方式: {bcrypt}$2a$10$eZeGvVV2ZXr/vgiV

随机推荐