Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程

小程序官方流程圖如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html :

創(chuàng)新互聯(lián)建站長期為上千家客戶提供的網(wǎng)站建設服務,團隊從業(yè)經(jīng)驗10年,關注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為下冶企業(yè)提供專業(yè)的網(wǎng)站建設、成都網(wǎng)站設計,下冶網(wǎng)站改版等技術服務。擁有10年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。

Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程

本文是對接微信小程序自定義登錄的一個完整例子實現(xiàn) ,技術棧為 : SpringBoot+Shiro+JWT+JPA+redis。

如果對該例子比較感興趣或者覺得言語表達比較啰嗦,可查看完整的項目地址 : https://github.com/EalenXie/shiro-jwt-applet

主要實現(xiàn) : 實現(xiàn)了小程序的自定義登陸,將自定義登陸態(tài)token返回給小程序作為登陸憑證。用戶的信息保存在數(shù)據(jù)庫中,登陸態(tài)token緩存在redis中。

效果如下 :

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

Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程

2 . 模擬使用該code,進行小程序的登陸獲取自定義登陸態(tài) token,用postman進行測試 :

Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程

3 . 調(diào)用我們需要認證的接口,并攜帶該token進行鑒權,獲取到返回信息  :

Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程

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

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,還有你的數(shù)據(jù)庫和redis

## 請自行修改下面信息
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 訪問數(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<WxAccount, Integer> {
 /**
 * 根據(jù)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 {
 /**
 * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開發(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)用其他需要認證的api,請在header的Authorization里面攜帶 token信息
 *
 * @param code 小程序端 調(diào)用 wx.login 獲取到的code,用于調(diào)用 微信code2session接口
 * @return Token 返回后端 自定義登陸態(tài) token 基于JWT實現(xiàn)
 */
 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的配置。本例是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核心過濾器配置
 * 所有的請求都會先經(jīng)過Filter,所以我們繼承官方的BasicHttpAuthenticationFilter,并且重寫鑒權的方法。
 * 執(zhí)行流程 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("");

 }
 /**
 * 此方法調(diào)用登陸,驗證邏輯
 */
 @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"));
 // 跨域時會首先發(fā)送一個option請求,這里我們給option請求直接返回正常狀態(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ù)期,解密驗證) :

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;
 /**
 * 根據(jù)微信用戶登陸信息創(chuàng)建 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 . 根據(jù)token解密,解密出jwt-id , 先從redis中查找出redisToken,匹配是否相同
 * 2 . 然后再對redisToken進行解密,解密成功則 繼續(xù)流程 和 進行token續(xù)期
 *
 * @param token 密鑰
 * @return 返回是否校驗通過
 */
 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ù)期姿勢
   .build();
  //3 . 驗證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) { //捕捉到任何異常都視為校驗失敗
  return false;
 }
 }
 /**
 * 根據(jù)Token獲取wxOpenId(注意坑點 : 就算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是自定義登陸及授權的邏輯配置 :

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會報錯
  * 因為創(chuàng)建了 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 :
 * 會創(chuàng)建默認的DefaultSubjectDAO(它又會默認創(chuàng)建DefaultSessionStorageEvaluator)
 * 會默認創(chuàng)建DefaultWebSubjectFactory
 * 會默認創(chuàng)建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 ,實現(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 . 實現(xiàn)實體的行為及業(yè)務邏輯,此例主要是調(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.
 * 實體 行為描述
 */
@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();
 }
 /**
 * 微信小程序用戶登陸,完整流程可參考下面官方地址,本例中是按此流程開發(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實現(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 . 先從本地數(shù)據(jù)庫中查找用戶是否存在
  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 返回自定義登陸態(tài) 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
 * 返回給小程序端 自定義登陸態(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);
 }
 }
 /**
 * 需要認證的測試接口 需要 @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 . 運行主類,檢查與數(shù)據(jù)庫和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);
 }

總結

以上所述是小編給大家介紹的Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對創(chuàng)新互聯(lián)網(wǎng)站的支持!

新聞名稱:Java中基于Shiro,JWT實現(xiàn)微信小程序登錄完整例子及實現(xiàn)過程
本文網(wǎng)址:http://muchs.cn/article6/jepiog.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供外貿(mào)網(wǎng)站建設、App開發(fā)、定制網(wǎng)站、網(wǎng)站設計公司、面包屑導航、網(wǎng)站內(nèi)鏈

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

h5響應式網(wǎng)站建設