通常情況下,把api直接暴露出去是風險很大的,不說別的,直接被機器攻擊就喝一壺的。那么一般來說,對api要劃分出一定的權限級別,然后做一個用戶的鑒權,依據鑒權結果給予用戶開放對應的api。目前,比較主流的方案有幾種:
- 用戶名和密碼鑒權,使用session保存用戶鑒權結果。
- 使用oauth進行鑒權(其實oauth也是一種基于token的鑒權,只是沒有規定token的生成方式)
- 自行采用token進行鑒權
第一種就不介紹了,由于依賴session來維護狀態,也不太適合移動時代,新的項目就不要采用了。第二種oauth的方案和jwt都是基于token的,但oauth其實對于不做開放平臺的公司有些過于復雜。我們主要介紹第三種:jwt。
什么是jwt?
jwt是 json web token 的縮寫。它是基于 rfc 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的json對象。由于數據是使用數字簽名的,所以是可信任的和安全的。jwt可以使用hmac算法對secret進行加密或者使用rsa的公鑰私鑰對來進行簽名。
jwt的工作流程
下面是一個jwt的工作流程圖。模擬一下實際的流程是這樣的(假設受保護的api在/protected中)
- 用戶導航到登錄頁,輸入用戶名、密碼,進行登錄
- 服務器驗證登錄鑒權,如果改用戶合法,根據用戶的信息和服務器的規則生成jwt token
- 服務器將該token以json形式返回(不一定要json形式,這里說的是一種常見的做法)
- 用戶得到token,存在localstorage、cookie或其它數據存儲形式中。
- 以后用戶請求/protected中的api時,在請求的header中加入 authorization: bearer xxxx(token)。此處注意token之前有一個7字符長度的 bearer
- 服務器端對此token進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
- 用戶取得結果
jwt工作流程圖
為了更好的理解這個token是什么,我們先來看一個token生成后的樣子,下面那坨亂糟糟的就是了。
eyjhbgcioijiuzuxmij9.eyjzdwiioij3yw5niiwiy3jlyxrlzci6mtq4ota3otk4mtm5mywizxhwijoxndg5njg0nzgxfq.rc-byce_uz2urtwddupwxip4nmsoeq2o6uf-8tvplqxy1-ci9u1-a-9daajgfnwkhe81mpnr3gxzfrbab3wuag
但仔細看到的話還是可以看到這個token分成了三部分,每部分用 . 分隔,每段都是用 base64 編碼的。如果我們用一個base64的解碼器的話 (https://www.base64decode.org/ ),可以看到第一部分 eyjhbgcioijiuzuxmij9 被解析成了:
1
2
3
|
{ "alg" : "hs512" } |
這是告訴我們hmac采用hs512算法對jwt進行的簽名。
第二部分 eyjzdwiioij3yw5niiwiy3jlyxrlzci6mtq4ota3otk4mtm5mywizxhwijoxndg5njg0nzgxfq 被解碼之后是
1
2
3
4
5
|
{ "sub" : "wang" , "created" : 1489079981393 , "exp" : 1489684781 } |
這段告訴我們這個token中含有的數據聲明(claim),這個例子里面有三個聲明:sub, created
和 exp
。在我們這個例子中,分別代表著用戶名、創建時間和過期時間,當然你可以把任意數據聲明在這里。
看到這里,你可能會想這是個什么鬼token,所有信息都透明啊,安全怎么保障?別急,我們看看token的第三段 rc-byce_uz2urtwddupwxip4nmsoeq2o6uf-8tvplqxy1-ci9u1-a-9daajgfnwkhe81mpnr3gxzfrbab3wuag
。同樣使用base64解碼之后,咦,這是什么東東
d x dmyte?luzcpz0$gzay_7wy@
最后一段其實是簽名,這個簽名必須知道秘鑰才能計算。這個也是jwt的安全保障。這里提一點注意事項,由于數據聲明(claim)是公開的,千萬不要把密碼等敏感字段放進去,否則就等于是公開給別人了。
也就是說jwt是由三段組成的,按官方的叫法分別是header(頭)、payload(負載)和signature(簽名):
header.payload.signature
頭中的數據通常包含兩部分:一個是我們剛剛看到的 alg,這個詞是 algorithm 的縮寫,就是指明算法。另一個可以添加的字段是token的類型(按rfc 7519實現的token機制不只jwt一種),但如果我們采用的是jwt的話,指定這個就多余了。
1
2
3
4
|
{ "alg" : "hs512" , "typ" : "jwt" } |
payload中可以放置三類數據:系統保留的、公共的和私有的:
- 系統保留的聲明(reserved claims):這類聲明不是必須的,但是是建議使用的,包括:iss (簽發者), exp (過期時間),
- sub (主題), aud (目標受眾)等。這里我們發現都用的縮寫的三個字符,這是由于jwt的目標就是盡可能小巧。
- 公共聲明:這類聲明需要在 iana json web token registry 中定義或者提供一個uri,因為要避免重名等沖突。
- 私有聲明:這個就是你根據業務需要自己定義的數據了。
簽名的過程是這樣的:采用header中聲明的算法,接受三個參數:base64編碼的header、base64編碼的payload和秘鑰(secret)進行運算。簽名這一部分如果你愿意的話,可以采用rsasha256的方式進行公鑰、私鑰對的方式進行,如果安全性要求的高的話。
1
2
3
4
|
hmacsha256( base64urlencode(header) + "." + base64urlencode(payload), secret) |
jwt的生成和解析
為了簡化我們的工作,這里引入一個比較成熟的jwt類庫,叫 jjwt ( https://github.com/jwtk/jjwt )。這個類庫可以用于java和android的jwt token的生成和驗證。
jwt的生成可以使用下面這樣的代碼完成:
1
2
3
4
5
6
7
|
string generatetoken(map<string, object> claims) { return jwts.builder() .setclaims(claims) .setexpiration(generateexpirationdate()) .signwith(signaturealgorithm.hs512, secret) //采用什么算法是可以自己選擇的,不一定非要采用hs512 .compact(); } |
數據聲明(claim)其實就是一個map,比如我們想放入用戶名,可以簡單的創建一個map然后put進去就可以了。
1
2
|
map<string, object> claims = new hashmap<>(); claims.put(claim_key_username, username()); |
解析也很簡單,利用 jjwt 提供的parser傳入秘鑰,然后就可以解析token了。
1
2
3
4
5
6
7
8
9
10
11
12
|
claims getclaimsfromtoken(string token) { claims claims; try { claims = jwts.parser() .setsigningkey(secret) .parseclaimsjws(token) .getbody(); } catch (exception e) { claims = null ; } return claims; } |
jwt本身沒啥難度,但安全整體是一個比較復雜的事情,jwt只不過提供了一種基于token的請求驗證機制。但我們的用戶權限,對于api的權限劃分、資源的權限劃分,用戶的驗證等等都不是jwt負責的。也就是說,請求驗證后,你是否有權限看對應的內容是由你的用戶角色決定的。所以我們這里要利用spring的一個子項目spring security來簡化我們的工作。
spring security
spring security是一個基于spring的通用安全框架,里面內容太多了,本文的主要目的也不是展開講這個框架,而是如何利用spring security和jwt一起來完成api保護。所以關于spring secruity的基礎內容或展開內容,請自行去官網學習( http://projects.spring.io/spring-security/ )。
簡單的背景知識
如果你的系統有用戶的概念的話,一般來說,你應該有一個用戶表,最簡單的用戶表,應該有三列:id,username和password,類似下表這種
而且不是所有用戶都是一種角色,比如網站管理員、供應商、財務等等,這些角色和網站的直接用戶需要的權限可能是不一樣的。那么我們就需要一個角色表:
當然我們還需要一個可以將用戶和角色關聯起來建立映射關系的表。
這是典型的一個關系型數據庫的用戶角色的設計,由于我們要使用的mongodb是一個文檔型數據庫,所以讓我們重新審視一下這個結構。
這個數據結構的優點在于它避免了數據的冗余,每個表負責自己的數據,通過關聯表進行關系的描述,同時也保證的數據的完整性:比如當你修改角色名稱后,沒有臟數據的產生。
但是這種事情在用戶權限這個領域發生的頻率到底有多少呢?有多少人每天不停的改的角色名稱?當然如果你的業務場景確實是需要保證數據完整性,你還是應該使用關系型數據庫。但如果沒有高頻的對于角色表的改動,其實我們是不需要這樣的一個設計的。在mongodb中我們可以將其簡化為
1
2
3
4
5
6
|
{ _id: <id_generated> username: 'user' , password: 'pass' , roles: [ 'user' , 'admin' ] } |
基于以上考慮,我們重構一下 user 類,
1
2
3
4
5
6
7
8
9
10
11
|
@data public class user { @id private string id; @indexed (unique= true , direction= indexdirection.descending, dropdups= true ) private string username; private string password; private string email; private date lastpasswordresetdate; private list<string> roles; } |
當然你可能發現這個類有點怪,只有一些field,這個簡化的能力是一個叫lombok類庫提供的 ,這個很多開發過android的童鞋應該熟悉,是用來簡化pojo的創建的一個類庫。簡單說一下,采用 lombok 提供的 @data 修飾符后可以簡寫成,原來的一坨getter和setter以及constructor等都不需要寫了。類似的 todo 可以改寫成:
1
2
3
4
5
6
7
|
@data public class todo { @id private string id; private string desc; private boolean completed; private user user; } |
增加這個類庫只需在 build.gradle 中增加下面這行
1
2
3
4
|
dependencies { // 省略其它依賴 compile( "org.projectlombok:lombok:${lombokversion}" ) } |
引入spring security
要在spring boot中引入spring security非常簡單,修改 build.gradle,增加一個引用 org.springframework.boot:spring-boot-starter-security:
1
2
3
4
5
6
7
8
|
dependencies { compile( "org.springframework.boot:spring-boot-starter-data-rest" ) compile( "org.springframework.boot:spring-boot-starter-data-mongodb" ) compile( "org.springframework.boot:spring-boot-starter-security" ) compile( "io.jsonwebtoken:jjwt:${jjwtversion}" ) compile( "org.projectlombok:lombok:${lombokversion}" ) testcompile( "org.springframework.boot:spring-boot-starter-test" ) } |
你可能發現了,我們不只增加了對spring security的編譯依賴,還增加 jjwt 的依賴。
spring security需要我們實現幾個東西,第一個是userdetails:這個接口中規定了用戶的幾個必須要有的方法,所以我們創建一個jwtuser類來實現這個接口。為什么不直接使用user類?因為這個userdetails完全是為了安全服務的,它和我們的領域類可能有部分屬性重疊,但很多的接口其實是安全定制的,所以最好新建一個類:
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
|
public class jwtuser implements userdetails { private final string id; private final string username; private final string password; private final string email; private final collection<? extends grantedauthority> authorities; private final date lastpasswordresetdate; public jwtuser( string id, string username, string password, string email, collection<? extends grantedauthority> authorities, date lastpasswordresetdate) { this .id = id; this .username = username; this .password = password; this .email = email; this .authorities = authorities; this .lastpasswordresetdate = lastpasswordresetdate; } //返回分配給用戶的角色列表 @override public collection<? extends grantedauthority> getauthorities() { return authorities; } @jsonignore public string getid() { return id; } @jsonignore @override public string getpassword() { return password; } @override public string getusername() { return username; } // 賬戶是否未過期 @jsonignore @override public boolean isaccountnonexpired() { return true ; } // 賬戶是否未鎖定 @jsonignore @override public boolean isaccountnonlocked() { return true ; } // 密碼是否未過期 @jsonignore @override public boolean iscredentialsnonexpired() { return true ; } // 賬戶是否激活 @jsonignore @override public boolean isenabled() { return true ; } // 這個是自定義的,返回上次密碼重置日期 @jsonignore public date getlastpasswordresetdate() { return lastpasswordresetdate; } } |
這個接口中規定的很多方法我們都簡單粗暴的設成直接返回某個值了,這是為了簡單起見,你在實際開發環境中還是要根據具體業務調整。當然由于兩個類還是有一定關系的,為了寫起來簡單,我們寫一個工廠類來由領域對象創建 jwtuser,這個工廠就叫 jwtuserfactory 吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public final class jwtuserfactory { private jwtuserfactory() { } public static jwtuser create(user user) { return new jwtuser( user.getid(), user.getusername(), user.getpassword(), user.getemail(), maptograntedauthorities(user.getroles()), user.getlastpasswordresetdate() ); } private static list<grantedauthority> maptograntedauthorities(list<string> authorities) { return authorities.stream() .map(simplegrantedauthority:: new ) .collect(collectors.tolist()); } } |
第二個要實現的是 userdetailsservice,這個接口只定義了一個方法 loaduserbyusername,顧名思義,就是提供一種從用戶名可以查到用戶并返回的方法。注意,不一定是數據庫哦,文本文件、xml文件等等都可能成為數據源,這也是為什么spring提供這樣一個接口的原因:保證你可以采用靈活的數據源。接下來我們建立一個 jwtuserdetailsserviceimpl 來實現這個接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@service public class jwtuserdetailsserviceimpl implements userdetailsservice { @autowired private userrepository userrepository; @override public userdetails loaduserbyusername(string username) throws usernamenotfoundexception { user user = userrepository.findbyusername(username); if (user == null ) { throw new usernamenotfoundexception(string.format( "no user found with username '%s'." , username)); } else { return jwtuserfactory.create(user); } } } |
為了讓spring可以知道我們想怎樣控制安全性,我們還需要建立一個安全配置類 websecurityconfig:
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
|
@configuration @enablewebsecurity @enableglobalmethodsecurity (prepostenabled = true ) public class websecurityconfig extends websecurityconfigureradapter{ // spring會自動尋找同樣類型的具體類注入,這里就是jwtuserdetailsserviceimpl了 @autowired private userdetailsservice userdetailsservice; @autowired public void configureauthentication(authenticationmanagerbuilder authenticationmanagerbuilder) throws exception { authenticationmanagerbuilder // 設置userdetailsservice .userdetailsservice( this .userdetailsservice) // 使用bcrypt進行密碼的hash .passwordencoder(passwordencoder()); } // 裝載bcrypt密碼編碼器 @bean public passwordencoder passwordencoder() { return new bcryptpasswordencoder(); } @override protected void configure(httpsecurity httpsecurity) throws exception { httpsecurity // 由于使用的是jwt,我們這里不需要csrf .csrf().disable() // 基于token,所以不需要session .sessionmanagement().sessioncreationpolicy(sessioncreationpolicy.stateless).and() .authorizerequests() //.antmatchers(httpmethod.options, "/**").permitall() // 允許對于網站靜態資源的無授權訪問 .antmatchers( httpmethod.get, "/" , "/*.html" , "/favicon.ico" , "/**/*.html" , "/**/*.css" , "/**/*.js" ).permitall() // 對于獲取token的rest api要允許匿名訪問 .antmatchers( "/auth/**" ).permitall() // 除上面外的所有請求全部需要鑒權認證 .anyrequest().authenticated(); // 禁用緩存 httpsecurity.headers().cachecontrol(); } } |
接下來我們要規定一下哪些資源需要什么樣的角色可以訪問了,在 usercontroller 加一個修飾符 @preauthorize("hasrole('admin')") 表示這個資源只能被擁有 admin 角色的用戶訪問。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/** * 在 @preauthorize 中我們可以利用內建的 spel 表達式:比如 'hasrole()' 來決定哪些用戶有權訪問。 * 需注意的一點是 hasrole 表達式認為每個角色名字前都有一個前綴 'role_'。所以這里的 'admin' 其實在 * 數據庫中存儲的是 'role_admin' 。這個 @preauthorize 可以修飾controller也可修飾controller中的方法。 **/ @restcontroller @requestmapping ( "/users" ) @preauthorize ( "hasrole('admin')" ) public class usercontroller { @autowired private userrepository repository; @requestmapping (method = requestmethod.get) public list<user> getusers() { return repository.findall(); } // 略去其它部分 } |
類似的我們給 todocontroller 加上 @preauthorize("hasrole('user')"),標明這個資源只能被擁有 user 角色的用戶訪問:
1
2
3
4
5
6
|
@restcontroller @requestmapping ( "/todos" ) @preauthorize ( "hasrole('user')" ) public class todocontroller { // 略去 } |
使用application.yml配置springboot應用
現在應該spring security可以工作了,但為了可以更清晰的看到工作日志,我們希望配置一下,在和 src 同級建立一個config文件夾,在這個文件夾下面新建一個 application.yml。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# server configuration server: port: 8090 contextpath: # spring configuration spring: jackson: serialization: indent_output: true data.mongodb: host: localhost port: 27017 database: springboot # logging configuration logging: level: org.springframework: data: debug security: debug |
我們除了配置了logging的一些東東外,也順手設置了數據庫和http服務的一些配置項,現在我們的服務器會在8090端口監聽,而spring data和security的日志在debug模式下會輸出到console。
現在啟動服務后,訪問http://localhost:8090 你可以看到根目錄還是正常顯示的
根目錄還是正常可以訪問的
但我們試一下http://localhost:8090/users ,觀察一下console,我們會看到如下的輸出,告訴由于用戶未鑒權,我們訪問被拒絕了。
1
2
3
|
2017 - 03 - 10 15 : 51 : 53.351 debug 57599 --- [nio- 8090 -exec- 4 ] o.s.s.w.a.exceptiontranslationfilter : access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.accessdeniedexception: access is denied at org.springframework.security.access.vote.affirmativebased.decide(affirmativebased.java: 84 ) ~[spring-security-core- 4.2 . 1 .release.jar: 4.2 . 1 .release] |
集成jwt和spring security
到現在,我們還是讓jwt和spring security各自為戰,并沒有集成起來。要想要jwt在spring中工作,我們應該新建一個filter,并把它配置在 websecurityconfig 中。
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
|
@component public class jwtauthenticationtokenfilter extends onceperrequestfilter { @autowired private userdetailsservice userdetailsservice; @autowired private jwttokenutil jwttokenutil; @value ( "${jwt.header}" ) private string tokenheader; @value ( "${jwt.tokenhead}" ) private string tokenhead; @override protected void dofilterinternal( httpservletrequest request, httpservletresponse response, filterchain chain) throws servletexception, ioexception { string authheader = request.getheader( this .tokenheader); if (authheader != null && authheader.startswith(tokenhead)) { final string authtoken = authheader.substring(tokenhead.length()); // the part after "bearer " string username = jwttokenutil.getusernamefromtoken(authtoken); logger.info( "checking authentication " + username); if (username != null && securitycontextholder.getcontext().getauthentication() == null ) { userdetails userdetails = this .userdetailsservice.loaduserbyusername(username); if (jwttokenutil.validatetoken(authtoken, userdetails)) { usernamepasswordauthenticationtoken authentication = new usernamepasswordauthenticationtoken( userdetails, null , userdetails.getauthorities()); authentication.setdetails( new webauthenticationdetailssource().builddetails( request)); logger.info( "authenticated user " + username + ", setting security context" ); securitycontextholder.getcontext().setauthentication(authentication); } } } chain.dofilter(request, response); } } |
事實上如果我們足夠相信token中的數據,也就是我們足夠相信簽名token的secret的機制足夠好,這種情況下,我們可以不用再查詢數據庫,而直接采用token中的數據。本例中,我們還是通過spring security的 @userdetailsservice 進行了數據查詢,但簡單驗證的話,你可以采用直接驗證token是否合法來避免昂貴的數據查詢。
接下來,我們會在 websecurityconfig 中注入這個filter,并且配置到 httpsecurity 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class websecurityconfig extends websecurityconfigureradapter{ // 省略其它部分 @bean public jwtauthenticationtokenfilter authenticationtokenfilterbean() throws exception { return new jwtauthenticationtokenfilter(); } @override protected void configure(httpsecurity httpsecurity) throws exception { // 省略之前寫的規則部分,具體看前面的代碼 // 添加jwt filter httpsecurity .addfilterbefore(authenticationtokenfilterbean(), usernamepasswordauthenticationfilter. class ); } } |
完成鑒權(登錄)、注冊和更新token的功能
到現在,我們整個api其實已經在安全的保護下了,但我們遇到一個問題:所有的api都安全了,但我們還沒有用戶啊,所以所有api都沒法訪問。因此要提供一個注冊、登錄的api,這個api應該是可以匿名訪問的。給它規劃的路徑呢,我們前面其實在websecurityconfig中已經給出了,就是 /auth。
首先需要一個authservice,規定一下必選動作:
1
2
3
4
5
|
public interface authservice { user register(user usertoadd); string login(string username, string password); string refresh(string oldtoken); } |
然后,實現這些必選動作,其實非常簡單:
- 登錄時要生成token,完成spring security認證,然后返回token給客戶端
- 注冊時將用戶密碼用bcrypt加密,寫入用戶角色,由于是開放注冊,所以寫入角色系統控制,將其寫成 role_user
- 提供一個可以刷新token的接口 refresh 用于取得新的token
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
|
@service public class authserviceimpl implements authservice { private authenticationmanager authenticationmanager; private userdetailsservice userdetailsservice; private jwttokenutil jwttokenutil; private userrepository userrepository; @value ( "${jwt.tokenhead}" ) private string tokenhead; @autowired public authserviceimpl( authenticationmanager authenticationmanager, userdetailsservice userdetailsservice, jwttokenutil jwttokenutil, userrepository userrepository) { this .authenticationmanager = authenticationmanager; this .userdetailsservice = userdetailsservice; this .jwttokenutil = jwttokenutil; this .userrepository = userrepository; } @override public user register(user usertoadd) { final string username = usertoadd.getusername(); if (userrepository.findbyusername(username)!= null ) { return null ; } bcryptpasswordencoder encoder = new bcryptpasswordencoder(); final string rawpassword = usertoadd.getpassword(); usertoadd.setpassword(encoder.encode(rawpassword)); usertoadd.setlastpasswordresetdate( new date()); usertoadd.setroles(aslist( "role_user" )); return userrepository.insert(usertoadd); } @override public string login(string username, string password) { usernamepasswordauthenticationtoken uptoken = new usernamepasswordauthenticationtoken(username, password); final authentication authentication = authenticationmanager.authenticate(uptoken); securitycontextholder.getcontext().setauthentication(authentication); final userdetails userdetails = userdetailsservice.loaduserbyusername(username); final string token = jwttokenutil.generatetoken(userdetails); return token; } @override public string refresh(string oldtoken) { final string token = oldtoken.substring(tokenhead.length()); string username = jwttokenutil.getusernamefromtoken(token); jwtuser user = (jwtuser) userdetailsservice.loaduserbyusername(username); if (jwttokenutil.cantokenberefreshed(token, user.getlastpasswordresetdate())){ return jwttokenutil.refreshtoken(token); } return null ; } } |
然后建立authcontroller就好,這個authcontroller中我們在其中使用了表達式綁定,比如 @value("${jwt.header}")中的 jwt.header 其實是定義在 applicaiton.yml 中的
1
2
3
4
5
6
7
8
9
10
11
|
# jwt jwt: header: authorization secret: mysecret expiration: 604800 tokenhead: "bearer " route: authentication: path: auth refresh: refresh register: "auth/register" |
同樣的 @requestmapping(value = "${jwt.route.authentication.path}", method = requestmethod.post) 中的 jwt.route.authentication.path 也是定義在上面的
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
|
@restcontroller public class authcontroller { @value ( "${jwt.header}" ) private string tokenheader; @autowired private authservice authservice; @requestmapping (value = "${jwt.route.authentication.path}" , method = requestmethod.post) public responseentity<?> createauthenticationtoken( @requestbody jwtauthenticationrequest authenticationrequest) throws authenticationexception{ final string token = authservice.login(authenticationrequest.getusername(), authenticationrequest.getpassword()); // return the token return responseentity.ok( new jwtauthenticationresponse(token)); } @requestmapping (value = "${jwt.route.authentication.refresh}" , method = requestmethod.get) public responseentity<?> refreshandgetauthenticationtoken( httpservletrequest request) throws authenticationexception{ string token = request.getheader(tokenheader); string refreshedtoken = authservice.refresh(token); if (refreshedtoken == null ) { return responseentity.badrequest().body( null ); } else { return responseentity.ok( new jwtauthenticationresponse(refreshedtoken)); } } @requestmapping (value = "${jwt.route.authentication.register}" , method = requestmethod.post) public user register( @requestbody user addeduser) throws authenticationexception{ return authservice.register(addeduser); } } |
驗證時間
接下來,我們就可以看看我們的成果了,首先注冊一個用戶 peng2,很完美的注冊成功了
注冊用戶
然后在 /auth 中取得token,也很成功
取得token
不使用token時,訪問 /users 的結果,不出意料的失敗,提示未授權。
不使用token訪問users列表
使用token時,訪問 /users 的結果,雖然仍是失敗,但這次提示訪問被拒絕,意思就是雖然你已經得到了授權,但由于你的會員級別還只是普卡會員,所以你的請求被拒絕。
image_1bas22va52vk1rj445fhm87k72a.png-156.9kb
接下來我們訪問 /users/?username=peng2,竟然可以訪問啊
訪問自己的信息是允許的
這是由于我們為這個方法定義的權限就是:擁有admin角色或者是當前用戶本身。spring security真是很方便,很強大。
1
2
3
4
5
|
@postauthorize ( "returnobject.username == principal.username or hasrole('role_admin')" ) @requestmapping (value = "/" ,method = requestmethod.get) public user getuserbyusername( @requestparam (value= "username" ) string username) { return repository.findbyusername(username); } |
本章代碼:https://github.com/wpcfan/spring-boot-tut/tree/chap04
以上所述是小編給大家介紹的spring boot(四)之使用jwt和spring security保護rest api,希望對大家有所幫助
原文鏈接:https://juejin.im/post/58c29e0b1b69e6006bce02f4