Java中基于Shiro,JWT实现微信小程序登录完整例子及实现过程

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

本文是对接微信小程序自定义登录的一个完整例子实现 ,技术栈为 : SpringBoot+Shiro+JWT+JPA+Redis。

如果对该例子比较感兴趣或者觉得言语表达比较啰嗦,可查看完整的项目地址 : https://github.com/EalenXie/shiro-jwt-applet

主要实现 : 实现了小程序的自定义登陆,将自定义登陆态token返回给小程序作为登陆凭证。用户的信息保存在数据库中,登陆态token缓存在redis中。

效果如下 :

1 . 首先从我们的小程序端调用wx.login() ,获取临时凭证code :

2 . 模拟使用该code,进行小程序的登陆获取自定义登陆态 token,用postman进行测试 :

3 . 调用我们需要认证的接口,并携带该token进行鉴权,获取到返回信息  :

前方高能,本例代码说明较多, 以下是主要的搭建流程 :

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,还有你的数据库和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 访问数据库 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的配置。本例是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);
 }

总结

以上所述是小编给大家介绍的Java中基于Shiro,JWT实现微信小程序登录完整例子及实现过程,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 微信小程序教程系列之页面跳转和参数传递(6)

    关于页面的跳转,微信小程序提供了3种方法: 方法一: 使用API  wx.navigateTo()函数 示例: 首先先新建一个test页面 如何新建页面? 请到先阅读下面教程 微信小程序的新建页面 -- 微信小程序教程系列(4) index.wxml: 在index.wxml新建一个button组件,并使用bindtap事件绑定一个函数 index.js: 在index.js中的Page函数内部,添加changeToTest 函数,函数里面使用wx.navigateTo,写上需要跳转的页面,里面

  • 微信小程序购物车、父子组件传值及calc的注意事项总结

    前言 在做微信小程序时,觉得小组里对购物车的实现不是很完美,就自己尝试的写了下,然后用到了父子组件传值,父子组件传值的话,和vue框架上是非常相似的,以及calc这个css函数,calc有个注意点,自己不怎么用,一时间有差点忘了,这里记录下 下面话不多说了,来一起看看详细的介绍吧 1.效果图 2.子组件实现 要实现图中删除的效果,使用组件的形式更好做点,我当时本想直接在pages里实现,不过结果就是,滑动时,所有的商品都显示了删除按钮,除非用数组将每个商品要移动的距离存储起来,不过这样的话就很麻

  • 微信小程序自定义组件封装及父子间组件传值的方法

    首先在我们可以直接写到需要的 page 中,然后再进行抽取组件,自定义组件建议 wxzx-xxx 命名 官网地址:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/ 例如,我们封装的组件名为 **wxzx-loadmore wxzx-loadmore.wxml <view hidden="{{response.length < 1}}"> <view class

  • 微信小程序--onShareAppMessage分享参数用处(页面分享)

    今天下午突然听到群里有人说微信小程序工具更新了,文档也更新了不少内容. 顾不上吃冬至的饺子.我就冲进来了. 先说分享功能,目前真机尚不能调试.开发工具上可以看看效果.后续还会更新. Page()中加上如下代码后在右上角就会出现三个小白点 title:分享的标题. desc:分享一段描述. path:这个参数有点意思.以前在微信中的分享一般都是url.这里是当前页面这里应该是pages/index?id=123这里的id目前还不知道是什么. 也就是说以后你可以在微信中像分享一个网页一样分享一个页面

  • 微信小程序中子页面向父页面传值实例详解

    微信小程序中子页面向父页面传值实例详解 上面一张图是编辑款项页面,下面一张图是点击了编辑款项页面中选择好友的图标后打开的子页面.这个时候点选子页面的某个好友时,需要把好友的名字传递回编辑款项父页面. 采取的方法: 从页面路由栈中直接获取和操作目标Page对象,这种方式,是通过调用小程序的API: getCurrentPages(),来获取当前页面路由栈的信息,这个路由栈中按照页面的路由顺序存放着相应的Page对象,我们可以很容易的获取到上一级页面的完整Page对象,从而使直接调用Page对象的属

  • 微信小程序实现带参数的分享功能(两种方法)

    微信小程序分享功能的实现方法有两种: 第一种 在page.js中实现onShareAppMessage,便可在小程序右上角选择分享该页面 onShareAppMessage: function () { return { title: '弹出分享时显示的分享标题', desc: '分享页面的内容', path: '/page/user?id=123' // 路径,传递参数到指定页面. } } 第二种 自定义按钮实现分享,在page中添加一个带有open-type='share'的button标签

  • 微信小程序 子级页面返回父级并把子级参数带回父级实现方法

    说到页面之间的跳转,跳转中顺带些参数,在程序猿的生活中是很常用的,下面就让我们来看看吧! 这里有两种方法来解决: 方法一 就是我们常用的本地储存,在当前子级页面用( wx.setStorage || wx.setStorageSync )储存好,跳转到父级页面的时候取出,采用( wx.getStorage || wx.getStorageSync ),在这里,退出的时候一定要记得清除缓存哦!!!( wx.clearStorage || wx.clearStorageSync ) 方法二 方法二就

  • 微信小程序之页面跳转和参数传递的实现

    微信小程序之页面跳转和参数传递的实现 前言: 在微信小程序里面的跳转其实和html里的超链接a差不多,我们实现跳转可以通过标签实现,也可以通过js实现,下面一一演示给大家看一下. 在展示demo前,我们需要先简单的建好项目文件夹做好准备.如下: 标签实现 小程序里面有一个类似于a标签的navigator标签,用来做跳转处理. index页面: <navigator url="../navigator/navigator?title=我是navi">跳转到新的页面</n

  • Java中基于Shiro,JWT实现微信小程序登录完整例子及实现过程

    小程序官方流程图如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html : 本文是对接微信小程序自定义登录的一个完整例子实现 ,技术栈为 : SpringBoot+Shiro+JWT+JPA+Redis. 如果对该例子比较感兴趣或者觉得言语表达比较啰嗦,可查看完整的项目地址 : https://github.com/EalenXie/shiro-jwt-applet

  • Spring Boot 2结合Spring security + JWT实现微信小程序登录

    项目源码:https://gitee.com/tanwubo/jwt-spring-security-demo 登录 通过自定义的WxAppletAuthenticationFilter替换默认的UsernamePasswordAuthenticationFilter,在UsernamePasswordAuthenticationFilter中可任意定制自己的登录方式. 用户认证 需要结合JWT来实现用户认证,第一步登录成功后如何颁发token. public class CustomAuthe

  • springboot+jwt+springSecurity微信小程序授权登录问题

    场景重现:1.微信小程序向后台发送请求 --而后台web采用的springSecuriry没有token生成,就会拦截请求,,所以小编记录下这个问题 微信小程序授权登录问题 思路 参考网上一大堆资料 核心关键字: 自定义授权+鉴权 (说的通俗就是解决办法就是改造springSecurity的过滤器) 参考文章 https://www.jb51.net/article/204704.htm 总的来说的 通过自定义的WxAppletAuthenticationFilter替换默认的UsernameP

  • 基于angular实现模拟微信小程序swiper组件

    这段时间的主业是完成一个家政类小程序,终于是过审核发布了.不得不说微信的这个小程序生态还是颇有想法的,抛开他现有的一些问题不说,其提供的组件系统乍一看还是蛮酷的.比如其提供的一个叫swiper的视图组件,就可以在写界面的时候省不少时间和代码,轮播图片跟可滑动列表都可以用.导致现在回来写angular项目时也想整一个这样的组件出来,本文就将使用angular的组件能力和服务能力完成这么一个比较通用,耦合度较低的swiper出来. 首先要选择使用的技术,要实现的是与界面打交道的东西,自然是实现成一个

  • java实现微信小程序登录态维护的示例代码

    相信不少喜欢开发的朋友都已经知道微信小程序是个什么物种了,楼主也是从小程序内测期间就开始关注,并且也写过几个已经上线的微信小程序.但是基本上都是写的纯前端,最近楼主从后端到前端写一个完整的小程序项目,中间碰到了一些问题,楼主会找一些个人觉得有学习价值的点不定时的拿出来跟大家分享,希望对你有一些帮助. 本次就从最基本的微信小程序登录态维护开始吧.小程序官方api文档里面有对登录态的一个完整的解释,并且有相关的代码.想看详情,可以出门右转:https://mp.weixin.qq.com/debug

  • 微信小程序登录对接Django后端实现JWT方式验证登录详解

    先上效果图 点击授权按钮后可以显示部分资料和头像,点击修改资料可以修改部分资料. 流程 1.使用微信小程序登录和获取用户信息Api接口 2.把Api获取的用户资料和code发送给django后端 3.通过微信接口把code换取成openid 4.后端将openid作为用户名和密码 5.后端通过JSON web token方式登录,把token和用户id传回小程序 6.小程序将token和用户id保存在storage中 下次请求需要验证用户身份的页面时,在header中加入token这个字段 微信

  • 微信小程序登录时如何获取input框中的内容

    这篇文章主要介绍了微信小程序登录时如何获取input框中的内容,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 最近写小程序项目遇到一些问题,今天整理下这些问题的解决方法,希望对用户有帮助.下面是登录页,点击登录时获取input框中的值, 效果如下: wxml布局如下: <view > <input type="text" placeholder-style="color:#fff;" bindin

  • 基于thinkphp5框架实现微信小程序支付 退款 订单查询 退款查询操作

    微信小程序或微信支付相关操作支付退款订单查询退款查询支付成功,进行回调退款成功 进行回调用到的方法 支付 /** * 预支付请求接口(POST) * @param string $openid openid * @param string $body 商品简单描述 * @param string $order_sn 订单编号 * @param string $total_fee 金额 * @return json的数据 */ public function prepay() { tp_log('

  • java实现基于UDP协议的聊天小程序操作

    UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议.它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去! UDP适用于一次只传送少量数据.对可靠性要求不高的应用环境.正因为UDP协议没有连接的过程,所以它的通信效率高:但也正因为如此,它的可靠性不如TCP协议高.QQ就使用UDP发消息,因此有时会出现收不到消息的情况. 利用UDP协议的发送和接收,模拟聊天小程序 创建聊天程序的A端: 1.发送信息到接收端                 1 .准

  • 微信小程序登录态控制深入分析

    微信小程序登录态控制深入分析 最近微信小程序终于开放了个人注册,我当然不能浪费这个炫技的好机会,"菲麦日程"小程序正在全力推进中,尽请期待~~ 在登录态控制中,摸索尝试了小一阵子,特此分享 一.微信建议的登录态控制 说明: 1)小程序内通过wx.login接口获得code 2)将code传入后台,后台对微信服务器发起一个https请求换取openid.session_key 3)后台生成一个自身的3rd_session(以此为key值保持openid和session_key),返回给前

随机推荐