spring security 自定义Provider 如何实现多种认证
作者:yfx000
我的系统里有两种用户,对应数据库两张表,所以必须自定义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
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。