2017.2.16 开涛shiro教程-第十七章-OAuth2集成(二)客户端

时间:2023-03-09 08:42:56
2017.2.16 开涛shiro教程-第十七章-OAuth2集成(二)客户端

原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398

根据下载的pdf学习。

开涛shiro教程-第十七章-OAuth2集成

3.客户端

客户端流程可以参照如很多网站的新浪微博登录功能,或其他的第三方帐号登录功能。

 客户端进行登录操作
跳到oauth2服务端,进行登录授权。成功后,服务端返回auth code。
客户端使用auth code去服务器换取access token。
客户端根据access token得到用户信息,进行客户端的登录绑定。

(1)POM依赖

此处我们使用 apache oltu oauth2 客户端实现。

        <dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>0.31</version>
</dependency>

附完整pom.xml

 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>shiro-example</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shiro-example-chapter17-client</artifactId>
<packaging>war</packaging>
<name>shiro-example-chapter17-client</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency> <dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency> <dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>0.31</version>
</dependency> <dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency> <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>0.2.23</version>
</dependency> <!-- aspectj相关jar包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.0.0.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.0.0.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.0.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.0.RELEASE</version>
</dependency> <!--jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.2.3</version>
</dependency> </dependencies>
<build>
<finalName>chapter17-client</finalName>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>8.1.8.v20121106</version>
<configuration>
<webAppConfig>
<contextPath>/${project.build.finalName}</contextPath>
</webAppConfig>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>9080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
</configuration>
</plugin> <plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/${project.build.finalName}</path>
<httpPort>9080</httpPort>
</configuration> </plugin>
</plugins> </build>
</project>

pom.xml

(2)代码

代码总览:

2017.2.16 开涛shiro教程-第十七章-OAuth2集成(二)客户端

OAuth2Token:类似于UsernamePasswordToken,用于存储oauth2服务端返回的auth code。

 1 import org.apache.shiro.authc.AuthenticationToken;
2
3 public class OAuth2Token implements AuthenticationToken {
4
5 public OAuth2Token(String authCode) {
6 this.authCode = authCode;
7 }
8
9 private String authCode;
10 private String principal;
11
12 //getter、setter略。
13
14 @Override
15 public Object getCredentials() {
16 return authCode;
17 }
18 }

OAuth2AuthenticationFilter :类似于前面提过的FormAuthenticationFilter,用于OAuth客户端的身份验证控制。

整个的操作流程,和前面的shiro登录其实是很类似的。

 如果当前用户还没有身份验证,首先判断url中是否有auth code,如果没有则重定向到服务端进行登录授权,得到auth code。
有了auth code后,OAuth2AuthenticationFilter 用auth code创建Auth2Token
Subject.login利用Auth2Token进行登录。
OAuth2Realm 根据OAuth2Token进行相应的登录逻辑处理。
 package com.github.zhangkaitao.shiro.chapter18.oauth2;

 import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.filter.authc.AuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException; public class OAuth2AuthenticationFilter extends AuthenticatingFilter { private String authcCodeParam = "code";//oauth2 authc code参数名
private String clientId; //客户端id
private String redirectUrl; //服务器端登录成功/失败后重定向到的客户端地址
private String responseType = "code"; //oauth2服务器响应类型
private String failureUrl; //setter略。没有getter。 @Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String code = httpRequest.getParameter(authcCodeParam);//从url中拿到auth code
return new OAuth2Token(code);//用auth code创建auth2Token
} @Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
} @Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String error = request.getParameter("error");//首先判定有没有error,有的话直接重定向到失败页面
String errorDescription = request.getParameter("error_description");
if(!StringUtils.isEmpty(error)) {//如果服务端返回了错误
WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);
return false;
} Subject subject = getSubject(request, response);
if(!subject.isAuthenticated()) {
if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) {
//如果用户没有身份验证,且没有auth code,则重定向到服务端授权
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
return executeLogin(request, response);//执行父类的登录逻辑,调用Subject.login登录
} @Override//登录成功后的回调方法
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
issueSuccessRedirect(request, response);
return false;
} @Override//登录失败后的回调方法
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
ServletResponse response) {
Subject subject = getSubject(request, response);
if (subject.isAuthenticated() || subject.isRemembered()) {
try {
issueSuccessRedirect(request, response);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
WebUtils.issueRedirect(request, response, failureUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
} }

OAuth2Realm:

 OAuth2Realm 首先只支持 OAuth2Token 类型的 Token
通过传入的 auth code 去换取 access token
再根据 access token 去获取用户信息(用户名)
然后根据此信息创建AuthenticationInfo
如果需要 AuthorizationInfo 信息,可以根据此处获取的用户名再根据自己的业务规则去获取。
 package com.github.zhangkaitao.shiro.chapter18.oauth2;

 import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthBearerClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse;
import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
import org.apache.oltu.oauth2.client.response.OAuthResourceResponse;
import org.apache.oltu.oauth2.common.OAuth;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection; public class OAuth2Realm extends AuthorizingRealm { private String clientId;
private String clientSecret;
private String accessTokenUrl;
private String userInfoUrl;
private String redirectUrl; //省略setter。 @Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;//表示此Realm只支持OAuth2Token类型
} @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//授权
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//验证
OAuth2Token oAuth2Token = (OAuth2Token) token;
String code = oAuth2Token.getAuthCode();
String username = extractUsername(code); SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(username, code, getName());
return authenticationInfo;
} private String extractUsername(String code) { try {
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient()); OAuthClientRequest accessTokenRequest = OAuthClientRequest
.tokenLocation(accessTokenUrl)
.setGrantType(GrantType.AUTHORIZATION_CODE)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setCode(code)
.setRedirectURI(redirectUrl)
.buildQueryMessage(); OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST); String accessToken = oAuthResponse.getAccessToken();
Long expiresIn = oAuthResponse.getExpiresIn(); OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(userInfoUrl)
.setAccessToken(accessToken).buildQueryMessage(); OAuthResourceResponse resourceResponse = oAuthClient.resource(userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);
String username = resourceResponse.getBody();
return username;
} catch (Exception e) {
e.printStackTrace();
throw new OAuth2AuthenticationException(e);
}
}
}

(3)配置文件spring-config-shiro.xml

 <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache/ehcache.xml"/>
</bean> <!-- Realm实现 -->
<bean id="oAuth2Realm" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
<property name="cachingEnabled" value="true"/>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorizationCache"/> <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
<property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
<property name="accessTokenUrl" value="http://localhost:8080/chapter17-server/accessToken"/>
<property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
<property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
</bean> <!-- 会话ID生成器 -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
</bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="2592000"/><!-- 30天 -->
</bean> <!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)-->
<property name="cipherKey"
value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean> <!-- 会话DAO -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean> <!-- 会话验证调度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<property name="sessionValidationInterval" value="1800000"/>
<property name="sessionManager" ref="sessionManager"/>
</bean> <!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="1800000"/>
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<property name="sessionDAO" ref="sessionDAO"/>
<property name="sessionIdCookieEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
</bean> <!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="oAuth2Realm"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean> <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean> <!-- OAuth2身份验证过滤器 -->
<bean id="oAuth2AuthenticationFilter" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
<property name="authcCodeParam" value="code"/>
<property name="failureUrl" value="/oauth2Failure.jsp"/>
</bean> <!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
<property name="successUrl" value="/"/>
<property name="filters">
<util:map>
<entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/ = anon
/oauth2Failure.jsp = anon
/oauth2-login = oauth2Authc
/logout = logout
/** = user
</value>
</property>
</bean> <!-- Shiro生命周期处理器-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>

spring-config-shiro.xml

这里的OAuth2Realm 需要配置在服务端申请的 clientId 和 clientSecret;

 1     <!-- Realm实现 -->
2 <bean id="oAuth2Realm" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
3 <property name="cachingEnabled" value="true"/>
4 <property name="authenticationCachingEnabled" value="true"/>
5 <property name="authenticationCacheName" value="authenticationCache"/>
6 <property name="authorizationCachingEnabled" value="true"/>
7 <property name="authorizationCacheName" value="authorizationCache"/>
8
9 <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
10 <property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
11 <property name="accessTokenUrl" value="http://localhost:8080/chapter17-server/accessToken"/>
12 <property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
13 <property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
14 </bean>

对应的数据参看server里的shiro-data.sql

2017.2.16 开涛shiro教程-第十七章-OAuth2集成(二)客户端

然后配置刚刚的OAuth2AuthenticationFilter:

1     <!-- OAuth2身份验证过滤器 -->
2 <bean id="oAuth2AuthenticationFilter" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
3 <property name="authcCodeParam" value="code"/>
4 <property name="failureUrl" value="/oauth2Failure.jsp"/>
5 </bean>

这个OAuth2AuthenticationFilter用来拦截服务端重定向回来的auth code。

这里的/oauth2-login = oauth2Authc 表示/oauth2-login 地址使用 oauth2Authc 拦截器拦截并进行 oauth2 客户端授权。

 <!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
<property name="successUrl" value="/"/>
<property name="filters">
<util:map>
<entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/ = anon
/oauth2Failure.jsp = anon
/oauth2-login = oauth2Authc
/logout = logout
/** = user
</value>
</property>
</bean>

(4)测试

a) 访问地址:http://localhost:9080/chapter17-client/ ,点击登录按钮,跳出如下界面:
2017.2.16 开涛shiro教程-第十七章-OAuth2集成(二)客户端

b) 输入用户名,密码,并点击登录并授权。

c) 如果登录成功,服务端会重定向到客户端,根据配置文件里提供的地址为:http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11 。注意这个url是带着auth code的。

d) 客户端的OAuth2AuthenticationFilter拿到此auth code,用来创建OAuth2Token,提交给Subject用于登录。

e) 客户端的Subject委托给OAuth2Realm进行身份验证。

f) OAuth2Realm根据token获取受保护的用户信息,用于客户端登录。

至此,OAuth的集成就完成了,只完成了基本功能,没有进行一些关于异常的检测。具体的可以参考新浪微博进行API及异常错误码的设计。