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

目录security内部认证流程是这样的1、 Controller2、spring security3、调用匹配的provider内部认证逻辑4、UserDetailsService5、继续走spring security内部逻辑6、所有调用完毕就会1、基础配置-SecurityConfig2、基础配置-自定义AuthenticationToken3、基础配置-自定义provider4、Controller发起身份认证5、service查询数据库中用户对象6、service返回的LoginUser7、另一套用户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使用

@Overridepublic 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; /** * 用户验证处理 */@Servicepublic 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

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

没有预兆目的地在哪,前进的脚步不能停下,

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

相关文章:

你感兴趣的文章:

标签云: