cas(六)之微信企业号集成
因为公司要集成微信的企业号,目的也就是cas支持微信登录,因为微信提供了oauth接口来获取用户信息,而且cas也支持oauth协议。所以现在要做的就是cas集成客户端访问微信获取用户信息完成登录即可。
- 微信api地址
http://qydev.weixin.qq.com/wiki/index.php?title=OAuth%E9%AA%8C%E8%AF%81%E6%8E%A5%E5%8F%A3
- 流程简单介绍
首先通过微信公众自定义按钮,跳转到OAuth验证接口,这个时候会设置一个回调。微信客户端会回调redirect_uri,而对于cas来说redirect_uri也就是http://www.cas.com/login,并且会带上用户的code,服务器接收到code以后调用根据code获取成员信息接口,获取到用户的工号,然后按照cas原有逻辑登录。
- 定制oauth验证接口的URL
https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=http%3a%2f%2f域名%2flogin%3foauth_provider%3dwechat&response_type=code&scope=snsapi_base&agentid=应用id&state=STATE#wechat_redirect
其中state不可少,后续判断需要,#wechat_redirect也不可少,需要微信服务器返回用户的code以便后续进一步获取工号。
- 新建cas-thrid-party-oauth(maven的java项目),并且在cas-server中依赖该项目
1,父pom里面添加模块
<modules> <module>cas-server</module> <module>cas-captcha</module> <module>cas-common</module> <module>cas-thrid-party-oauth</module> </modules>
2,cas-server的pom添加
<dependency> <groupId>com.fzhsh</groupId> <artifactId>cas-thrid-party-oauth</artifactId> <version>1.0.0</version> </dependency>
3,新建cas-thrid-party-oauth项目,并加入cas-server-support-oauth依赖,支持oauth
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <parent> <artifactId>cas</artifactId> <groupId>com.fzhsh</groupId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>cas-thrid-party-oauth</artifactId> <version>1.0.0</version> <name>cas-thrid-party-oauth</name> <modelVersion>4.0.0</modelVersion> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-support-oauth</artifactId> <version>${cas.version}</version> </dependency> </dependencies> </project>
4,WechatApi20主要是获取Token
public class WechatApi20 extends DefaultApi20 { private String authorizeUrl; private String scopeAuthorizeUrl; private static final String accessTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; private static final String CORPID = "corpid"; private static final String CORPSECRET = "corpsecret"; @Override public AccessTokenExtractor getAccessTokenExtractor() { return new WechatJsonTokenExtractor(); } @Override public Verb getAccessTokenVerb() { return Verb.GET; } @Override public String getAccessTokenEndpoint() { return accessTokenUrl; } @Override public String getAuthorizationUrl(OAuthConfig config) { // Append scope if present if(config.hasScope()) { return String.format(scopeAuthorizeUrl, config.getApiKey(), OAuthEncoder.encode(config.getCallback()), OAuthEncoder.encode(config.getScope())); } else { return String.format(authorizeUrl, config.getApiKey(), OAuthEncoder.encode(config.getCallback())); } } @Override public OAuthService createService(OAuthConfig config) { final DefaultApi20 api = this; final OAuthConfig oaconfig = config; return new OAuth20ServiceImpl(this, config){ @Override public Token getAccessToken(Token requestToken, Verifier verifier) { OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint()); request.addQuerystringParameter(CORPID, oaconfig.getApiKey()); request.addQuerystringParameter(CORPSECRET, oaconfig.getApiSecret()); Response response = request.send(); return api.getAccessTokenExtractor().extract(response.getBody()); } }; } }
5,WechatProvider主要是获取用户授权后的用户信息
public class WechatProvider extends BaseOAuth20Provider { private String profileUrl; private String scope; private Token accessToken; private static final String USER_ID = "UserId"; private static final String DEVICE_ID = "DeviceId"; @Override protected UserProfile extractUserProfile(String body) { UserProfile userProfile = new UserProfile(){ @Override public String getTypedId() { return this.id; } }; JsonNode rootNode = JsonHelper.getFirstNode(body); if(rootNode.has(USER_ID)){ JsonNode user = rootNode.get(USER_ID); userProfile.setId(user.asText()); } if(rootNode.has(DEVICE_ID)){ JsonNode device = rootNode.get(DEVICE_ID); userProfile.addAttribute(DEVICE_ID, device.asText()); } return userProfile; } @Override protected String getProfileUrl() { return profileUrl; } @Override protected void internalInit() { service = new ServiceBuilder().provider(WechatApi20.class).apiKey(key).apiSecret(secret).callback(callbackUrl).scope(scope).build(); } @Override public UserProfile getUserProfile(OAuthCredential credential) { init(); Token token = getCacheToken(credential); return getUserProfile(credential, token); } private Token getCacheToken(OAuthCredential credential) { try{ if(accessToken != null && accessToken instanceof WechatToken){ WechatToken weToken = (WechatToken) accessToken; //如果是有效的token if(weToken.isValid()){ return weToken; } } }catch(Exception e){ logger.error("get cache token error", e); } accessToken = getAccessToken(credential); return accessToken; } public UserProfile getUserProfile(OAuthCredential credential, Token accessToken) { final String body = sendRequestForData(credential, accessToken, getProfileUrl()); if (body == null) { return null; } final UserProfile profile = extractUserProfile(body); addAccessTokenToProfile(profile, accessToken); return profile; } public Token getAccessToken(OAuthCredential credential) { return super.getAccessToken(credential); } protected String sendRequestForData(final OAuthCredential credential, final Token accessToken, final String dataUrl) { logger.debug("accessToken : {} / dataUrl : {}", accessToken, dataUrl); final long t0 = System.currentTimeMillis(); final ProxyOAuthRequest request = new ProxyOAuthRequest(Verb.GET, dataUrl, this.proxyHost, this.proxyPort); if (this.connectTimeout != 0) { request.setConnectTimeout(this.connectTimeout, TimeUnit.MILLISECONDS); } if (this.readTimeout != 0) { request.setReadTimeout(this.readTimeout, TimeUnit.MILLISECONDS); } this.service.signRequest(accessToken, request); wrapWechatParameter(request, credential); final Response response = request.send(); final int code = response.getCode(); final String body = response.getBody(); final long t1 = System.currentTimeMillis(); logger.debug("Request took : " + (t1 - t0) + " ms for : " + dataUrl); logger.debug("response code : {} / response body : {}", code, body); if (code != 200) { logger.error("Failed to get user data, code : " + code + " / body : " + body); return null; } return body; } private void wrapWechatParameter(ProxyOAuthRequest request, OAuthCredential credential) { request.addQuerystringParameter(OAuthConstants.CODE, credential.getVerifier()); } @Override protected BaseOAuthProvider newProvider() { return this; } public void setProfileUrl(String profileUrl) { this.profileUrl = profileUrl; } public void setScope(String scope) { this.scope = scope; } }
6,WechatJsonTokenExtractor微信解析josn获取token有效时间
public class WechatJsonTokenExtractor extends JsonTokenExtractor{ private static final Pattern p = Pattern.compile("\"expires_in\":\\s*(\\d+)"); @Override public Token extract(String response) { return new WechatToken(super.extract(response), extractExpireTime(response)); } private long extractExpireTime(String response) { Matcher m = p.matcher(response); if(m.find()){ return Long.parseLong(m.group(1)); } return 0L; } }
7,存放token和有效时间实体
public class WechatToken extends Token{ private long expireTime; private long startTime; private static final long TIME_UNIT = 1000; public WechatToken(String token, String secret, String rawResponse, long expireTime) { super(token, secret, rawResponse); this.expireTime = expireTime * TIME_UNIT; this.startTime = System.currentTimeMillis(); } public WechatToken(Token token, long expireTime) { this(token.getToken(), token.getSecret(), token.getRawResponse(), expireTime); } public boolean isValid(){ long nowTime = System.currentTimeMillis(); return nowTime - startTime < expireTime; } public long getExpireTime() { return expireTime; } public void setExpireTime(long expireTime) { this.expireTime = expireTime; } }
至此,cas-thrid-party-oauth项目完。
- 在cas-server加入微信登录流程
在application.xml加入前面的provider定义
<bean id="wechatProvider" class="com.fzhsh.cas.oauth.wechat.WechatProvider"> <property name="key" value="appcorpid"/> <property name="secret" value="appsecret"/> <property name="callbackUrl" value="https://域名/login"/> <property name="profileUrl" value="https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo"/> <!-- 固定写死 --> <property name="type" value="wechat"/> <property name="scope" value="snsapi_base" /> </bean>
在deployerConfigContext.xml的authenticationHandlers处添加OAuthAuthenticationHandler增加对oauth的支持
<bean class="org.jasig.cas.support.oauth.authentication.handler.support.OAuthAuthenticationHandler"> <property name="configuration" ref="oauthConfiguration" /> </bean>
在deployerConfigContext.xml的credentialsToPrincipalResolvers处添加OAuthCredentialsToPrincipalResolver
<bean class="org.jasig.cas.support.oauth.authentication.principal.OAuthCredentialsToPrincipalResolver"/>
在deployerConfigContext.xml的authenticationMetaDataPopulators处增加OAuthAuthenticationMetaDataPopulator
<property name="authenticationMetaDataPopulators"> <list> <bean class="org.jasig.cas.support.oauth.authentication.OAuthAuthenticationMetaDataPopulator"/> </list> </property>
在deployerConfigContext.xml增加一下内容
<bean id="oauthConfiguration" class="org.jasig.cas.support.oauth.OAuthConfiguration"> <property name="providers"> <list> <ref bean="wechatProvider"/> </list> </property> <property name="loginUrl" value="https://域名/login" /> </bean>
在cas-servlet.xml中定义OAuthAction
<bean id="oauthAction" class="org.jasig.cas.support.oauth.web.flow.OAuthAction" p:centralAuthenticationService-ref="centralAuthenticationService"> <property name="configuration" ref="oauthConfiguration"/> </bean>
最后在login-webflow.xml增加一个decision-state节点和action-state节点
<decision-state id="checkOauthLogin"> <if test="externalContext.requestParameterMap['code'] neq '' && externalContext.requestParameterMap['code'] neq null && externalContext.requestParameterMap['state'] eq 'STATE'" then="oauthAction" else="ticketGrantingTicketExistsCheck"/> </decision-state> <action-state id="oauthAction"> <evaluate expression="oauthAction"/> <transition on="success" to="sendTicketGrantingTicket"/> <transition on="error" to="ticketGrantingTicketExistsCheck"/> </action-state>
至此整合完毕。
- 最后整理
当微信客户端内嵌浏览器向微信服务端调用oauth接口时,会让微信客户端内嵌浏览器重定向到cas服务器,并且带上用户的code,这时url配置的login,而cas的login走的是spring-web-flow,所以在login-webflow.xml里面增加判断节点,判断是否为oauth协议的登陆,如果是则走OauthAction;在走OauthAction的时候会调用deployerConfigContext.xml配置的OAuthAuthenticationHandler,也就会触发WechatProvider,获取请求参数中code,并且请求微信的Token,这时再使用code和token调用《获取用户信息接口》,获取用户工号,到这后,按照验证完用户名密码的流程接着走下去,就登录认为工号登录成功。
- 参考资料
https://item.congci.com/-/content/cas-jicheng-oauth2-client-server

浙公网安备 33010602011771号