微信小程序的用户注册登录通常是通过微信提供的登录凭证 code 进行实现的。具体步骤如下:
- 用户在小程序中点击登录按钮,小程序调用
wx.login
方法,获取到用户登录凭证 code。 - 小程序将该 code 发送给开发者服务器。
- 开发者服务器通过微信提供的接口,使用该 code 换取用户的唯一标识 OpenID 和会话密钥 SessionKey。
- 开发者服务器根据 OpenID 进行用户识别,判断用户是否已经注册,如果未注册则进行注册,如果已注册则直接登录。
- 开发者服务器生成一个用户唯一标识符,如用户ID,并将该标识符与用户在该次会话中的登录状态关联。
- 开发者服务器生成一个登录态 Token,可以使用 JWT(JSON Web Token)或其他方式生成,将该 Token 发送给小程序。
- 小程序收到 Token 后,将其保存在本地,作为用户的登录状态凭证。
- 小程序之后的请求可以携带该 Token,开发者服务器通过验证 Token 的有效性来判断用户的登录状态,并进行相应的操作。
JWT(JSON Web Token)是一种用于在网络上传输信息的基于 JSON 的开放标准(RFC 7519),主要用于在用户和服务器之间传递安全可靠的信息。在微信小程序中,开发者可以选择使用 JWT 来生成和验证用户的登录态 Token,以实现用户注册登录的功能。
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class User {
@Id
private String userId;
private String username;
private String password;
// 省略getter和setter方法
}
创建一个Repository来管理用户数据:
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, String> {
User findByUsername(String username);
}
实现一个Service类来处理微信API调用和Token生成:
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class WeChatApiService {
private final String appId;
private final String appSecret;
private final RestTemplate restTemplate;
public WeChatApiService(String appId, String appSecret, RestTemplate restTemplate) {
this.appId = appId;
this.appSecret = appSecret;
this.restTemplate = restTemplate;
}
public WeChatSessionResponse getSession(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId +
"&secret=" + appSecret +
"&js_code=" + code +
"&grant_type=authorization_code";
return restTemplate.getForObject(url, WeChatSessionResponse.class);
}
}
创建一个Controller来处理登录请求:
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;
@RestController
public class LoginController {
@Autowired
private WeChatApiService weChatApiService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserRepository userRepository;
@PostMapping("/login")
public String login(@RequestBody String code) {
WeChatSessionResponse sessionResponse = weChatApiService.getSession(code);
String openid = sessionResponse.getOpenid();
// Check if the user is registered, if not, register the user
User user = userRepository.findByUsername(openid);
if (user == null) {
user = new User();
user.setUserId(openid);
user.setUsername(openid);
user.setPassword(""); // No password needed for WeChat login
userRepository.save(user);
}
return jwtTokenUtil.generateToken(user.getUserId());
}
}
配置Spring Security来使用我们实现的UserDetailsService:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这段代码是Spring Security中的配置代码,用于配置HTTP请求的安全性。让我来解释一下这段代码的含义:
-
.csrf().disable()
: 这段代码禁用了CSRF(Cross Site Request Forgery)保护机制。CSRF是一种网络攻击,它利用用户的身份发起恶意请求。在一些情况下,这种保护机制可能会妨碍一些操作,因此在这里我们选择禁用它。但是在实际应用中,如果存在安全风险,应该慎重考虑是否禁用CSRF保护。 -
.authorizeRequests()
: 这段代码开始对请求进行授权配置。 -
.antMatchers("/login").permitAll()
: 这段代码表示允许对"/login"路径的请求放行,不需要身份认证,即使没有登录也可以访问这个路径。通常登录页面是不需要身份认证的,所以我们在这里允许访问。 -
.anyRequest().authenticated()
: 这段代码表示除了"/login"路径之外的所有请求都需要进行身份认证才能访问。换句话说,除了登录页面之外的其他页面都需要用户登录才能访问。 -
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
: 这段代码将自定义的JwtAuthenticationFilter添加到了Spring Security的过滤器链中,并指定了它在UsernamePasswordAuthenticationFilter之前执行。这个过滤器的作用是验证请求中的JWT Token,如果验证通过,则设置用户的认证信息到Spring Security上下文中,从而实现用户的身份认证。
编写JwtAuthenticationFilter来验证Token:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
if (jwtTokenValidator.validateToken(token)) {
String userId = jwtTokenValidator.getUserIdFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenUtil {
private static final String SECRET_KEY = "your_secret_key";
private static final long EXPIRATION_TIME = 864_000_000; // 10 days in milliseconds
public String generateToken(String userId) {
return Jwts.builder()
.setSubject(userId)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public String getUserIdFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
这个工具类包含了生成Token、从Token中解析出用户ID、以及验证Token的方法。
然后,我们在 JwtAuthenticationFilter
中使用这个工具类来验证Token:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
if (jwtTokenUtil.validateToken(token)) {
String userId = jwtTokenUtil.getUserIdFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userId, // 用户ID
null, // 密码,因为使用JWT认证,所以这里设为null
null // 权限信息,通常会在生成Token时就包含在内
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}