在web中,我們使用spring-security基于http進(jìn)行請(qǐng)求認(rèn)證。在cordova中,由于請(qǐng)求實(shí)際上是訪問手機(jī)本地的資源,因此,其資源的url,不是 http://ip:port/res,而是 file://res 。
它帶來的問題大概有如下問題:
1. 從手機(jī)app端h5頁面訪問后臺(tái)url資源時(shí)的跨域訪問問題。
問題原因:
從手機(jī)app端主要訪問本地路徑上的h5頁面,頁面域名是 file://;而后臺(tái)的url資源,其域名要么是 http://ip:port/appName,要么是類似 http://www.fredworks.cn/appName。因此構(gòu)成了跨域。
一般而言,會(huì)因?yàn)閏ors規(guī)范導(dǎo)致請(qǐng)求被拒絕,結(jié)果得到403異常。
解決方案:
h5頁面的跨域訪問問題,需要后臺(tái)服務(wù)器資源啟用cors服務(wù),并允許來自cordova的請(qǐng)求訪問。
spring-mvc,是通過如下代碼啟用cors控制的:
/**
* 各模塊安全配置類的基類。
* @author wangqiang
* 2018年8月26日 上午12:53:40
*/
public abstract class ModuleSecurityConfig extends WebSecurityConfigurerAdapter {
...
/**
* 啟用cors控制
* 2018年8月26日 下午11:07:37 wangqiang添加此方法
* @param http
* @throws Exception
*/
private void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
...
}
}
上述代碼的目的,是為了確保啟用了cors服務(wù)。
你不啟用cors配置,甚至通過類似這樣的配置 http.cors().disable() 顯式的關(guān)閉cors服務(wù),并不會(huì)讓cors不工作。你關(guān)閉的,其實(shí)只是服務(wù)器端對(duì)cors的授權(quán)控制。
而現(xiàn)代的瀏覽器,基本都已經(jīng)實(shí)現(xiàn)了對(duì)cors規(guī)范的默認(rèn)支持。無論服務(wù)器端是否有開啟cors授權(quán),瀏覽器都會(huì)向服務(wù)器詢問cors授權(quán)結(jié)果。關(guān)閉服務(wù)器端的cors控制,只會(huì)導(dǎo)致瀏覽器向服務(wù)器發(fā)出跨域檢查請(qǐng)求時(shí),服務(wù)器無法對(duì)此進(jìn)行授權(quán),從而導(dǎo)致請(qǐng)求無法通過cors控制而得到403異常。
自定義CORS配置內(nèi)容
SpringMvc通過CorsFilter來完成CORS控制。SpringMvc創(chuàng)建CorsFilter時(shí),會(huì)自動(dòng)尋找一個(gè)名字為 “corsConfigurationSource“,類型為 CorsConfigurationSource 的 Bean 作為配置參數(shù)來完成初始化。因此,我們只要自定義一個(gè)CorsConfigurationSource,就可以完成對(duì)cors的配置。
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
config.setAllowedOrigins(Arrays.asList("http://localhost:8100", "file://"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
上述代碼中, .allowedOrigins 方法支持兩個(gè)特定的域:
- "http://localhost:8100":是為了允許使用 ionic serve 進(jìn)行瀏覽器端模擬測(cè)試階段的請(qǐng)求不要被跨域控制拒絕。
- "file://":是為了允許ionic的手機(jī)包中的頁面資源請(qǐng)求服務(wù)器資源時(shí)不要被跨域控制拒絕。
2. 會(huì)話保持問題。
問題的原因:
來自cordova的請(qǐng)求和來自web的請(qǐng)求不同,來自cordova的請(qǐng)求不會(huì)攜帶cookie信息,從而導(dǎo)致不會(huì)攜帶會(huì)話ID,丟失認(rèn)證信息。
當(dāng)訪問需要認(rèn)證的資源時(shí),會(huì)因?yàn)闆]有會(huì)話ID,從而被spring-security的安全控制攔截和拒絕,最終得到403異常。
解決方案:
第一步,要在服務(wù)器端的cors控制中,允許跨域請(qǐng)求攜帶認(rèn)證信息。
否則spring-mvc將不會(huì)處理http請(qǐng)求頭信息中攜帶的會(huì)話ID,從而導(dǎo)致請(qǐng)求找不到匹配的會(huì)話:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
config.setAllowedOrigins(Arrays.asList("http://localhost:8100", "file://"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
上述代碼中,
.allowCredentials(true)就是用來告訴spring-mvc要處理跨域請(qǐng)求中攜帶的會(huì)話ID信息。
第二步,服務(wù)器端登陸通過后,要返回會(huì)話ID給前端
cordova使用webview來渲染web頁面,和正常的瀏覽器不一樣,它不支持cookie,無法像正常的瀏覽器一樣,在cookie中自動(dòng)存儲(chǔ)會(huì)話ID。因此,服務(wù)器端需要在登陸通過后,將會(huì)話ID作為請(qǐng)求返回?cái)?shù)據(jù)的一部分來返回給cordova端的h5頁面。
/**
* 登錄成功后,不要跳轉(zhuǎn)到新頁面,而是返回json數(shù)據(jù),以滿足手機(jī)端使用spring-security的目的。
* @author wangqiang
* 2018年8月26日 上午12:41:05
*/
@Service(SecurityModuleBeanNames.SecurityModuleAuthenticationSuccessHandler)
public class JsonReturningAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String username = request.getParameter("username");
AuthenInfo authenInfo = GsonUtils.fromJson(AuthenInfo.class, username);
switch (authenInfo.getLoginType()) {
case MobileUserLoginType.Value: {//手機(jī)app端用戶登錄,需要將會(huì)話ID作為數(shù)據(jù)返回
SingleResult<AuthenResultDto> retObj = new SingleResult<>();
retObj.setSuccess(true);
AuthenResultDto data = new AuthenResultDto();
retObj.setData(data);
String sessionId = request.getSession().getId();
data.setSessionId(sessionId);
ISecurityUser user = (ISecurityUser) authentication.getPrincipal();
data.setName(user.getUsername());
data.setMobile(user.getMobile());
response.getWriter().write(retObj.toString());
response.getWriter().flush();
break;
}
case WapUserLoginType.Value: {//手機(jī)wap端用戶登錄,重定向到登錄前原頁面或主頁。
//設(shè)置手機(jī)wap的默認(rèn)登錄后頁面為wap端主頁
this.setDefaultTargetUrl("");
super.onAuthenticationSuccess(request, response, authentication);
break;
}
default: {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
}
以上代碼,是在登陸成功處理器中,判斷是否是手機(jī)端登陸。如果是,則覆蓋默認(rèn)的行為(跳轉(zhuǎn)到登陸前url,或配置中指定的特定頁面),不跳轉(zhuǎn)到特定頁面(cordova的頁面是無法在服務(wù)器端進(jìn)行跳轉(zhuǎn)的,它根本就不在服務(wù)器上,而是在手機(jī)app本地),而是通過json格式返回?cái)?shù)據(jù)結(jié)構(gòu),其中就包含有登陸后的會(huì)話ID。
由于會(huì)話ID,一般在cookie中使用變量名 JSESSIONID 來記錄,因此我們也使用該變量名,只是做了java風(fēng)格的命名規(guī)范改造。
第三步,需要在cordova的h5頁面端接收并存儲(chǔ)登陸后的會(huì)話ID
cordova使用webview來渲染web頁面,和正常的瀏覽器不一樣,它不支持cookie,無法像正常的瀏覽器一樣,在cookie中自動(dòng)存儲(chǔ)會(huì)話ID。因此,h5頁面需要將接收到的會(huì)話ID保存在本地。
比如如下的angular代碼:
/**
* 客戶登錄功能
*/
login() {
let me = this;
// 密碼登錄
const url = '/security/authen/login';
let username = {
loginType: 1,
authenKey: me.mobileNo,
password: me.password
};
let params = new HttpParams()
.set('username', JSON.stringify(username))
.set('password', me.password);
me.http.post<SingleResult<UserInfo>>(url, params).subscribe(
rejObj => {
if (rejObj.success) {
UserInfo.currentUser = rejObj.data;
me.navCtrl.navigateForward(this.targetUrl);
} else {
me.errorMsg = rejObj.message;
}
},
error => {
console.error(error.message, error);
me.errorMsg = error.message;
}
)
}
這段代碼的重點(diǎn),在于通過
UserInfo.currentUser = rejObj.data;將返回的數(shù)據(jù)存儲(chǔ)起來了。其中,UserInfo.currentUser是一個(gè)存儲(chǔ)當(dāng)前登陸用戶信息的和后端約定好的數(shù)據(jù)結(jié)構(gòu),其中就有會(huì)話ID。
你可以采用自己的存儲(chǔ)數(shù)據(jù)的方法,比如存在全局變量中,或存儲(chǔ)在手機(jī)端的sqlite數(shù)據(jù)庫(kù)中,或其他適當(dāng)?shù)姆绞健?/p>
第四步,設(shè)置angular的http請(qǐng)求基礎(chǔ)方法,在http header中設(shè)置會(huì)話參數(shù) JSEESIONID,并啟用認(rèn)證
我用的是angular8,因此代碼大致如下:
/**
* 預(yù)處理options對(duì)象,主要是在headers中添加會(huì)話ID的cookie的屬性。
* @param options 待處理的options對(duì)象
*/
private processOptions(options?: {
headers?: HttpHeaders ;
observe: 'body';
params?: HttpParams;
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}): any {
if (options) {
let httpHeaders = options.headers || new HttpHeaders();
if (UserInfo.currentUser && UserInfo.currentUser.sessionId) {
options.headers = httpHeaders.set('Cookie', 'JSESSIONID=' + UserInfo.currentUser.sessionId);
}
options.withCredentials = true;
} else {
let httpHeaders = new HttpHeaders();
if (UserInfo.currentUser && UserInfo.currentUser.sessionId) {
httpHeaders = httpHeaders.set('Cookie', 'JSESSIONID=' + UserInfo.currentUser.sessionId);
}
options = {
headers: httpHeaders,
observe: 'body',
params: null,
reportProgress: false,
responseType: 'json',
withCredentials: true
};
}
return options;
}
/**
* 構(gòu)建一個(gè)Get請(qǐng)求,它發(fā)送請(qǐng)求前,先預(yù)處理請(qǐng)求參數(shù),增加認(rèn)證會(huì)話信息。
* @return an `Observable` of the `HttpResponse` for the request, with a body type of `T`.
*/
get<T>(url: string, options?: {
headers?: HttpHeaders;
observe: 'body';
params?: HttpParams;
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}): Observable<T> {
options = this.processOptions(options);
return this.http.get<T>(environment.ctx + url, options);
}
- 在
processOptions方法中,檢查全局變量中存儲(chǔ)的已認(rèn)證用戶信息,并設(shè)置到請(qǐng)求的cookie中。- 設(shè)置請(qǐng)求的
withCredentials: true,確保認(rèn)證信息會(huì)被傳遞過去。