1.微信小程序登录的流程
根据官网给的流程图,可以知道微信小程序的登录流程如下:
- 小程序前端通过wx.login()获取到code值,将code值传递给后端服务器
- 后端服务程序携带小程序的appid+appsecrete+code以及grant_type=authorization_code向微信接口服务发起请求,实例url为
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
- 拿到返回的openid和session_key后,小程序的后端服务程序应该返回给一个自定义登录状态,在本次项目中,我们返回的是一个jwt 的 token
- 随后小程序前端,就可以携带token来与后端服务程序进行数据请求了
微信小程序前端获取code值的示例为:
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
2.创建后端项目
直接创建一个springboot
项目,然后导入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
以上是项目所必须的依赖,包括jpa、security、jwt等。
相关配置
由于需要连接数据库和使用jpa,配置文件要进行相应配置,这里把用到的全部配置都放到这里
server:
port: 8081
spring:
application:
name: wx-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wxserver?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
wx:
appId: wxxxxxxxxxxxxx
appSecret: 1cxxxxxxxxxxx
loginUrl: https://api.weixin.qq.com/sns/jscode2session
jwt:
secretKey: wxminiappsecretkey
expirationTime: 3600000
其中wx:开头的配置,都是在请求微信接口要用到的相关数据,jwt开头的配置是生成jwt的token是使用的密钥和过期时间。
创建一个jwt的工具类,用于生成和解析jwt的token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtil {
private static String SECRET_KEY;
private static long EXPIRATION_TIME;
@Value("${jwt.secretKey}")
public void setSecretKey(String secretKey) {
SECRET_KEY = secretKey;
}
@Value("${jwt.expirationTime}")
public void setExpirationTime(long expirationTime) {
EXPIRATION_TIME = expirationTime;
}
// 生成token
public static String generateToken(String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME); // 1小时后过期,3600000
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 解析token
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
创建一个JwtAuthenticationFilter
类,用于在每个请求中检查JWT
import com.ycc.config.JwtAuthenticationToken;
import com.ycc.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取token
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7); // 去掉"Bearer "前缀
try {
Claims claims = JwtUtil.parseToken(token);
if (claims.getExpiration().before(new Date())) { // 判断token是否过期
throw new RuntimeException("Token expired");
}
// 生成Authentication对象,保存到SecurityContextHolder中
Authentication authentication = new JwtAuthenticationToken(claims);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
throw new RuntimeException("Token expired");
}
}
filterChain.doFilter(request, response);
}
}
创建一个JwtAuthenticationToken
类,用于表示一个通过JWT认证的用户。
import org.springframework.security.authentication.AbstractAuthenticationToken;
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public JwtAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return principal;
}
}
创建微信认证的接口,并调用wxLoginService
import com.ycc.service.WxLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class WxLoginController {
@Autowired
private WxLoginService wxLoginService;
@PostMapping("/wxlogin")
public String wxLogin(@RequestBody Map<String, String> req) {
return wxLoginService.wxLogin(req);
}
}
相应的WxLoginService的实现类来实现具体的远程调用微信接口获取信息,并验证用户是否是新用户,最后返回token
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ycc.dao.UserRepository;
import com.ycc.entity.User;
import com.ycc.service.WxLoginService;
import com.ycc.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
public class WxLoginServiceImpl implements WxLoginService {
@Value("${wx.appId}")
private String appId;
@Value("${wx.appSecret}")
private String appSecret;
@Value("${wx.loginUrl}")
private String wxLoginUrl;
@Autowired
private UserRepository userRepository;
@Override
public String wxLogin(Map<String, String> req) {
String code = req.get("code");
String url = wxLoginUrl + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
// 解析微信服务器的响应
String responseBody = response.getBody();
//提取openid和session_key
Map<String, String> wxResponse = parseWxResponse(responseBody);
String openid = wxResponse.get("openid");
String session_key = wxResponse.get("session_key");
// 检查数据库中是否已经有一个与这个openid关联的用户
User user = userRepository.findByOpenid(openid);
if (user == null) {
// 如果没有,创建一个新的用户
user = new User();
user.setOpenid(openid);
user.setPassword("123456");
}
// 更新用户的session_key
user.setSessionKey(session_key);
// 保存用户
User save = userRepository.save(user);
// 1.会话密钥 session_key 是对用户数据进行 加密签名 的密钥。
// 为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
// 2.临时登录凭证 code 只能使用一次,5分钟未被使用自动过期。
// 根据用户的openId生成token返回给前端
return JwtUtil.generateToken(save.getOpenid());
}
private Map<String, String> parseWxResponse(String responseBody) {
Map<String, String> wxResponse = new HashMap<>();
JSONObject jsonObject = JSON.parseObject(responseBody);
wxResponse.put("openid", jsonObject.getString("openid"));
wxResponse.put("session_key", jsonObject.getString("session_key"));
return wxResponse;
}
}
最后就是创建一个SecurityConfig
类,用于配置Spring Security。
import com.ycc.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// 设置会话管理策略为无状态(STATELESS)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().disable() // 禁用HTTP Basic认证
// 添加自定义的JwtAuthenticationFilter
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/wxlogin").permitAll() // 开发微信登录接口,不需要认证
.anyRequest().authenticated();
}
}
3.测试
测试微信登录
携带从微信小程序前端获取到的code值,来请求登录接口。成功的实现登录,获取到token
测试接待token请求测试接口,成功的请求到了测试接口的返回值。
项目地址:https://gitee.com/yc59717/wx-server
欢迎给个星鼓励一下