我的系统里有两种用户,对应数据库两张表,所以必须自定义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