6网页前台登录模块
六网页前台登录模块
1具体功能
1.1注册
- 前端输入用户名,密码,手机号
- 点击发送验证码按钮,调用后台接口
- 使用阿里云短信sdk,发送验证码
- 将验证码保存到redis中,key为手机号,value为验证码,时间可以为5分钟
- 五分钟内再次点击前端接口,直接从redis中查询并返回验证码
- 超过五分钟,再起调用阿里云短信sdk,重新发送
- 前端点击确认注册,调用后台接口
- 获取注册的数据
- 手机号密码非空判断
- 从redis中根据手机号判断验证码是否正确
- 判断手机号是否重复
- 将用户名,手机号,密码保存到数据库(注意密码md5加密)
1.2登录
- 前端输入用户名,密码,手机号
- 点击确认登录,调用后台接口
- 获取登录手机号和密码
- 手机号非空以及正确性判断
- 密码非空以及正确性判断
- 把输入的密码进行加密,再和数据库密码进行比较
- 加密方式 MD5
- 根据用户名,手机号生成token字符串,使用jwt工具类
- 设置头部分,过期时间,主体部分,签名哈希
- 返回token给前端
- 前端调用接口后
- 前端调用登录接口获得返回的token字符串
- 把返回的token字符串放到cookie里面
- 根据token值,调用后端接口,返回用户Id,再根据用户Id返回用户信息,前端得到用户信息
- 将获取到的用户信息,放到cookie里面(注意两次cookie的key不一样)
- 在首页面显示用户信息
- 在渲染前从cookie中获得用户信息
- 从cookie中取到的用户信息是字符串 ,变成json就能正常使用了
- 退出时清除cookie里面的两个key就行了
- 前端拦截器
- 为了每次请求方便用request拦截器来使token自动增加到请求头
2具体知识点
2.1登录方式简介
-
单一服务器模式
- 使用session对象实现
- 登录成功之后,把用户数据放到session里面
- 判断是否登录,从session获取数据,可以获取到登录
- session.setAttribute("user",user);
- session.getAttribute("user");
- 但是微服务模式下不能用session模式,用的是单点模式
- 就是在一个模块登录了 其他模块都不用登录了 ,就像百度 有很多产品 我在贴吧里面登录 再去百度文库就不需要登录 这就是单点登录
- 使用session对象实现
-
单点登录三种常见方式
-
a.session广播机制实现
- 简单来说就是session复制
- 有一个致命的缺点 就是项目中有几十个模块 就要把session复制好多次 不适合特别多的模块
-
b.使用cookie+redis实现
- 项目中任何一个模块进行登录,登录之后把数据放到两个地方
- redis,在key中生成一个唯一随机值(ip,用户id等等),在value中存用户数据
- cookie,把redis里面生成key值放到cookie里面
- 访问项目中其他模块,发送请求带着cookie进行发送,获取cookie值,拿着cookie做事情
- 把cookie获取值,到redis进行查询,根据key进行查询,如果查询数据就是登录
- 项目中任何一个模块进行登录,登录之后把数据放到两个地方
-
c.使用token实现
- token是按照一定规则生成字符串(规则自己约定),字符串里面包含用户信息
- 在某个项目登录后 按照规则(官方规则JWT)生成字符串,把字符串返回;可以把字符串通过cookie返回也可以把字符串通过地址栏返回;
- 再去访问其他模块时,地址栏都带着生成的字符串,在访问模块里面获取地址栏字符串,根据字符串获取用户信息;如果可以获取到就是登陆
-
-
细节
- session有默认的30分钟过期时间
- 第二种和第三种方法也能做到过期
- 第二种就是设置redis过期时间 第三种token生成时候也能设计过期时间
2.2JWT简介
-
jwt介绍
- token是按照一定规则生成字符串,包括用户信息,规则是怎么样的,不一定
- 一般采用通用的规则,官方规则JWT
- JWT就是给我们规定好了规则,使用jwt规则可以生成字符串,包括用户信息
-
jwt组成
-
![]()
-
第一部分:jwt头信息(公共部分)
-
第二部分:有效载荷,包含主体信息(用户信息)(私有部分)
-
第三部分:签名哈希,防伪标签(签名部分)
-
-
jwt头
-
jwt头部分是一个描述jwt元数据的json对象,通常如下所示
-
{ "alg":"HS256", “typ":"JWT" } -
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)
-
typ属性表示令牌的类型,jwt令牌统一写为JWT
-
最后使用Base64 URL算法将上述json对象转换为字符串保存
-
-
有效载荷
-
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
-
iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于标识该JWT -
除以上默认字段外,我们还可以自定义私有字段,如下例:
-
{ "sub": "1234567890", "name": "Helen", "admin": true } -
JSON对象也使用Base64 URL算法转换为字符串保存
-
-
签名哈希
-
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改
-
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
-
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret) -
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象
-
-
Base64URL算法
- 如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别
- 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符
是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法 - base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
2.3JWT使用
-
引入依赖
-
使用JWT工具类
-
第一部分:常量(token过期时间,秘钥)
-
//常量 public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间 public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥 这边随便写就行 实际肯定是按照公司规则生成
-
-
第二部分:根据Id生成token字符串的方法
-
//生成token字符串的方法 //一般就是用户id 和name来生成token public static String getJwtToken(String id, String nickname){ String JwtToken = Jwts.builder() //头部分 .setHeaderParam("typ", "JWT") .setHeaderParam("alg", "HS256") //设置过期时间 .setSubject("guli-user") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置token主体部分 ,存储用户信息 .claim("id", id) .claim("nickname", nickname) //签名哈希 .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; }
-
-
第三部分: 判断token是否存在与有效
-
// 判断token是否存在与有效 public static boolean checkToken(String jwtToken) { if(StringUtils.isEmpty(jwtToken)) return false; try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
-
-
第四部分:根据token字符串获取会员id
-
public static String getMemberIdByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return ""; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return (String)claims.get("id"); }
-
-
2.4阿里云短信(只有注册需要用到,和登录无关)
-
开通阿里云短信服务
- 使用用测试api
-
编写代码实现短信发送
-
直接复制官方demo
-
@Service public class MsmServiceImpl implements MsmService { /** * 使用AK&SK初始化账号Client * * @param accessKeyId * @param accessKeySecret * @return Client * @throws Exception */ public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception { Config config = new Config() // 您的AccessKey ID .setAccessKeyId("xxxxx") // 您的AccessKey Secret .setAccessKeySecret("Oxxx"); // 访问的域名 config.endpoint = "dysmsapi.aliyuncs.com"; return new com.aliyun.dysmsapi20170525.Client(config); } //发送短信的方法 @Override public boolean send(Map<String, Object> param, String phone) { com.aliyun.dysmsapi20170525.Client client = null; try { client = MsmServiceImpl.createClient("accessKeyId", "accessKeySecret"); } catch (Exception e) { e.printStackTrace(); return false; } SendSmsRequest sendSmsRequest = new SendSmsRequest() .setSignName("阿里云短信测试") .setTemplateCode("xxxx909") .setPhoneNumbers("xxxxxx") .setTemplateParam("{\"code\":\"1234\"}"); //最终发送 try { SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest); return true; } catch (Exception e) { e.printStackTrace(); return false; } } }
-
-
redis解决验证码有效时间问题
-
这样5分钟内再次点击就会到redis去查
-
如果超时了就再次发送
-
//发送短信的方法 @GetMapping("send/{phone}") public R sendMsm(@PathVariable String phone) throws Exception { //1 从redis获取验证码,如果获取到直接返回 String code = redisTemplate.opsForValue().get(phone); if(!StringUtils.isEmpty(code)) { return R.ok(); } //2 如果redis获取 不到,进行阿里云发送 //生成随机值,传递阿里云进行发送(由于是测试账号只能1234 这边是模拟正式场景) //注意randomutil是老师的工具类 code = RandomUtil.getFourBitRandom(); Map<String,Object> param = new HashMap<>(); param.put("code",code); //调用service发送短信的方法(就是上面复制的代码) boolean isSend = msmService.send(param,phone); if(isSend) { //发送成功,把发送成功验证码放到redis里面 //设置有效时间 redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES); return R.ok(); } else { return R.error().message("短信发送失败"); } }
-
2.5前台登录模块完整
-
在service下面创建子模块service_ucenter
-
编写实体类
- 用户UcenterMember(主要字段)
- 用户id
- 用户名
- 微信openid
- 手机号
- 密码
- 性别
- 年龄
- 用户签名
- RegisterVo
- 用户名
- 手机号
- 密码
- 验证码
- 用户UcenterMember(主要字段)
-
注册流程
-
controller
-
//注册 @PostMapping("register") public R registerUser(@RequestBody RegisterVo registerVo) { memberService.register(registerVo); return R.ok(); } -
service
-
注意密码加密使用md5加密密码(有工具类)
-
//注册的方法 @Override public void register(RegisterVo registerVo) { //1获取注册的数据 String code = registerVo.getCode(); //验证码 String mobile = registerVo.getMobile(); //手机号 String nickname = registerVo.getNickname(); //昵称 String password = registerVo.getPassword(); //密码 //2非空判断 if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password) || StringUtils.isEmpty(code) || StringUtils.isEmpty(nickname)) { throw new GuliException(20001,"注册失败"); } //3判断验证码(注意之前以及获得过阿里云的短信验证码了) //获取redis验证码 String redisCode = redisTemplate.opsForValue().get(mobile); if(!code.equals("1234")){//真实业务就是!code.equals(redissCode) if(!code.equals(redisCode)) { throw new GuliException(20001,"注册失败"); } } //4判断手机号是否重复,表里面存在相同手机号不进行添加 QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile",mobile); Integer count = baseMapper.selectCount(wrapper); if(count > 0) { throw new GuliException(20001,"注册失败"); } //5数据添加数据库中 UcenterMember member = new UcenterMember(); member.setMobile(mobile); member.setNickname(nickname); member.setPassword(MD5.encrypt(password));//密码需要加密的 member.setIsDisabled(false);//用户不禁用 member.setAvatar("xxxxx.jpeg"); baseMapper.insert(member); }
-
-
登录流程
-
controller
-
//登录 @PostMapping("login") public R loginUser(@RequestBody UcenterMember member) { //member对象封装手机号和密码 //调用service方法实现登录 //返回token值,使用jwt生成 String token = memberService.login(member); return R.ok().data("token",token); } -
service
-
//登录的方法 @Override public String login(UcenterMember member) { //1获取登录手机号和密码 String mobile = member.getMobile(); String password = member.getPassword(); //2手机号和密码非空以及准确性判断 //手机号和密码非空判断 if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) { throw new GuliException(20001,"登录失败"); } //判断手机号是否正确 QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile",mobile); UcenterMember mobileMember = baseMapper.selectOne(wrapper); //判断查询对象是否为空 if(mobileMember == null) {//没有这个手机号 throw new GuliException(20001,"登录失败"); } //判断密码 //因为存储到数据库密码肯定加密的 //把输入的密码进行加密,再和数据库密码进行比较 //加密方式 MD5 if(!MD5.encrypt(password).equals(mobileMember.getPassword())) { throw new GuliException(20001,"登录失败"); }//3登录成功生成token字符串返回 //使用jwt工具类 String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname()); return jwtToken; }
-
2.6注册和登录成功之后首页面显示数据全过程
-
第一步:前端调用登录接口返回token字符串
-
第二步:把第一步返回token字符串放到cookie里面(前端)
-
//第一个参数cookie名称,第二个参数值,第三个参数作用范围 cookie.set('fao_token',response.data.data.token,{domain: 'localhost'})
-
-
第三步:根据token值,调用后端接口,返回用户Id,再根据用户Id返回用户信息用于前台显示
-
//后端接口 //根据token获取用户信息 @GetMapping("getMemberInfo") public R getMemberInfo(HttpServletRequest request) { //调用jwt工具类的方法。根据request对象获取头信息,返回用户id String memberId = JwtUtils.getMemberIdByJwtToken(request); //查询数据库根据用户id获取用户信息 UcenterMember member = memberService.getById(memberId); return R.ok().data("userInfo",member); }
-
-
第四步:获取返回用户信息,放到cookie里面
-
//第四步获取返回用户信息,放到cookie里面 cookie.set('fao_ucenter',this.loginInfo,{domain: 'localhost'})
-
-
第五步:在首页面显示用户信息
-
从cookie中取到的用户信息是字符串 我们要变成json
-
created() { this.showInfo(); }, methods: { //创建方法,从cookie获取用户信息 showInfo() { //从cookie获取用户信息 var userStr = cookie.get("guli_ucenter"); // 把字符串转换json对象(js对象) if (userStr) { console.log(userStr); this.loginInfo = JSON.parse(userStr); } } } -
注意 后端都是字符串 前端才是json 后端是没有这个概念的
-
-
第六步:退出
-
就是清除cookie
-
//退出 logout() { //清空cookie值 cookie.set("guli_token", "", { domain: "localhost" }); cookie.set("guli_ucenter", "", { domain: "localhost" }); //回到首页面 window.location.href = "/"; }
-
2.7前端请求拦截器
-
简介
- 在前后端交互时,经常会使用到token
- 上面首次请求后台把token返回给前端,已经把这个token可以保存到cookie里面了
- 之后每次请求后端都需要带上这个token 进行鉴权验证,太麻烦
- 这时候就可以用拦截器来使token自动增加
-
创建前端拦截器,判断cookie里面是否有token字符串
-
// http request 拦截器 service.interceptors.request.use( config => { //debugger //判断cookie里面是否有名称为fao_token数据 if (cookie.get('fao_token')) { //如果有,把获取cookie值自动放到header(请求头)里面 config.headers['token'] = cookie.get('fao_token'); } return config }, err => { return Promise.reject(err); })
-


浙公网安备 33010602011771号