摘要:小程序官方流程圖如下,官方地址如果此圖理解不清楚的地方也可參看我的博客本文是對(duì)接微信小程序自定義登錄的一個(gè)完整例子實(shí)現(xiàn),技術(shù)棧為。調(diào)用微信接口獲取和根據(jù)和自定義登陸態(tài)返回自定義登陸態(tài)給小程序端。
小程序官方流程圖如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html :
如果此圖理解不清楚的地方,也可參看我的博客 : https://www.cnblogs.com/ealenxie/p/9888064.html
本文是對(duì)接微信小程序自定義登錄的一個(gè)完整例子實(shí)現(xiàn) ,技術(shù)棧為 : SpringBoot+Shiro+JWT+JPA+Redis。
如果對(duì)該例子比較感興趣或者覺得言語表達(dá)比較啰嗦,可查看完整的項(xiàng)目地址 : https://github.com/EalenXie/shiro-jwt-applet
主要實(shí)現(xiàn) : 實(shí)現(xiàn)了小程序的自定義登陸,將自定義登陸態(tài)token返回給小程序作為登陸憑證。用戶的信息保存在數(shù)據(jù)庫中,登陸態(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),獲取到返回信息 :
前方高能,本例代碼說明較多, 以下是主要的搭建流程 :
1 . 首先新建maven項(xiàng)目 shiro-jwt-applet ,pom依賴 ,主要是shiro和jwt的依賴,和SpringBoot的一些基礎(chǔ)依賴。
4.0.0 name.ealen shiro-jwt-applet 0.0.1-SNAPSHOT jar shiro-wx-jwt Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 2.0.6.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java org.apache.shiro shiro-spring 1.4.0 com.auth0 java-jwt 3.4.1 com.alibaba fastjson 1.2.47 org.springframework.boot spring-boot-maven-plugin
2 . 配置你的application.yml ,主要是配置你的小程序appid和secret,還有你的數(shù)據(jù)庫和redis
## 請(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 :
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í)體 屬性描述 這里只是簡單示例,你可以自定義相關(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è)簡單的dao 訪問數(shù)據(jù)庫 WxAccountRepository :
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{ /** * 根據(jù)OpenId查詢用戶信息 */ WxAccount findByWxOpenid(String wxOpenId); }
4 . 定義我們應(yīng)用的服務(wù)說明 WxAppletService :
package name.ealen.application; import name.ealen.interfaces.dto.Token; /** * Created by EalenXie on 2018/11/26 10:40. * 微信小程序自定義登陸 服務(wù)說明 */ public interface WxAppletService { /** * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開發(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 :
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:
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的CacheManager配置。本例是Springboot2.0的寫法(和1.8的版本寫法略有不同) :
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的核心過濾器配置。繼承了Shiro的BasicHttpAuthenticationFilter,并重寫了其鑒權(quán)的過濾方法 :
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核心過濾器配置 * 所有的請(qǐng)求都會(huì)先經(jīng)過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)證) :
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 過期時(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的過期時(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 配置過期時(shí)間的正確姿勢(shì) .sign(algorithm); //2 . Redis緩存JWT, 注 : 請(qǐng)和JWT過期時(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)通過 */ 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)的邏輯配置 :
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,方便起見,應(yīng)對(duì)可能有多個(gè)realm的情況 */ public ListallRealm() { List realmList = new LinkedList<>(); AuthorizingRealm jwtRealm = jwtRealm(); realmList.add(jwtRealm); return Collections.unmodifiableList(realmList); } /** * 自定義 JWT的 Realm * 重寫 Realm 的 supports() 方法是通過 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形式,就無需密碼校驗(yàn)和加密,直接讓其返回為true(如果不設(shè)置的話,該值默認(rèn)為false,即始終驗(yàn)證不通過) */ private CredentialsMatcher credentialsMatcher() { return (token, info) -> true; } }
Shiro的核心配置,包含配置Realm :
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的訪問策略 */ @Bean public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); MapfilterMap = new HashMap<>(); filterMap.put("jwt", new JwtFilter()); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); Map filterRuleMap = new HashMap<>(); //登陸相關(guān)api不需要被過濾器攔截 filterRuleMap.put("/api/wx/user/login/**", "anon"); filterRuleMap.put("/api/response/**", "anon"); // 所有請(qǐng)求通過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ò)的問題 return defaultAdvisorAutoProxyCreator; } /** * 添加注解依賴 */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 開啟注解驗(yàn)證 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
用于Shiro鑒權(quán)的JwtToken對(duì)象 :
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 :
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 接口 獲取微信用戶信息 * 官方說明 : 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"; MultiValueMapparams = 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 (new HttpHeaders()), String.class).getBody(); } /** * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開發(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ù)庫中查找用戶是否存在 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 :
package name.ealen.domain.vo; /** * 微信小程序 Code2Session 接口返回值 對(duì)象 * 具體可以參考小程序官方API說明 : 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 :
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 Maprequest) { if (!request.containsKey("code") || request.get("code") == null || request.get("code").equals("")) { Map 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 result = new HashMap<>(); result.put("words", "hello World"); return new ResponseEntity<>(result, HttpStatus.OK); } }
10 . 運(yùn)行主類,檢查與數(shù)據(jù)庫和redis的連接,進(jìn)行測(cè)試 :
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); } }
以上,就是基于Shiro,JWT實(shí)現(xiàn)微信小程序登錄完整例子的邏輯過程說明及其實(shí)現(xiàn)。
原創(chuàng)不易,轉(zhuǎn)載請(qǐng)注明出處,十分感謝各位支持和提出意見。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/72493.html
摘要:框架具有輕便,開源的優(yōu)點(diǎn),所以本譯見構(gòu)建用戶管理微服務(wù)五使用令牌和來實(shí)現(xiàn)身份驗(yàn)證往期譯見系列文章在賬號(hào)分享中持續(xù)連載,敬請(qǐng)查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務(wù)邏輯數(shù)據(jù)訪問層和前端控制器但是忽略了對(duì)身份進(jìn)行驗(yàn)證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護(hù)REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:參考鏈接微信小程序七日談第五天你可能要在登錄功能上花費(fèi)大力氣理解認(rèn)證及實(shí)踐網(wǎng)站微信登錄實(shí)現(xiàn)最后,感謝女朋友支持。 開發(fā)微信小程序時(shí),接入小程序的授權(quán)登錄可以快速實(shí)現(xiàn)用戶注冊(cè)登錄的步驟,是快速建立用戶體系的重要一步。這篇文章將介紹 python + sanic + 微信小程序?qū)崿F(xiàn)用戶快速注冊(cè)登錄全棧方案。 微信小程序登錄時(shí)序圖如下: showImg(https://segmentfaul...
摘要:參考鏈接微信小程序七日談第五天你可能要在登錄功能上花費(fèi)大力氣理解認(rèn)證及實(shí)踐網(wǎng)站微信登錄實(shí)現(xiàn)最后,感謝女朋友支持。 開發(fā)微信小程序時(shí),接入小程序的授權(quán)登錄可以快速實(shí)現(xiàn)用戶注冊(cè)登錄的步驟,是快速建立用戶體系的重要一步。這篇文章將介紹 python + sanic + 微信小程序?qū)崿F(xiàn)用戶快速注冊(cè)登錄全棧方案。 微信小程序登錄時(shí)序圖如下: showImg(https://segmentfaul...
摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫的前端管理。 自己在前后端分離上的實(shí)踐 要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshir...
摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫的前端管理。 自己在前后端分離上的實(shí)踐 要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshir...
閱讀 1993·2021-11-24 10:45
閱讀 1849·2021-10-09 09:43
閱讀 1291·2021-09-22 15:38
閱讀 1219·2021-08-18 10:19
閱讀 2837·2019-08-30 15:55
閱讀 3056·2019-08-30 12:45
閱讀 2960·2019-08-30 11:25
閱讀 356·2019-08-29 11:30