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

网友投稿 230 2022-11-13

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使用

@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() {

EcStaffUsernamePhttp://asswordAuthenticationProvider 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 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 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 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 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)

{

thishttp://.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 getPermissions()

{

return permissions;

}

public void setPermissions(Set 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

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:不按需付费的SaaS和耍流氓有什么分别
下一篇:亚马逊AWS:一个字头的诞生
相关文章

 发表评论

暂时没有评论,来抢沙发吧~