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 ''
        &amp;&amp; externalContext.requestParameterMap['code'] neq null
        &amp;&amp; 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

posted @ 2017-05-16 15:50  風之殤  阅读(2426)  评论(0)    收藏  举报