【Spring Security OAuth2 同时支持传统页面登录和微信小程序登录两种登录方式的实现】
·
【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过期时间和刷新策略
监控日志:记录不同登录方式的访问情况
安全防护:针对不同客户端实施不同的安全策略
更多推荐
所有评论(0)