【Spring Security OAuth2 同时支持传统页面登录和微信小程序登录两种登录方式的实现】

要实现同时支持传统页面用户登录和微信小程序自定义授权登录,我们需要配置多种授权模式并确保它们能协同工作。以下是完整的实现方案:

1. 整体架构设计

页面登录:使用标准的password授权模式

微信小程序登录:使用自定义的wechat_miniapp授权模式

共用Token服务:两种方式生成的Token可以访问相同的资源

2. 完整配置实现

2.1 安全配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .csrf().disable();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.2 授权服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired(required = false)
    private WxMaService wxMaService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("webapp")
                .secret(passwordEncoder.encode("websecret"))
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("read", "write")
                .accessTokenValiditySeconds(3600)
            .and()
            .withClient("miniapp")
                .secret(passwordEncoder.encode("minisecret"))
                .authorizedGrantTypes("wechat_miniapp", "refresh_token")
                .scopes("read", "write")
                .accessTokenValiditySeconds(3600);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService)
            .tokenGranter(tokenGranter(endpoints));
    }

    private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
        List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
        
        // 添加微信小程序授权模式
        if (wxMaService != null) {
            granters.add(new WechatMiniAppTokenGranter(
                endpoints.getTokenServices(),
                endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(),
                wxMaService
            ));
        }
        
        return new CompositeTokenGranter(granters);
    }
}

2.3 自定义微信TokenGranter(更新版)

public class WechatMiniAppTokenGranter extends AbstractTokenGranter {
    
    private static final String GRANT_TYPE = "wechat_miniapp";
    
    private final WxMaService wxMaService;
    
    public WechatMiniAppTokenGranter(AuthorizationServerTokenServices tokenServices,
                                   ClientDetailsService clientDetailsService,
                                   OAuth2RequestFactory requestFactory,
                                   WxMaService wxMaService) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.wxMaService = wxMaService;
    }
    
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        
        String code = parameters.get("code");
        String encryptedData = parameters.get("encryptedData");
        String iv = parameters.get("iv");
        
        try {
            // 1. 用code换取session_key和openid
            WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
            String openid = session.getOpenid();
            
            // 2. 如果需要,解密用户信息
            WxMaUserInfo userInfo = null;
            if (StringUtils.isNotBlank(encryptedData) {
                userInfo = wxMaService.getUserService().getUserInfo(session.getSessionKey(), encryptedData, iv);
            }
            
            // 3. 根据openid查询或创建用户
            UserDetails userDetails = loadOrCreateUser(openid, userInfo);
            
            // 4. 构建认证信息
            UsernamePasswordAuthenticationToken userAuth = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
            
            // 5. 构建OAuth2请求
            OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
            
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } catch (WxErrorException e) {
            throw new InvalidGrantException("微信登录失败: " + e.getMessage());
        }
    }
    
    private UserDetails loadOrCreateUser(String openid, WxMaUserInfo userInfo) {
        // 实现根据openid查询或创建用户的逻辑
        // 可以使用userDetailsService或自定义的用户服务
        // ...
    }
}

3. 用户详情服务

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 传统用户名密码登录的用户加载逻辑
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            getAuthorities(user));
    }

    public UserDetails loadUserByOpenid(String openid) {
        // 微信小程序登录的用户加载逻辑
        User user = userRepository.findByOpenid(openid);
        if (user == null) {
            // 可以在这里创建新用户或抛出异常
            user = createNewUserFromOpenid(openid);
        }
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            getAuthorities(user));
    }

    private Collection<? extends GrantedAuthority> getAuthorities(User user) {
        // 返回用户的权限
        return Collections.emptyList();
    }
}

4. 资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated();
    }
}

5. 使用方式

5.1 传统页面登录

前端请求示例:

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=password
&username=user
&password=pass
&client_id=webapp
&client_secret=websecret

5.2 微信小程序登录

小程序端获取code:

wx.login({
  success(res) {
    if (res.code) {
      // 发送code到后端
      wx.request({
        url: 'http://your-server/oauth/token',
        method: 'POST',
        data: {
          grant_type: 'wechat_miniapp',
          code: res.code,
          client_id: 'miniapp',
          client_secret: 'minisecret'
        },
        success(res) {
          console.log('登录成功', res.data);
        }
      });
    }
  }
});

6. 关键点说明

多客户端配置

webapp客户端用于传统登录

miniapp客户端用于微信小程序登录

TokenGranter组合:

默认的TokenGranter处理password等标准模式

自定义的WechatMiniAppTokenGranter处理微信登录

用户统一管理

无论哪种登录方式,最终都转换为UserDetails对象

可以共用相同的权限系统和资源保护

扩展性

可以轻松添加其他登录方式(如短信登录)

各种登录方式生成的Token可以访问相同的资源

7. 最佳实践建议

用户合并:如果用户可能同时使用多种登录方式,考虑设计用户合并机制

会话管理:合理设置Token过期时间和刷新策略

监控日志:记录不同登录方式的访问情况

安全防护:针对不同客户端实施不同的安全策略

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐