基於Shiro,JWT實現微信小程序登陸完整例子

小程序官方流程圖以下,官方地址 : developers.weixin.qq.com/miniprogram… : php

avatar

若是此圖理解不清楚的地方,也可參看個人博客 : www.cnblogs.com/ealenxie/p/…html

本文是對接微信小程序自定義登陸的一個完整例子實現 ,技術棧爲 : SpringBoot+Shiro+JWT+JPA+Redis。java

若是對該例子比較感興趣或者以爲言語表達比較囉嗦,可查看完整的項目地址 : github.com/EalenXie/sh…mysql

主要實現 : 實現了小程序的自定義登錄,將自定義登錄態token返回給小程序做爲登錄憑證。用戶的信息保存在數據庫中,登錄態token緩存在redis中。git

效果以下 :github

1 . 首先從咱們的小程序端調用wx.login() ,獲取臨時憑證code : web

image.png
2 . 模擬使用該code,進行小程序的登錄獲取自定義登錄態 token,用postman進行測試 :
image.png
3 . 調用咱們須要認證的接口,並攜帶該token進行鑑權,獲取到返回信息 :
image.png

前方高能,本例代碼說明較多, 如下是主要的搭建流程 :redis

1 . 首先新建maven項目 shiro-jwt-applet ,pom依賴 ,主要是shiro和jwt的依賴,和SpringBoot的一些基礎依賴。算法

<?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,還有你的數據庫和redisspring

## 請自行修改下面信息
spring:
 application:
 name: shiro-jwt-applet
 jpa:
 hibernate:
 ddl-auto: create      # 請自行修改 請自行修改 請自行修改

# datasource本地配置
 datasource:
 url: jdbc:mysql://localhost:3306/yourdatabase
 username: yourname
 password: yourpass
 driver-class-name: com.mysql.jdbc.Driver

# redis本地配置 請自行配置
 redis:
 database: 0
 host: localhost
 port: 6379

# 微信小程序配置 appid /appsecret
wx:
 applet:
 appid: yourappid
 appsecret: yourappsecret
複製代碼

3 . 定義咱們存儲的微信小程序登錄的實體信息 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. * 實體 屬性描述 這裏只是簡單示例,你能夠自定義相關用戶信息 */
@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 */
}
複製代碼

和一個簡單的dao 訪問數據庫 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<WxAccount, Integer> {
    /** * 根據OpenId查詢用戶信息 */
    WxAccount findByWxOpenid(String wxOpenId);
}
複製代碼

4 . 定義咱們應用的服務說明 WxAppletService :

package name.ealen.application;
import name.ealen.interfaces.dto.Token;
/** * Created by EalenXie on 2018/11/26 10:40. * 微信小程序自定義登錄 服務說明 */
public interface WxAppletService {
    /** * 微信小程序用戶登錄,完整流程可參考下面官方地址,本例中是按此流程開發 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html * 1 . 咱們的微信小程序端傳入code。 * 2 . 調用微信code2session接口獲取openid和session_key * 3 . 根據openid和session_key自定義登錄態(Token) * 4 . 返回自定義登錄態(Token)給小程序端。 * 5 . 咱們的小程序端調用其餘須要認證的api,請在header的Authorization裏面攜帶 token信息 * * @param code 小程序端 調用 wx.login 獲取到的code,用於調用 微信code2session接口 * @return Token 返回後端 自定義登錄態 token 基於JWT實現 */
    public Token wxUserLogin(String code);
}
複製代碼

返回給微信小程序token對象聲明 Token :

package name.ealen.interfaces.dto;
/** * Created by EalenXie on 2018/11/26 18:49. * DTO 返回值token對象 */
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);     //讀取超時時間爲單位爲60秒
        factory.setConnectTimeout(1000 * 10);  //鏈接超時時間設置爲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,並重寫了其鑑權的過濾方法 :

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核心過濾器配置 * 全部的請求都會先通過Filter,因此咱們繼承官方的BasicHttpAuthenticationFilter,而且重寫鑑權的方法。 * 執行流程 preHandle->isAccessAllowed->isLoginAttempt->executeLogin */
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /** * 判斷用戶是否想要進行 須要驗證的操做 * 檢測header裏面是否包含Authorization字段便可 */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String auth = getAuthzHeader(request);
        return auth != null && !auth.equals("");
    }
    /** * 此方法調用登錄,驗證邏輯 */
    @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"));
        // 跨域時會首先發送一個option請求,這裏咱們給option請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}
複製代碼

JWT的核心配置(包含Token的加密建立,JWT續期,解密驗證) :

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 過時時間值 這裏寫死爲和小程序時間一致 7200 秒,也就是兩個小時 */
    private static long expire_time = 7200;
    @Autowired
    private StringRedisTemplate redisTemplate;
    /** * 根據微信用戶登錄信息建立 token * 注 : 這裏的token會被緩存到redis中,用做爲二次驗證 * redis裏面緩存的時間應該和jwt token的過時時間設置相同 * * @param wxAccount 微信用戶信息 * @return 返回 jwt token */
    public String createTokenByWxAccount(WxAccount wxAccount) {
        String jwtId = UUID.randomUUID().toString();                 //JWT 隨機ID,作爲驗證的key
        //1 . 加密算法進行簽名獲得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 配置過時時間的正確姿式
                .sign(algorithm);
        //2 . Redis緩存JWT, 注 : 請和JWT過時時間一致
        redisTemplate.opsForValue().set("JWT-SESSION-" + jwtId, token, expire_time, TimeUnit.SECONDS);
        return token;
    }
    /** * 校驗token是否正確 * 1 . 根據token解密,解密出jwt-id , 先從redis中查找出redisToken,匹配是否相同 * 2 . 而後再對redisToken進行解密,解密成功則 繼續流程 和 進行token續期 * * @param token 密鑰 * @return 返回是否校驗經過 */
    public boolean verifyToken(String token) {
        try {
            //1 . 根據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 正確的配置續期姿式
                    .build();
            //3 . 驗證token
            verifier.verify(redisToken);
            //4 . Redis緩存JWT續期
            redisTemplate.opsForValue().set("JWT-SESSION-" + getJwtIdByToken(token), redisToken, expire_time, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) { //捕捉到任何異常都視爲校驗失敗
            return false;
        }
    }
    /** * 根據Token獲取wxOpenId(注意坑點 : 就算token不正確,也有可能解密出wxOpenId,同下) */
    public String getWxOpenIdByToken(String token) throws JWTDecodeException {
        return JWT.decode(token).getClaim("wxOpenId").asString();
    }
    /** * 根據Token獲取sessionKey */
    public String getSessionKeyByToken(String token) throws JWTDecodeException {
        return JWT.decode(token).getClaim("sessionKey").asString();
    }
    /** * 根據Token 獲取jwt-id */
    private String getJwtIdByToken(String token) throws JWTDecodeException {
        return JWT.decode(token).getClaim("jwt-id").asString();
    }
}
複製代碼

7 . 自定義Shiro的Realm配置,Realm是自定義登錄及受權的邏輯配置 :

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 的一個配置管理類 allRealm()方法獲得全部的realm */
@Component
public class ShiroRealmConfig {
    @Resource
    private JwtConfig jwtConfig;
    /** * 配置全部自定義的realm,方便起見,應對可能有多個realm的狀況 */
    public List<Realm> allRealm() {
        List<Realm> realmList = new LinkedList<>();
        AuthorizingRealm jwtRealm = jwtRealm();
        realmList.add(jwtRealm);
        return Collections.unmodifiableList(realmList);
    }
    /** * 自定義 JWT的 Realm * 重寫 Realm 的 supports() 方法是經過 JWT 進行登陸判斷的關鍵 */
    private AuthorizingRealm jwtRealm() {
        AuthorizingRealm jwtRealm = new AuthorizingRealm() {
            /** * 注意坑點 : 必須重寫此方法,否則Shiro會報錯 * 由於建立了 JWTToken 用於替換Shiro原生 token,因此必須在此方法中顯式的進行替換,不然在進行判斷時會一直失敗 */
            @Override
            public boolean supports(AuthenticationToken token) {
                return token instanceof JwtToken;
            }
            @Override
            protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
                return new SimpleAuthorizationInfo();
            }
            /** * 校驗 驗證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;
    }
    /** * 注意坑點 : 密碼校驗 , 這裏由於是JWT形式,就無需密碼校驗和加密,直接讓其返回爲true(若是不設置的話,該值默認爲false,即始終驗證不經過) */
    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,安全管理器,全部與安全相關的操做都會與之進行交互; * 它管理着全部Subject,全部Subject都綁定到SecurityManager,與Subject的全部交互都會委託給SecurityManager * DefaultWebSecurityManager : * 會建立默認的DefaultSubjectDAO(它又會默認建立DefaultSessionStorageEvaluator) * 會默認建立DefaultWebSubjectFactory * 會默認建立ModularRealmAuthenticator */
    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRealmConfig shiroRealmConfig) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealms(shiroRealmConfig.allRealm());     //設置realm
        DefaultSubjectDAO subjectDAO = (DefaultSubjectDAO) securityManager.getSubjectDAO();
        // 關閉自帶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();
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        Map<String, String> filterRuleMap = new HashMap<>();
        //登錄相關api不須要被過濾器攔截
        filterRuleMap.put("/api/wx/user/login/**", "anon");
        filterRuleMap.put("/api/response/**", "anon");
        // 全部請求經過JWT Filter
        filterRuleMap.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }
    /** * 添加註解支持 */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); // 強制使用cglib,防止重複代理和可能引發代理出錯的問題
        return defaultAdvisorAutoProxyCreator;
    }
    /** * 添加註解依賴 */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    /** * 開啓註解驗證 */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}
複製代碼

用於Shiro鑑權的JwtToken對象 :

package name.ealen.domain.vo;
import org.apache.shiro.authc.AuthenticationToken;
/** * Created by EalenXie on 2018/11/22 18:21. * 鑑權用的token vo ,實現 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 . 實現實體的行爲及業務邏輯,此例主要是調用微信接口code2session和建立返回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. * 實體 行爲描述 */
@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";
        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();
    }
    /** * 微信小程序用戶登錄,完整流程可參考下面官方地址,本例中是按此流程開發 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html * @param code 小程序端 調用 wx.login 獲取到的code,用於調用 微信code2session接口 * @return 返回後端 自定義登錄態 token 基於JWT實現 */
    @Override
    public Token wxUserLogin(String code) {
        //1 . code2session返回JSON數據
        String resultJson = code2Session(code);
        //2 . 解析數據
        Code2SessionResponse response = JSONUtil.jsonString2Object(resultJson, Code2SessionResponse.class);
        if (!response.getErrcode().equals("0"))
            throw new AuthenticationException("code2session失敗 : " + response.getErrmsg());
        else {
            //3 . 先從本地數據庫中查找用戶是否存在
            WxAccount wxAccount = wxAccountRepository.findByWxOpenid(response.getOpenid());
            if (wxAccount == null) {
                wxAccount = new WxAccount();
                wxAccount.setWxOpenid(response.getOpenid());    //不存在就新建用戶
            }
            //4 . 更新sessionKey和 登錄時間
            wxAccount.setSessionKey(response.getSession_key());
            wxAccount.setLastTime(new Date());
            wxAccountRepository.save(wxAccount);
            //5 . JWT 返回自定義登錄態 Token
            String token = jwtConfig.createTokenByWxAccount(wxAccount);
            return new Token(token);
        }
    }
}
複製代碼

小程序code2session接口的返回VO對象Code2SessionResponse :

package name.ealen.domain.vo;
/** * 微信小程序 Code2Session 接口返回值 對象 * 具體能夠參考小程序官方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,此例包含一個登陸獲取token的api和一個須要認證的測試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. * 小程序後臺 某 API */
@RestController
public class WxAppletController {
    @Resource
    private WxAppletService wxAppletService;
    /** * 微信小程序端用戶登錄api * 返回給小程序端 自定義登錄態 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", "缺乏參數code或code不合法");
            return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
        } else {
            return new ResponseEntity<>(wxAppletService.wxUserLogin(request.get("code")), HttpStatus.OK);
        }
    }
    /** * 須要認證的測試接口 須要 @RequiresAuthentication 註解,則調用此接口須要 header 中攜帶自定義登錄態 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 . 運行主類,檢查與數據庫和redis的鏈接,進行測試 :

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實現微信小程序登陸完整例子的邏輯過程說明及其實現。
原創不易,轉載請註明出處,十分感謝各位支持和提出意見。

相關文章
相關標籤/搜索