layui登录页面模板_(eblog)6、博客分类填充、登录注册逻辑

小Hub领读:

继续我们的eblog,今天来完成博客分类的展示,还有登录注册!


项目名称:eblog

项目 Git 仓库:https://github.com/MarkerHub/eblog(给个 star 支持哈)

项目演示地址:https://markerhub.com:8082

前几篇项目讲解文章:

1、(eblog)Github 上最值得学习的 Springboot 开源博客项目!

2、(eblog)小 Hub 手把手教你如何从 0 搭建一个开源项目架构

3、(eblog)整合Redis,以及项目优雅的异常处理与返回结果封装

4、(eblog)用Redis的zset有序集合实现一个本周热议功能

5、(eblog)自定义Freemaker标签实现博客首页数据填充


这一次作业我们来完善一下首页中的内容,比如我们的首页文章列表,首页导航分类,分类列表,文章详情。

同时,上一期的作业中我写了不少bug,然后我又偷偷改了很多,都是比较细的,我可能不会全部都写出来,大家如果不知道我改了哪里,有两个办法:

  • 1、看git的提交记录,点击一下文件就有对比出来

f58f12061c5b885c93aa7e867e5ad6f4.png

  • 2、运行我的项目和你的项目,链接统一数据库,判断页面的内容显示和功能是否一致,不一致说明我已经偷偷改了一些不为人知的bug了。

1、首页内容填充

列表分页

578d574f6bb80ce287bc435a904cbb34.png

这里说的列表分页讲得是首页的内容列表,可以看到列表内容每一行的内容其实和置顶的列表是一致的,所以原来的sql我们是可以再应用。包括后面我们点击具体导航分类的列表也是一致的。内容一致,我们就可以想到,首先前端的列表我们可以单独提出来作为一个模板,这样所有的地方都只修改一次,可以控制所有的地方了。

然后后端的处理方式有两种:

  • 1、延用我们freemarker标签的方式

  • 2、使用controller中传送数据到前端

标签的方式我们之前已经学习过了,那么我们这次在controller中再提交数据到前端。

首先来看下首页的controller

@RequestMapping({"", "/", "/index"})

public String index () {

IPage results = postService.paging(getPage(), null, null, null, null, "created");

req.setAttribute("pageData", results);

return "index";

}

上面的postService.paging就是我们之前写过的,只不过参数又多了几个,我偷偷改的。给你看看最新的版本吧

@Override

@Cacheable(cacheNames = "cache_post", key = "'page_' + #page.current + '_' + #page.size " +

"+ '_query_' +#userId + '_' + #categoryId + '_' + #level + '_' + #recommend + '_' + #order")

public IPage paging(Page page, Long userId, Long categoryId, Integer level, Boolean recommend, String order) {

if(level == null) level = -1;

QueryWrapper wrapper = new QueryWrapper<Post>()

.eq(userId != null, "user_id", userId)

.eq(categoryId != null && categoryId != 0, "category_id", categoryId)

.gt(level > 0, "level", 0)

.eq(level == 0, "level", 0)

.eq(recommend != null, "recommend", recommend)

.orderByDesc(order);

IPage<PostVo> pageData = postMapper.selectPosts(page, wrapper);

return pageData;

}

其实就是加多了几个参数,为了应付更多的场景。回到刚才说的index方法,有个getPage()我写在了BaseController,这是分页数据的获取封装,获取前端的分页信息,然后封装成poge对象。然后给一下参数默认值。

public Page getPage() {

int pn = ServletRequestUtils.getIntParameter(req, "pn", 1);

int size = ServletRequestUtils.getIntParameter(req, "size", 10);

Page page = new Page(pn, size);

return page;

}

然后可以看到index中,我传了个pageData对象到前端,我们再看看前端。

class="fly-list">

@listing>

#list>

找找中间的内容部分,然后写成了上面那样,因为我把记录列表封装成了宏(macro) e7f570b8a0024888df9db09303cd242e.png

具体的内容就是这样

href="${base}/user/${post.authorId}" class="fly-avatar">

src="${post.authorAvatar}" alt="${post.authorName}">

class="layui-badge">${post.categoryName}

href="${base}/post/${post.id}">${post.title}

class="fly-list-info">

href="${base}/user/${post.authorId}" link>

${post.authorName}

class="layui-badge fly-badge-vip">VIP${post.authorVip}

${post.created?string('yyyy-MM-dd')}

class="fly-list-nums">

class="iconfont icon-pinglun1" title="回答"> ${post.commentCount}

class="fly-list-badge">

class="layui-badge layui-bg-black">置顶#if>

class="layui-badge layui-bg-red">精帖#if>

#macro>

关于freemarker标签macro的用法,不懂的就去百度一下啦,貌似之前我们说过是不是?忘了~

...#macro>

代表定义了一个macro名词叫listing,参数是post,标签内容就是宏的内容。需要调用这个宏的地方直接使用标签就可以,所以你就看到了我刚才的写法。

82695688d5f4b41e58c0959b974fbf02.png

好了,列表是循环出来了,但是有个问题还没解决,就是分页问题,前端中我们需要一个分页的导航给我们点击页数。二期作业中我们使用的是一个其他插件,这次我们直接用layui的分页插件,还是挺简单的。因为分页的这个页码还是很多页码需要用到的,所以我又把分页的内容搞了一个宏,然后参考一下layui的分页写法https://www.layui.com/demo/laypage.html

id="laypage-main">

#macro>

上面的js还是比较简单的,就调用了layui的一个laypage.render就可以吧页码给渲染出来了,我们在需要的地方调用一下代码

style="text-align: center">

@page>

pageData是controller传过来的数据,渲染效果如下:

efd70d642171c29d52e464de2739835d.png

简直完美,我真是个天才,人见人爱,花见花开~

导航分类

接下来我们来完善一下导航分类信息,这个比较简单,我们有个表专门存储分类信息的,只需要把列表获取出来就是了(id,name)不需要关联其他表,那么mybatis plus可以直接帮我搞定,不用我写service了,那我来想应该再那里传送数据过去呢,首页index?导航分类是所有的地方都用到的,所以不合适,这时候定义一个freemarker标签是个好办法。

但这里我们没用采用标签方式,我是吧数据放在了全局应用上下文Context中了,这样初始化项目时候我们就加载数据,和我们之前初始化本周热议有点类似,所以我们直接在那个启动类中添加我们的代码

71a623c3c753ee73ead09142c67cfa56.png

ok,2行代码,绝不写多,有些人可能有个status控制分类的展示,可以做个条件。

currentCategoryId是为了回显当前选择的分类,默认为0(首页)

再看前端,就是展示数据:

class="${(0 == currentCategoryId)?string('layui-hide-xs layui-this', '')}">

href="/">首页

class="${(category.id == currentCategoryId)?string('layui-hide-xs layui-this', '')}">

href="${base}/category/${category.id}">${category.name}

#list>

emm~,注意freemarker的二元写法,其他的简单~

分类详情

点击导航分类之后,我们跳转到http://localhost:8080/category/1,内容又和我们的首页列表有点像了,com.example.controller.PostController中

@RequestMapping("/category/{id:\\d*}")

public String category(@PathVariable Long id) {

Page page = getPage();

IPage<PostVo> pageData = postService.paging(page, null, id, null, null, "created");

req.setAttribute("pageData", pageData);

req.setAttribute("currentCategoryId", id);

return "post/category";

}

currentCategoryId是为了回显我当前选择的栏目。

  • templates/post/category.ftl

class="fly-list">

@listing>

#list>

style="text-align: center">

@page>

博客详情

好了,接下来我们看博客详情,点击列表之后跳转到的页面,展示博客内容和评论列表等信息。

  • com.example.controller.PostController

@RequestMapping("/post/{id:\\d*}")

public String view(@PathVariable Long id) {

QueryWrapper wrapper = new QueryWrapper<Post>()

.eq(id != null, "p.id", id);

PostVo vo = postService.selectOne(wrapper);

IPage commentPage = commentService.paging(getPage(), null, id, "id");

req.setAttribute("post", vo);

req.setAttribute("pageData", commentPage);

return "post/view";

}

上面我写了两个service方法

  • postService.selectOne

其中selectOne的方法的sql其实元原来post的selectPosts是一样的,只是返回的一个是分页,一个bean,参数没有page对象。

id="selectOne" resultType="com.example.vo.PostVo">

select p.*

, c.id as categoryId, c.name as categoryName

, u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip

from post p

left join user u on p.user_id = u.id

left join category c on p.category_id = c.id

${ew.customSqlSegment}

commentService.paging

这个方法我写了一个commentVo用于传输数据,vo中添加一下关联的信息,比如用户名等

id="selectComments" resultType="com.example.vo.CommentVo">

select c.*

, u.id as authorId, u.username as authorName, u.avatar as authorAvatar, u.vip_level as authorVip

from comment c

left join user u on c.user_id = u.id

${ew.customSqlSegment}

前端的话就简单了,就list循环展示数据就行了

...

#list>

style="text-align: center">

@page>

具体看我们的代码了,这里就没必要贴出来了。好了,数据的展示就先到这里~

2、用户状态

上面我们完成了数据的展示,数据的编辑我们需要用到登录用户的权限才行,所以在编辑之前我们先来做下用户的登录认证问题,这里我们使用shiro框架来完成。

关于登录模块,我们先来梳理一下逻辑,首先是把登录注册的页面复制进来,然后改成模板形式(头和尾,侧边栏等),再然后集成shiro框架,写登录注册接口,login -> realm(认证)-> 写登录注册逻辑->页面的shiro标签->分布式session的相关配置,然后

登录逻辑

  • com.example.controller.IndexController

@GetMapping("/login")

public String login() {

return "auth/login";

}

@ResponseBody

@PostMapping("/login")

public Result doLogin(String email, String password) {

if(StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {

return Result.fail("用户名或密码不能为空!");

}

AuthenticationToken token = new UsernamePasswordToken(email, SecureUtil.md5(password));

try {

//尝试登陆,将会调用realm的认证方法

SecurityUtils.getSubject().login(token);

}catch (AuthenticationException e) {

if (e instanceof UnknownAccountException) {

return Result.fail("用户不存在");

} else if (e instanceof LockedAccountException) {

return Result.fail("用户被禁用");

} else if (e instanceof IncorrectCredentialsException) {

return Result.fail("密码错误");

} else {

return Result.fail("用户认证失败");

}

}

return Result.succ("登录成功", null, "/");

}

@GetMapping("/register")

public String register() {

return "auth/register";

}

@ResponseBody

@PostMapping("/register")

public Result doRegister(User user, String captcha, String repass) {

String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);

if(!kaptcha.equalsIgnoreCase(captcha)) {

return Result.fail("验证码不正确");

}

if(repass == null || !repass.equals(user.getPassword())) {

return Result.fail("两次输入密码不一致");

}

Result result = userService.register(user);

result.setAction("/login"); // 注册成功之后跳转的页面

return result;

}

@GetMapping("/logout")

public String logout() throws IOException {

SecurityUtils.getSubject().logout();

return "redirect:/";

}

上面的代码,首先分别写了一下login和register的get和post的方式,一个是跳转到login,然后我们通过异步的post方式来提交form表单数据,login的主要逻辑很简单,主要就一行代码:

SecurityUtils.getSubject().login(token);

根据我们对shiro的理解,login之后会最终委托给realm完成登录逻辑的认证,那么我们先来看看realm的内容(doGetAuthenticationInfo)

@Slf4j

@Component

public class AccountRealm extends AuthorizingRealm {

@Autowired

UserService userService;

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

return null;

}

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;

//注意token.getUsername()是指email!!

AccountProfile profile = userService.login(token.getUsername(), String.valueOf(token.getPassword()));

log.info("---------------->进入认证步骤");

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profile, token.getCredentials(), getName());

return info;

}

}

doGetAuthenticationInfo就是我们认证的方法,authenticationToken就是我们的传过来的UsernamePasswordToken ,包含着邮箱和密码。然后userService.login的内容就是校验一下账户的合法性,不合法就抛出对应的异常,合法最终就返回封装对象AccountProfile

@Override

public AccountProfile login(String username, String password) {

log.info("------------>进入用户登录判断,获取用户信息步骤");

User user = this.getOne(new QueryWrapper<User>().eq("email", username));

if(user == null) {

throw new UnknownAccountException("账户不存在");

}

if(!user.getPassword().equals(password)) {

throw new IncorrectCredentialsException("密码错误");

}

//更新最后登录时间

user.setLasted(new Date());

this.updateById(user);

AccountProfile profile = new AccountProfile();

BeanUtil.copyProperties(user, profile);

return profile;

}

ok,登录逻辑已经梳理完毕,等下页面我们再弄,再来弄下注册逻辑。

注册逻辑

注册过程设计到一个验证码校验的插件,这里我们使用google的验证码生成器kaptcha。

先来整合一下,首先导入jar包

com.github.axet

kaptcha

0.0.9

然后配置一下验证码的图片生成规则:(边框、颜色、字体大小、长、高等)

@Configuration

public class WebMvcConfig {

@Bean

public DefaultKaptcha producer () {

Properties propertis = new Properties();

propertis.put("kaptcha.border", "no");

propertis.put("kaptcha.image.height", "38");

propertis.put("kaptcha.image.width", "150");

propertis.put("kaptcha.textproducer.font.color", "black");

propertis.put("kaptcha.textproducer.font.size", "32");

Config config = new Config(propertis);

DefaultKaptcha defaultKaptcha = new DefaultKaptcha();

defaultKaptcha.setConfig(config);

return defaultKaptcha;

}

}

好了,插件我们已经集成完毕,接下来我们提供一个访问的接口用于生成验证码图片 首先注入插件

@Autowired

private Producer producer;

然后com.example.controller.IndexController中

@GetMapping("/capthca.jpg")

public void captcha(HttpServletResponse response) throws IOException {

response.setHeader("Cache-Control", "no-store, no-cache");

response.setContentType("image/jpeg");

//生成文字验证码

String text = producer.createText();

//生成图片验证码

BufferedImage image = producer.createImage(text);

//把验证码存到shrio的session中

SecurityUtils.getSubject().getSession().setAttribute(KAPTCHA_SESSION_KEY, text);

ServletOutputStream outputStream = response.getOutputStream();

ImageIO.write(image, "jpg", outputStream);

}

所以访问这个接口就能得到验证码图片流,页面中:

id="capthca" src="/capthca.jpg">

那么流是接通前端后端的,到后端还需要验证验证码的正确性,所以生成验证码的时候我们需要把验证码先存到session中,然后注册接口中再从session中获取出来然后比较是否正确。

@ResponseBody

@PostMapping("/register")

public Result doRegister(User user, String captcha, String repass) {

String kaptcha = (String) SecurityUtils.getSubject().getSession().getAttribute(KAPTCHA_SESSION_KEY);

if(!kaptcha.equalsIgnoreCase(captcha)) {

return Result.fail("验证码不正确");

}

...

return result;

}

所以注册接口的第一件事就是校验验证码是否正确。

Result result = userService.register(user);

我们看下里面的逻辑

@Override

public Result register(User user) {

if(StringUtils.isEmpty(user.getEmail()) || StringUtils.isEmpty(user.getPassword())

|| StringUtils.isEmpty(user.getUsername())) {

return Result.fail("必要字段不能为空");

}

User po = this.getOne(new QueryWrapper<User>().eq("email", user.getEmail()));

if(po != null) {

return Result.fail("邮箱已被注册");

}

String passMd5 = SecureUtil.md5(user.getPassword());

po = new User();

po.setEmail(user.getEmail());

po.setPassword(passMd5);

po.setCreated(new Date());

po.setUsername(user.getUsername());

po.setAvatar("/res/images/avatar/default.png");

po.setPoint(0);

return this.save(po)? Result.succ("") : Result.fail("注册失败");

}

其实就是校验一下用户是否已经注册了,没注册就插入一条记录,这里的密码我只搞了md5加密,如果觉得密码的加密不够严谨,可以加盐,或者换其他加密方式。ok,这里后端的注册逻辑我们已经弄完,接下来我们来看下前端。layui已经帮我们封装好了form表单的提交逻辑

4acfaceabb9f5fbec48538206960fc1f.png

所以返回值中属性要有action、status、msg等。所以我们之前封装的Result类现在需要修改一下,以前我们Result只有code、data、msg,现在加多一个action和status。

  • com.example.common.lang.Result

@Data

public class Result implements Serializable {

private Integer code;

private Integer status;

private String msg;

private Object data;

private String action;

...

}

上面就是我们最新的返回的封装类,具体还有点封装方法要看看具体代码哈。所以注册方法的放回值最后是这样的

Result result = userService.register(user);

result.setAction("/login"); // 注册成功之后跳转的页面

return result;

action表示form处理成功之后跳转的链接。853d031f306ae6ce6a398bf6e911716f.gif

上面可以看到,我点击了操作成功的确定按钮之后调到了登录页面,就是我这个action这里设置的。

刚才我们已经完成了业务层面的逻辑,现在我们来看下页面端的。原本layui的后台界面已经帮我们完成页面逻辑。其实也没什么逻辑,form表单对应好字段之后,我们知道js中已经有了监测所有form表单的提交按钮,会触发一下方法:

  • static/res/mods/index.js

//表单提交

form.on('submit(*)', function(data){

var action = $(data.form).attr('action'), button = $(data.elem);

fly.json(action, data.field, function(res){

var end = function(){

if(res.action){

location.href = res.action;

} else {

fly.form[action||button.attr('key')](data.field, data.form);

}

};

if(res.status == 0){

button.attr('alert') ? layer.alert(res.msg, {

icon: 1,

time: 10*1000,

end: end

}) : end();

};

});

return false;

});

所以,在注册页面,我们不需要写啥js,可以给图片验证码一个点击事件,因为有时候看不清楚可以点击换一张

  • templates/auth/register.ftl

$(function () {

$("#capthca").click(function () {

this.src="/capthca.jpg";

});

});

登录页面中我们也不需要写啥js。搞定!以上就是我们的注册逻辑。

shiro页面标签

下面我们在前端用上shiro的一些标签,这样在页面中我们才能控制按钮的权限、用户的登录状态、用户信息等。因为我们页面用的是freemarker,所以我们用一个freemarker-shiro的jar包

net.mingsoft

shiro-freemarker-tags

0.1

第二步,需要把shiro的标签注入到freemarker的标签配置中:

  • com.example.config.FreemarkerConfig

96336a92e1a17a39f7ba57b9d15dd4d7.png

第三步,我们在页面的右上角中展示用户登录后的信息

6c8a4a03d0c8b70ed63ed5ac8b1c0659.png

依稀记得,我们的的头部的内容是放在

  • templates/inc/header.ftl

那么shiro的标签如何用呢?具体的用法,大家看看这篇文章科普一下

  • https://www.cnblogs.com/Jimc/p/10031094.html

*shiro.guest*>

*shiro.user*>

*shiro.principal property="username" */>

所以学会shiro的标签之后,那么我们就可以用了

class="layui-nav fly-nav-user">

class="layui-nav-item">

class="iconfont icon-touxiang layui-hide-xs" href="/login">

class="layui-nav-item">

href="/login">登入

class="layui-nav-item">

href="/register">注册

@shiro.guest>

class="layui-nav-item">

class="fly-nav-avatar" href="javascript:;">

class="layui-hide-xs">

class="iconfont icon-renzheng layui-hide-xs" title="认证信息:layui 作者">

class="layui-badge fly-badge-vip layui-hide-xs">VIP

">

class="layui-nav-child">

href="user/set.html">class="layui-icon">基本设置

href="user/message.html">class="iconfont icon-tongzhi" style="top: 4px;">我的消息

href="user/home.html">class="layui-icon" style="margin-left: 2px; font-size: 22px;">我的主页

style="margin: 5px 0;">

href="/logout" style="text-align: center;">退出

@shiro.user>

上面就是通过和两个标签来辨别用户是否已经登录了。这样登录前,我们看到的是登录注册按钮,登录之后看到的是用户的用户名称,头像等~

好了,上面shiro的标签我们已经搞定~

今天的作业就先到这里哈,大家先做好登录注册功能。

3536ac9937366d67f30d8b48724d5645.png

c7f1cacc534409f20024c7d3daa0b33d.png 给eblog一个star,感谢支持哈


版权声明:本文为weixin_30785057原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。