spring?security?自定義Provider?如何實(shí)現(xiàn)多種認(rèn)證
我的系統(tǒng)里有兩種用戶,對(duì)應(yīng)數(shù)據(jù)庫兩張表,所以必須自定義provider 和 AuthenticationToken,這樣才能走到匹配自定義的UserDetailsService。
必須自定義原因在于,security內(nèi)部是遍歷prodvider,根據(jù)其support 方法判斷是否匹配Controller提交的token,然后走provider注入的認(rèn)證service方法。
security內(nèi)部認(rèn)證流程是這樣的
1、 Controller
用用戶名和密碼構(gòu)造AuthenticationToken 并提交給 authenticationManager,
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
2、spring security
會(huì)遍歷自定義和內(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)部認(rèn)證邏輯
過程中會(huì)調(diào)用UserDetailsService.loadUserByUsername,這個(gè)service可以在SecurityConfig中配置注入到provider
4、UserDetailsService
需要我們自己查詢數(shù)據(jù)庫中用戶對(duì)象,返回對(duì)象UserDetails,
我返回的是LoginUser ( implements UserDetails ),這樣把數(shù)據(jù)庫查出來用戶對(duì)象加進(jìn)去,方便前臺(tái)Controller使用
@Override public UserDetails loadUserByUsername(String username) //查詢數(shù)據(jù)庫
5、繼續(xù)走spring security內(nèi)部邏輯
包括判斷密碼是否匹配等,如果密碼不匹配或帳號(hào)過期等spring會(huì)上拋異常到Controller
6、所有調(diào)用完畢就會(huì)
回到Controller的方法,并返回authentication。對(duì)于異常需要自己捕獲,詳情可參見后面的代碼。
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); LoginUser loginUser = (LoginUser) authentication.getPrincipal();
說明:
大部分人是在流程最前面使用filter實(shí)現(xiàn)各種校驗(yàn),而我的項(xiàng)目全部是前后端分離,所以我的filter只校驗(yàn)token有效性,我把各種非空校驗(yàn)放在controller。
1、基礎(chǔ)配置-SecurityConfig
@Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; @Autowired @Qualifier("ecStaffDetailsServiceImpl") private UserDetailsService ecStaffDetailsServiceImpl; /** * token認(rèn)證過濾器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 解決 無法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有請(qǐng)求路徑 * access | SpringEl表達(dá)式結(jié)果為true時(shí)可以訪問 * anonymous | 匿名可以訪問 * denyAll | 用戶不能訪問 * fullyAuthenticated | 用戶完全認(rèn)證可以訪問(非remember-me下自動(dòng)登錄) * hasAnyAuthority | 如果有參數(shù),參數(shù)表示權(quán)限,則其中任何一個(gè)權(quán)限可以訪問 * hasAnyRole | 如果有參數(shù),參數(shù)表示角色,則其中任何一個(gè)角色可以訪問 * 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禁用,因?yàn)椴皇褂胹ession .csrf().disable() // 認(rèn)證失敗處理類 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 過濾請(qǐng)求 .authorizeRequests() // 對(duì)于登錄login 驗(yàn)證碼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() // 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } /** * 強(qiáng)散列哈希加密實(shí)現(xiàn) */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份認(rèn)證接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定義provider及service,一套身份認(rèn)證 auth.authenticationProvider(getEcStaffUsernamePasswordAuthenticationProvider()) //使用系統(tǒng)自帶provider,及自定義service,另一套認(rèn)證 .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ā)起身份認(rèn)證
// 用戶驗(yàn)證 Authentication authentication = null; try { // 該方法會(huì)去調(diào)用EcStaffDetailsServiceImpl.loadUserByUsername // 因?yàn)檫@個(gè)自定token只被自定provider的support所支持 // 所以才會(huì)provider中注入的EcStaffDetailsServiceImpl,在security配置文件注入的 authentication = authenticationManager.authenticate(new EcStaffUsernamePasswordAuthenticationToken(username, password)); } catch (Exception e) { if (e instanceof BadCredentialsException) { //密碼不匹配,需自定義返回前臺(tái)消息 throw new UserPasswordNotMatchException(); } else { throw new CustomException(e.getMessage()); } } //登錄成功 LoginUser loginUser = (LoginUser) authentication.getPrincipal();
5、service查詢數(shù)據(jù)庫中用戶對(duì)象
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; /** * 用戶驗(yàn)證處理 */ @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
因?yàn)橛袃煞N用戶sysuser和ecstaff,為了基于這個(gè)LoginUser統(tǒng)一提供getUsername方法,讓他們繼承或?qū)崿F(xiàn)統(tǒng)一BaseUser,
可以不統(tǒng)一封裝因?yàn)長oginUser構(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; /** * 用戶唯一標(biāo)識(shí) */ private String token; /** * 登陸時(shí)間 */ private Long loginTime; /** * 過期時(shí)間 */ private Long expireTime; /** * 登錄IP地址 */ private String ipaddr; /** * 登錄地點(diǎn) */ 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(); } /** * 賬戶是否未過期,過期無法驗(yàn)證 */ @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } /** * 指定用戶是否解鎖,鎖定的用戶無法進(jìn)行身份驗(yàn)證 * * @return */ @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } /** * 指示是否已過期的用戶的憑據(jù)(密碼),過期的憑據(jù)防止認(rèn)證 * * @return */ @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 ,禁用的用戶不能身份驗(yàn)證 * * @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登錄認(rèn)證方法
注意這里換了security提供的AuthToken,這個(gè)token會(huì)調(diào)用security內(nèi)部的DaoAuthenticationProvider進(jìn)行認(rèn)證
// 用戶驗(yàn)證 Authentication authentication = null; try { // 該方法會(huì)去調(diào)用UserDetailsServiceImpl.loadUserByUsername // 該方式使用的security內(nèi)置token會(huì)使用內(nèi)置DaoAuthenticationProvider認(rèn)證 // 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();// 該方法會(huì)去調(diào)用
8、另一套用戶service
可參照上述service寫,查詢另一張用戶表即可,返回UserDetails
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis使用JSONObject接收數(shù)據(jù)庫查詢的方法
這篇文章主要介紹了Mybatis使用JSONObject接收數(shù)據(jù)庫查詢,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12Java微信公眾平臺(tái)開發(fā)(8) 多媒體消息回復(fù)
這篇文章主要為大家詳細(xì)介紹了Java微信公眾平臺(tái)開發(fā)第八步,微信多媒體消息回復(fù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04Java基礎(chǔ)高級(jí)綜合練習(xí)題撲克牌的創(chuàng)建
今天小編就為大家分享一篇關(guān)于Java基礎(chǔ)高級(jí)綜合練習(xí)題撲克牌的創(chuàng)建,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01Java synchronized關(guān)鍵字和Lock接口實(shí)現(xiàn)原理
這篇文章主要介紹了Java synchronized關(guān)鍵字和Lock接口實(shí)現(xiàn)原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12mybatis對(duì)于list更新sql語句的寫法說明
這篇文章主要介紹了mybatis對(duì)于list更新sql語句的寫法說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08springboot基于Mybatis mysql實(shí)現(xiàn)讀寫分離
這篇文章主要介紹了springboot基于Mybatis mysql實(shí)現(xiàn)讀寫分離,需要的朋友可以參考下2019-06-06dubbo如何實(shí)現(xiàn)consumer從多個(gè)group中調(diào)用指定group的provider
這篇文章主要介紹了dubbo如何實(shí)現(xiàn)consumer從多個(gè)group中調(diào)用指定group的provider問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03