聲明: 本文先演示效果,然後再給出幾個相對關鍵的類;完整測試項目,可詳見文末鏈接
。
效果演示:
說明:
- 張三屬於普通用戶,能訪問一些普通的頁面以及/user頁。
- 李四屬於數據庫管理員,能訪問一些普通的頁面以及/user頁、/dba頁。
- 王五屬於超級管理員,能訪問一些普通的頁面以及/user頁、/dba頁、/admin頁。
演示:
-
普通用戶張三:
-
數據庫管理員李四:
-
超級管理員王五:
相關代碼(庫表):
項目整體說明:
注:上圖中,只對相對關鍵的內容進行了簡單說明;完整測試項目,可詳見文末鏈接
。
相關庫表說明:
幾個相對關鍵的類:
提示: 完整測試項目,可詳見文末鏈接
。
-
MyAccessDecisionManager:
import com.pingan.springsecurity.model.MyUserDetails; import com.pingan.springsecurity.service.impl.MyUserDetailsService; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * 決策器 (判斷鑑權是否通過) * * @author JustryDeng * @date 2019/12/14 11:34 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager { /** * 決策 * * @param authentication * 當前用戶的信息模型 * 注: 由於重寫了{@link MyUserDetailsService#loadUserByUsername}, * 重寫後該方法實際返回的類型是{@link MyUserDetails}, 所以這裏直接 * 將Authentication強轉爲MyUserDetails。 * @param object * 當前request的封裝 * @param configAttributes * 與訪問的目標uri相關聯的配置屬性 * 注:在本示例中,不需要用到此屬性; 如果在這裏需要用到此屬性的話,可以在通過 * 實現{@link FilterInvocationSecurityMetadataSource},重寫相關返 * 回Collection<ConfigAttribute>的方法,該返回值會作爲形參傳遞到本方法 * 然後在這裏就能拿到對應的值了。 * 可參考網友的示例<linked>https://www.jianshu.com/p/e715cc993bf0</linked> * * @throws AccessDeniedException * 當前用戶無權訪問 * @throws InsufficientAuthenticationException * 當前用戶信任級別不夠,無法訪問 * @date 2019/12/14 12:06 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); String targetPath = request.getRequestURI(); Object principal = authentication.getPrincipal(); /* * 若認證過,那麼principal instanceof MyUserDetails爲true, 否則用戶沒有認證過 * * 注: 鑑權一般都在認證之後, 沒有認證談何鑑權。 * * 注: 一旦拋出異常後, * 在{@link ExceptionTranslationFilter#doFilter}中會對拋出的AuthenticationException異常(包括其子異常) * 進行相關處理。如: 拋出AuthenticationCredentialsNotFoundException異常,就會被處理然後頁面跳轉至登錄頁 * */ if (!(principal instanceof MyUserDetails)) { throw new AuthenticationCredentialsNotFoundException(" there is no any Authentication object MyUserDetails in the SecurityContext"); } MyUserDetails myUserDetails = (MyUserDetails)principal; // 這個用戶可訪問的所有資源信息 Collection<? extends GrantedAuthority> grantedAuthority = myUserDetails.getAuthorities(); List<String> list = grantedAuthority.parallelStream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); if(list.contains(targetPath)) { // 鑑權通過, 有訪問權限 return; } // 鑑權不通過, 沒有訪問權限 throw new AccessDeniedException( String.format("You(%s) don't have any authorizion access %s", myUserDetails.getName(), targetPath) ); } /** * 當前AccessDecisionManager實例能否處理 傳遞的ConfigAttribute呈現的授權請求 */ @Override public boolean supports(ConfigAttribute attribute) { return true; } /** * 當前AccessDecisionManager實例是否支持提供訪問控制決策 */ @Override public boolean supports(Class<?> clazz) { return true; } }
-
MyFilterInvocationSecurityMetadataSource:
import com.pingan.springsecurity.mapper.DaoMapper; import com.pingan.springsecurity.model.ApiResource; import lombok.RequiredArgsConstructor; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * 存儲ConfigAttribute信息, 並根據ConfigAttribute信息的有無, 決定是否走 決策器 * 即: 若{@link this#getAttributes}返回的集合滿足CollectionUtils.isEmpty(list)爲true的話, * 那麼不會走決策器, * 否者會走決策器 * * @author JustryDeng * @date 2019/12/14 11:20 */ @Component @RequiredArgsConstructor public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private List<String> needAuthPaths = new ArrayList<>(16); private final DaoMapper mapper; @PostConstruct private void init() { needAuthPaths.addAll(mapper.selectNeedAuthPaths()); } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); String targetPath = request.getRequestURI(); if (needAuthPaths.contains(targetPath)) { // 需要鑑權 List<ConfigAttribute> list = new ArrayList<>(1); list.add(ApiResource.builder().build()); return list; } // 不需要鑑權 return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
-
MySecurityInterceptor:
import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.web.FilterInvocation; import org.springframework.stereotype.Component; import javax.servlet.*; import java.io.IOException; /** * 通過MySecurityInterceptor 註冊 MyAccessDecisionManager * * @author JustryDeng * @date 2019/12/14 12:50 */ @Component public class MySecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private final MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource; public MySecurityInterceptor(MyAccessDecisionManager myAccessDecisionManager, MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource) { this.myFilterInvocationSecurityMetadataSource = myFilterInvocationSecurityMetadataSource; // 設置 以 自定義的決策權 進行 鑑權管理 super.setAccessDecisionManager(myAccessDecisionManager); } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } /** * 返回自定義的SecurityMetadataSource */ @Override public SecurityMetadataSource obtainSecurityMetadataSource() { // 如果有實現SecurityMetadataSource的話,可以設置採用自定義的 資源器, 如: // 如果實現有FilterInvocationSecurityMetadataSource的話,可以將其進行註冊(即:返回其實例) return myFilterInvocationSecurityMetadataSource; } }
-
MyWebSecurityConfigurerAdapter:
import com.pingan.springsecurity.service.impl.MyUserDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; /** * SpringSecurity配置 * * @author JustryDeng * @date 2019/12/7 14:08 */ @Configuration @RequiredArgsConstructor public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { private final MyUserDetailsService myUserDetailsService; @Override public void configure(WebSecurity web) { /* * 對於那些沒必要進行保護的資源, 可以使用ignoring,使其跳過SpringSecurity * * 注:configure(HttpSecurity http)方法裏的permitAll();也有類似的效果, * 不過permitAll會走SpringSecurity,只是說無條件放行而已。 */ web.ignoring().antMatchers("/picture/**"); web.ignoring().antMatchers("/md/**"); // 開發時,可以將SpringSecurity的debug打開 web.debug(false); } /** * SpringSecurity提供有一些基本的頁面(如:login、logout等);如果覺得它提供的 * 基礎頁面難看,想使用自己的頁面的話,可以在此方法裏面進行相關配置。 */ @Override protected void configure(HttpSecurity http) throws Exception { // 設置登錄方式爲 表單登錄 http.formLogin(); /// 設置登錄方式爲 彈框登錄 /// http.httpBasic(); /// 自定義登錄頁 /// http.formLogin().loginPage("myLoginPae"); /// 自定義登出頁 /// http.logout().logoutUrl("myLogoutPae"); // 登出成功時,跳轉至此url http.logout().logoutSuccessUrl("/logout/success"); // 登錄成功時,跳轉至此url // 注意:如果未登錄,直接訪問 登錄失敗頁的話,會被DefaultLoginPageGeneratingFilter識別,並跳轉至登錄頁進行登錄 http.formLogin().successForwardUrl("/index"); // 登錄失敗時,跳轉至此url // 注意:如果未登錄,直接訪問 登錄失敗頁的話,會被DefaultLoginPageGeneratingFilter識別,並跳轉至登錄頁進行登錄 http.formLogin().failureUrl("/login/failed"); /// 當鑑權不通過,是 跳轉至此url http.exceptionHandling().accessDeniedPage("/403"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 配置 UserDetailsService, 用戶自定義查詢用戶的信息 auth.userDetailsService(myUserDetailsService); } /** * 自定義 加密器 * * 注:只需要將其註冊進入容器中即可,InitializeUserDetailsBeanManagerConfigurer類會從容器 * 拿去PasswordEncoder.class實現,作爲其加密器 * * @date 2019/12/21 17:59 */ @Bean public PasswordEncoder myPasswordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence rawPassword) { return rawPassword == null ? "" : rawPassword.toString(); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (rawPassword == null || rawPassword.length() == 0) { return false; } return rawPassword.equals(encodedPassword); } }; } }
-
MyUserDetailsService:
import com.pingan.springsecurity.mapper.DaoMapper; import com.pingan.springsecurity.model.ApiResource; import com.pingan.springsecurity.model.MyUserDetails; import com.pingan.springsecurity.model.Role; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; /** * 對{@link UserDetailsService#loadUserByUsername(String)}進行重寫 * * @author JustryDeng * @date 2019/12/14 10:20 */ @Service @RequiredArgsConstructor public class MyUserDetailsService implements UserDetailsService { private final DaoMapper mapper; /** * 根據賬號名, 查詢用戶信息 * * todo 用戶名不存在時,不處理的話,最終拋出InternalAuthenticationServiceException */ @Override public UserDetails loadUserByUsername(String accountNo) throws UsernameNotFoundException { // 查詢用戶基本信息 MyUserDetails myUserDetails = mapper.selectUserBasicInfoByAccountNo(accountNo); // 查詢用戶角色信息 List<Role> roleList = mapper.selectRolesByUserId(myUserDetails.getId()); // 查詢用戶權限信息(即:查詢用戶可訪問的資源) List<Integer> roleIdList = roleList.parallelStream().map(Role::getId).collect(Collectors.toList()); List<ApiResource> apiResources = mapper.selectApiResourcesByRoleIds(roleIdList); // 組裝信息並返回 myUserDetails.setRoles(roleList); myUserDetails.setAccessibleApis(apiResources); return myUserDetails; } }
Spring Security賬號密碼認證 + 自定義鑑權,簡單示例完畢 !
^_^ 如有不當之處,歡迎指正
^_^ 參考資料
《SpringSecurity5.2.1RELEASE源碼》
^_^ 測試代碼託管鏈接
https://github.com/JustryDeng…SpringSecurity…
^_^ 本文已經被收錄進《程序員成長筆記(六)》,筆者JustryDeng