栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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的一些基础依赖。



 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,还有你的数据库和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;

@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;
 
}

  和一个简单的dao 访问数据库 WxAccountRepository :

package name.ealen.domain.repository;
import name.ealen.domain.entity.WxAccount;
import org.springframework.data.jpa.repository.JpaRepository;

public interface WxAccountRepository extends JpaRepository {
 
 WxAccount findByWxOpenid(String wxOpenId);
}

4 . 定义我们应用的服务说明 WxAppletService :

package name.ealen.application;
import name.ealen.interfaces.dto.Token;

public interface WxAppletService {
 
 public Token wxUserLogin(String code);
}

   返回给微信小程序token对象声明 Token :

package name.ealen.interfaces.dto;

public class Token {
 private String token;
 public Token(String token) {
 this.token = token;
 }
 
}

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;

@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;

@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;

public class JwtFilter extends BasicHttpAuthenticationFilter {

 
 @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;

@Component
public class JwtConfig {
 
 private static final String SECRET_KEY = "5371f568a45e5ab1f442c38e0932aef24447139b";
 
 private static long expire_time = 7200;
 @Autowired
 private StringRedisTemplate redisTemplate;
 
 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;
 }
 
 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;
 }
 }
 
 public String getWxOpenIdByToken(String token) throws JWTDecodeException {
 return JWT.decode(token).getClaim("wxOpenId").asString();
 }
 
 public String getSessionKeyByToken(String token) throws JWTDecodeException {
 return JWT.decode(token).getClaim("sessionKey").asString();
 }
 
 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;

@Component
public class ShiroRealmConfig {
 @Resource
 private JwtConfig jwtConfig;
 
 public List allRealm() {
 List realmList = new linkedList<>();
 AuthorizingRealm jwtRealm = jwtRealm();
 realmList.add(jwtRealm);
 return Collections.unmodifiableList(realmList);
 }
 
 private AuthorizingRealm jwtRealm() {
 AuthorizingRealm jwtRealm = new AuthorizingRealm() {
  
  @Override
  public boolean supports(AuthenticationToken token) {
  return token instanceof JwtToken;
  }
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  return new SimpleAuthorizationInfo();
  }
  
  @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;
 }
 
 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;

@Configuration
public class ShirConfig {
 
 @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;
 }
 
 @Bean
 public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
 ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
 Map filterMap = new HashMap<>();
 filterMap.put("jwt", new JwtFilter());
 factoryBean.setFilters(filterMap);
 factoryBean.setSecurityManager(securityManager);
 Map filterRuleMap = new HashMap<>();
 //登陆相关api不需要被过滤器拦截
 filterRuleMap.put("/api/wx/user/login
 @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;

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;

@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;
 
 private String code2Session(String jsCode) {
 String code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
 MultiValueMap 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(new HttpHeaders()), String.class).getBody();
 }
 
 @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;

public class Code2SessionResponse {
 private String openid;
 private String session_key;
 private String unionid;
 private String errcode = "0";
 private String errmsg;
 private int expires_in;
 
}

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;

@RestController
public class WxAppletController {
 @Resource
 private WxAppletService wxAppletService;
 
 @PostMapping("/api/wx/user/login")
 public ResponseEntity wxAppletLoginApi(@RequestBody Map request) {
 if (!request.containsKey("code") || request.get("code") == null || request.get("code").equals("")) {
  Map 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
 @PostMapping("/sayHello")
 public ResponseEntity sayHello() {
 Map 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;

@SpringBootApplication
public class ShiroJwtAppletApplication {
 public static void main(String[] args) {
 SpringApplication.run(ShiroJwtAppletApplication.class, args);
 }

总结

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/139583.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号