在我们日常使用的各类软件中,自动登录是一个非常常见的功能,因为有许多功能是必须用户登录后(或者说需要用户信息)才能使用的(例如,收藏功能,查看个人信息功能等)。
简而言之,通过持久化存储用户登录或注册成功后服务器端返回的用户名密码Cookie,并在下次访问需用户信息的接口时,拦截网络请求并将本地保存的用户账号密码cookie添加上去后再进行访问即可。
我们先来了解一下注册功能的逻辑,首先用户在客户端输入用户名密码,进行注册,此时通过post请求向服务器端提交用户注册数据,若服务器端校验完毕无误,则会将用户名密码存储于服务器端,以便下次登录时进行验证,同时也会在cookie中返回账号密码告诉客户端注册成功。
其次我们再来了解一下需要登录注册才能使用的功能的实现逻辑,以收藏功能为例,当用户点击收藏按钮后,app需要做的事情有:保存当前收藏的条目id作为参数,进行带用户账号密码Cookie的网络请求,通知服务器端当前用户要收藏此item。可以简单理解为,服务器端需要知道用户id和用户密码以及用户要收藏的数据id,才能将当前收藏的数据id挂在用户id下,标识此用户收藏了此数据。
了解完上述操作的逻辑,实现自动登录功能的逻辑就显而易见了,在用户注册/登录成功后,通过Cookie拦截器拦截服务器端返回的cookie,解析并在本地进行持久化存储。每次进行网络请求时,通过头拦截器拦截请求的url,判断当前url是否需要用户信息cookie,若需要,则为其添加后再进行网络请求,则可实现自动登录功能了。
创建OkHttpClient客户端对象并为其添加自定义的Cookie拦截器和Hearder拦截器
/**
* 网络请求接口
*/
interface ApiInterface {
companion object {
private const val BASE_URL = "https://www.xxx.com"
//使用懒加载
val api: WanAndroidApiInterface by lazy { createApi() }
/**
* 通过Retrofit的动态代理生成ApiInterface实现类
* @return WanAndroidApiInterface
*/
private fun createApi(): WanAndroidApiInterface {
val build = OkHttpClient.Builder()
build.connectTimeout(Constant.CONNECT_TIME_OUT,TimeUnit.SECONDS)
build.readTimeout(Constant.CONNECT_TIME_OUT,TimeUnit.SECONDS)
build.writeTimeout(Constant.CONNECT_TIME_OUT,TimeUnit.SECONDS)
build.retryOnConnectionFailure(true)
//为OkHttp添加拦截器,以实现自动拦截存储Cookie和为需要Cookie的接口拦截添加Cookie
build.addInterceptor(CookiesInterceptor())
build.addInterceptor(HeaderInterceptor())
val loggingInterceptor = HttpLoggingInterceptor { message: String ->
LogUtils.i(
this,
message
)
}//日志拦截器
build.addInterceptor(loggingInterceptor)
val client = build.build()
val retrofit = Retrofit.Builder().baseUrl(BASE_URL).client(client)
.addConverterFactory(GsonConverterFactory.create()).build()
return retrofit.create(WanAndroidApiInterface::class.java)
}
}
/**
* 收藏站内文章
* @param collectId Int
*/
@POST("/lg/collect/{id}/json")
suspend fun collectArticle(@Path("id") collectId: Int): BaseResult?
}
Cookie拦截器:此拦截器的具体拦截规则需依据请求接口返回的Cookie自行添加拦截条件,因为有的接口即使登录失败也会返回默认的Cookie,即需要根据实际情况判断拦截存储下来的Cookie是否为用户信息Cookie!
class CookiesInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newBuilder = request.newBuilder()
val response = chain.proceed(newBuilder.build())
val url = request.url.toString()
//如果当前请求URL为登录或注册url,则拦截返回的Cookie
if ((url.contains(USER_LOGIN_URL) || url.contains(USER_REGISTER_URL))) {
val cookies = response.headers(SET_COOKIE)
val cookiesStr = CookiesManager.encodeCookie(cookies)//解析Cookie
CookiesManager.saveCookies(cookiesStr)//保存Cookie
LogUtils.e(this@CookiesInterceptor,"CookiesInterceptor:cookies:$cookies")
}
//拦截退出登录请求
if(url.contains(USER_LOGOUT_URL) && response.headers(SET_COOKIE).isNotEmpty()){
//清除本地Cookie
CookiesManager.clearCookies()
}
return response
}
}
Header拦截器
class HeaderInterceptor:Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newBuilder = request.newBuilder()
newBuilder.addHeader("Content-type", "application/json; charset=utf-8")
val host = request.url.host
val url = request.url.toString()
//给需要登录才能访问的接口添加Cookies
if (host.isNotEmpty() && url.contains("/lg/collect")){
val cookies = CookiesManager.getCookies()//获取之前保存的用户信息Cookie
LogUtils.e(this,"HeaderInterceptor:cookies:$cookies")
if (!cookies.isNullOrEmpty()) {
newBuilder.addHeader(KEY_COOKIE, cookies)//添加Cookie
}
}
return chain.proceed(newBuilder.build())
}
}
Cookie管理类
/**
* @description: Cookies管理类
* @author YL Chen
* @date 2025/2/25 18:23
* @version 1.0
*/
object CookiesManager {
/**
* 解析Cookies
* @param cookies
*/
fun encodeCookie(cookies: List?): String {
val sb = StringBuilder()
val set = HashSet()
cookies
?.map { cookie ->
cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
?.forEach { it ->
it.filterNot { set.contains(it) }.forEach { set.add(it) }
}
LogUtils.e(this,"cookiesList:$cookies")
val ite = set.iterator()
while (ite.hasNext()) {
val cookie = ite.next()
sb.append(cookie).append(";")
}
val last = sb.lastIndexOf(";")
if (sb.length - 1 == last) {
sb.deleteCharAt(last)
}
return sb.toString()
}
/**
* 保存Cookies
* @param cookies
*/
fun saveCookies(cookies: String) {
val mmkv = MMKV.defaultMMKV()
mmkv.encode(HTTP_COOKIES_INFO, cookies)
}
/**
* 获取Cookies
* @return cookies
*/
fun getCookies(): String? {
val mmkv = MMKV.defaultMMKV()
LogUtils.d(this,"getCookies-->${mmkv.decodeString(HTTP_COOKIES_INFO, "")}")
return mmkv.decodeString(HTTP_COOKIES_INFO, "")
}
/**
* 清除Cookies
*/
fun clearCookies() {
saveCookies("")
}
}