spring-security权限控制详解
在本例中,主要講解spring-boot與spring-security的集成,實現方式為:
- 將用戶、權限、資源(url)采用數據庫存儲
- 自定義過濾器,代替原有的 FilterSecurityInterceptor
- 自定義實現 UserDetailsService、AccessDecisionManager和InvocationSecurityMetadataSourceService,并在配置文件進行相應的配置
GitHub 地址:https://github.com/fp2952/spring-boot-security-demo
用戶角色表(基于RBAC權限控制)
- 用戶表(base_user)
| ID | varchar | 32 |
| USER_NAME | varchar | 50 |
| USER_PASSWORD | varchar | 100 |
| NIKE_NAME | varchar | 50 |
| STATUS | int | 11 |
- 用戶角色表(base_user_role)
| ID | varchar | 32 |
| USER_ID | varchar | 32 |
| ROLE_ID | varchar | 32 |
- 角色表(base_role)
| ID | varchar | 32 |
| ROLE_CODE | varchar | 32 |
| ROLE_NAME | varchar | 64 |
- 角色菜單表(base_role_menu)
| ID | varchar | 32 |
| ROLE_ID | varchar | 32 |
| MENU_ID | varchar | 32 |
- 菜單表(base_menu)
| ID | varchar | 32 |
| MENU_URL | varchar | 120 |
| MENU_SEQ | varchar | 120 |
| MENU_PARENT_ID | varchar | 32 |
| MENU_NAME | varchar | 50 |
| MENU_ICON | varchar | 20 |
| MENU_ORDER | int | 11 |
| IS_LEAF | varchar | 20 |
實現主要配置類
實現AbstractAuthenticationProcessingFilter
用于用戶表單驗證,內部調用了authenticationManager完成認證,根據認證結果執行successfulAuthentication或者unsuccessfulAuthentication,無論成功失敗,一般的實現都是轉發或者重定向等處理。
@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//獲取表單中的用戶名和密碼String username = obtainUsername(request);String password = obtainPassword(request);if (username == null) {username = "";}if (password == null) {password = "";}username = username.trim();//組裝成username+password形式的tokenUsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//交給內部的AuthenticationManager去認證,并返回認證信息return this.getAuthenticationManager().authenticate(authRequest);}AuthenticationManager
AuthenticationManager是一個用來處理認證(Authentication)請求的接口。在其中只定義了一個方法authenticate(),該方法只接收一個代表認證請求的Authentication對象作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等信息的Authentication對象進行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在Spring Security中,AuthenticationManager的默認實現是ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的AuthenticationProvider列表,然后會依次使用每一個AuthenticationProvider進行認證,如果有一個AuthenticationProvider認證后的結果不為null,則表示該AuthenticationProvider已經認證成功,之后的AuthenticationProvider將不再繼續認證。然后直接以該AuthenticationProvider的認證結果作為ProviderManager的認證結果。如果所有的AuthenticationProvider的認證結果都為null,則表示認證失敗,將拋出一個ProviderNotFoundException。
校驗認證請求最常用的方法是根據請求的用戶名加載對應的UserDetails,然后比對UserDetails的密碼與認證請求的密碼是否一致,一致則表示認證通過。
Spring Security內部的DaoAuthenticationProvider就是使用的這種方式。其內部使用UserDetailsService來負責加載UserDetails。在認證成功以后會使用加載的UserDetails來封裝要返回的Authentication對象,加載的UserDetails對象是包含用戶權限等信息的。認證成功返回的Authentication對象將會保存在當前的SecurityContext中。
實現UserDetailsService
UserDetailsService只定義了一個方法 loadUserByUsername,根據用戶名可以查到用戶并返回的方法。
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {logger.debug("權限框架-加載用戶");List<GrantedAuthority> auths = new ArrayList<>();BaseUser baseUser = new BaseUser();baseUser.setUserName(username);baseUser = baseUserService.selectOne(baseUser);if (baseUser == null) {logger.debug("找不到該用戶 用戶名:{}", username);throw new UsernameNotFoundException("找不到該用戶!");}if(baseUser.getStatus()==2){logger.debug("用戶被禁用,無法登陸 用戶名:{}", username);throw new UsernameNotFoundException("用戶被禁用!");}List<BaseRole> roles = baseRoleService.selectRolesByUserId(baseUser.getId());if (roles != null) {//設置角色名稱for (BaseRole role : roles) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleCode());auths.add(authority);}}return new org.springframework.security.core.userdetails.User(baseUser.getUserName(), baseUser.getUserPassword(), true, true, true, true, auths);}實現AbstractSecurityInterceptor
訪問url時,會被AbstractSecurityInterceptor攔截器攔截,然后調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager鑒權。
public class CustomSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {private FilterInvocationSecurityMetadataSource securityMetadataSource;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);invoke(fi);}@Overridepublic void destroy() {}@Overridepublic Class<?> getSecureObjectClass() {return FilterInvocation.class;}@Overridepublic SecurityMetadataSource obtainSecurityMetadataSource() {return this.securityMetadataSource;}public void invoke(FilterInvocation fi) throws IOException {InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} catch (ServletException e) {super.afterInvocation(token, null);}}public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {return securityMetadataSource;}public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {this.securityMetadataSource = securityMetadataSource;} }FilterInvocationSecurityMetadataSource 獲取所需權限
@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {//獲取當前訪問urlString url = ((FilterInvocation) object).getRequestUrl();int firstQuestionMarkIndex = url.indexOf("?");if (firstQuestionMarkIndex != -1) {url = url.substring(0, firstQuestionMarkIndex);}List<ConfigAttribute> result = new ArrayList<>();try {//設置不攔截if (propertySourceBean.getProperty("security.ignoring") != null) {String[] paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");//判斷是否符合規則for (String path: paths) {String temp = StringUtil.clearSpace(path);if (matcher.match(temp, url)) {return SecurityConfig.createList("ROLE_ANONYMOUS");}}}//如果不是攔截列表里的, 默認需要ROLE_ANONYMOUS權限if (!isIntercept(url)) {return SecurityConfig.createList("ROLE_ANONYMOUS");}//查詢數據庫url匹配的菜單List<BaseMenu> menuList = baseMenuService.selectMenusByUrl(url);if (menuList != null && menuList.size() > 0) {for (BaseMenu menu : menuList) {//查詢擁有該菜單權限的角色列表List<BaseRole> roles = baseRoleService.selectRolesByMenuId(menu.getId());if (roles != null && roles.size() > 0) {for (BaseRole role : roles) {ConfigAttribute conf = new SecurityConfig(role.getRoleCode());result.add(conf);}}}}} catch (Exception e) {e.printStackTrace();}return result;}/*** 判斷是否需要過濾* @param url* @return*/public boolean isIntercept(String url) {String[] filterPaths = propertySourceBean.getProperty("security.intercept").toString().split(",");for (String filter: filterPaths) {if (matcher.match(StringUtil.clearSpace(filter), url) & !matcher.match(indexUrl, url)) {return true;}}return false;}AccessDecisionManager 鑒權
@Overridepublic void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {if (collection == null) {return;}for (ConfigAttribute configAttribute : collection) {String needRole = configAttribute.getAttribute();for (GrantedAuthority ga : authentication.getAuthorities()) {if (needRole.trim().equals(ga.getAuthority().trim()) || needRole.trim().equals("ROLE_ANONYMOUS")) {return;}}}throw new AccessDeniedException("無權限!");}配置 WebSecurityConfigurerAdapter
/*** spring-security配置*/ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {private final Logger logger = LoggerFactory.getLogger(this.getClass());@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate PropertySource propertySourceBean;@Overrideprotected void configure(HttpSecurity http) throws Exception {logger.debug("權限框架配置");String[] paths = null;//設置不攔截if (propertySourceBean.getProperty("security.ignoring") != null) {paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");paths = StringUtil.clearSpace(paths);}//設置過濾器http // 根據配置文件放行無需驗證的url.authorizeRequests().antMatchers(paths).permitAll().and().httpBasic()// 配置驗證異常處理.authenticationEntryPoint(getCustomLoginAuthEntryPoint())// 配置登陸過濾器.and().addFilterAt(getCustomLoginFilter(), UsernamePasswordAuthenticationFilter.class)// 配置 AbstractSecurityInterceptor.addFilterAt(getCustomSecurityInterceptor(), FilterSecurityInterceptor.class)// 登出成功處理.logout().logoutSuccessHandler(getCustomLogoutSuccessHandler())// 關閉csrf.and().csrf().disable()// 其他所有請求都需要驗證.authorizeRequests().anyRequest().authenticated()// 配置登陸url, 登陸頁面并無需驗證.and().formLogin().loginProcessingUrl("/login").loginPage("/login.ftl").permitAll()// 登出.and().logout().logoutUrl("/logout").permitAll();logger.debug("配置忽略驗證url");}@Autowired@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(getDaoAuthenticationProvider());}/*** spring security 配置* @return*/@Beanpublic CustomLoginAuthEntryPoint getCustomLoginAuthEntryPoint() {return new CustomLoginAuthEntryPoint();}/*** 用戶驗證* @return*/@Beanpublic DaoAuthenticationProvider getDaoAuthenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setHideUserNotFoundExceptions(false);provider.setPasswordEncoder(new BCryptPasswordEncoder());return provider;}/*** 登陸* @return*/@Beanpublic CustomLoginFilter getCustomLoginFilter() {CustomLoginFilter filter = new CustomLoginFilter();try {filter.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}filter.setAuthenticationSuccessHandler(getCustomLoginAuthSuccessHandler());filter.setAuthenticationFailureHandler(new CustomLoginAuthFailureHandler());return filter;}@Beanpublic CustomLoginAuthSuccessHandler getCustomLoginAuthSuccessHandler() {CustomLoginAuthSuccessHandler handler = new CustomLoginAuthSuccessHandler();if (propertySourceBean.getProperty("security.successUrl")!=null){handler.setAuthSuccessUrl(propertySourceBean.getProperty("security.successUrl").toString());}return handler;}/*** 登出* @return*/@Beanpublic CustomLogoutSuccessHandler getCustomLogoutSuccessHandler() {CustomLogoutSuccessHandler handler = new CustomLogoutSuccessHandler();if (propertySourceBean.getProperty("security.logoutSuccessUrl")!=null){handler.setLoginUrl(propertySourceBean.getProperty("security.logoutSuccessUrl").toString());}return handler;}/*** 過濾器* @return*/@Beanpublic CustomSecurityInterceptor getCustomSecurityInterceptor() {CustomSecurityInterceptor interceptor = new CustomSecurityInterceptor();interceptor.setAccessDecisionManager(new CustomAccessDecisionManager());interceptor.setSecurityMetadataSource(getCustomMetadataSourceService());try {interceptor.setAuthenticationManager(this.authenticationManagerBean());} catch (Exception e) {e.printStackTrace();}return interceptor;}@Beanpublic CustomMetadataSourceService getCustomMetadataSourceService() {CustomMetadataSourceService sourceService = new CustomMetadataSourceService();if (propertySourceBean.getProperty("security.successUrl")!=null){sourceService.setIndexUrl(propertySourceBean.getProperty("security.successUrl").toString());}return sourceService;} }轉載于:https://www.cnblogs.com/fp2952/p/8933107.html
總結
以上是生活随笔為你收集整理的spring-security权限控制详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [洛谷P2124] 奶牛美容
- 下一篇: java中的类型转换