Restful风格的验证码

Restful风格的验证码


  • Restful风格的验证码
  • 接口
    • 生成验证码
      • 接口信息
      • 前端显示
    • 校验
      • 接口信息
      • 前端校验
  • 移动端使用
    • Android Retrofit Api
    • Android UI
    • 效果展示
  • 其他

原有的验证码使用流的方式,对移动端不友好,并且现在后端是分布式的微服务系统,原有的基于cookie的验证码方式,显得力不从心。

Restful 风格的验证码,图片使用Base64编码,后端使用Redis存储验证码。Android 客户端使用Retrofit + OkHttp。

接口

生成验证码

接口信息

curl -X POST \
  http://localhost:8001/captcha/gen \
  -H 'Accept: application/json' \
  -d '{
    "channel": "account_change_pwd",
    "userId": "12345"
}'

{
    "code": 200,
    "msg": null,
    "data": {
        "captchaId": "6593486a-dd27-4e7b-8772-433868555114",
        "imageBase64Header": "data:image/jpeg;base64,",
        "imageBase64": "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"
    }
}
{
    "channel": "account_change_pwd", // 渠道,一般为模块名称
    "userId": "12345" // 用户唯一编号,区别当前模块的某个用户
}
{
    "code": 200,
    "msg": null,
    "data": {
        "captchaId": "6593486a-dd27-4e7b-8772-433868555114", // 唯一编号
        "imageBase64Header": "data:image/jpeg;base64,", // base64 头部信息
        "imageBase64": "" // 验证码信息
    }
}

前端显示

前端使用 imageBase64HeaderimageBase64即可。

function loadImage() {
    $.ajax({
        type: "post",
        url: "/captcha/gen",
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify({
            channel: "account_change_pwd",
            userId: "12345"
        }),
        success: function (data, status) {
            captchaData = data;
            $('#captcha').attr('src', data.data.imageBase64Header + data.data.imageBase64);
        }
    });
}

校验

接口信息

curl -X POST \
  http://localhost:8001/captcha/check \
  -H 'Accept: application/json' \
  -d '{
    "captchaId": "cb9ec4a1-8a79-41db-a567-b742aa1879a3",
    "captchaText": "fynpf",
    "channel": "account_change_pwd",
    "userId": "12345"
}'

{
    "code": 200,
    "msg": null,
    "data": false
}

前端校验

function check() {
    var text = $('#code').val();
    if (captchaData != null && text.length > 0) {
        $.ajax({
            type: "post",
            url: "/captcha/check",
            dataType: "json",
            contentType: "application/json",
            data: JSON.stringify({
                channel: "account_change_pwd",
                userId: "12345",
                captchaId: captchaData.data.captchaId,
                captchaText: text
            }),
            success: function (data, status) {
                if (data.data) {
                    alert('check success')
                } else {
                    alert('check fail')
                }
            }
        });
    }
}

移动端使用

Android Retrofit Api

interface ICaptchaApi {

    @POST("/captcha/gen")
    fun gen(@Body req: CaptchaGenReq): Call<BaseResp<CaptchaGenData>>

    @POST("/captcha/check")
    fun check(@Body req: CaptchaCheckReq): Call<BaseResp<Boolean>>
}

Android UI

class MainActivity : AppCompatActivity() {

    var captchaGenData: CaptchaGenData? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ivCode.setOnClickListener {
            loadImageCode()
        }
        btnCheck.setOnClickListener {
            check()
        }

        loadImageCode()
    }

    private fun loadImageCode() {
        val req = CaptchaGenReq()
        req.channel = "account_pwd_change"
        req.userId = "12345"

        Apis.captchaApi.gen(req).enqueue(object : Callback<BaseResp<CaptchaGenData>> {

            override fun onResponse(call: Call<BaseResp<CaptchaGenData>>,
                                    response: Response<BaseResp<CaptchaGenData>>) {
                if (response.isSuccessful && response.body() != null) {
                    response.body()?.data?.let {
                        captchaGenData = it

                        val bytes = Base64.decode(it.imageBase64, Base64.DEFAULT)
                        val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
                        ivCode.setImageBitmap(bitmap)
                    }
                }
            }

            override fun onFailure(call: Call<BaseResp<CaptchaGenData>>, t: Throwable) {
            }
        })
    }

    private fun check() {
        val code = edtCode.text.toString()
        if (code.isNotEmpty()) {
            captchaGenData?.let {
                val req = CaptchaCheckReq().apply {
                    captchaId = it.captchaId
                    captchaText = code
                    channel = "account_pwd_change"
                    userId = "12345"
                }

                Apis.captchaApi.check(req).enqueue(object : Callback<BaseResp<Boolean>> {

                    override fun onResponse(call: Call<BaseResp<Boolean>>,
                                            response: Response<BaseResp<Boolean>>) {
                        if (response.isSuccessful) {
                            response.body()?.data?.let {
                                val msg = if (it) "check success " else "check fail"
                                Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
                            }
                        }
                    }

                    override fun onFailure(call: Call<BaseResp<Boolean>>, t: Throwable) {
                    }
                })
            }
        } else {
            Toast.makeText(this, "请输入验证码", Toast.LENGTH_LONG).show()
        }
    }
}

效果展示

[图片上传失败...(image-c0813a-1575269043190)]

其他

Java 后端

Android 客户端

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容