spring?security?自定義Provider?如何實現(xiàn)多種認證
我的系統(tǒng)里有兩種用戶,對應(yīng)數(shù)據(jù)庫兩張表,所以必須自定義provider 和 AuthenticationToken,這樣才能走到匹配自定義的UserDetailsService。
必須自定義原因在于,security內(nèi)部是遍歷prodvider,根據(jù)其support 方法判斷是否匹配Controller提交的token,然后走provider注入的認證service方法。
security內(nèi)部認證流程是這樣的
1、 Controller
用用戶名和密碼構(gòu)造AuthenticationToken 并提交給 authenticationManager,
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
2、spring security
會遍歷自定義和內(nèi)置provider,根據(jù)provider的support方法判斷入?yún)oken所匹配provider
public boolean supports(Class<?> authentication) {
return (EcStaffUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
3、調(diào)用匹配的provider內(nèi)部認證邏輯
過程中會調(diào)用UserDetailsService.loadUserByUsername,這個service可以在SecurityConfig中配置注入到provider
4、UserDetailsService
需要我們自己查詢數(shù)據(jù)庫中用戶對象,返回對象UserDetails,
我返回的是LoginUser ( implements UserDetails ),這樣把數(shù)據(jù)庫查出來用戶對象加進去,方便前臺Controller使用
@Override public UserDetails loadUserByUsername(String username) //查詢數(shù)據(jù)庫
5、繼續(xù)走spring security內(nèi)部邏輯
包括判斷密碼是否匹配等,如果密碼不匹配或帳號過期等spring會上拋異常到Controller
6、所有調(diào)用完畢就會
回到Controller的方法,并返回authentication。對于異常需要自己捕獲,詳情可參見后面的代碼。
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); LoginUser loginUser = (LoginUser) authentication.getPrincipal();
說明:
大部分人是在流程最前面使用filter實現(xiàn)各種校驗,而我的項目全部是前后端分離,所以我的filter只校驗token有效性,我把各種非空校驗放在controller。
1、基礎(chǔ)配置-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表達式結(jié)果為true時可以訪問
* anonymous | 匿名可以訪問
* denyAll | 用戶不能訪問
* fullyAuthenticated | 用戶完全認證可以訪問(非remember-me下自動登錄)
* hasAnyAuthority | 如果有參數(shù),參數(shù)表示權(quán)限,則其中任何一個權(quán)限可以訪問
* hasAnyRole | 如果有參數(shù),參數(shù)表示角色,則其中任何一個角色可以訪問
* hasAuthority | 如果有參數(shù),參數(shù)表示權(quán)限,則其權(quán)限可以訪問
* hasIpAddress | 如果有參數(shù),參數(shù)表示IP地址,如果用戶IP和參數(shù)匹配,則可以訪問
* hasRole | 如果有參數(shù),參數(shù)表示角色,則其角色可以訪問
* 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()
// 除上面外的所有請求全部需要鑒權(quán)認證
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 強散列哈希加密實現(xiàn)
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份認證接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//自定義provider及service,一套身份認證
auth.authenticationProvider(getEcStaffUsernamePasswordAuthenticationProvider())
//使用系統(tǒng)自帶provider,及自定義service,另一套認證
.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* 自定義provider,注入自定義service
*/
public EcStaffUsernamePasswordAuthenticationProvider getEcStaffUsernamePasswordAuthenticationProvider() {
EcStaffUsernamePasswordAuthenticationProvider provider = new EcStaffUsernamePasswordAuthenticationProvider();
provider.setPasswordEncoder(bCryptPasswordEncoder());
provider.setUserDetailsService(ecStaffDetailsServiceImpl);
return provider;
}
2、基礎(chǔ)配置-自定義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、基礎(chǔ)配置-自定義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發(fā)起身份認證
// 用戶驗證
Authentication authentication = null;
try
{
// 該方法會去調(diào)用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查詢數(shù)據(jù)庫中用戶對象
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);
}
/**
* 查詢用戶權(quán)限
* @param user
* @return
*/
public UserDetails createLoginUser(EcStaff user)
{
return new LoginUser(user, permissionService.getMenuPermission(user));
}
}
6、service返回的LoginUser
因為有兩種用戶sysuser和ecstaff,為了基于這個LoginUser統(tǒng)一提供getUsername方法,讓他們繼承或?qū)崿F(xiàn)統(tǒng)一BaseUser,
可以不統(tǒng)一封裝因為LoginUser構(gòu)造方法入?yún)⑹莖bject , 即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;
/**
* 登錄用戶身份權(quán)限
*
* @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;
/**
* 操作系統(tǒng)
*/
private String os;
/**
* 權(quán)限列表
*/
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;
}
/**
* 指示是否已過期的用戶的憑據(jù)(密碼),過期的憑據(jù)防止認證
*
* @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會調(diào)用security內(nèi)部的DaoAuthenticationProvider進行認證
// 用戶驗證
Authentication authentication = null;
try
{
// 該方法會去調(diào)用UserDetailsServiceImpl.loadUserByUsername
// 該方式使用的security內(nèi)置token會使用內(nèi)置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();// 該方法會去調(diào)用
8、另一套用戶service
可參照上述service寫,查詢另一張用戶表即可,返回UserDetails
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis使用JSONObject接收數(shù)據(jù)庫查詢的方法
這篇文章主要介紹了Mybatis使用JSONObject接收數(shù)據(jù)庫查詢,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12
Java微信公眾平臺開發(fā)(8) 多媒體消息回復(fù)
這篇文章主要為大家詳細介紹了Java微信公眾平臺開發(fā)第八步,微信多媒體消息回復(fù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
Java基礎(chǔ)高級綜合練習(xí)題撲克牌的創(chuàng)建
今天小編就為大家分享一篇關(guān)于Java基礎(chǔ)高級綜合練習(xí)題撲克牌的創(chuàng)建,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01
Java synchronized關(guān)鍵字和Lock接口實現(xiàn)原理
這篇文章主要介紹了Java synchronized關(guān)鍵字和Lock接口實現(xiàn)原理,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
springboot基于Mybatis mysql實現(xiàn)讀寫分離
這篇文章主要介紹了springboot基于Mybatis mysql實現(xiàn)讀寫分離,需要的朋友可以參考下2019-06-06
dubbo如何實現(xiàn)consumer從多個group中調(diào)用指定group的provider
這篇文章主要介紹了dubbo如何實現(xiàn)consumer從多個group中調(diào)用指定group的provider問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03

