微信小程序的用户注册登录通常是通过微信提供的登录凭证 code 进行实现的。具体步骤如下:

  1. 用户在小程序中点击登录按钮,小程序调用 wx.login 方法,获取到用户登录凭证 code。
  2. 小程序将该 code 发送给开发者服务器。
  3. 开发者服务器通过微信提供的接口,使用该 code 换取用户的唯一标识 OpenID 和会话密钥 SessionKey。
  4. 开发者服务器根据 OpenID 进行用户识别,判断用户是否已经注册,如果未注册则进行注册,如果已注册则直接登录。
  5. 开发者服务器生成一个用户唯一标识符,如用户ID,并将该标识符与用户在该次会话中的登录状态关联。
  6. 开发者服务器生成一个登录态 Token,可以使用 JWT(JSON Web Token)或其他方式生成,将该 Token 发送给小程序。
  7. 小程序收到 Token 后,将其保存在本地,作为用户的登录状态凭证。
  8. 小程序之后的请求可以携带该 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请求的安全性。让我来解释一下这段代码的含义:

  1. .csrf().disable(): 这段代码禁用了CSRF(Cross Site Request Forgery)保护机制。CSRF是一种网络攻击,它利用用户的身份发起恶意请求。在一些情况下,这种保护机制可能会妨碍一些操作,因此在这里我们选择禁用它。但是在实际应用中,如果存在安全风险,应该慎重考虑是否禁用CSRF保护。
  2. .authorizeRequests(): 这段代码开始对请求进行授权配置。
  3. .antMatchers("/login").permitAll(): 这段代码表示允许对"/login"路径的请求放行,不需要身份认证,即使没有登录也可以访问这个路径。通常登录页面是不需要身份认证的,所以我们在这里允许访问。
  4. .anyRequest().authenticated(): 这段代码表示除了"/login"路径之外的所有请求都需要进行身份认证才能访问。换句话说,除了登录页面之外的其他页面都需要用户登录才能访问。
  5. .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);
    }
}