前言
在我們項目中經常會涉及到權限管理,特別是一些企業級后臺應用中,那權限管理是必不可少的。這個時候就涉及到技術選型的問題。在我以前項目中也沒用到什么權限框架,就全部到一個spring mvc攔截器中去校驗權限,當然,對需求比較少,小型的項目這也不失一個好的實現(實現簡單,功能單一),但是對于一些比較大的應用,權限認證,session管理要求比較高的項目,如果再使用mvc攔截器,那就得不償失了(需要自己去實現很多的代碼)。
現在比較流行的權限校驗框架有spring-security 和 apache-shiro。鑒于我們一直在使用spring全家桶,那我的項目中當然首選spring-security。下面我我所認識的spring-security來一步一步的看怎么實現。這里我拋磚引玉,歡迎大家指正。
我的完整代碼在我的github中 我的github ,歡迎大家留言討論!!
一、spring-security是什么?
spring security 是 spring 家族中的一個安全管理框架,類似的安全框架還有apache-shiro。shiro以使用簡單,功能強大而著稱,本篇我們只討論spring security,shiro就不再鋪開討論了。
以前我們在使用springmvc與security 結合的時候,那一堆堆配置文件,長篇大論的xml看的人頭大,幸好,現在有了springboot,可以基于java config的方式配置,實現零配置,而且又兼有springboot的約定大于配置的前提,我們項目中的配置文件或需要配置的代碼大大減少了。
二、spring-security能為我們做什么?
spring-security最主要的功能包含:
1、認證(就是,你是誰)
2、授權(就是,你能干什么)
3、攻擊防護 (防止偽造身份)
這三點其實就是我們在應用中常用到的。現在有了spring-security框架,就使得我們代碼實現起來非常簡單。
下面就來跟著我一步一步來看,怎么使用它
三、使用步驟
1.maven依賴
1
2
3
4
5
6
7
8
|
<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文件
1
2
3
4
5
|
server.port= 8080 server.servlet.context-path=/demo spring.main.allow-bean-definition-overriding= true spring.profiles.active=dev |
這個時候,我們啟動項目,就會在控制臺看到這一行信息
紅色框出來的就是spring-security為你自動分配的賬號為 user 的密碼。
3.訪問接口
你現在如果想要訪問系統里面的接口,那必須要經過這個權限驗證。
隨便打開一個接口都會跳轉到內置的一個登陸頁面中
我們看到,他跳轉到一個地址為login的頁面去了。這個時候我們輸入用戶名 user,密碼為控制臺打印出來的一串字符, 點擊按鈕 sign in 頁面正常跳轉到接口返回的數據。
我們發現,我們沒有寫一行代碼,僅僅是在pom里面依賴了spring-boot-starter-security,框架就自動為我們做了最簡單的驗證功能,驚不驚喜意不意外。當然僅僅這么點當然不能滿足我們項目的要求,不急,聽我一步一步慢慢道來。
4.功能進階
下面我們以最常見的企業級應用管理后臺的權限為例
我們需要提出幾個問題
1、用戶的賬號,密碼保存在數據庫中,登錄的時候驗證
2、用戶登錄成功后,每訪問一個地址,后臺都要判斷該用戶有沒有這個菜單的權限,有,則放行;沒有,則,拒絕訪問。
3、系統中的一些靜態資源則直接放行,不需要經過權限校驗
4、系統中可能存在三種類型的資源地址
①:所有用戶都能訪問的地址(如:登錄頁面)
②:只要登錄,就可以訪問的地址(如:首頁)
③:需要授權才能訪問的地址
針對上面提出的幾個問題,我們設計最常用的權限表結構模型
sys_user(用戶表:保存用戶的基本信息,登錄名,密碼等等)sys_role(角色表:保存了創建的角色)sys_menu(菜單表:保存了系統可訪問的資源(包含菜單url等))sys_user_role(用戶關聯的角色:一個用戶可以關聯多個角色,最后用戶的權限就是這多個角色權限的并集)sys_role_menu(角色關聯的菜單:一個角色可以關聯多個菜單) 5.spring-security主配置類
spring-security的主配置類,就需要我們自定義一個類繼承 websecurityconfigureradapter 并且實現里面方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
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 { @autowired private commonwebmvcconfigurer commonwebmvcconfigurer; @override protected void configure(authenticationmanagerbuilder auth) throws exception { // 設置超級管理員 auth.inmemoryauthentication() .withuser(adminconstants.admin_user) ; // 其余賬號通過數據庫查詢驗證 auth.userdetailsservice(adminuserdetailsservice()).passwordencoder(passwordencoder()); } @override public void configure(websecurity web) throws exception { // 靜態資源 string[] ignorearray = commonwebmvcconfigurer.getmergestaticpatternarray(); // 設置系統的靜態資源。靜態資源不會走權限框架 web.ignoring().antmatchers(ignorearray); } @override protected void configure(httpsecurity http) throws exception { // 驗證碼過濾器 http.addfilterbefore( new validatecodefilter(), usernamepasswordauthenticationfilter. class ); // 第一層免過濾列表 // 就是所有人都可以訪問的地址。區別于靜態資源 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" ) // 設置登錄頁面用戶名和密碼的input對應name值(其實默認值就是username,password,所以這里可以不用設置) .loginpage(adminconstants.login_page_url) // 設置登錄頁面的地址 .loginprocessingurl(adminconstants.login_processing_url) // 登錄頁面輸入用戶名密碼后提交的地址 .successhandler(adminauthenticationsuccesshandler()) // 登錄成功處理 .failurehandler(adminauthenticationfailurehandler()) // 登錄失敗的處理 .permitall() // 以上url全部放行,不需要校驗權限 .and() // 注銷相關配置 .logout() .logouturl(adminconstants.logout_url) // 注銷地址 .logoutsuccessurl(adminconstants.login_page_url) // 注銷成功后跳轉地址(這里就是跳轉到登錄頁面) .permitall() // 以上地址全部放行 .and().authorizerequests() // 第一層免過濾列表 // 不需要登錄,就可以直接訪問的地址 .antmatchers(nofilterlist.toarray( new string[nofilterlist.size()])).permitall() // 全部放行 // 其他都需要權限控制 // 這里使用.anyrequest().access方法,把權限驗證交給指定的一個方法去處理。 // 這里 haspermission接受兩個參數request和authentication .anyrequest().access( "@urlauthenticationinterceptor.haspermission(request, authentication)" ) // 異常處理 .and().exceptionhandling() .accessdeniedhandler( new adminaccessdeniedhandler()) // 登錄用戶訪問無權限的資源 .authenticationentrypoint( new adminauthenticationentrypoint()) // 匿名用戶訪問無權限的資源 .and().csrf().disable() // 禁用csrf // session管理 .sessionmanagement() .invalidsessionurl(adminconstants.login_page_url) // session失效后,跳轉的地址 .maximumsessions( 1 ) // 同一個賬號最大允許同時在線數 ; } /** * @title: passwordencoder * @description: 加密方式 * @return */ @bean public passwordencoder passwordencoder() { return new bcryptpasswordencoder(); } /** * @title: adminuserdetailsservice * @description: 用戶信息 * @return */ @bean public userdetailsservice adminuserdetailsservice() { return new adminuserdetailsservice(); } /** * d * @title: adminauthenticationfailurehandler * @description: 登錄異常處理 * @return */ @bean public adminauthenticationfailurehandler adminauthenticationfailurehandler() { return new adminauthenticationfailurehandler(); } /** * @title: adminauthenticationsuccesshandler * @description: 登錄成功后的處理 * @return */ @bean public adminauthenticationsuccesshandler adminauthenticationsuccesshandler() { return new adminauthenticationsuccesshandler(); } /** * @title: urlauthenticationinterceptor * @description: 查詢權限攔截器 * @return */ @bean ( "urlauthenticationinterceptor" ) public urlauthenticationinterceptor urlauthenticationinterceptor() { return new urlauthenticationinterceptor(); } } |
解讀一下這個類:
類繼承websecurityconfigureradapter 說明是一個spring-security配置類注解 @configuration 說明是一個springboot的配置類注解 @enableglobalmethodsecurity 不是必須。開啟注解用的第一個 configure 方法,設置登錄的用戶和賬號驗證方法
這里設置了兩種方式,一個是內置的admin賬號,一個是通過數據庫驗證賬號
這樣設置有個好處,就是我們在后臺的用戶管理頁面里面是看不到admin賬號的,這樣就不會存在把所有用戶都刪除了,就登錄不了系統的bug(好多年前做系統的時候,一個測試人員一上來就打開用戶管理菜單,然后把所有用戶都刪除,再退出。然后就登錄不了系統了,隨即提了一個bug。只能手動插入數據到數據庫才行,當時我看的一臉懵逼,還能這樣操作???)。現在有了這樣設置,就保證admin用戶永遠不可能被刪除,也就不存在上面提到的bug了。第二個configure方法。這個方法是設置一些靜態資源的。可以在這里設置系統所有的靜態資源第三個configure方法。這個是這個類中最重要的配置。里面設置了登錄方式、url過濾規則、權限校驗規則、成功處理、失敗處理、session管理、登出處理等等。這里是鏈式的調用方式,可以把需要的都在里面配置
這里有幾個特別要說明的:
1、我們項目中保存到session中的用戶對象一般是我們項目中自定義的一個類(我這里是sysuserresponsebo),在項目中我們用 securitycontextholder.getcontext().getauthentication().getprincipal() 這個方法獲取當前登錄用戶信息時,如果是admin用戶,則返回的對象是org.springframework.security.core.userdetails.user對象
2、密碼需要加密,保存在數據庫里面的密碼也是加密方式,不允許直接保存明文(這個也是規范)
3、第三個 configure 方法中,我們使用了.anyrequest().access("@urlauthenticationinterceptor.haspermission(request, authentication)")交給這個方法去驗證。驗證的方法有很多,我們也可以這樣去寫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
.anyrequest().authenticated().withobjectpostprocessor( new objectpostprocessor<filtersecurityinterceptor>() { @override public <o extends filtersecurityinterceptor> o postprocess(o object) { // 權限查詢器 // 設置你有哪些權限 object.setsecuritymetadatasource( null ); // 權限決策器 // 判斷你有沒有權限訪問當前的url object.setaccessdecisionmanager( null ); return object; } }) |
這里之所以沒有用.antmatchers("/xxx").hasrole(“rolet_xxx”),是因為,一般項目中的權限都是動態的,所有的資源菜單都是可配置的,在這里是無法寫死的。當然這個要根據實際項目需求來做。總之配置很靈活,可以隨意組合。
3、驗證碼過濾器那邊validatecodefilter一定不能交給spring bean去管理,不然這個過濾器會執行兩遍,只能直接new 出來。
adminuserdetailsservice類
該類是用來在登錄的時候,進行登錄校驗的。也就是校驗你的賬號密碼是否正確(其實這里只根據賬號查詢,密碼的驗證是框架里面自帶的)。來看下這個類的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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需要的操作用戶的接口實現 * 執行登錄,構建authentication對象必須的信息 * 如果用戶不存在,則拋出usernamenotfoundexception異常 * 作者:黃平 * 時間:2021年1月12日 */ public class adminuserdetailsservice implements userdetailsservice { private static logger log = loggerfactory.getlogger(adminuserdetailsservice. class ); @autowired private isysuserdao sysuserdao; /** * 執行登錄,構建authentication對象必須的信息, */ @override public userdetails loaduserbyusername(string username) throws usernamenotfoundexception { log.info( "loaduserbyusername with username={}" , username); //根據登錄名,查詢用戶 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( "用戶不存在" ); } // 對象裝換,轉換成sysuserresponsebo對象 sysuserresponsebo resp = sysuserconvert.dal2boresponse(user); return resp; } } |
這個里面很簡單,我們的類實現 userdetailsservice 這個接口,并且實現一下loaduserbyusername這個方法。就是根據登錄名,查詢用戶的功能。
這里有必須要主要的:
我們返回值是userdetails,所以sysuserresponsebo必須要實現userdetails這個接口
userdetails里面有好幾個必須實現的方法,基本上看方法名就可以猜到是干什么用的,其中最重要的的一個方法
1
2
3
4
5
6
|
/** * 獲取該用戶的角色 */ public collection<? extends grantedauthority> getauthorities() { return this .authorities; } |
這個就是獲取當前用戶所擁有的權限。這個需要根據用戶所擁有的角色去獲取。這個在上面使用 withobjectpostprocessor 這種方式校驗的時候是必須要的,但是我這里.anyrequest().access()方法,在這里校驗,并沒有使用到這個屬性,所以這個也可以直接return null.
第二個configure方法。這里面定義了靜態資源,這個跟springmvc的靜態資源差不多。重點來說下第三個configure方法
①、如果需要,那就加上一個過濾器增加圖形驗證碼校驗
②、登錄成功后處理adminauthenticationsuccesshandler這個類。該類實現了authenticationsuccesshandler接口,必須實現一個方法,直接上代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@override public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception { response.setcontenttype(contenttypeconstant.application_json_utf8); // 獲取session對象 httpsession session = request.getsession(); //設置登錄用戶session setusersession(session); //查詢用戶的菜單和按鈕 setusermenu(session); //session中獲取當前登錄的用戶 sysuserresponsebo user = securitysessionutil.getsessiondata(); // 更新最近登錄時間 sysuserservice.updatelastlogintime(user.getid()); //項目名稱 session.setattribute( "projectname" , projectname); // 注銷地址 session.setattribute( "logouturl" , adminconstants.logout_url); // 返回json格式數據 response<object> resp = response.success(); try (printwriter out = response.getwriter()) { out.write(resp.tostring()); out.flush(); } } |
基本上看注釋也就了解每一步的意義。
我們代碼中無需寫登錄的controller,因為這個方法框架已經根據你配置的loginprocessingurl給你生成好了。這個是用戶輸入用戶名密碼后,點擊登錄按鈕后執行的操作。能夠進入這個方法,那說明用戶輸入的用戶名和密碼是正確的,后續只要保存用戶的信息,查詢用戶權限等操作。
③、登錄失敗處理。adminauthenticationfailurehandler。登錄失敗后交給這個類去處理,看下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
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 ); @override public 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 = "賬戶被鎖定,請聯系管理員!" ; } else if (exception instanceof credentialsexpiredexception) { message = "密碼過期,請聯系管理員!" ; } else if (exception instanceof accountexpiredexception) { message = "賬戶過期,請聯系管理員!" ; } else if (exception instanceof disabledexception) { message = "賬戶被禁用,請聯系管理員!" ; } else if (exception instanceof validatecodeexception) { // 圖形驗證碼輸入錯誤 message = exception.getmessage(); } else if (exception instanceof insufficientauthenticationexception) { message = exception.getmessage(); } else { message = "登錄失敗!" ; } // 返回json格式數據 response<object> resp = response.error(message); try (printwriter out = response.getwriter()) { out.write(resp.tostring()); out.flush(); } } } |
看代碼也基本上看出來每一步的作用。有用戶名密碼錯誤,有用戶被禁用,有過期,鎖定等等。我這里前臺都是ajax請求,所以這個也是返回json格式,如果你不需要json格式,那可以按照你的要求返回指定的格式。
④、注銷相關。注銷接口也不需要我們在controller里面寫,框架會自動根據你的logouturl配置生成注銷地址。也可以自定義一個logoutsuccesshandler去在注銷后執行。
⑤、權限校驗。當訪問一個除開第一層免過濾列表里面的url的地址時,都會需要權限校驗,就都會走到urlauthenticationinterceptor.haspermission(request, authentication)這個方法里面去,這個里面可以根據你的項目的實際邏輯去校驗。
⑥、異常處理。框架里面處理異常有好多種,這里常用的accessdeniedhandler(登錄用戶訪問無權限的資源)、authenticationentrypoint(匿名用戶訪問無權限的資源)這些都按照項目的實際需求去寫異常處理。
⑦、session管理。security框架里面對session管理非常多,可以按照鏈式調用的方式打開看看。我這里使用了invalidsessionurl來指定session無效后跳轉到的地址,maximumsessions同一個賬號最多同時在線數。
好了,這樣一個最基本的權限控制框架就完成了。
其實我這里只使用了security的一些皮毛而且,他里面集成了非常復雜而又強大的功能,這個需要我們一點一點去發掘他。
總結
以上是我在項目中使用的一些總結,完整的代碼在我的github中 我的github ,歡迎大家留言討論!!
到此這篇關于spring-security權限控制和校驗的文章就介紹到這了,更多相關spring-security權限控制校驗內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/terry2870/article/details/114699134