几种简单的登录方式的实现——前端+后端

登录方式的实现

引言

想了一下之前项目中用到的登录方式,简单的总结一下

1、普通登录

  • 普通登录的实现:根据用户输入的用户名和密码,提交到后台,后台判断用户输入的信息是否在数据库中存在,如果存在就给前端返回数据。
  • 出现的问题:只要数据库存在用户信息,不管任何时候都可以登录,所以存在安全问题,就需要考虑权限控制,安全认证,防止CSRF攻击等问题。

前端代码

代码语言:txt
复制
$.ajax({
            url: '/login',
            type: 'POST',
            dataType: "json",
            data: {
                "username": username,
                "password": password,
            },
            success: function (result1) {
                //获取后台数据result1
                if ("true" === result1.flag) {
                    //信息正确,跳转到首页
                    window.location.href = "/common/index";
                } else if ("false" === result1.flag) {
                    $("#tip2").html("用户不存在!");
                }
            },
            async: true,
            error: function () {
                //请求失败跳转到登录
                window.location.href = "/tologin";
           }
})

后端Controller代码

代码语言:txt
复制
@RequestMapping("/login")
    @ResponseBody
    public Map<String, String> userLogin(@RequestParam("username") String username,
                                         @RequestParam("password") String password,
                                         HttpServletRequest request) {
        Users users = userService.userLogin(username, password);
        Map<String, String> result = new HashMap<String, String>();
        if (users != null) {
            result.put("flag", "true");
        } else {
            result.put("flag", "false");
        }
        return result;
    }

后端Service代码

代码语言:txt
复制
public Users userLogin(String username, String password) {
        return usermapper.userLogin(username, password);
 }

2、Token验证

  • 什么是Token

它是在后台也就是服务端产生的一串字符串,用来给前端鉴权的一种方法,前端如果遇到很频繁的请求后台数据时,每次都需要把当前登录用户信息与数据库的比对,判断是否正确,才返回数据,这样无疑会增加服务器压力

  • Token的作用

避免CSRF攻击

Token属于无状态的,可以在多个服务中共享

  • 在项目中的实现:把用户登录信息提交到后台,后台会先判断数据库表中是否有这个人,如果不等于空,就生成Token令牌,把信息传给前端,前端收到Token令牌后,保存到Local Storage,可以弄一个axios的拦截器,每次进行axios请求时,判断一下Local Storage中是否含有Token,保证了登录安全性

前端代码

代码语言:txt
复制
async success() {
      // 发起登入请求
      const { data: res } = await this.$http.post(
        "api/system/user/login",
        this.userLoginForm
      );
      if (res.success) {
        this.$message({
          title: "登入成功",
          message: "欢迎你进入系统",
          type: "success"
        });
        // 把后台返回的token信息保存到LocalStorage
        LocalStorage.set(LOCAL_KEY_XINGUAN_ACCESS_TOKEN, res.data);
        // 获取当前登录用户用户信息
        await this.getUserInfo();
      } else {
        this.$message.error({
          title: "登入失败",
          message: res.data.errorMsg,
          type: "error"
      });
}

后端Controller代码

代码语言:txt
复制
    @PostMapping("/login")
public ResponseBean<String> login(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) throws SystemException {
       log.info(userLoginDTO.getUsername()+userLoginDTO.getPassword()+userLoginDTO.getImageCode());
String token=
userService.login(userLoginDTO.getUsername(),userLoginDTO.getPassword(),userLoginDTO.getImageCode());
       loginLogService.add(request);
       return ResponseBean.success(token);
}

后端Service代码

代码语言:txt
复制
 @Override
    public String login(String username, String password,String code) throws SystemException {
        String token;
        //获取随机验证码
        String verifyCode = (String) redisTemplate.opsForValue().get("imageCode");
        if(code.equals(verifyCode)){
            User user = apiUserService.findUserByName(username);
            if (user != null) {
                //对密码进行加盐加密
                String salt = user.getUSalt();
                //秘钥为盐
                String target = MD5Utils.md5Encryption(password, salt);
                //生成Token
                token = JWTUtils.sign(username, target);
                JWTToken jwtToken = new JWTToken(token);
                try {
                    SecurityUtils.getSubject().login(jwtToken);
                } catch (AuthenticationException e) {
                    throw new SystemException(SystemCodeEnum.PARAMETER_ERROR,e.getMessage());
                }
            } else {
                throw new SystemException(SystemCodeEnum.PARAMETER_ERROR,"用户不存在");
            }
        }else{
            throw new SystemException(SystemCodeEnum.PARAMETER_ERROR,"验证码错误");
        }
        return token;
    }

3、微信登录

微信登录也是一种安全登录方式,它录是基于OAuth 2.0协议标准构建的微信OAuth 2.0授权登录系统,时序图如下

img

官方文档

https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

前端代码

代码语言:txt
复制
//后台接口
const api_name = `/api/ucenter/wx`
export default {
  getLoginParam() {
    return request({
      url: `${api_name}/getLoginParam`,
      method: `get`
    })
  }
}
代码语言:txt
复制
weixinApi.getLoginParam().then(response => {
        console.log(response);
        let REDIRECT_URI = encodeURIComponent(response.data.redirectUri);
        var obj = new WxLogin({
          self_redirect: true,
          id: "weixinLogin", // 需要显示的容器id
          appid: response.data.appid, // 公众号appid wx*******
          scope: response.data.scope, // 网页默认即可
          redirect_uri: REDIRECT_URI, // 授权成功后回调的url
          state: response.data.state, // 可设置为简单的随机数加session用来校验
          style: "black", // 提供"black"、"white"可选。二维码的样式
          href: "" // 外部css文件url,需要https
        });
});

后端代码

application.properties文件配置

代码语言:txt
复制
//微信开发平台appid
wx.open.app_id=
//微信开发平台appsecret
wx.open.app_secret=
//微信开发平台重定向地址
wx.open.redirect_url=
//配置前端域名地址
baseUrl=

后端Controller代码

代码语言:txt
复制
//微信扫码
@GetMapping("getLoginParam")
    @ResponseBody
    public Result genQrConnect() {
        try {
            Map<String, Object> map = new HashMap<>();
            map.put("appid", ConstantWxPropertiesUtils.WX_OPEN_APP_ID);
            map.put("scope","snsapi_login");
            String wxOpenRedirectUrl = ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL;
            wxOpenRedirectUrl = URLEncoder.encode(wxOpenRedirectUrl, "utf-8");
            map.put("redirect_uri",wxOpenRedirectUrl);
            map.put("state",System.currentTimeMillis()+"");
            return Result.ok(map);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
}
// 微信扫描后回调的方法
@GetMapping(&#34;callback&#34;)
public String callback(String code,String state) {
    // 第一步 获取临时票据 code
    System.out.println(&#34;code:&#34;+code);
    // 第二步 拿着code和微信id和秘钥,请求微信固定地址,得到两个值
    // 使用code和appid以及appscrect换取access_token
    //  %s占位符
    StringBuffer baseAccessTokenUrl = new StringBuffer()
            .append(&#34;https://api.weixin.qq.com/sns/oauth2/access_token&#34;)
            .append(&#34;?appid=%s&#34;)
            .append(&#34;&amp;secret=%s&#34;)
            .append(&#34;&amp;code=%s&#34;)
            .append(&#34;&amp;grant_type=authorization_code&#34;);
    String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
            ConstantWxPropertiesUtils.WX_OPEN_APP_ID,
            ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET,
            code);
    //使用httpclient请求这个地址
    try {
        String accesstokenInfo = HttpClientUtils.get(accessTokenUrl);
        System.out.println(&#34;accesstokenInfo:&#34;+accesstokenInfo);
        //从返回字符串获取两个值 openid  和  access_token
        JSONObject jsonObject = JSONObject.parseObject(accesstokenInfo);
        String access_token = jsonObject.getString(&#34;access_token&#34;);
        String openid = jsonObject.getString(&#34;openid&#34;);

        // 判断数据库是否存在微信的扫描人信息
        // 根据openid判断
        UserInfo userInfo = userInfoService.selectWxInfoOpenId(openid);
        if(userInfo == null) { // 数据库不存在微信信息
            // 第三步 拿着openid和access_token请求微信地址,得到扫描人信息
            String baseUserInfoUrl = &#34;https://api.weixin.qq.com/sns/userinfo&#34; +
                    &#34;?access_token=%s&#34; +
                    &#34;&amp;openid=%s&#34;;
            String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
            String resultInfo = HttpClientUtils.get(userInfoUrl);
            System.out.println(&#34;resultInfo:&#34;+resultInfo);
            JSONObject resultUserInfoJson = JSONObject.parseObject(resultInfo);
            // 解析用户信息
            // 用户昵称
            String nickname = resultUserInfoJson.getString(&#34;nickname&#34;);
            // 用户头像
            String headimgurl = resultUserInfoJson.getString(&#34;headimgurl&#34;);

            // 获取扫描人信息添加数据库
            userInfo = new UserInfo();
            userInfo.setNickName(nickname);
            userInfo.setOpenid(openid);
            userInfo.setStatus(1);
            userInfoService.save(userInfo);
        }
        // 返回name和token字符串
        Map&lt;String,String&gt; map = new HashMap&lt;&gt;();
        String name = userInfo.getName();
        if(StringUtils.isEmpty(name)) {
            name = userInfo.getNickName();
        }
        if(StringUtils.isEmpty(name)) {
            name = userInfo.getPhone();
        }
        map.put(&#34;name&#34;, name);

        // 判断userInfo是否有手机号,如果手机号为空,返回openid
        // 如果手机号不为空,返回openid值是空字符串
        // 前端判断:如果openid不为空,绑定手机号,如果openid为空,不需要绑定手机号
        if(StringUtils.isEmpty(userInfo.getPhone())) {
            map.put(&#34;openid&#34;, userInfo.getOpenid());
        } else {
            map.put(&#34;openid&#34;, &#34;&#34;);
        }
        // 使用jwt生成token字符串
        String token = JwtHelper.createToken(userInfo.getId(), name);
        map.put(&#34;token&#34;, token);
        // 跳转到前端页面
        return &#34;redirect:&#34; + ConstantWxPropertiesUtils.BASE_URL + &#34;/weixin/callback?token=&#34;+map.get(&#34;token&#34;)+ &#34;&amp;openid=&#34;+map.get(&#34;openid&#34;)+&#34;&amp;name=&#34;+URLEncoder.encode(map.get(&#34;name&#34;),&#34;utf-8&#34;);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }

}

4、手机号登录

手机号的登录实现:根据用户输入的手机号,当提交登录后,后台会先判断手机号是否会空,如果不为空,利用一个可以生成随机验证码的方法,把验证码保存到Redis中,并设置有效时间,再把配置参数信息包括生成的验证码,提交到阿里云那里,判断配置信息是否正确,如果正确,向用户手机号发送短信验证码,用户输入验证码后,最后把输入的验证码用来与Redis中的验证码对比,如果相同就返回数据给前端

引入依赖

代码语言:txt
复制
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>

application.properties配置

代码语言:txt
复制
//配置阿里云API的密钥
aliyun.sms.regionId=default
aliyun.sms.accessKeyId=
aliyun.sms.secret=

前端代码

代码语言:txt
复制
<div class="operate-view" v-if="dialogAtrr.showLoginType === 'phone'">
<div class="wrapper" style="width: 100%">
<div class="mobile-wrapper" style="position: static;width: 70%">
<span class="title">{{ dialogAtrr.labelTips }}</span>
<el-form>
<el-form-item>
<el-input
v-model="dialogAtrr.inputValue"
:placeholder="dialogAtrr.placeholder"
:maxlength="dialogAtrr.maxlength"
class="input v-input"
>
<span
slot="suffix"
class="sendText v-link"
v-if="dialogAtrr.second > 0"
>{{ dialogAtrr.second }}s</span>
<span
slot="suffix"
class="sendText v-link highlight clickable selected"
v-if="dialogAtrr.second == 0"
@click="getCodeFun()"
>重新发送</span>
</el-input>
</el-form-item>
</el-form>
<div class="send-button v-button" @click="btnClick()">{{ dialogAtrr.loginBtn }}</div>
</div>
<div class="bottom">
<div class="wechat-wrapper" @click="weixinLogin()">
<span class="iconfont icon"></span>
</div>
<span class="third-text">第三方账号登录</span>
</div>
</div>
</div>
代码语言:txt
复制
//后台接口
const api_name = /api/sms
export default {
sendCode(mobile) {
return request({
url: ${api_name}/send/${mobile},
method: get
})
}
}
代码语言:txt
复制
	// 获取验证码
getCodeFun() {
if (!/^1[34578]\d{9}$/.test(this.userInfo.phone)) {
this.$message.error("手机号码不正确");
return;
}

  // 初始化验证码相关属性
  this.dialogAtrr.inputValue = &#34;&#34;;
  this.dialogAtrr.placeholder = &#34;请输入验证码&#34;;
  this.dialogAtrr.maxlength = 6;
  this.dialogAtrr.loginBtn = &#34;马上登录&#34;;

  // 控制重复发送
  if (!this.dialogAtrr.sending) return;
  // 发送短信验证码
  this.timeDown();
  this.dialogAtrr.sending = false;
  smsApi
    .sendCode(this.userInfo.phone)
    .then(response =&gt; {
      this.timeDown();
    })
    .catch(e =&gt; {
      this.$message.error(&#34;发送失败,重新发送&#34;);
      // 发送失败,回到重新获取验证码界面
      this.showLogin();
    });
},

// 倒计时
timeDown() {
  if (this.clearSmsTime) {
    clearInterval(this.clearSmsTime);
  }
  this.dialogAtrr.second = 60;
  this.dialogAtrr.labelTips = &#34;验证码已发送至&#34; + this.userInfo.phone;
  this.clearSmsTime = setInterval(() =&gt; {
    --this.dialogAtrr.second;
    if (this.dialogAtrr.second &lt; 1) {
      clearInterval(this.clearSmsTime);
      this.dialogAtrr.sending = true;
      this.dialogAtrr.second = 0;
    }
  }, 1000);
},</code></pre></div></div><p>后端Controller代码</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>txt</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-txt"><code class="language-txt" style="margin-left:0">	// 用户手机号登录接口
@PostMapping(&#34;login&#34;)
public Result login(@RequestBody LoginVo loginVo) {
    Map&lt;String,Object&gt; info = userInfoService.loginUser(loginVo);
    return Result.ok(info);

}

代码语言:txt
复制
	// 发送手机验证码
@GetMapping("send/{phone}")
public Result sendCode(@PathVariable String phone) {
//从redis获取验证码,如果获取获取到,返回ok
// key 手机号 value 验证码
String code = redisTemplate.opsForValue().get(phone);
if(!StringUtils.isEmpty(code)) {
return Result.ok();
}
//如果从redis获取不到,
// 生成验证码,
code = RandomUtil.getSixBitRandom();
//调用service方法,通过整合短信服务进行发送
boolean isSend = msmService.send(phone,code);
//生成验证码放到redis里面,设置有效时间
if(isSend) {
redisTemplate.opsForValue().set(phone,code,1, TimeUnit.MINUTES);
return Result.ok();
} else {
return Result.fail().message("发送短信失败");
}
}

后端Service代码

代码语言:txt
复制
	// 手机号登录service
@Override
public Map<String, Object> loginUser(LoginVo loginVo) {
//从loginVo获取输入的手机号,和验证码
String phone = loginVo.getPhone();
String code = loginVo.getCode();

    //判断手机号和验证码是否为空
    if(StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
        throw new Exception(ResultCodeEnum.PARAM_ERROR);
    }

    //判断手机验证码和输入的验证码是否一致
    String redisCode = redisTemplate.opsForValue().get(phone);
    if(!code.equals(redisCode)) {
        throw new Exception(ResultCodeEnum.CODE_ERROR);
    }

    //绑定手机号码
    UserInfo userInfo = null;
    if(!StringUtils.isEmpty(loginVo.getOpenid())) {
        userInfo = this.selectWxInfoOpenId(loginVo.getOpenid());
        if(null != userInfo) {
            userInfo.setPhone(loginVo.getPhone());
            this.updateById(userInfo);
        } else {
            throw new Exception(ResultCodeEnum.DATA_ERROR);
        }
    }

    //如果userinfo为空,进行正常手机登录
    if(userInfo == null) {
        //判断是否第一次登录:根据手机号查询数据库,如果不存在相同手机号就是第一次登录
        QueryWrapper&lt;UserInfo&gt; wrapper = new QueryWrapper&lt;&gt;();
        wrapper.eq(&#34;phone&#34;,phone);
        userInfo = baseMapper.selectOne(wrapper);
        if(userInfo == null) { //第一次使用这个手机号登录
            //添加信息到数据库
            userInfo = new UserInfo();
            userInfo.setName(&#34;&#34;);
            userInfo.setPhone(phone);
            userInfo.setStatus(1);
            baseMapper.insert(userInfo);
        }
    }

    //校验是否被禁用
    if(userInfo.getStatus() == 0) {
        throw new Exception(ResultCodeEnum.LOGIN_DISABLED_ERROR);
    }

    //不是第一次,直接登录
    //返回登录信息
    //返回登录用户名
    //返回token信息
    Map&lt;String, Object&gt; map = new HashMap&lt;&gt;();
    String name = userInfo.getName();
    if(StringUtils.isEmpty(name)) {
        name = userInfo.getNickName();
    }
    if(StringUtils.isEmpty(name)) {
        name = userInfo.getPhone();
    }
    map.put(&#34;name&#34;,name);

    //jwt生成token字符串
    String token = JwtHelper.createToken(userInfo.getId(), name);
    map.put(&#34;token&#34;,token);
    return map;
}</code></pre></div></div><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>txt</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-txt"><code class="language-txt" style="margin-left:0">	//提交验证码
@Override
public boolean send(String phone, String code) {
    //判断手机号是否为空
    if(StringUtils.isEmpty(phone)) {
        return false;
    }
    //整合阿里云短信服务
    //设置相关参数
    DefaultProfile profile = DefaultProfile.
            getProfile(ConstantPropertiesUtils.REGION_Id,
                    ConstantPropertiesUtils.ACCESS_KEY_ID,
                    ConstantPropertiesUtils.SECRECT);
    IAcsClient client = new DefaultAcsClient(profile);
    CommonRequest request = new CommonRequest();
    //request.setProtocol(ProtocolType.HTTPS);
    request.setMethod(MethodType.POST);
    request.setDomain(&#34;dysmsapi.aliyuncs.com&#34;);
    request.setVersion(&#34;2017-05-25&#34;);
    request.setAction(&#34;SendSms&#34;);

    //手机号
    request.putQueryParameter(&#34;PhoneNumbers&#34;, phone);
    //签名名称
    request.putQueryParameter(&#34;SignName&#34;, &#34;****&#34;);
    //模板code
    request.putQueryParameter(&#34;TemplateCode&#34;, &#34;******&#34;);

    request.putQueryParameter(&#34;TemplateCode&#34;, &#34;*****&#34;);
    //验证码  使用json格式   {&#34;code&#34;:&#34;123456&#34;}
    Map&lt;String,Object&gt; param = new HashMap();
    param.put(&#34;code&#34;,code);
    request.putQueryParameter(&#34;TemplateParam&#34;, JSONObject.toJSONString(param));

    //调用方法进行短信发送
    try {
        CommonResponse response = client.getCommonResponse(request);
        System.out.println(response.getData());
        return response.getHttpResponse().isSuccess();
    } catch (ServerException e) {
        e.printStackTrace();
    } catch (ClientException e) {
        e.printStackTrace();
    }
    return false;
}

@Override
public boolean send(MsmVo msmVo) {
    if(!StringUtils.isEmpty(msmVo.getPhone())) {
        boolean isSend = this.send(msmVo.getPhone(), msmVo.getParam());
        return isSend;
    }
    return false;

}

代码语言:txt
复制
@Data
@ApiModel(description = "短信实体")
public class MsmVo {

@ApiModelProperty(value = &#34;phone&#34;)
private String phone;

@ApiModelProperty(value = &#34;短信模板code&#34;)
private String templateCode;

@ApiModelProperty(value = &#34;短信模板参数&#34;)
private Map&lt;String,Object&gt; param;

}