72-cms项目(2)-Layui封装和登录功能实现

197 阅读15分钟

cms项目(2)--Layui封装和登录功能实现

笔记中涉及资源:

链接:pan.baidu.com/s/18cgWBpiV…

提取码:Coke


一、Layui封装和登录功能的实现

①:重写executeLogin方法实现登录功能

  1. 引入fastJson依赖
<!-- fastJson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.55</version>
</dependency>
  1. 重写executeLogin方法

com.it.portal.security.filter.CmsAuthenticationFilter

@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    response.setCharacterEncoding("utf-8");
    response.setContentType("application/json; charset=UTF-8");
    response.getWriter().write(JSON.toJSONString("密码错误!"));
    return false;
}
  1. login.html页面发送ajax请求
$.ajax({
    url : '${adminPath}/login.do',
    method : 'POST',
    data:{},
    success : function(data) {
    }
})

image.png

②:Layui封装使用

  1. 封装前的改动(较大)更改login.html页面script标签中的内容

删除原本的内容,我们自己来写

<script>
    layui.use('form', function () {
        var form = layui.form;

        // 粒子线条背景
        $(document).ready(function(){
            $('.layui-container').particleground({
                dotColor:'#5cbdaa',
                lineColor:'#5cbdaa'
            });
        });

        // 监听提交
        form.on('submit(login)', function (data) {
            $.ajax({
                url : '${adminPath}/login.do',
                method : 'POST',
                data:{},
                success : function(data) {
                    console.log(data);
                }
            })
            return false;
        });
    });
</script>

修改后测试

image.png

  1. 开始封装(主要封装ajax请求)
<script>
    layui.use('form', function () {
        var form = layui.form;

        // 粒子线条背景
        $(document).ready(function () {
            $('.layui-container').particleground({
                dotColor: '#5cbdaa',
                lineColor: '#5cbdaa'
            });
        });

        // 封装ajax请求
        let core = {
            http: function (option) {
                let opt = options = {
                    url: '',
                    method: 'POST',
                    data: {},
                    success: function (res) {
                        if (res === "密码错误!"){
                            console.log("密码错误了!")
                        }
                    }
                }
                // 传入的参数option替换掉默认的options最后赋值给opt
                Object.assign(opt,options,option);
                $.ajax(opt);
            }
        }
        
        // 监听提交
        form.on('submit(login)', function (data) {
            core.http({
                url: '${adminPath}/login.do'
            })
            return false;
        });
        // $.ajax({
        //     url: '${adminPath}/login.do',
        //     method: 'POST',
        //     data: {},
        //     success: function (data) {
        //         console.log(data);
        //     }
        // })
    });
</script>

修改后测试

image.png

③:安装SonarLin插件

  1. 插件安装

SonarLint是一个代码质量检测插件,可以帮助我们检测出代码中的坏味道

image.png

  1. 插件使用

image.png

④:封装结果集Result

1.创建Result类

com.it.context.foundation.Result

@Setter
@Getter
@AllArgsConstructor
public class Result<T extends Serializable> implements Serializable {
    private int code;
    private String msg;
    private T  data;

    public static <W extends Serializable> Result<W> success() {
        return new Result<>(200,null,null);
    }
    public static <W extends Serializable> Result<W> success(String msg) {
        return new Result<>(200,msg,null);
    }
    public static <W extends Serializable> Result<W> success(W data) {
        return new Result<>(200,null,data);
    }
    public static <W extends Serializable> Result<W> success(String msg,W data) {
        return new Result<>(200,msg,data);
    }
    public static <W extends Serializable> Result<W> success(int code, String msg) {
        return new Result<>(code,msg,null);
    }
    public static <W extends Serializable> Result<W> success(int code, String msg,W data) {
        return new Result<>(code,msg,data);
    }
    public static <W extends Serializable> Result<W> fail() {
        return new Result<>(500,null,null);
    }
    public static <W extends Serializable> Result<W> fail(String msg,W data) {
        return new Result<>(500,msg,data);
    }
    public static <W extends Serializable> Result<W> fail(int code, String msg) {
        return new Result<>(code,msg,null);
    }
    public static <W extends Serializable> Result<W> fail(int code, String msg,W data) {
        return new Result<>(code,msg,data);
    }
}

2.修改com.it.portal.security.filter.CmsAuthenticationFilter类

@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    response.setCharacterEncoding("utf-8");
    response.setContentType("application/json; charset=UTF-8");
    response.getWriter().write(JSON.toJSONString(Result.success("登录成功!")));
    return false;
}

3.测试

image.png

⑤:发送请求出现加载效果

参考layui layui.org.cn/layer/index…

image.png

修改login.html页面代码

image.png

layui.use(['form','layer'], function () {
    var form = layui.form, layer=layui.layer;

    // 粒子线条背景
    $(document).ready(function () {
        $('.layui-container').particleground({
            dotColor: '#5cbdaa',
            lineColor: '#5cbdaa'
        });
    });
    
    // 封装ajax请求
    let core = {
        http: function (option) {
            let opt = options = {
                url: '',
                method: 'POST',
                data: {},
                beforeSend:function() {
                    //加载层-默认风格
                    layui.layer.load();
                    //此处演示关闭
                    setTimeout(function(){
                        layer.closeAll('loading');
                    }, 2000);
                },
                success: function (res) {
                    if (res === "密码错误!"){
                        console.log("密码错误了!")
                    }
                }
            }
            // 传入的参数option替换掉默认的options最后赋值给opt
            Object.assign(opt,options,option);
            $.ajax(opt);
        }
    }

测试

image.png

但是现在有一个问题,每次请求都有一个加载效果,有些请求是不需要的

  • 解决办法:配置一个可控制的加载效果,默认有加载效果

image.png

image.png

// 封装ajax请求
let core = {
    http: function (option) {
        let opt ={load:true}, options = {
            url: '',
            method: 'POST',
            contentType:"application/x-www-from-urlencoded",
            dataType:"json",
            data: {},
            beforeSend:function() {
                //加载层-默认风格
                this.load && layui.layer.load(0,{shade:0.1});
            },
            success: function (res) {
                if (res.code === CONSTANT.HTTP.SUCCESS){
                    //此处演示关闭
                    setTimeout(function(){
                        layer.closeAll('loading');
                    }, 2000);
                    console.log("登录成功!")
                }
            }
        }
        // 传入的参数option替换掉默认的options最后赋值给opt
        Object.assign(opt,options,option);
        $.ajax(opt);
    }
}

⑥:优化加载效果代码

1.新建文件

cms-portal/src/main/webapp/admin/js/core.js

image.png

2.访问测试(正常)

  1. 登录成功后再关闭加载效果

image.png

4.将状态码定义为常量

image.png

⑦:前端节流(throttle)

节流指的都是某个函数在一定时间间隔内只执行第一次回调.举个常见的节流案例: 我们把某个表单的提交按钮--button 设成每三秒内最多执行一次 click 响应;当你首次点击后,函数会无视之后三秒的所有响应; 三秒结束后,button又恢复正常 click响应功能,以此类推.

有什么用呢?通常,这类提交button的@click响应会给后端发送API请求,频繁的点击意味着频繁的请求(流量)-会给后端带来很大的压力;此外,这些回调请求返回后,往往会在前端响应其他事件(如刷新页面),可能导致页面不停的加载,影响用户体验.所以我们要给这个 button添加节流函数,防止一些无意义的点击响应.

1.添加限流工具类

image.png

2.短时内发送多次同样的ajax请求时,ajax取消请求

image.png

3.为了演示效果,后台处理请求时加上睡眠时间

image.png

4.测试

image.png

⑧:封装layui

1. 在core.js中添加以下代码

image.png

let core={
    //限流工具类
    throttle:function(method,args,context){
        clearTimeout(method.tId);
        method.tId=setTimeout(function(){
            method.call(context,args);
        },200);
    },
    http:function(option){
        this.cancel && this.cancel.abort();
        let opt={load:true},loadHandler,options ={
            url:"",
            method:"post",
            contentType:"application/x-www-form-urlencoded",
            dataType:"json",
            beforeSend:function(){
                this.load &&  (loadHandler = LayUtil.layer.init(function(inner,layer){
                    inner.loading(0,{shade:0.1})
                }))
            },
            success:function(res){
                if(res.code != null ){
                    loadHandler.closeLoading();
                }
            }
        };
        Object.assign(opt,options,option);
        this.cancel=$.ajax(opt);
    }
};
const  CONSTANT = {
    //http相关
    HTTP:{
        SUCCESS:200,
        ERROR:500
    }
};

// layui工具类
function LayUtil(){
}

LayUtil.prototype = {
    construct:LayUtil,
    //弹窗
    layer:(function(LayUtil){
        function Inner(){

        }

        Inner.prototype={
            construct:Inner,
            init:function(callback){
                let that = this;
                layui.use('layer',function(){
                    that.layer = layui.layer;
                    if(callback instanceof Function){
                        callback(that,that.layer);
                    }
                })
                return this;
            },
            //显示loading加载
            loading:function(config={}){
                this.layer.load(config);
            },
            closeLoading:function(){
                this.layer.closeAll('loading');
            }
        }
        LayUtil.layer = new Inner();
    })(LayUtil),
    //form表单
    form:(function(LayUtil){
        function Inner(){
        }
        Inner.prototype={
            construct:Inner,
            init:function(callback){
                let that =this;
                layui.use('form',function(){
                    that.form = layui.form;
                    that.form.render();
                    if(callback instanceof Function){
                        callback(that,that.form)
                    }
                });
                return this;
            },
            //提交表单
            submit:function(callback,name,type="submit"){
                this.form.on(type+"("+(name===undefined?'go':name)+")",function(obj){
                    if(callback instanceof Function){
                        callback(obj);
                        return false;
                    }
                    return true;
                })
            },
            //验证
            verify:function(validator){
                this.form.verify(validator);
            }
        }
        LayUtil.form = new Inner();
    })(LayUtil)

}

2. 在login.html页面调用自定义的工具方法

image.png

<script>
    // 粒子线条背景
    $(document).ready(function(){
        $('.layui-container').particleground({
            dotColor:'#5cbdaa',
            lineColor:'#5cbdaa'
        });
    });


    LayUtil.form.init(function(inner,form){
        inner.submit(function(data){
            core.throttle(submit,data.field);
        })
    });

    function submit(data) {
        core.http({
            url:"${adminPath}/login.do",
            data:data
        });
    }
</script>
  • 测试

image.png

⑨:完善验证码功能

1. 编写获取Sessioon的工具类

创建 com.it.context.utils.UtilsShiro 类

public class UtilsShiro {

    /**
     * 通过shiro获取session
     * @return Session
     */
    public static Session getSession(){
        return SecurityUtils.getSubject().getSession();
    }
}

2. 编写获取request和response的工具类

创建 com.it.context.utils.UtilsHttp 类

如何在service中获取request和response?

正常来说在service层是没有request的,直接从controller传过去的话是不规范的行为,而且传递的话也很麻烦.

在Web开发中,service层或者某个工具类中需要获取到HttpServletRequest对象还是比较常见的。一种方式是将HttpServletRequest作为方法的参数从contrller层一直放下传递,不过 这种有点费劲,且做起来不是优雅;还有另一种则是RequestContextHolder,直接在需要用的地方使用如下方式取HttpServletRequest即可,使用代码如下:

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getResponse();
public class UtilsHttp {

    /**
     * 获取request
     * @return HttpServletRequest
     */
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getRequest();
    }

    /**
     * 获取response
     * @return HttpServletResponse
     */
    public static HttpServletResponse getResponse() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getResponse();
    }
}

3. 将验证码提取出来做为公共的

创建 com.it.service.api.CommonService 接口

public interface CommonService {
    void imageCaptcha();
}

创建 com.it.service.impl.CommonServiceImpl 实现类

禁止图像缓存

// 设置响应的类型格式为图片格式
response.setContentType("image/jpeg");
// 禁止图像缓存
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires",0);
@Service
@Slf4j
public class CommonServiceImpl implements CommonService {
    private static final String IMAGE_CAPTCHA = "image_captcha";
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Autowired
    private Producer captchaProducer;

    @Override
    public void imageCaptcha() {
        // 生成验证码字符
        String text = captchaProducer.createText();
        // 根据生成的验证码字符生成图片
        BufferedImage image = captchaProducer.createImage(text);
        // 保存到redis中
        Serializable sessionId = UtilsShiro.getSession().getId();
        redisTemplate.opsForValue().set(sessionId + IMAGE_CAPTCHA, text, 60, TimeUnit.SECONDS);

        HttpServletResponse response = UtilsHttp.getResponse();
        // 设置响应的类型格式为图片格式
        response.setContentType("image/jpeg");
        // 禁止图像缓存
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        // try()括号中的流会自动关闭
        try( ServletOutputStream outputStream = response.getOutputStream();) {
            ImageIO.write(image, "jpg", outputStream);
        } catch (IOException e) {
            log.error("验证码生成失败!");
            e.printStackTrace();
        }
    }
}

4. 获取用户输入的验证码

com.it.portal.security.filter.CmsAuthenticationFilter

@Autowired
private CommonService commonService;
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    response.setCharacterEncoding("utf-8");
    response.setContentType("application/json; charset=UTF-8");
    // 获取验证码
    String captcha = WebUtils.getCleanParam(request, "captcha");

    String imageCaptcha = commonService.verifyImageCaptcha(captcha);
    if (Objects.nonNull(imageCaptcha)) {
        response.getWriter().write(JSON.toJSONString(Result.fail(imageCaptcha,null)));
        return false;
    }
    response.getWriter().write(JSON.toJSONString(Result.success("登录成功!")));
    return false;
}

5. 验证图片验证码

com.it.service.api.CommonService 接口中添加方法

/**
 * 验证码图片验证码
 * @param captcha 验证码
 * @return String
 */
String verifyImageCaptcha(String captcha);

com.it.service.impl.CommonServiceImpl 实现类中实现接口方法

/**
 * 验证码图片验证码
 * @param captcha 验证码
 * @return String
 */
@Override
public String verifyImageCaptcha( String captcha) {
    // 1. 从redis中获取验证码
    Serializable sessionId = UtilsShiro.getSession().getId();
    String imageCaptcha =  redisTemplate.opsForValue().get(sessionId + IMAGE_CAPTCHA);
    // 2. 判断验证码是否为空
    if (Objects.isNull(imageCaptcha)) {
        // 验证码失效
        return "验证码失效,请重新输入!";
    }
    // 3. 判断验证码是否相等
    if (!StringUtils.equals(captcha, imageCaptcha)) {
        return "验证码错误!";
    }
    return null;
}

6. 测试

image.png

image.png

⑩:分层和层间数据传递

1. 分层介绍

最基本就是三层了,其实多层的基础也是三层:界面层、业务逻辑层、存储层

我们着重讨论的不是如何分层和层的定义,而是在分层情况下,讨论层与层之间的数据传递问题。现在的程序很少仔细地去分析层与层之间的数据传递问题,通常都是一个对象从界面生成开始一路向后传,直接保存到数据库.

我们来看一个简单的例子,应用程序的添加用户功能:

image.png

要为这个界面设计接收的数据结构也很简单:

class Login2 {
    private String name;
    private String password;
}

然后我们在添加的时候把这个对象save(login)到数据库里边就可以了.这种类似Login,可以直接存储到数据库中的数据结构命名为Persistence Object,简称PO.看起来从头到脚用一个数据结构并没有什么问题啊?

问题来了现在需要改变需求,通常我们需要给用户密码输入两次,所以界面修改如下:

image.png

这样,提交到服务器的数据结构就应该是这样:

class Login2 {
    private String name;
    private String password;
    private String password2;
}

然后服务器做的第一件事情就是比较password和password2是否相等,然后new—个Login结构,把name和password填充到里边,然后保存到数据库.我们把Login2类似的界面层和其它层沟通的数据结构叫做View Object,简称VO.

Java开发过程中,基本实体类包都以entity或者model来称呼,可是不少项目中,却以Bo、Vo来命名,面试的时候,也有可能被问到这些问题。那么,这几者分别代表什么意思呢?

1.Entity:\color{#00FF00}{1. Entity:}

最常用实体类,基本和数据表——对应,一个实体一张表。

2.Bo(businessobject)\color{#00FF00}{2. Bo(business object)}

代表业务对象的意思,Bo就是把业务逻辑封装为一个对象(注意是逻辑,业务逻辑),这个对象可以包括一个或多个其它的对象。通过调用Dao方法,结合Po或Vo进行务操作。

3.Vo(valueobject)\color{#00FF00}{3. Vo(value object)}

代表值对象的意思,通常用于业务层之间的数据传递,由new创建,由GC回收。 主要体现在视图的对象,对于一个WEB页面将整个页面的属性封装成一个对象,然后用一个VO对象在控制层与视图层进行传输交换。

4.Po(persistantobject)\color{#00FF00}{4. Po(persistant object)}

代表持久层对象的意思,对应数据库中表的字段,数据库表中的记录在java对象中的显示状态,最形象的理解就是一个PO就是数据库中的一条记录。

5.Dto(datatransferobject)\color{#00FF00}{5. Dto(data transfer object)}

代表数据传输对象的意思 是一种设计模式之间传输数据的软件应用系统,数据传输目标往往是数据访问对象从数据库中检索数据.

6.Pojo(plianordinaryjavaobject)\color{#00FF00}{6. Pojo(plian ordinary java object)}

代表简单无规则java对象 纯的传统意义的java对象,最基本的Java Bean只有属性加上属性的get和set方法.

2. 邻域模型

  1. 如果常规的MVC模式公用一个bean,会带来很多问题:

a.接口只需要3个参数,而你返回表里全部参数,泄露了部分信息;

b.你不想别人知道数据库里字段,如果保持一致,能根据常规的驼峰命名完全能知道

c.数据库一个字段在不通的接口里返回的业务意义不一样,需要转换翻译;

  1. 于是引入业界目前常用的领域模型来划分,避免上述问题:DO,BO,DTO

DO:数据库层面,与你的表结构一致

BO:业务处理对象,基础服务于服务组件处理与返回的对象

DTO:数据传输对象,请求和返回对象

image.png

带来的影响:引入上述模型,尽量各层做到隔离,但是带来问题是不同领域之间需要转换.

3. mapStruct

01. 介绍

MapStruct是一个代码生成器的工具类,简化了不同的Java Bean之间映射的处理,所以映射指的就是从一个实体变化成另一个实体。在实际项目中,我们经常会将PO转DTO、DTO转PO等一些实体间的转换。在转换时大部分属性都是相同的,只有少部分的不同,这时我们可以通过mapStruct的一些注解来匹配不同属性,可以让不同实体之间的转换变的简单。

MapStruct官网地址: mapstruct.org/

相比于apache-beanutils,beanutils对—些深层次对象拷贝做不到,虽然可以通过改写其内部源码实现对嵌套对象属性拷贝,但是出现特殊业务转换,如属性名字不匹配,beanutils对于mapstruct就相形见绌了。

02. mapStruct的使用及注意事项

  • 引入依赖
<!--mapStruct-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.2.0.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

再使用 mapstruct,项目的时候出现错误:java: Internal error in the mapping processor: java.lang.NullPointerException

解决方案:修改编辑器配置

路径:Setting --> Build,Execution,Deployment --> Compiler --> User-local build process VM options (overrides Shared options)

配置内容:-Djps.track.ap.dependencies=false

image.png

image.png

4. mapStruct自定义转换规则

对一些特殊的转换 我们可以进行自定义订制,有时我们需要int转boolean,那么我们就可以定义转换规则.

public class PersonTransRule {
    public boolean intToBoolean(int age) {
        return age >18;
)

在mapping中引入规则:使用mapper注解的uses属性,参数类型为class数组,可以指定多个转换规则类

@Mapper(uses = PersonTransRule.class)

二、 完善登录功能

①:编写持久层代码

1. 创建实体类

com.it.dao.entity.CmsUserPrimaryEntity

@Getter
@Setter
public class CmsUserPrimaryEntity {
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Integer id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private Integer loginCount;
}

2. 创建mapper接口

com.it.dao.mapper.CmsUserPrimaryMapper

public interface CmsUserPrimaryMapper {

    /**
     * 根据名称查询
     * @param username 姓名
     * @return 用户信息实体类
     */
    CmsUserPrimaryEntity getByUsername(String username);
}

3. 创建mapper.xml文件

com/it/dao/mapper/mappers/CmsUserPrimaryMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it.dao.mapper.CmsUserPrimaryMapper">

    <resultMap id="baseUserMap" type="com.it.dao.entity.CmsUserPrimaryEntity">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="status" column="status"/>
        <result property="salt" column="salt"/>
        <result property="deleted" column="deleted"/>
    </resultMap>

    <sql id="baseUserSql">
        id,
        username,
        password,
        salt,
        email,
        status,
        deleted
    </sql>

    <select id="getByUsername" resultMap="baseUserMap">
        select
        <include refid="baseUserSql"/>
        from cms_user_primary where username = #{username};
    </select>
</mapper>

②:编写应用层代码

1. 创建Dto类

com.it.service.dto.CmsUserPrimaryDto

public class CmsUserPrimaryDto {
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    private Integer id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private Integer loginCount;
}

2. 定义Entity和Dto之间的转换关系

com.it.service.converter.CmsUserPrimaryConverter

@Mapper
public interface CmsUserPrimaryConverter {
    CmsUserPrimaryConverter CONVERTER = Mappers.getMapper(CmsUserPrimaryConverter.class);

    CmsUserPrimaryDto entityToDto(CmsUserPrimaryEntity entity);
}

3. 创建Service接口和实现类

com.it.service.api.CmsUserService

public interface CmsUserService {
    /**
     * 根据用户名查找
     * @param username 用户名
     * @return 用户实体类
     */
    CmsUserPrimaryDto getByUsername(String username);
}

com.it.service.impl.CmsUserServiceImpl

@Service
public class CmsUserServiceImpl implements CmsUserService {
    @Autowired
    private CmsUserPrimaryMapper cmsUserPrimaryMapper;

    @Override
    public CmsUserPrimaryDto getByUsername(String username) {
        CmsUserPrimaryEntity userPrimaryEntity = cmsUserPrimaryMapper.getByUsername(username);
        return CmsUserPrimaryConverter.CONVERTER.entityToDto(userPrimaryEntity);
    }
}

③:完善登录功能

1. 获取shiro的subject

com.it.context.utils.UtilsShiro

/**
 * 获取shiro的subject
 * @return Subject
 */
public static Subject getSubject(){
    return SecurityUtils.getSubject();
}

2. 修改executeLogin方法

完善登录流程 com.it.portal.security.filter.CmsAuthenticationFilter

@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    response.setCharacterEncoding("utf-8");
    response.setContentType("application/json; charset=UTF-8");
    // 获取验证码
    String captcha = WebUtils.getCleanParam(request, "captcha");

    String imageCaptcha = commonService.verifyImageCaptcha(captcha);
    if (1 >2 &&Objects.nonNull(imageCaptcha)) {
        response.getWriter().write(JSON.toJSONString(Result.fail(imageCaptcha,null)));
        return false;
    }
    Subject subject = UtilsShiro.getSubject();
    AuthenticationToken token = this.createToken(request, response);

    try {
        // 使用token登录
        subject.login(token);
        response.getWriter().write(JSON.toJSONString(Result.success("登录成功!")));
    } catch (IncorrectCredentialsException | UnknownAccountException e) {
        response.getWriter().write(JSON.toJSONString(Result.fail("用户名或密码错误!",null)));
    }
    response.getWriter().write(JSON.toJSONString(Result.success("登录成功!")));
    return false;
}

④:提取出公用字段

1. 提取公用字段

创建 com.it.core.BaseDto 类

public class BaseDto<PK extends Serializable> implements Serializable {
    private PK id;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    public PK getId() {
        return id;
    }

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

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
}

修改 com.it.service.dto.CmsUserPrimaryDto 类

@Getter
@Setter
public class CmsUserPrimaryDto extends BaseDto<Integer> {
    private String username;
    private String password;
    private String salt;
    private String email;
    private Integer loginCount;
}

创建 com.it.core.BaseEntity 类

public class BaseEntity<PK extends Serializable> implements Serializable {
    private PK id;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    public PK getId() {
        return id;
    }

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

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
}

2. 创建副表实体类

com.it.dao.entity.CmsUserEntity

@Setter
@Getter
public class CmsUserEntity extends BaseEntity<Integer> {
    private String username;
    private Boolean status;
    private Boolean admin;
    /**
     * 超级管理员
     */
    private Boolean administrator;
}

3. 创建副表Dto

创建 com.it.service.dto.CmsUserDto 类

@Getter
@Setter
public class CmsUserDto extends BaseDto<Integer> {
    private String username;
    private Integer status;
    private Boolean admin;
    /**
     * 超级管理员
     */
    private Boolean administrator;
}

4. 修改主表Dto

修改 com.it.dao.entity.CmsUserPrimaryEntity

@Getter
@Setter
public class CmsUserPrimaryEntity extends BaseEntity<Integer>{
    private String username;
    private String password;
    private String salt;
    private String email;
    private Integer loginCount;
}

⑤:创建副表的Mapper类和文件

创建 com.it.dao.mapper.CmsUserMapper 接口

public interface CmsUserMapper {
    /**
     * 根据名称查询
     * @param username 姓名
     * @return 用户信息实体类
     */
    CmsUserEntity getByUsername(String username);
}

创建 com/it/dao/mapper/mappers/CmsUserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it.dao.mapper.CmsUserMapper">

    <resultMap id="baseUserMap" type="com.it.dao.entity.CmsUserEntity">
        <id property="id" column="id"/>
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/>
        <result property="username" column="username"/>
        <result property="status" column="status"/>
        <result property="admin" column="is_admin"/>
        <result property="administrator" column="is_super"/>
    </resultMap>
    <sql id="baseUserSql">
        id,
        username,
        status
    </sql>

    <select id="getByUsername" resultMap="baseUserMap">
        select
        <include refid="baseUserSql"/>
        from cms_user where username = #{username} and deleted = 1
    </select>
</mapper>

⑥:定义副表Entity和Dto之间的转换

修改 com.it.service.api.CmsUserService

public interface CmsUserService {
    /**
     * 根据用户名查找
     * @param username 用户名
     * @return 用户实体类
     */
    CmsUserDto selectByUsername(String username);
}

创建

@Mapper
public interface CmsUserConverter {
    CmsUserConverter CONVERTER = Mappers.getMapper(CmsUserConverter.class);

    // CmsUserEntity dtoToEntity(CmsUserDto cmsUserDto);

    CmsUserDto entityToDto(CmsUserEntity entity);
}

修改 com.it.service.impl.CmsUserServiceImpl

@Service
public class CmsUserServiceImpl implements CmsUserService {
    @Autowired
    private CmsUserMapper cmsUserMapper;

    @Override
    public CmsUserDto selectByUsername(String username) {
        return CmsUserConverter.CONVERTER.entityToDto(cmsUserMapper.getByUsername(username));
    }
}

修改 com.it.portal.security.realm.UsernamePasswordCaptchaRealm

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   // 获取username
    String usename = (String) authenticationToken.getPrincipal();
    // 现在副表中查找用户是否存在
    CmsUserDto cmsUserDto = cmsUserService.selectByUsername(usename);
    if (Objects.isNull(cmsUserDto)) {
        throw new UnknownAccountException();
    }
    // 校验用户状态 是否禁用
    
    return null;
}

⑦:登录状态验证

1. 使用枚举定义用户状态

创建 com.it.core.BaseEnum 接口约束必须有的状态

public interface BaseEnum {
    /**
     * 获取标号
     * @return 标号
     */
    int getOrdinal();

    /**
     * 获取状态信息
     * @return 状态信息
     */
    String getLabel();
}

创建 com.it.dao.enums.UserStatusEunm 枚举

@Getter
public enum UserStatusEunm implements BaseEnum {
    NORMAL(1, "正常"),
    DISABLED(2, "禁用"),
    LOCKED(3, "锁定"),
    UNACTIVATED(4,"未激活");


    private int ordinal;
    private String label;

    UserStatusEunm(int ordinal, String label) {
        this.ordinal = ordinal;
        this.label = label;
    }
}

2. 修改副表Dto用户的状态

com.it.service.dto.CmsUserDto

private UserStatusEunm status;

3. 校验状态代码

com.it.portal.security.realm.UsernamePasswordCaptchaRealm

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   // 获取username
    String usename = (String) authenticationToken.getPrincipal();
    // 现在副表中查找用户是否存在
    CmsUserDto cmsUserDto = cmsUserService.selectByUsername(usename);
    if (Objects.isNull(cmsUserDto)) {
        throw new UnknownAccountException();
    }
    // 校验用户状态 是否禁用
    verifyStatus(cmsUserDto.getStatus());
    return null;
}

/**
 * 校验状态
 * @param userStatusEunm 校验状态
 */
private void verifyStatus(UserStatusEunm userStatusEunm){
    if (UserStatusEunm.DISABLED.equals(userStatusEunm)) {
        throw new DisabledAccountException("您的账号已被禁用,请联系管理员!");
    }else if (UserStatusEunm.LOCKED.equals(userStatusEunm)){
        throw new DisabledAccountException("您的账号已被锁定,请联系管理员!");
    }else if(UserStatusEunm.UNACTIVATED.equals(userStatusEunm)){
        throw new DisabledAccountException("您的账号未激活,请联系管理员!");
    }
}

4. Key promoter插件-提示快捷键

image.png

⑧:校验密码

1. 创建Mapper接口

创建通用Mapper接口 com.it.core.BaseMapper

public interface BaseMapper<ENTITY extends BaseEntity<PK>, PK extends Serializable> {
    /**
     * 根据id查找
     * @param id id
     * @return 实体类
     */
    ENTITY selectById(PK id);
}

com.it.dao.mapper.CmsUserPrimaryMapper

public interface CmsUserPrimaryMapper extends BaseMapper<CmsUserPrimaryEntity, Integer>{

}

修改 com/it/dao/mapper/mappers/CmsUserPrimaryMapper.xml

<mapper namespace="com.it.dao.mapper.CmsUserPrimaryMapper">

    <resultMap id="baseUserMap" type="com.it.dao.entity.CmsUserPrimaryEntity">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="salt" column="salt"/>
    </resultMap>

    <sql id="baseUserSql">
        id,
        username,
        password,
        salt,
        email
    </sql>

    <select id="selectById" resultMap="baseUserMap">
        select
        <include refid="baseUserSql"/>
        from cms_user_primary where id = #{id}
    </select>
</mapper>

2. 创建主表Service接口和实体类

创建统一的Service接口 com.it.core.BaseService

public interface BaseService<DTO extends BaseDto<PK>, PK extends Serializable>{

    /**
     * 根据id查询
     * @param id id
     * @return Dto
     */
    DTO getById(PK id);
}

创建 com.it.service.api.CmsUserPrimaryService

public interface CmsUserPrimaryService extends BaseService<CmsUserPrimaryDto,Integer> {

}

创建 com.it.service.impl.CmsUserPrimaryServiceImpl 实现类

@Service
public class CmsUserPrimaryServiceImpl implements CmsUserPrimaryService {

    @Autowired
    private CmsUserPrimaryMapper cmsUserPrimaryMapper;

    @Override
    public CmsUserPrimaryDto getById(Integer id) {
        return CmsUserPrimaryConverter.CONVERTER.entityToDto(cmsUserPrimaryMapper.selectById(id));
    }
}

3. 自定义转换规则

创建 com.it.dao.enums.EnumConverter

public class EnumConverter {
    /**
     * 将entity中的int类型转换为UserStatusEnum 枚举类型
     * @param status 状态
     * @return 枚举
     */
    public static UserStatusEnum toUserStatusEunm(int status){
        return (UserStatusEnum)converter(UserStatusEnum.values(),status);
    }

    /**
     * 通用枚举转换器,统一循环枚举比对
     * @param baseEnums 枚举数组
     * @param status 状态
     * @return 枚举
     */
    public static BaseEnum converter(BaseEnum[] baseEnums, int status){
        for (BaseEnum baseEnum : baseEnums) {
            if (Objects.equals(baseEnum.getOrdinal(),status)){
                return baseEnum;
            }
        }
        return null;
    }

}

com.it.service.converter.CmsUserConverter 中使用自定义转换规则

@Mapper(uses = EnumConverter.class)
public interface CmsUserConverter {
    CmsUserConverter CONVERTER = Mappers.getMapper(CmsUserConverter.class);

    // CmsUserEntity dtoToEntity(CmsUserDto cmsUserDto);

    CmsUserDto entityToDto(CmsUserEntity entity);
}

4. 校验密码

com.it.portal.security.realm.UsernamePasswordCaptchaRealm

@Autowired
private CmsUserService cmsUserService;
@Autowired
private CmsUserPrimaryService cmsUserPrimaryService;
    
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   // 获取username
    String usename = (String) authenticationToken.getPrincipal();
    // 现在副表中查找用户是否存在
    CmsUserDto cmsUserDto = cmsUserService.selectByUsername(usename);
    if (Objects.isNull(cmsUserDto)) {
        throw new UnknownAccountException();
    }
    // 校验用户状态 是否禁用
    verifyStatus(cmsUserDto.getStatus());
    // 查询用户主表信息
    CmsUserPrimaryDto cmsUserPrimaryDto = cmsUserPrimaryService.getById(cmsUserDto.getId());
    // 校验密码
    SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(cmsUserDto, cmsUserPrimaryDto.getPassword(),
            ByteSource.Util.bytes(cmsUserPrimaryDto.getSalt()), getName());
    // 清理验证信息
    super.clearCachedAuthenticationInfo(simpleAuthenticationInfo.getPrincipals());
    return simpleAuthenticationInfo;
}

5. 测试

image.png

⑨:完赛前端的跳转

1. 刷新页面时光标聚焦在输入框中

cms-portal/src/main/webapp/WEB-INF/admin/login.html 添加以下代码

$('input[name="username"]').focus();

2. 指定跳转的页面

cms-portal/src/main/webapp/WEB-INF/admin/login.html 修改以下代码

function submit(data) {
    core.http({
        url:"${adminPath}/login.do",
        data:data
    }, function (res){
        if (res.code === CONSTANT.HTTP.SUCCESS){
            setTimeout(function (){
                location.href = "${adminPath}/index.do";
            }, 600);
        }
    });
}

cms-portal/src/main/webapp/admin/js/core.js

image.png

image.png

let core = {
    //限流工具类
    throttle: function (method, args, context) {
        clearTimeout(method.tId);
        method.tId = setTimeout(function () {
            method.call(context, args);
        }, 200);
    },

    http: function (option, callback) {
        this.cancel && this.cancel.abort();
        let opt = {load: true}, loadHandler, options = {
            url: "",
            method: "post",
            contentType: "application/x-www-form-urlencoded",
            dataType: "json",
            beforeSend: function () {
                this.load && ((loadTime = new Date().getTime()) && (loadHandler = LayUtil.layer.init(function (inner, layer) {
                    inner.loading(0, {shade: 0.1})
                })))
            },
            success: function (res) {
                // 处理loading 加载
                if (this.load && loadHandler) {
                    let time = 0;
                    if (new Date().getTime() - loadTime < 500) {
                        time = 500;
                    }
                    setTimeout(function () {
                        loadHandler.closeLoading();
                    }, time)
                }
                // 判断请求接口
                switch (res.code) {
                    case CONSTANT.HTTP.SUCCESS:
                        console.log(res);
                        core.prompt.msg(res.msg, {shade: 0.3, time: 1200}, null);
                        break;
                    case CONSTANT.HTTP.ERROR:
                        break;
                }
                // 处理自定义回调
                (callback instanceof Function) && callback(res)
            }
        }
        Object.assign(opt, options, option);
        this.cancel = $.ajax(opt);
    },
    // 提示相关
    prompt: {
        msg: function (content, option, callback) {
            LayUtil.layer.init(function (inner) {
                inner.msg(content, option, callback);
            })
        }
    }
};
const CONSTANT = {
    //http相关
    HTTP: {
        SUCCESS: 200,
        ERROR: 500
    }
};

// layui工具类
function LayUtil() {
}

LayUtil.prototype = {
    construct: LayUtil,
    //弹窗
    layer: (function (LayUtil) {
        function Inner() {

        }

        Inner.prototype = {
            construct: Inner,
            init: function (callback) {
                let that = this;
                layui.use('layer', function () {
                    that.layer = layui.layer;
                    if (callback instanceof Function) {
                        callback(that, that.layer);
                    }
                })
                return this;
            },
            //显示loading加载
            loading: function (config = {}) {
                this.layer.load(config);
            },
            // 关闭loading
            closeLoading: function () {
                this.layer.closeAll('loading');
            },
            msg:function (content,option,callback){
                console.log(layer.msg(content,option,callback));
            }
        }
        LayUtil.layer = new Inner();
    })(LayUtil),
    //form表单
    form: (function (LayUtil) {
        function Inner() {
        }

        Inner.prototype = {
            construct: Inner,
            init: function (callback) {
                let that = this;
                layui.use('form', function () {
                    that.form = layui.form;
                    that.form.render();
                    if (callback instanceof Function) {
                        callback(that, that.form)
                    }
                });
                return this;
            },
            //提交表单
            submit: function (callback, name, type = "submit") {
                this.form.on(type + "(" + (name === undefined ? 'go' : name) + ")", function (obj) {
                    if (callback instanceof Function) {
                        callback(obj);
                        return false;
                    }
                    return true;
                })
            },
            //验证
            verify: function (validator) {
                this.form.verify(validator);
            }
        }
        LayUtil.form = new Inner();
    })(LayUtil)
}

com.it.portal.controller.admin.LoginController

@GetMapping("login.do")
public String toLogin() {
    // 判断用户是否已经登录
    Subject subject = UtilsShiro.getSubject();
    if (subject.isAuthenticated()){
        return "redirect:index.do";
    }
    return "/admin/login";
}

3. 测试

image.png

image.png

⑩:创建常量池存放常量值

com.it.context.constant.ConstantsPool

public class ConstantsPool {
    private ConstantsPool(){}

    /**
     * 异常相关
     */
    public static final String EXCEPTION_NETWORK = "网络范围,请稍后重试!";
}

com.it.portal.security.filter.CmsAuthenticationFilter

image.png