Spring Cloud + Vue前后端分离-第11章 用户管理与登录

 源代码在GitHub - 629y/course: Spring Cloud + Vue前后端分离-在线课程

Spring Cloud + Vue前后端分离-第11章 用户管理与登录

11-1 增加用户管理功能

用户表设计与基本代码生成

1.用户管理与登录:用户表设计与基本代码生成

all.sql

generatorConfig.xml

ServerGenerator.java

VueGenerator.java

admin.vue

router.js

测试

增加用户名是否已存在校验

1.用户管理与登录:增加用户名是否已存在校验

2.增加自定义业务异常

登录名是不可编辑的,登录名一般会跟其它表有关联,一旦登录名改了,这些关联信息就没有了

UserService.java

根据传入登录名到数据库中查找是否有记录,有记录就说明用户名已经存在

loginName 是唯一的,所以查出来要么没有记录,要么只有一条记录

我们要的功能是校验用户名是否存在,所以也可以把返回值改成true或false,而不是返回User对象,但是这种写法不够通用,所以我们选择返回User。

BusinessException.java

抛出业务异常时,不打印堆栈信息,一方面是提高性能,另一方面是没有业务异常没必要看堆栈信息

继承RuntimeException的一个好处就是代码不需要捕获,如果是直接继承Exception,代码需要捕获,否则编译不通过。

BusinessExceptionCode.java

大家也可以自己把code加上,定义一套异常码

ControllerExceptionHandler.java

user.vue

aria-*的属性主要用于一些读屏设备,方便残障人士使用,比如盲人

测试

这里打印的异常不会打印出堆栈信息,我们之前把堆栈的打印重写了

使用方法:

1.在BusinessExceptionCode.java增加一种异常枚举;

2.在业务代码中抛出指定枚举类型的业务异常

侧边栏激活样式优化

1.用户管理与登录:侧边栏激活样式优化

当前菜单的父菜单的同级菜单,下面所有的子菜单,清空激活样式

admin.vue

可以把active清空掉

测试

随便切换都没有问题

问题:访问跟目录时,页面显示的是admin.vue,但是右边的content部分是空的,没有路由到任何一个子路由

router.js

测试

解决从登录页面跳到控台主页时,侧边栏失效的问题

1.用户管理与登录:解决从登录页面跳到控台主页时,侧边栏失效的问题

问题:从登录页面跳转到控台主页时,菜单失效了

重新刷新,才会显示

打开登录页面时,会去加载所需的js,包括ace.min.js,这里会去做很多的初始化,包括侧边栏的点击事件,但是此时还没有侧边栏

解决方法:进入控台主页时,重新加载ace.min.js

admin.vue

测试成功,登录进来就可以点击出现子模块了

小提示:大家在用很多第三方框架或jquery插件时,如果发现有些功能不起作用,如果看不懂源码,可以尝试这种方法解决,把核心的js重新加载一遍 

11-2 密码的加密传输与加密存储

加密算法MD5与盐值

网站的数据库里已经把原值和密文都算好并存储起来了,如果刚好你输入的密文在数据库里有,就能解(查)出来。

盐值也叫salt值,加上盐值后,密文不容易被破解(查询)。

密码加密传输和加密存储

1.用户管理与登录:密码加密传输和加密存储

存成明文的话,至少程序员可以直接到生产上看到所有人的密码。

从路由器的日志,我可以看到所有人浏览的网站的地址、用户名、密码,如果密码刚好是明文传输,那就泄露了,如果是简单的md5,也很容易被破解,所以需要加个盐值。

md5.js

盐值可以是随机的一串值,但是所有用到盐值的地方必须是同一个值。如果你是做平台系统,有多个客户用你的平台,最好是一个客户一个盐值。

user.vue

这里可以考虑写个通用方法,把hex_md5+盐值包装起来

保险方案:对密码做两层加密

UserController.java

测试

增加修改密码功能

1.用户管理与登录:增加重置密码功能;编辑用户信息的时候不修改密码

修改用户信息和修改密码应该分开,做成两个功能

密码发生改变

user.vue

UserService.java

UserMapper.java

 UserMapper.xml

mybatis-generator 生成的方法里,updateByPrimaryKeySelective会对字段进行非空判断,再更新,如果值为空就不更新,原理就是利用mybatis的if拼成动态sql

测试

user.vue

<div id="edit-password-modal" class="modal fade" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
            <h4 class="modal-title">修改密码</h4>
          </div>
          <div class="modal-body">
            <form class="form-horizontal">
              <div class="form-group">
                <label class="control-label col-lg-3">密码</label>
                <div class="col-lg-9">
                  <input class="form-control" type="password" v-model="user.password" name="password">
                </div>
              </div>
            </form>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-white btn-default btn-round" data-dismiss="modal">
              <i class="ace-icon fa fa-times"></i>
              取消
            </button>
            <button type="button" class="btn btn-white btn-info btn-round" v-on:click="savePassword()">
              <i class="ace-icon fa fa-plus blue"></i>
              保存密码
            </button>
          </div>
        </div><!-- /.modal-content -->
      </div><!-- /.modal-dialog -->
    </div><!-- /.modal -->

UserService.java

UserController.java

测试

11-3基本的登录功能开发

基本的登录功能开发

1.用户管理与登录:基本的登录功能开发,校验用户名密码

思考:我现在存到数据库里面的密码是密文,那用户登录的时候,我数据库里面的密码解密不出来了,我怎么知道用户输入的密码对不对?

login.vue

UserController.java

UserService.java

登录验证思考:是否是根据用户名+密码到数据中去查找记录?

用户名+密码去数据库查找的话,程序不知道是用户名不对,还是密码不对。程序应该要能知道,比如我如果发现有大量的用户名不对的报错,说明有人正在不断的探测我系统的用户名。

再次思考:如果用户名不对,提示给前端的是:用户名不存在。如果是密码不对,提示给前端的是:密码不对。是否是这样?

如果你直接告诉前端说用户名不存在,我作为一个黑客的话,可以拿着现成的一堆用户名,包括手机号邮箱,不断的探测哪些用户名是你系统里有了,不要给别人任何机会获取你系统的关键信息。

BusinessExceptionCode.java

LoginUserDto.java

package com.course.server.dto;


public class LoginUserDto {

    /**
    * id
    */
    private String id;

    /**
    * 登录名
    */
    private String loginName;

    /**
    * 昵称
    */
    private String name;
    

    public String getId() {
    return id;
    }

    public void setId(String id) {
    this.id = id;
    }

    public String getLoginName() {
    return loginName;
    }

    public void setLoginName(String loginName) {
    this.loginName = loginName;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }
    
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", loginName=").append(loginName);
        sb.append(", name=").append(name);
        sb.append("]");
        return sb.toString();
    }
}

UserService.java 

UserController.java

测试

登录后保存登录信息

1.用户管理与登录:登录后前端保存登录信息并显示

前端的信息保存有多种选择:h5的localStorage, sessionStorage; js的全局变量,vue 的store等。

sessionStorage在页面刷新的时候,信息不会丢;关闭页面后,信息自动清空,适合用来存储登录信息。

localStorage在关闭页面后,登录信息还是在的,适合一些内网使用的系统保存登录信息。

用js 全局变量或vue 的store,刷新浏览器的时候,信息会丢失。

login.vue

admin.vue

把登录信息的保存和读取做成通用的方法。

tool.js

小技巧:在获取一些对象的时候,加上|| {},避免获取属性值时报错。

session-storage.js

login.vue

admin.vue

测试

11-4 退出登录与记住登录

增加退出登录功能

1.用户管理与登录:增加退出登录功能,清空前后端的会话缓存

它会动态的改变大小,适应屏幕

退出登录:清空当前登录的缓存信息,并跳到登录页面

admin.vue

一般登录信息在前后端都会保存

Constants.java

UserController.java

测试

增加记住登录信息功能

1.用户管理与登录:增加记住登录信息功能

使用localStorage来保存输入的用户名密码

login.vue

能获取到缓存的值,说明上一次有勾选“记住我” 

如果不清空本地缓存,重新打开页面时,会再次显示记住的用户名密码

测试之前要把浏览器自带的记住用户名密码清除

安全加固,本地缓存保存密码密文

1.用户管理与登录:增加记住登录信息功能,安全加固,本地缓存保存密码密文

local-storage.js

login.vue

测试

从刚才的记住我这个功能,大家可以看出来,一个是记住明文,一个是记住密文,虽然实现的功能是一样的,但是安全性上不一样,所以我们写程序不只是把功能写出来,还要严谨,不要留下坑

11-5 增加登录图形验证码

集成图形验证码kaptcha

1.用户管理与登录:集成图形验证码kaptcha

pom.xml(course)

pom.xml(server)

KaptchaConfig.java

package com.course.server.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.kaptcha.util.Config;

import java.util.Properties;

@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border", "no");
//        properties.setProperty("kaptcha.border.color", "105,179,90");
        properties.setProperty("kaptcha.textproducer.font.color", "blue");
        properties.setProperty("kaptcha.image.width", "90");
        properties.setProperty("kaptcha.image.height", "32");
        properties.setProperty("kaptcha.textproducer.font.size", "24");
        properties.setProperty("kaptcha.session.key", "code");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.textproducer.font.names", "Arial");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
        properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

    //如果项目中有多个页面会用到验证码图片,且图片的大小,颜色等都不一样,就可以增加多个生成验证码图片方法
    @Bean
    public DefaultKaptcha getWebKaptcha() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border", "no");
//        properties.setProperty("kaptcha.border.color", "105,179,90");
        properties.setProperty("kaptcha.textproducer.font.color", "blue");
        properties.setProperty("kaptcha.image.width", "90");
        properties.setProperty("kaptcha.image.height", "45");
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        properties.setProperty("kaptcha.session.key", "code");
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        properties.setProperty("kaptcha.textproducer.font.names", "Arial");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
        properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

如果项目中有多个页面会用到验证码图片,且图片的大小,颜色等都不一样,就可以增加多个生成验证码图片方法

KaptchaController.java

package com.course.system.controller.admin;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;

@RestController
@RequestMapping("/admin/kaptcha")
public class KaptchaController {

    @Qualifier("getDefaultKaptcha")
    @Autowired
    DefaultKaptcha defaultKaptcha;

    @GetMapping("/image-code/{imageCodeToken}")
    public void imageCode(@PathVariable(value = "imageCodeToken") String imageCodeToken,
    HttpServletRequest request, HttpServletResponse httpServletResponse) throws Exception{
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            // 生成验证码字符串
            String createText = defaultKaptcha.createText();

            // 将生成的验证码放入会话缓存中,后续验证的时候用到
            request.getSession().setAttribute(imageCodeToken, createText);

            // 使用验证码字符串生成验证码图片
            BufferedImage challenge = defaultKaptcha.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (IllegalArgumentException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
        byte[] captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-store");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }
}

测试http://localhost:9000/system/admin/kaptcha/image-code/123

alt+enter,将字符加到字典里,就不会有波浪线警告了 

页面显示验证码及刷新验证码

1.用户管理与登录:页面显示验证码及刷新验证码

login.vue

tool.js

radix 默认63
10个数字+26个大小字母+26个小写字母,共62个字符,可以用来表示62进制的数值,也可以加入一些特殊字符,组成更大进制的数。
原理:以62进制为例,随机生成一个0~61的数值,比如41,那边就取chars数组中的第41个字符,这样重复做8遍,就生成了8位的62进制数,重复的概览是62的8次方。也可以生成更长的数值。

登录增加验证码校验

1.用户管理与登录:登录增加验证码校验

2.解决每次ajax请求,对应的sessionId不一致的问题

UserDto.java

增加属性后,记得alt+insert生成get, set, toString()方法。比如lombok,代码侵入性太强,如果我用了插件,那大家都得安装这个插件,否则会报错。

UserController.java

在登录里面,增加验证码校验:通过token去缓存中获取验证码字符串,并和用户输入的字符串做比较。

main.js

登录验证出错时,将密码清空,同时刷新验证码图片 

login.vue

 

 

测试

刷新验证码会让网站更安全,但是会牺牲一点用户体验,需要折中选择

测试一种场景:刷新过的验证码,还能不能用。

正规的项目中,都会有专门的测试团队来编写测试用例。如果是个人开发,只能自己设计测试用例,要尽可能的覆盖各种使用场景

11-6 单点登录功能开发

单点登录解决方案介绍

生产发布时,至少是双节点,防止单台宕机。

不管是在哪一台做的登录,登录完成后,会把登录信息保存到redis中。当业务请求进来时,再到redis中获取登录信息,能获取到就表示已登录,未获取到就表示未登录,拦截掉请求。

功能:只要在其中一个产品中登录过,其他关联的产品都不需要再登录。

token:登录标识,每个用户每次登录,都会生成不同的token。

单点登录(Single Sign On),简称为SSO,核心功能:session共享。

需要解决Session共享的场景:

1.同个应用多节点共享登录信息;

2.多个项目间共享登录信息。

一般我们通常说的单点登录系统,是用来解决场景2的。

集成redis

1.用户管理与登录:集成redis,图片验证码的存储从session改为redis

pom.xml(server)

application.properties

KaptchaController.java

UserController.java

回归验证码功能,更换缓存不要对原有流程产生影响

测试

生成登录token并存储到redis中

1.用户管理与登录:生成登录token并存储到redis中,退出登录时删除token

LoginUserDto.java

UserController.java

这里也可以直接保存loginUserDto对象,但是需要序列化。如果是跨应用使用的,比如A应用存,B应用取,一般会把值转成JSON字符串

admin.vue

KaptchaController.java

测试

11-7 前后端登录拦截

基于Vue路由的登录拦截

1.用户管理与登录:基于Vue路由的登录拦截

此时,直接访问user页面,是可以直接跳过身份验证,这是绝对不允许的

router.js

只需要在父路由增加拦截就可以了,子路由就会都有这个拦截,不需要子路由再一个一个添加 

main.js

测试

但是现在还有一个问题:界面拦住了,但是所有的接口都可以直接访问,非常危险,这个就是基于后端的请求拦截

在请求headers 中统一增加token

1.用户管理与登录:在请求headers 中统一增加token

main.js

可以用这种方法给所有请求加了统一的系统参数,比如在header里加上请求流水,请求时间等。

测试

gateway增加登录拦截

1.用户管理与登录:在gateway中增加登录拦截

application.properties

LoginAdminGatewayFilter.java

LoginAdminGatewayFilterFactory.java

测试

不是所有的请求都需要做登录拦截,比如登录接口、验证码图片接口

gateway实现控台登录拦截功能

1.用户管理与登录:gateway实现控台登录拦截功能

pom.xml(course)

pom.xml(gateway)

gateway模块并没有依赖server模块,所以有些jar包如redis , json等,需要单独在pom.xml中增加依赖

application.properties

LoginAdminGatewayFilter.java

package com.course.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;

@Component
public class LoginAdminGatewayFilter implements GatewayFilter, Ordered {
    private static final Logger LOG = LoggerFactory.getLogger(LoginAdminGatewayFilter.class);

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        //请求地址中不包含/admin/的,不是控台请求,不需要拦截
        if (!path.contains("/admin/")){
            return chain.filter(exchange);
        }
        if (path.contains("/system/admin/user/login")
         || path.contains("/system/admin/user/logout")
         || path.contains("/system/admin/kaptcha")){
            LOG.info("不需要控台登录验证:{}",path);
            return chain.filter(exchange);
        }

        //获取header的token参数
        String token = exchange.getRequest().getHeaders().getFirst("token");
        LOG.info("控台登录验证开始,token:{}",token);
        if (token == null || token.isEmpty()){
            LOG.info("token为空,请求被拦截");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        Object object = redisTemplate.opsForValue().get(token);
        if (object == null){
            LOG.warn("token无效,请求被拦截");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }else {
            LOG.info("已登录:{}",object);
            return chain.filter(exchange);
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

http协议中有一些约定好的状态码,比如401未授权,404未找到,200正常处理等。

HttpStatus.java

测试

 直接访问这个地址,是会报错的

11-8 用户登录流程图

白底是前端部分,蓝底是后端部分

验证码流程

开始->生成验证码token唯一标识->调用服务端图形验证码接口->生成验证码字符串->以token为key将字符串放入redis中,设置时效->返回验证码图片->结束

登录流程

开始->输入用户名、密码、验证码->前端密码加密(防止数据传输时泄漏)->组装登录参数包含验证码token->调用登录接口->密码加密->根据token到redis获取正确的验证码->

验证码正确?(这里的验证码错误分为两种,验证码已过期和验证码不正确)

1.->清除redis验证码根据用户名查询->用户存在?

        -是>比较密码正确?

                -是>生成登录token->以token为key将登录信息放入缓存中->返回用户信息-----success=true----->

                -否>打印日志:密码错->返回用户名不存在或密码错误----- success=false----->success=true----->

        -否>打印日志:用户名不存在->返回用户名不存在或密码错误

2.->打印日志并返回验证码错误----- success=false----->success=true----->

success=true?

-是>保存用户信息到h5 session缓存->勾选记住我

        -是>保存用户名密码到h5 local缓存中->结束

        -否>清空h5 local缓存中的用户密码->结束

-否>弹出登录失败提示框->结束

                                                                                                                                     |

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值