Example on integration of spring-cloud-security and WeChat MP's oauth 简单示例演示如何使用spring-cloud-security配置微信公众号平台的网页认证
- 因为微信公众号的oauth过程不复合标准, 比如
client_id变成了appid等 - 如果不想麻烦配置spring-oauth2, 可以使用
weixin-java-mp自己处理跳转. 令需要自己处理spring-security相关的部分, 比如principal.
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${weixin.mp.version}</version>
</dependency>- 这个例子并不全面, 不一定适用所有的情境, 当前选用的
spring-boot版本是
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
<relativePath />
</parent>- 用户同意授权获取code时
client_id变成了appid
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
- 通过code换取网页授权access_token需要
appid和secret参数,要求HTTP GET方法
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
- 拉取用户信息时需要
access_token/openid和lang参数
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
spring oauth2的实现是参照oauth2标准的,因此代码中hard code了很多client_id字符串, 不能通过override类似getClientIdParameterValue()这样的形式来将client_id修改为appid
经过查看spring的相关代码, 发现是通过org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter.redirectUser(UserRedirectRequiredException, HttpServletRequest, HttpServletResponse)来进行authorize地址的拼装. 而OAuth2ClientContextFilter是由org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration配置并且创建的. 并且OAuth2ClientConfiguration是被@EnableOAuth2Client注解引入的.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OAuth2ClientConfiguration.class)
public @interface EnableOAuth2Client {
}EnableOAuth2Client是被EnableOAuth2Sso引用的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({
OAuth2SsoDefaultConfiguration.class,
OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
}所以只需要实现自己的OAuth2ClientConfiguration在其中派生OAuth2ClientContextFilter并override其中的redirectUser方法增加appid参数就可以了
/**
* 跳转到微信认证时需要appid参数用于携带client_id
*/
@Configuration
public class MyOAuth2ClientConfiguration extends OAuth2ClientConfiguration {
static public class MyOAuth2ClientContextFilter extends OAuth2ClientContextFilter {
@Override
protected void redirectUser(
UserRedirectRequiredException e,
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
String clientId = e.getRequestParams().get("client_id");
e.getRequestParams().put("appid", clientId);
super.redirectUser(e, request, response);
}
}
@Override
public OAuth2ClientContextFilter oauth2ClientContextFilter() {
return new MyOAuth2ClientContextFilter();
}
}从e.getRequestParams()中remove掉client_id也可以, 实际发现微信appid和client_id同时存在不影响认证, 可以保留
为了用我们自己的MyOAuth2ClientConfiguration因此不能直接用@EnableOAuth2Sso注解 @SpringBootApplication , 展开来自己写
/**
* 这是UI服务器
*/
@SpringBootApplication
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({
MyOAuth2ClientConfiguration.class,
OAuth2SsoDefaultConfiguration.class,
OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public class WxAppApplication {
public static void main(String[] args){
SpringApplication.run(WxAppApplication.class, args);
}
}org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(AccessTokenRequest, OAuth2ProtectedResourceDetails, MultiValueMap<String, String>, HttpHeaders)(AuthorizationCodeAccessTokenProvider派生于该类)中提供了通过tokenRequestEnhancer在想auth server请求access token前修改请求参数和请求头的机会, 而AuthorizationCodeAccessTokenProvider是在OAuth2RestTemplate中被调用的, 因此我们通过UserInfoRestTemplateCustomizer来设定OAuth2RestTemplate.
/**
* 用于通过customize方法修改OAuth2RestTemplate中的AuthorizationCodeAccessTokenProvider,
* 给AuthorizationCodeAccessTokenProvider设置新的TokenRequestEnhancer,
* TokenRequestEnhancer中可以修改获取AccessToken时的uri参数
*/
@Configuration
@Component
public class MyUserInfoRestTemplateCustomizer implements UserInfoRestTemplateCustomizer {
/**
* 需要通过TokenRequestEnhancer设置appid
*/
@Override
public void customize(OAuth2RestTemplate template) {
AuthorizationCodeAccessTokenProvider accessTokenProvider = new MyAuthorizationCodeAccessTokenProvider();
accessTokenProvider.setTokenRequestEnhancer(new MyWxAccessTokenRequestEnhancer());
template.setAccessTokenProvider(accessTokenProvider);
}
}其中MyAuthorizationCodeAccessTokenProvider和MyWxAccessTokenRequestEnhancer都是MyUserInfoRestTemplateCustomizer的内部类(不是内部类也没关系).
static class MyAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
/**
* 微信用GET方式, spring oauth2框架只在GET时将form中参数拼接到url中
*/
@Override
protected HttpMethod getHttpMethod() {
return HttpMethod.GET;
}
/**
* 微信的response body是json格式的
*/
@Override
protected ResponseExtractor<OAuth2AccessToken> getResponseExtractor() {
getRestTemplate(); // force initialization
return new HttpMessageConverterExtractor<OAuth2AccessToken>(
OAuth2AccessToken.class,
Arrays.asList(new WxOAuth2AccessTokenMessageConverter()));
}
}
其中override getHttpMethod使通过GET方法获取access token, 默认是POST方法并且不会将参数拼接到请求url中.
其中spring oauth默认把response body当成form来解析, 造成失败, 因此需要自定义消息转换器WxOAuth2AccessTokenMessageConverter(实现看源码, 很简单).
spring oauth的默认实现中, 获取用户详情的方法没有带任何参数, 直接就是application.properties中的配置值.
经过分析发现org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices.loadAuthentication(String)的org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices.getMap(String, String)有机会修改userInfoEndpointUrl, 因此需要配置自己的UserInfoTokenServices(spring发现创建了就不自动创建这个bean)
@Configuration
static protected class MyUserInfoTokenServicesConfiguration {
//自动装配需要用到的beans
}
其中内嵌类MyUserInfoTokenServices覆盖loadAuthentication方法拼接带参数的userInfoEndpointUrl. 注意其中的openid在上一步拿到的access token的additionalInformation中.
class MyUserInfoTokenServices extends UserInfoTokenServices {
//...
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
//...
/**
* 如果希望在应用中@RequestMapping装配Principal需要手动设置, 因为没有检查openid这个属性名
* 如果想用自己的账号体系, 可以在这个位置访问自己的用户服务获取用户详情
*/
}
//...
}更多详情请看源代码
@Configuration
static class XXXConfiguration {
XXXConfiguration(
ExistBean bean
){
}
}@Configuration注解的类框架会自动调用构造函数(例子中是XXXConfiguration)并且自动装配构造参数(例子中是ExistBean)