小程序官方流程圖如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html :
本文是對(duì)接微信小程序自定義登錄的一個(gè)完整例子實(shí)現(xiàn) ,技術(shù)棧為 : springboot+shiro+jwt+jpa+redis。
如果對(duì)該例子比較感興趣或者覺(jué)得言語(yǔ)表達(dá)比較啰嗦,可查看完整的項(xiàng)目地址 : https://github.com/ealenxie/shiro-jwt-applet
主要實(shí)現(xiàn) : 實(shí)現(xiàn)了小程序的自定義登陸,將自定義登陸態(tài)token返回給小程序作為登陸憑證。用戶的信息保存在數(shù)據(jù)庫(kù)中,登陸態(tài)token緩存在redis中。
效果如下 :
1 . 首先從我們的小程序端調(diào)用wx.login() ,獲取臨時(shí)憑證code :
2 . 模擬使用該code,進(jìn)行小程序的登陸獲取自定義登陸態(tài) token,用postman進(jìn)行測(cè)試 :
3 . 調(diào)用我們需要認(rèn)證的接口,并攜帶該token進(jìn)行鑒權(quán),獲取到返回信息 :
前方高能,本例代碼說(shuō)明較多, 以下是主要的搭建流程 :
1 . 首先新建maven項(xiàng)目 shiro-jwt-applet ,pom依賴 ,主要是shiro和jwt的依賴,和springboot的一些基礎(chǔ)依賴。
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
|
<?xml version= "1.0" encoding= "utf-8" ?> <project xmlns= "http://maven.apache.org/pom/4.0.0" xmlns:xsi= "http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation= "http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelversion> 4.0 . 0 </modelversion> <groupid>name.ealen</groupid> <artifactid>shiro-jwt-applet</artifactid> <version> 0.0 . 1 -snapshot</version> <packaging>jar</packaging> <name>shiro-wx-jwt</name> <description>demo project for spring boot</description> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version> 2.0 . 6 .release</version> <relativepath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceencoding>utf- 8 </project.build.sourceencoding> <project.reporting.outputencoding>utf- 8 </project.reporting.outputencoding> <java.version> 1.8 </java.version> </properties> <dependencies> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-actuator</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-redis</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-spring</artifactid> <version> 1.4 . 0 </version> </dependency> <dependency> <groupid>com.auth0</groupid> <artifactid>java-jwt</artifactid> <version> 3.4 . 1 </version> </dependency> <dependency> <groupid>com.alibaba</groupid> <artifactid>fastjson</artifactid> <version> 1.2 . 47 </version> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project> |
2 . 配置你的application.yml ,主要是配置你的小程序appid和secret,還有你的數(shù)據(jù)庫(kù)和redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
## 請(qǐng)自行修改下面信息 spring: application: name: shiro-jwt-applet jpa: hibernate: ddl-auto: create # 請(qǐng)自行修改 請(qǐng)自行修改 請(qǐng)自行修改 # datasource本地配置 datasource: url: jdbc:mysql: //localhost:3306/yourdatabase username: yourname password: yourpass driver- class -name: com.mysql.jdbc.driver # redis本地配置 請(qǐng)自行配置 redis: database: 0 host: localhost port: 6379 # 微信小程序配置 appid /appsecret wx: applet: appid: yourappid appsecret: yourappsecret |
3 . 定義我們存儲(chǔ)的微信小程序登陸的實(shí)體信息 wxaccount :
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
|
package name.ealen.domain.entity; import org.springframework.format.annotation.datetimeformat; import javax.persistence.entity; import javax.persistence.generatedvalue; import javax.persistence.id; import javax.persistence.table; import java.util.date; /** * created by ealenxie on 2018/11/26 10:26. * 實(shí)體 屬性描述 這里只是簡(jiǎn)單示例,你可以自定義相關(guān)用戶信息 */ @entity @table public class wxaccount { @id @generatedvalue private integer id; private string wxopenid; private string sessionkey; @datetimeformat (pattern = "yyyy-mm-dd hh:mm:ss" ) private date lasttime; /** * 省略getter/setter */ } |
和一個(gè)簡(jiǎn)單的dao 訪問(wèn)數(shù)據(jù)庫(kù) wxaccountrepository :
1
2
3
4
5
6
7
8
9
10
11
12
|
package name.ealen.domain.repository; import name.ealen.domain.entity.wxaccount; import org.springframework.data.jpa.repository.jparepository; /** * created by ealenxie on 2018/11/26 10:32. */ public interface wxaccountrepository extends jparepository<wxaccount, integer> { /** * 根據(jù)openid查詢用戶信息 */ wxaccount findbywxopenid(string wxopenid); } |
4 . 定義我們應(yīng)用的服務(wù)說(shuō)明 wxappletservice :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package name.ealen.application; import name.ealen.interfaces.dto.token; /** * created by ealenxie on 2018/11/26 10:40. * 微信小程序自定義登陸 服務(wù)說(shuō)明 */ public interface wxappletservice { /** * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開(kāi)發(fā) * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html * 1 . 我們的微信小程序端傳入code。 * 2 . 調(diào)用微信code2session接口獲取openid和session_key * 3 . 根據(jù)openid和session_key自定義登陸態(tài)(token) * 4 . 返回自定義登陸態(tài)(token)給小程序端。 * 5 . 我們的小程序端調(diào)用其他需要認(rèn)證的api,請(qǐng)?jiān)趆eader的authorization里面攜帶 token信息 * * @param code 小程序端 調(diào)用 wx.login 獲取到的code,用于調(diào)用 微信code2session接口 * @return token 返回后端 自定義登陸態(tài) token 基于jwt實(shí)現(xiàn) */ public token wxuserlogin(string code); } |
返回給微信小程序token對(duì)象聲明 token :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package name.ealen.interfaces.dto; /** * created by ealenxie on 2018/11/26 18:49. * dto 返回值token對(duì)象 */ public class token { private string token; public token(string token) { this .token = token; } /** * 省略getter/setter */ } |
5. 配置需要的基本組件,resttemplate,redis:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package name.ealen.infrastructure.config; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.http.client.clienthttprequestfactory; import org.springframework.http.client.simpleclienthttprequestfactory; import org.springframework.web.client.resttemplate; /** * created by ealenxie on 2018-03-23 07:37 * resttemplate的配置類 */ @configuration public class resttemplateconfig { @bean public resttemplate resttemplate(clienthttprequestfactory factory) { return new resttemplate(factory); } @bean public clienthttprequestfactory simpleclienthttprequestfactory() { simpleclienthttprequestfactory factory = new simpleclienthttprequestfactory(); factory.setreadtimeout( 1000 * 60 ); //讀取超時(shí)時(shí)間為單位為60秒 factory.setconnecttimeout( 1000 * 10 ); //連接超時(shí)時(shí)間設(shè)置為10秒 return factory; } } |
redis的配置。本例是springboot2.0的寫法(和1.8的版本寫法略有不同):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package name.ealen.infrastructure.config; import org.springframework.cache.cachemanager; import org.springframework.cache.annotation.enablecaching; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.data.redis.cache.rediscachemanager; import org.springframework.data.redis.connection.redisconnectionfactory; /** * created by ealenxie on 2018-03-23 07:37 * redis的配置類 */ @configuration @enablecaching public class redisconfig { @bean public cachemanager cachemanager(redisconnectionfactory factory) { return rediscachemanager.create(factory); } } |
6. jwt的核心過(guò)濾器配置。繼承了shiro的basichttpauthenticationfilter,并重寫了其鑒權(quán)的過(guò)濾方法 :
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
|
package name.ealen.infrastructure.config.jwt; import name.ealen.domain.vo.jwttoken; import org.apache.shiro.web.filter.authc.basichttpauthenticationfilter; import org.springframework.http.httpstatus; import org.springframework.web.bind.annotation.requestmethod; import javax.servlet.servletrequest; import javax.servlet.servletresponse; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; /** * created by ealenxie on 2018/11/26 10:26. * jwt核心過(guò)濾器配置 * 所有的請(qǐng)求都會(huì)先經(jīng)過(guò)filter,所以我們繼承官方的basichttpauthenticationfilter,并且重寫鑒權(quán)的方法。 * 執(zhí)行流程 prehandle->isaccessallowed->isloginattempt->executelogin */ public class jwtfilter extends basichttpauthenticationfilter { /** * 判斷用戶是否想要進(jìn)行 需要驗(yàn)證的操作 * 檢測(cè)header里面是否包含authorization字段即可 */ @override protected boolean isloginattempt(servletrequest request, servletresponse response) { string auth = getauthzheader(request); return auth != null && !auth.equals( "" ); } /** * 此方法調(diào)用登陸,驗(yàn)證邏輯 */ @override protected boolean isaccessallowed(servletrequest request, servletresponse response, object mappedvalue) { if (isloginattempt(request, response)) { jwttoken token = new jwttoken(getauthzheader(request)); getsubject(request, response).login(token); } return true ; } /** * 提供跨域支持 */ @override protected boolean prehandle(servletrequest request, servletresponse response) throws exception { httpservletrequest httpservletrequest = (httpservletrequest) request; httpservletresponse httpservletresponse = (httpservletresponse) response; httpservletresponse.setheader( "access-control-allow-origin" , httpservletrequest.getheader( "origin" )); httpservletresponse.setheader( "access-control-allow-methods" , "get,post,options,put,delete" ); httpservletresponse.setheader( "access-control-allow-headers" , httpservletrequest.getheader( "access-control-request-headers" )); // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求,這里我們給option請(qǐng)求直接返回正常狀態(tài) if (httpservletrequest.getmethod().equals(requestmethod.options.name())) { httpservletresponse.setstatus(httpstatus.ok.value()); return false ; } return super .prehandle(request, response); } } |
jwt的核心配置(包含token的加密創(chuàng)建,jwt續(xù)期,解密驗(yàn)證) :
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
|
package name.ealen.infrastructure.config.jwt; import com.auth0.jwt.jwt; import com.auth0.jwt.jwtverifier; import com.auth0.jwt.algorithms.algorithm; import com.auth0.jwt.exceptions.jwtdecodeexception; import name.ealen.domain.entity.wxaccount; import org.springframework.beans.factory.annotation.autowired; import org.springframework.data.redis.core.stringredistemplate; import org.springframework.stereotype.component; import java.util.date; import java.util.uuid; import java.util.concurrent.timeunit; /** * created by ealenxie on 2018/11/22 17:16. */ @component public class jwtconfig { /** * jwt 自定義密鑰 我這里寫死的 */ private static final string secret_key = "5371f568a45e5ab1f442c38e0932aef24447139b" ; /** * jwt 過(guò)期時(shí)間值 這里寫死為和小程序時(shí)間一致 7200 秒,也就是兩個(gè)小時(shí) */ private static long expire_time = 7200 ; @autowired private stringredistemplate redistemplate; /** * 根據(jù)微信用戶登陸信息創(chuàng)建 token * 注 : 這里的token會(huì)被緩存到redis中,用作為二次驗(yàn)證 * redis里面緩存的時(shí)間應(yīng)該和jwt token的過(guò)期時(shí)間設(shè)置相同 * * @param wxaccount 微信用戶信息 * @return 返回 jwt token */ public string createtokenbywxaccount(wxaccount wxaccount) { string jwtid = uuid.randomuuid().tostring(); //jwt 隨機(jī)id,做為驗(yàn)證的key //1 . 加密算法進(jìn)行簽名得到token algorithm algorithm = algorithm.hmac256(secret_key); string token = jwt.create() .withclaim( "wxopenid" , wxaccount.getwxopenid()) .withclaim( "sessionkey" , wxaccount.getsessionkey()) .withclaim( "jwt-id" , jwtid) .withexpiresat( new date(system.currenttimemillis() + expire_time* 1000 )) //jwt 配置過(guò)期時(shí)間的正確姿勢(shì) .sign(algorithm); //2 . redis緩存jwt, 注 : 請(qǐng)和jwt過(guò)期時(shí)間一致 redistemplate.opsforvalue().set( "jwt-session-" + jwtid, token, expire_time, timeunit.seconds); return token; } /** * 校驗(yàn)token是否正確 * 1 . 根據(jù)token解密,解密出jwt-id , 先從redis中查找出redistoken,匹配是否相同 * 2 . 然后再對(duì)redistoken進(jìn)行解密,解密成功則 繼續(xù)流程 和 進(jìn)行token續(xù)期 * * @param token 密鑰 * @return 返回是否校驗(yàn)通過(guò) */ public boolean verifytoken(string token) { try { //1 . 根據(jù)token解密,解密出jwt-id , 先從redis中查找出redistoken,匹配是否相同 string redistoken = redistemplate.opsforvalue().get( "jwt-session-" + getjwtidbytoken(token)); if (!redistoken.equals(token)) return false ; //2 . 得到算法相同的jwtverifier algorithm algorithm = algorithm.hmac256(secret_key); jwtverifier verifier = jwt.require(algorithm) .withclaim( "wxopenid" , getwxopenidbytoken(redistoken)) .withclaim( "sessionkey" , getsessionkeybytoken(redistoken)) .withclaim( "jwt-id" , getjwtidbytoken(redistoken)) .acceptexpiresat(system.currenttimemillis() + expire_time* 1000 ) //jwt 正確的配置續(xù)期姿勢(shì) .build(); //3 . 驗(yàn)證token verifier.verify(redistoken); //4 . redis緩存jwt續(xù)期 redistemplate.opsforvalue().set( "jwt-session-" + getjwtidbytoken(token), redistoken, expire_time, timeunit.seconds); return true ; } catch (exception e) { //捕捉到任何異常都視為校驗(yàn)失敗 return false ; } } /** * 根據(jù)token獲取wxopenid(注意坑點(diǎn) : 就算token不正確,也有可能解密出wxopenid,同下) */ public string getwxopenidbytoken(string token) throws jwtdecodeexception { return jwt.decode(token).getclaim( "wxopenid" ).asstring(); } /** * 根據(jù)token獲取sessionkey */ public string getsessionkeybytoken(string token) throws jwtdecodeexception { return jwt.decode(token).getclaim( "sessionkey" ).asstring(); } /** * 根據(jù)token 獲取jwt-id */ private string getjwtidbytoken(string token) throws jwtdecodeexception { return jwt.decode(token).getclaim( "jwt-id" ).asstring(); } } |
7 . 自定義shiro的realm配置,realm是自定義登陸及授權(quán)的邏輯配置 :
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
|
package name.ealen.infrastructure.config.shiro; import name.ealen.domain.vo.jwttoken; import name.ealen.infrastructure.config.jwt.jwtconfig; import org.apache.shiro.authc.authenticationexception; import org.apache.shiro.authc.authenticationinfo; import org.apache.shiro.authc.authenticationtoken; import org.apache.shiro.authc.simpleauthenticationinfo; import org.apache.shiro.authc.credential.credentialsmatcher; import org.apache.shiro.authz.authorizationinfo; import org.apache.shiro.authz.simpleauthorizationinfo; import org.apache.shiro.realm.authorizingrealm; import org.apache.shiro.realm.realm; import org.apache.shiro.subject.principalcollection; import org.springframework.stereotype.component; import javax.annotation.resource; import java.util.collections; import java.util.linkedlist; import java.util.list; /** * created by ealenxie on 2018/11/26 12:12. * realm 的一個(gè)配置管理類 allrealm()方法得到所有的realm */ @component public class shirorealmconfig { @resource private jwtconfig jwtconfig; /** * 配置所有自定義的realm,方便起見(jiàn),應(yīng)對(duì)可能有多個(gè)realm的情況 */ public list<realm> allrealm() { list<realm> realmlist = new linkedlist<>(); authorizingrealm jwtrealm = jwtrealm(); realmlist.add(jwtrealm); return collections.unmodifiablelist(realmlist); } /** * 自定義 jwt的 realm * 重寫 realm 的 supports() 方法是通過(guò) jwt 進(jìn)行登錄判斷的關(guān)鍵 */ private authorizingrealm jwtrealm() { authorizingrealm jwtrealm = new authorizingrealm() { /** * 注意坑點(diǎn) : 必須重寫此方法,不然shiro會(huì)報(bào)錯(cuò) * 因?yàn)閯?chuàng)建了 jwttoken 用于替換shiro原生 token,所以必須在此方法中顯式的進(jìn)行替換,否則在進(jìn)行判斷時(shí)會(huì)一直失敗 */ @override public boolean supports(authenticationtoken token) { return token instanceof jwttoken; } @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { return new simpleauthorizationinfo(); } /** * 校驗(yàn) 驗(yàn)證token邏輯 */ @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) { string jwttoken = (string) token.getcredentials(); string wxopenid = jwtconfig.getwxopenidbytoken(jwttoken); string sessionkey = jwtconfig.getsessionkeybytoken(jwttoken); if (wxopenid == null || wxopenid.equals( "" )) throw new authenticationexception( "user account not exits , please check your token" ); if (sessionkey == null || sessionkey.equals( "" )) throw new authenticationexception( "sessionkey is invalid , please check your token" ); if (!jwtconfig.verifytoken(jwttoken)) throw new authenticationexception( "token is invalid , please check your token" ); return new simpleauthenticationinfo(token, token, getname()); } }; jwtrealm.setcredentialsmatcher(credentialsmatcher()); return jwtrealm; } /** * 注意坑點(diǎn) : 密碼校驗(yàn) , 這里因?yàn)槭莏wt形式,就無(wú)需密碼校驗(yàn)和加密,直接讓其返回為true(如果不設(shè)置的話,該值默認(rèn)為false,即始終驗(yàn)證不通過(guò)) */ private credentialsmatcher credentialsmatcher() { return (token, info) -> true ; } } |
shiro的核心配置,包含配置realm :
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
|
package name.ealen.infrastructure.config.shiro; import name.ealen.infrastructure.config.jwt.jwtfilter; import org.apache.shiro.mgt.defaultsessionstorageevaluator; import org.apache.shiro.mgt.defaultsubjectdao; import org.apache.shiro.spring.lifecyclebeanpostprocessor; import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor; import org.apache.shiro.spring.web.shirofilterfactorybean; import org.apache.shiro.web.mgt.defaultwebsecuritymanager; import org.springframework.aop.framework.autoproxy.defaultadvisorautoproxycreator; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.context.annotation.dependson; import javax.servlet.filter; import java.util.hashmap; import java.util.map; /** * created by ealenxie on 2018/11/22 18:28. */ @configuration public class shirconfig { /** * securitymanager,安全管理器,所有與安全相關(guān)的操作都會(huì)與之進(jìn)行交互; * 它管理著所有subject,所有subject都綁定到securitymanager,與subject的所有交互都會(huì)委托給securitymanager * defaultwebsecuritymanager : * 會(huì)創(chuàng)建默認(rèn)的defaultsubjectdao(它又會(huì)默認(rèn)創(chuàng)建defaultsessionstorageevaluator) * 會(huì)默認(rèn)創(chuàng)建defaultwebsubjectfactory * 會(huì)默認(rèn)創(chuàng)建modularrealmauthenticator */ @bean public defaultwebsecuritymanager securitymanager(shirorealmconfig shirorealmconfig) { defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager(); securitymanager.setrealms(shirorealmconfig.allrealm()); //設(shè)置realm defaultsubjectdao subjectdao = (defaultsubjectdao) securitymanager.getsubjectdao(); // 關(guān)閉自帶session defaultsessionstorageevaluator evaluator = (defaultsessionstorageevaluator) subjectdao.getsessionstorageevaluator(); evaluator.setsessionstorageenabled( boolean . false ); subjectdao.setsessionstorageevaluator(evaluator); return securitymanager; } /** * 配置shiro的訪問(wèn)策略 */ @bean public shirofilterfactorybean factory(defaultwebsecuritymanager securitymanager) { shirofilterfactorybean factorybean = new shirofilterfactorybean(); map<string, filter> filtermap = new hashmap<>(); filtermap.put( "jwt" , new jwtfilter()); factorybean.setfilters(filtermap); factorybean.setsecuritymanager(securitymanager); map<string, string> filterrulemap = new hashmap<>(); //登陸相關(guān)api不需要被過(guò)濾器攔截 filterrulemap.put( "/api/wx/user/login/**" , "anon" ); filterrulemap.put( "/api/response/**" , "anon" ); // 所有請(qǐng)求通過(guò)jwt filter filterrulemap.put( "/**" , "jwt" ); factorybean.setfilterchaindefinitionmap(filterrulemap); return factorybean; } /** * 添加注解支持 */ @bean @dependson ( "lifecyclebeanpostprocessor" ) public defaultadvisorautoproxycreator defaultadvisorautoproxycreator() { defaultadvisorautoproxycreator defaultadvisorautoproxycreator = new defaultadvisorautoproxycreator(); defaultadvisorautoproxycreator.setproxytargetclass( true ); // 強(qiáng)制使用cglib,防止重復(fù)代理和可能引起代理出錯(cuò)的問(wèn)題 return defaultadvisorautoproxycreator; } /** * 添加注解依賴 */ @bean public lifecyclebeanpostprocessor lifecyclebeanpostprocessor() { return new lifecyclebeanpostprocessor(); } /** * 開(kāi)啟注解驗(yàn)證 */ @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(defaultwebsecuritymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } } |
用于shiro鑒權(quán)的jwttoken對(duì)象 :
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
|
package name.ealen.domain.vo; import org.apache.shiro.authc.authenticationtoken; /** * created by ealenxie on 2018/11/22 18:21. * 鑒權(quán)用的token vo ,實(shí)現(xiàn) authenticationtoken */ public class jwttoken implements authenticationtoken { private string token; public jwttoken(string token) { this .token = token; } @override public object getprincipal() { return token; } @override public object getcredentials() { return token; } public string gettoken() { return token; } public void settoken(string token) { this .token = token; } } |
8 . 實(shí)現(xiàn)實(shí)體的行為及業(yè)務(wù)邏輯,此例主要是調(diào)用微信接口code2session和創(chuàng)建返回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
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
|
package name.ealen.domain.service; import name.ealen.application.wxappletservice; import name.ealen.domain.entity.wxaccount; import name.ealen.domain.repository.wxaccountrepository; import name.ealen.domain.vo.code2sessionresponse; import name.ealen.infrastructure.config.jwt.jwtconfig; import name.ealen.infrastructure.util.httputil; import name.ealen.infrastructure.util.jsonutil; import name.ealen.interfaces.dto.token; import org.apache.shiro.authc.authenticationexception; import org.springframework.beans.factory.annotation.value; import org.springframework.http.httpentity; import org.springframework.http.httpheaders; import org.springframework.http.httpmethod; import org.springframework.stereotype.service; import org.springframework.util.linkedmultivaluemap; import org.springframework.util.multivaluemap; import org.springframework.web.client.resttemplate; import javax.annotation.resource; import java.net.uri; import java.util.date; /** * created by ealenxie on 2018/11/26 10:50. * 實(shí)體 行為描述 */ @service public class wxaccountservice implements wxappletservice { @resource private resttemplate resttemplate; @value ( "${wx.applet.appid}" ) private string appid; @value ( "${wx.applet.appsecret}" ) private string appsecret; @resource private wxaccountrepository wxaccountrepository; @resource private jwtconfig jwtconfig; /** * 微信的 code2session 接口 獲取微信用戶信息 * 官方說(shuō)明 : https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2session.html */ private string code2session(string jscode) { string code2sessionurl = "https://api.weixin.qq.com/sns/jscode2session" ; multivaluemap<string, string> params = new linkedmultivaluemap<>(); params.add( "appid" , appid); params.add( "secret" , appsecret); params.add( "js_code" , jscode); params.add( "grant_type" , "authorization_code" ); uri code2session = httputil.geturiwithparams(code2sessionurl, params); return resttemplate.exchange(code2session, httpmethod.get, new httpentity<string>( new httpheaders()), string. class ).getbody(); } /** * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開(kāi)發(fā) * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html * * @param code 小程序端 調(diào)用 wx.login 獲取到的code,用于調(diào)用 微信code2session接口 * @return 返回后端 自定義登陸態(tài) token 基于jwt實(shí)現(xiàn) */ @override public token wxuserlogin(string code) { //1 . code2session返回json數(shù)據(jù) string resultjson = code2session(code); //2 . 解析數(shù)據(jù) code2sessionresponse response = jsonutil.jsonstring2object(resultjson, code2sessionresponse. class ); if (!response.geterrcode().equals( "0" )) throw new authenticationexception( "code2session失敗 : " + response.geterrmsg()); else { //3 . 先從本地?cái)?shù)據(jù)庫(kù)中查找用戶是否存在 wxaccount wxaccount = wxaccountrepository.findbywxopenid(response.getopenid()); if (wxaccount == null ) { wxaccount = new wxaccount(); wxaccount.setwxopenid(response.getopenid()); //不存在就新建用戶 } //4 . 更新sessionkey和 登陸時(shí)間 wxaccount.setsessionkey(response.getsession_key()); wxaccount.setlasttime( new date()); wxaccountrepository.save(wxaccount); //5 . jwt 返回自定義登陸態(tài) token string token = jwtconfig.createtokenbywxaccount(wxaccount); return new token(token); } } } |
小程序code2session接口的返回vo對(duì)象code2sessionresponse :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package name.ealen.domain.vo; /** * 微信小程序 code2session 接口返回值 對(duì)象 * 具體可以參考小程序官方api說(shuō)明 : https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2session.html */ public class code2sessionresponse { private string openid; private string session_key; private string unionid; private string errcode = "0" ; private string errmsg; private int expires_in; /** * 省略getter/setter */ } |
9. 定義我們的接口信息wxappletcontroller,此例包含一個(gè)登錄獲取token的api和一個(gè)需要認(rèn)證的測(cè)試api :
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
|
package name.ealen.interfaces.facade; import name.ealen.application.wxappletservice; import org.apache.shiro.authz.annotation.requiresauthentication; import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.web.bind.annotation.*; import javax.annotation.resource; import java.util.hashmap; import java.util.map; /** * created by ealenxie on 2018/11/26 10:44. * 小程序后臺(tái) 某 api */ @restcontroller public class wxappletcontroller { @resource private wxappletservice wxappletservice; /** * 微信小程序端用戶登陸api * 返回給小程序端 自定義登陸態(tài) token */ @postmapping ( "/api/wx/user/login" ) public responseentity wxappletloginapi( @requestbody map<string, string> request) { if (!request.containskey( "code" ) || request.get( "code" ) == null || request.get( "code" ).equals( "" )) { map<string, string> result = new hashmap<>(); result.put( "msg" , "缺少參數(shù)code或code不合法" ); return new responseentity<>(result, httpstatus.bad_request); } else { return new responseentity<>(wxappletservice.wxuserlogin(request.get( "code" )), httpstatus.ok); } } /** * 需要認(rèn)證的測(cè)試接口 需要 @requiresauthentication 注解,則調(diào)用此接口需要 header 中攜帶自定義登陸態(tài) authorization */ @requiresauthentication @postmapping ( "/sayhello" ) public responseentity sayhello() { map<string, string> result = new hashmap<>(); result.put( "words" , "hello world" ); return new responseentity<>(result, httpstatus.ok); } } |
10 . 運(yùn)行主類,檢查與數(shù)據(jù)庫(kù)和redis的連接,進(jìn)行測(cè)試 :
1
2
3
4
5
6
7
8
9
10
11
|
package name.ealen; import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.springbootapplication; /** * created by ealenxie on 2018/11/26 10:25. */ @springbootapplication public class shirojwtappletapplication { public static void main(string[] args) { springapplication.run(shirojwtappletapplication. class , args); } |
總結(jié)
以上所述是小編給大家介紹的java中基于shiro,jwt實(shí)現(xiàn)微信小程序登錄完整例子及實(shí)現(xiàn)過(guò)程,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)服務(wù)器之家網(wǎng)站的支持!
原文鏈接:https://www.cnblogs.com/ealenxie/p/10031569.html