spring security 自定義Provider 如何實現多種認證
我的系統裡有兩種用戶,對應數據庫兩張表,所以必須自定義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
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 一文詳解Spring Security的基本用法
- Spring boot整合security詳解
- 關於SpringSecurity配置403權限訪問頁面的完整代碼
- SpringBoot結合JWT登錄權限控制的實現
- Spring Security權限想要細化到按鈕實現示例