解析spring-security權(quán)限控制和校驗的問題
在我們項目中經(jīng)常會涉及到權(quán)限管理,特別是一些企業(yè)級后臺應(yīng)用中,那權(quán)限管理是必不可少的。這個時候就涉及到技術(shù)選型的問題。在我以前項目中也沒用到什么權(quán)限框架,就全部到一個spring mvc攔截器中去校驗權(quán)限,當(dāng)然,對需求比較少,小型的項目這也不失一個好的實現(xiàn)(實現(xiàn)簡單,功能單一),但是對于一些比較大的應(yīng)用,權(quán)限認(rèn)證,session管理要求比較高的項目,如果再使用mvc攔截器,那就得不償失了(需要自己去實現(xiàn)很多的代碼)。 現(xiàn)在比較流行的權(quán)限校驗框架有spring-security 和 apache-shiro。鑒于我們一直在使用spring全家桶,那我的項目中當(dāng)然首選spring-security。下面我我所認(rèn)識的spring-security來一步一步的看怎么實現(xiàn)。這里我拋磚引玉,歡迎大家指正。
我的完整代碼在我的github中 我的github ,歡迎大家留言討論!!
一、spring-security是什么?Spring Security 是 Spring 家族中的一個安全管理框架,類似的安全框架還有apache-shiro。shiro以使用簡單,功能強(qiáng)大而著稱,本篇我們只討論Spring Security,shiro就不再鋪開討論了。 以前我們在使用springMVC與Security 結(jié)合的時候,那一堆堆配置文件,長篇大論的xml看的人頭大,幸好,現(xiàn)在有了springboot,可以基于java config的方式配置,實現(xiàn)零配置,而且又兼有springboot的約定大于配置的前提,我們項目中的配置文件或需要配置的代碼大大減少了。
二、spring-security能為我們做什么?spring-security最主要的功能包含:1、認(rèn)證(就是,你是誰)2、授權(quán)(就是,你能干什么)3、攻擊防護(hù) (防止偽造身份)這三點(diǎn)其實就是我們在應(yīng)用中常用到的。現(xiàn)在有了spring-security框架,就使得我們代碼實現(xiàn)起來非常簡單。
下面就來跟著我一步一步來看,怎么使用它
三、使用步驟1.maven依賴<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
版本號就跟著springboot版本走
2.application.properties文件server.port=8080server.servlet.context-path=/demospring.main.allow-bean-definition-overriding=truespring.profiles.active=dev
這個時候,我們啟動項目,就會在控制臺看到這一行信息
紅色框出來的就是spring-security為你自動分配的賬號為 user 的密碼。
3.訪問接口你現(xiàn)在如果想要訪問系統(tǒng)里面的接口,那必須要經(jīng)過這個權(quán)限驗證。隨便打開一個接口都會跳轉(zhuǎn)到內(nèi)置的一個登陸頁面中
我們看到,他跳轉(zhuǎn)到一個地址為login的頁面去了。這個時候我們輸入用戶名 user,密碼為控制臺打印出來的一串字符, 點(diǎn)擊按鈕 sign in 頁面正常跳轉(zhuǎn)到接口返回的數(shù)據(jù)。我們發(fā)現(xiàn),我們沒有寫一行代碼,僅僅是在pom里面依賴了spring-boot-starter-security,框架就自動為我們做了最簡單的驗證功能,驚不驚喜意不意外。當(dāng)然僅僅這么點(diǎn)當(dāng)然不能滿足我們項目的要求,不急,聽我一步一步慢慢道來。
4.功能進(jìn)階下面我們以最常見的企業(yè)級應(yīng)用管理后臺的權(quán)限為例我們需要提出幾個問題1、用戶的賬號,密碼保存在數(shù)據(jù)庫中,登錄的時候驗證2、用戶登錄成功后,每訪問一個地址,后臺都要判斷該用戶有沒有這個菜單的權(quán)限,有,則放行;沒有,則,拒絕訪問。3、系統(tǒng)中的一些靜態(tài)資源則直接放行,不需要經(jīng)過權(quán)限校驗4、系統(tǒng)中可能存在三種類型的資源地址 ①:所有用戶都能訪問的地址(如:登錄頁面) ②:只要登錄,就可以訪問的地址(如:首頁) ③:需要授權(quán)才能訪問的地址
針對上面提出的幾個問題,我們設(shè)計最常用的權(quán)限表結(jié)構(gòu)模型
sys_user(用戶表:保存用戶的基本信息,登錄名,密碼等等)sys_role(角色表:保存了創(chuàng)建的角色)sys_menu(菜單表:保存了系統(tǒng)可訪問的資源(包含菜單url等))sys_user_role(用戶關(guān)聯(lián)的角色:一個用戶可以關(guān)聯(lián)多個角色,最后用戶的權(quán)限就是這多個角色權(quán)限的并集)sys_role_menu(角色關(guān)聯(lián)的菜單:一個角色可以關(guān)聯(lián)多個菜單) 5.spring-security主配置類
spring-security的主配置類,就需要我們自定義一個類繼承 WebSecurityConfigurerAdapter 并且實現(xiàn)里面方法,如下:
package com.hp.springboot.admin.security;import java.util.ArrayList;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;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.method.configuration.EnableGlobalMethodSecurity;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.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import com.hp.springboot.admin.constant.AdminConstants;import com.hp.springboot.admin.interceptor.UrlAuthenticationInterceptor;import com.hp.springboot.admin.security.handler.AdminAccessDeniedHandler;import com.hp.springboot.admin.security.handler.AdminAuthenticationEntryPoint;import com.hp.springboot.admin.security.handler.AdminAuthenticationFailureHandler;import com.hp.springboot.admin.security.handler.AdminAuthenticationSuccessHandler;import com.hp.springboot.common.configuration.CommonWebMvcConfigurer;/** 1. 描述:security全局配置 2. 作者:黃平 3. 時間:2021年1月11日 */@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟Security注解的功能,如果你項目中不用Security的注解(hasRole,hasAuthority等),則可以不加該注解public class AdminWebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Autowiredprivate CommonWebMvcConfigurer commonWebMvcConfigurer;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 設(shè)置超級管理員auth.inMemoryAuthentication().withUser(AdminConstants.ADMIN_USER);// 其余賬號通過數(shù)據(jù)庫查詢驗證auth.userDetailsService(adminUserDetailsService()).passwordEncoder(passwordEncoder());}@Overridepublic void configure(WebSecurity web) throws Exception {// 靜態(tài)資源String[] ignoreArray = commonWebMvcConfigurer.getMergeStaticPatternArray();// 設(shè)置系統(tǒng)的靜態(tài)資源。靜態(tài)資源不會走權(quán)限框架web.ignoring().antMatchers(ignoreArray);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 驗證碼過濾器http.addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class);// 第一層免過濾列表// 就是所有人都可以訪問的地址。區(qū)別于靜態(tài)資源List<String> noFilterList = new ArrayList<>();noFilterList.add(AdminConstants.ACCESS_DENIED_URL);noFilterList.add(AdminConstants.VERIFY_CODE_URL);noFilterList.addAll(commonWebMvcConfigurer.getMergeFirstNoFilterList());http.formLogin()// 登錄頁面使用form提交的方式.usernameParameter('username').passwordParameter('password')// 設(shè)置登錄頁面用戶名和密碼的input對應(yīng)name值(其實默認(rèn)值就是username,password,所以這里可以不用設(shè)置).loginPage(AdminConstants.LOGIN_PAGE_URL)// 設(shè)置登錄頁面的地址.loginProcessingUrl(AdminConstants.LOGIN_PROCESSING_URL)// 登錄頁面輸入用戶名密碼后提交的地址.successHandler(adminAuthenticationSuccessHandler())// 登錄成功處理.failureHandler(adminAuthenticationFailureHandler())// 登錄失敗的處理.permitAll()// 以上url全部放行,不需要校驗權(quán)限.and()// 注銷相關(guān)配置.logout().logoutUrl(AdminConstants.LOGOUT_URL)// 注銷地址.logoutSuccessUrl(AdminConstants.LOGIN_PAGE_URL)// 注銷成功后跳轉(zhuǎn)地址(這里就是跳轉(zhuǎn)到登錄頁面).permitAll()// 以上地址全部放行.and().authorizeRequests()// 第一層免過濾列表// 不需要登錄,就可以直接訪問的地址.antMatchers(noFilterList.toArray(new String[noFilterList.size()])).permitAll() // 全部放行// 其他都需要權(quán)限控制// 這里使用.anyRequest().access方法,把權(quán)限驗證交給指定的一個方法去處理。// 這里 hasPermission接受兩個參數(shù)request和authentication.anyRequest().access('@UrlAuthenticationInterceptor.hasPermission(request, authentication)')// 異常處理.and().exceptionHandling().accessDeniedHandler(new AdminAccessDeniedHandler())// 登錄用戶訪問無權(quán)限的資源.authenticationEntryPoint(new AdminAuthenticationEntryPoint())// 匿名用戶訪問無權(quán)限的資源.and().csrf().disable()// 禁用csrf// session管理.sessionManagement().invalidSessionUrl(AdminConstants.LOGIN_PAGE_URL)// session失效后,跳轉(zhuǎn)的地址.maximumSessions(1)// 同一個賬號最大允許同時在線數(shù);}/** * @Title: passwordEncoder * @Description: 加密方式 * @return */@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }/** * @Title: adminUserDetailsService * @Description: 用戶信息 * @return */@Beanpublic UserDetailsService adminUserDetailsService() {return new AdminUserDetailsService();}/** * d * @Title: adminAuthenticationFailureHandler * @Description: 登錄異常處理 * @return */@Beanpublic AdminAuthenticationFailureHandler adminAuthenticationFailureHandler() {return new AdminAuthenticationFailureHandler();}/** * @Title: adminAuthenticationSuccessHandler * @Description: 登錄成功后的處理 * @return */@Beanpublic AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler() {return new AdminAuthenticationSuccessHandler();}/** * @Title: urlAuthenticationInterceptor * @Description: 查詢權(quán)限攔截器 * @return */@Bean('UrlAuthenticationInterceptor')public UrlAuthenticationInterceptor urlAuthenticationInterceptor() {return new UrlAuthenticationInterceptor();}}
解讀一下這個類:
類繼承WebSecurityConfigurerAdapter 說明是一個spring-Security配置類注解 @Configuration 說明是一個springboot的配置類注解 @EnableGlobalMethodSecurity 不是必須。開啟注解用的第一個 configure 方法,設(shè)置登錄的用戶和賬號驗證方法 這里設(shè)置了兩種方式,一個是內(nèi)置的admin賬號,一個是通過數(shù)據(jù)庫驗證賬號 這樣設(shè)置有個好處,就是我們在后臺的用戶管理頁面里面是看不到admin賬號的,這樣就不會存在把所有用戶都刪除了,就登錄不了系統(tǒng)的bug(好多年前做系統(tǒng)的時候,一個測試人員一上來就打開用戶管理菜單,然后把所有用戶都刪除,再退出。然后就登錄不了系統(tǒng)了,隨即提了一個bug。只能手動插入數(shù)據(jù)到數(shù)據(jù)庫才行,當(dāng)時我看的一臉懵逼,還能這樣操作???)。現(xiàn)在有了這樣設(shè)置,就保證admin用戶永遠(yuǎn)不可能被刪除,也就不存在上面提到的bug了。第二個configure方法。這個方法是設(shè)置一些靜態(tài)資源的。可以在這里設(shè)置系統(tǒng)所有的靜態(tài)資源第三個configure方法。這個是這個類中最重要的配置。里面設(shè)置了登錄方式、url過濾規(guī)則、權(quán)限校驗規(guī)則、成功處理、失敗處理、session管理、登出處理等等。這里是鏈?zhǔn)降恼{(diào)用方式,可以把需要的都在里面配置
這里有幾個特別要說明的:1、我們項目中保存到session中的用戶對象一般是我們項目中自定義的一個類(我這里是SysUserResponseBO),在項目中我們用 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 這個方法獲取當(dāng)前登錄用戶信息時,如果是admin用戶,則返回的對象是org.springframework.security.core.userdetails.User對象2、密碼需要加密,保存在數(shù)據(jù)庫里面的密碼也是加密方式,不允許直接保存明文(這個也是規(guī)范)3、第三個 configure 方法中,我們使用了.anyRequest().access('@UrlAuthenticationInterceptor.hasPermission(request, authentication)')交給這個方法去驗證。驗證的方法有很多,我們也可以這樣去寫
.anyRequest().authenticated().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O object) {// 權(quán)限查詢器// 設(shè)置你有哪些權(quán)限object.setSecurityMetadataSource(null);// 權(quán)限決策器// 判斷你有沒有權(quán)限訪問當(dāng)前的urlobject.setAccessDecisionManager(null);return object;}})
這里之所以沒有用.antMatchers('/XXX').hasRole(“ROLET_XXX”),是因為,一般項目中的權(quán)限都是動態(tài)的,所有的資源菜單都是可配置的,在這里是無法寫死的。當(dāng)然這個要根據(jù)實際項目需求來做。總之配置很靈活,可以隨意組合。
3、驗證碼過濾器那邊ValidateCodeFilter一定不能交給spring bean去管理,不然這個過濾器會執(zhí)行兩遍,只能直接new 出來。
AdminUserDetailsService類 該類是用來在登錄的時候,進(jìn)行登錄校驗的。也就是校驗?zāi)愕馁~號密碼是否正確(其實這里只根據(jù)賬號查詢,密碼的驗證是框架里面自帶的)。來看下這個類的實現(xiàn)
package com.hp.springboot.admin.security;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.security.core.userdetails.UsernameNotFoundException;import com.hp.springboot.admin.convert.SysUserConvert;import com.hp.springboot.admin.dal.ISysUserDAO;import com.hp.springboot.admin.dal.model.SysUser;import com.hp.springboot.admin.model.response.SysUserResponseBO;import com.hp.springboot.database.bean.SQLBuilders;import com.hp.springboot.database.bean.SQLWhere;/** * 描述:Security需要的操作用戶的接口實現(xiàn) * 執(zhí)行登錄,構(gòu)建Authentication對象必須的信息 * 如果用戶不存在,則拋出UsernameNotFoundException異常 * 作者:黃平 * 時間:2021年1月12日 */public class AdminUserDetailsService implements UserDetailsService {private static Logger log = LoggerFactory.getLogger(AdminUserDetailsService.class);@Autowiredprivate ISysUserDAO sysUserDAO;/** * 執(zhí)行登錄,構(gòu)建Authentication對象必須的信息, */@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {log.info('loadUserByUsername with username={}', username);//根據(jù)登錄名,查詢用戶SysUser user = sysUserDAO.selectOne(SQLBuilders.create().withWhere(SQLWhere.builder().eq('login_name', username).build()));if (user == null) {log.warn('loadUserByUsername with user is not exists. with username={}', username);throw new UsernameNotFoundException('用戶不存在');}// 對象裝換,轉(zhuǎn)換成SysUserResponseBO對象SysUserResponseBO resp = SysUserConvert.dal2BOResponse(user);return resp;}}
這個里面很簡單,我們的類實現(xiàn) UserDetailsService 這個接口,并且實現(xiàn)一下loadUserByUsername這個方法。就是根據(jù)登錄名,查詢用戶的功能。
這里有必須要主要的:我們返回值是UserDetails,所以SysUserResponseBO必須要實現(xiàn)UserDetails這個接口UserDetails里面有好幾個必須實現(xiàn)的方法,基本上看方法名就可以猜到是干什么用的,其中最重要的的一個方法
/** * 獲取該用戶的角色 */public Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}
這個就是獲取當(dāng)前用戶所擁有的權(quán)限。這個需要根據(jù)用戶所擁有的角色去獲取。這個在上面使用 withObjectPostProcessor 這種方式校驗的時候是必須要的,但是我這里.anyRequest().access()方法,在這里校驗,并沒有使用到這個屬性,所以這個也可以直接return null.
第二個configure方法。這里面定義了靜態(tài)資源,這個跟springMVC的靜態(tài)資源差不多。重點(diǎn)來說下第三個configure方法 ①、如果需要,那就加上一個過濾器增加圖形驗證碼校驗 ②、登錄成功后處理AdminAuthenticationSuccessHandler這個類。該類實現(xiàn)了AuthenticationSuccessHandler接口,必須實現(xiàn)一個方法,直接上代碼
@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {response.setContentType(ContentTypeConstant.APPLICATION_JSON_UTF8);// 獲取session對象HttpSession session = request.getSession();//設(shè)置登錄用戶sessionsetUserSession(session);//查詢用戶的菜單和按鈕setUserMenu(session);//session中獲取當(dāng)前登錄的用戶SysUserResponseBO user = SecuritySessionUtil.getSessionData();// 更新最近登錄時間sysUserService.updateLastLoginTime(user.getId());//項目名稱session.setAttribute('projectName', projectName);// 注銷地址session.setAttribute('logoutUrl', AdminConstants.LOGOUT_URL);// 返回json格式數(shù)據(jù)Response<Object> resp = Response.success();try (PrintWriter out = response.getWriter()) {out.write(resp.toString());out.flush();}}
基本上看注釋也就了解每一步的意義。我們代碼中無需寫登錄的controller,因為這個方法框架已經(jīng)根據(jù)你配置的loginProcessingUrl給你生成好了。這個是用戶輸入用戶名密碼后,點(diǎn)擊登錄按鈕后執(zhí)行的操作。能夠進(jìn)入這個方法,那說明用戶輸入的用戶名和密碼是正確的,后續(xù)只要保存用戶的信息,查詢用戶權(quán)限等操作。 ③、登錄失敗處理。AdminAuthenticationFailureHandler。登錄失敗后交給這個類去處理,看下代碼:
package com.hp.springboot.admin.security.handler;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.security.authentication.AccountExpiredException;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.authentication.CredentialsExpiredException;import org.springframework.security.authentication.DisabledException;import org.springframework.security.authentication.InsufficientAuthenticationException;import org.springframework.security.authentication.LockedException;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import com.hp.springboot.admin.exception.ValidateCodeException;import com.hp.springboot.common.bean.Response;import com.hp.springboot.common.constant.ContentTypeConstant;/** * 描述:登錄失敗處理 作者:黃平 時間:2021年1月15日 */public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {private static Logger log = LoggerFactory.getLogger(AdminAuthenticationFailureHandler.class);@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {log.warn('login error with exception is {}', exception.getMessage());response.setContentType(ContentTypeConstant.APPLICATION_JSON_UTF8);String message = '';if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) {message = '賬戶名或者密碼輸入錯誤!';} else if (exception instanceof LockedException) {message = '賬戶被鎖定,請聯(lián)系管理員!';} else if (exception instanceof CredentialsExpiredException) {message = '密碼過期,請聯(lián)系管理員!';} else if (exception instanceof AccountExpiredException) {message = '賬戶過期,請聯(lián)系管理員!';} else if (exception instanceof DisabledException) {message = '賬戶被禁用,請聯(lián)系管理員!';} else if (exception instanceof ValidateCodeException) {// 圖形驗證碼輸入錯誤message = exception.getMessage();} else if (exception instanceof InsufficientAuthenticationException) {message = exception.getMessage();} else {message = '登錄失敗!';}// 返回json格式數(shù)據(jù)Response<Object> resp = Response.error(message);try (PrintWriter out = response.getWriter()) {out.write(resp.toString());out.flush();}}}
看代碼也基本上看出來每一步的作用。有用戶名密碼錯誤,有用戶被禁用,有過期,鎖定等等。我這里前臺都是ajax請求,所以這個也是返回json格式,如果你不需要json格式,那可以按照你的要求返回指定的格式。 ④、注銷相關(guān)。注銷接口也不需要我們在controller里面寫,框架會自動根據(jù)你的logoutUrl配置生成注銷地址。也可以自定義一個logoutSuccessHandler去在注銷后執(zhí)行。 ⑤、權(quán)限校驗。當(dāng)訪問一個除開第一層免過濾列表里面的url的地址時,都會需要權(quán)限校驗,就都會走到UrlAuthenticationInterceptor.hasPermission(request, authentication)這個方法里面去,這個里面可以根據(jù)你的項目的實際邏輯去校驗。 ⑥、異常處理。框架里面處理異常有好多種,這里常用的accessDeniedHandler(登錄用戶訪問無權(quán)限的資源)、authenticationEntryPoint(匿名用戶訪問無權(quán)限的資源)這些都按照項目的實際需求去寫異常處理。 ⑦、session管理。security框架里面對session管理非常多,可以按照鏈?zhǔn)秸{(diào)用的方式打開看看。我這里使用了invalidSessionUrl來指定session無效后跳轉(zhuǎn)到的地址,maximumSessions同一個賬號最多同時在線數(shù)。
好了,這樣一個最基本的權(quán)限控制框架就完成了。其實我這里只使用了security的一些皮毛而且,他里面集成了非常復(fù)雜而又強(qiáng)大的功能,這個需要我們一點(diǎn)一點(diǎn)去發(fā)掘他。
總結(jié)
以上是我在項目中使用的一些總結(jié),完整的代碼在我的github中 我的github ,歡迎大家留言討論!!
到此這篇關(guān)于spring-security權(quán)限控制和校驗的文章就介紹到這了,更多相關(guān)spring-security權(quán)限控制校驗內(nèi)容請搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
