查看原文
其他

(eblog)6、博客分类填充、登录注册逻辑

吕一明 MarkerHub 2022-11-04

小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的提交记录,点击一下文件就有对比出来

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

1、首页内容填充

列表分页

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

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

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

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

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

首先来看下首页的controller

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

  2. public String index () {


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

  4. req.setAttribute("pageData", results);

  5. return "index";

  6. }

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

  1. @Override

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

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

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


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


  6. QueryWrapper wrapper = new QueryWrapper<Post>()

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

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

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

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

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

  12. .orderByDesc(order);


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


  14. return pageData;

  15. }

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

  1. public Page getPage() {

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

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

  4. Page page = new Page(pn, size);

  5. return page;

  6. }

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

  1. <ul class="fly-list">

  2. <#list pageData.records as post>

  3. <@listing post></@listing>

  4. </#list>

  5. </ul>

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

具体的内容就是这样

  1. <#macro listing post>

  2. <li>

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

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

  5. </a>

  6. <h2>

  7. <a class="layui-badge">${post.categoryName}</a>

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

  9. </h2>

  10. <div class="fly-list-info">

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

  12. <cite>${post.authorName}</cite>

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

  14. </a>

  15. <span>${post.created?string('yyyy-MM-dd')}</span>

  16. <span class="fly-list-nums">

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

  18. </span>

  19. </div>

  20. <div class="fly-list-badge">

  21. <#if post.level gt 0><span class="layui-badge layui-bg-black">置顶</span></#if>

  22. <#if post.recommend><span class="layui-badge layui-bg-red">精帖</span></#if>

  23. </div>

  24. </li>

  25. </#macro>

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

  1. <#macro listing post>...</#macro>

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

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

  1. <#--分页模板-->

  2. <#macro page data>


  3. <div id="laypage-main"></div>


  4. <script type="application/javascript">

  5. $(function () {


  6. layui.use(['laypage', 'layer'], function(){

  7. var laypage = layui.laypage

  8. ,layer = layui.layer;


  9. //总页数大于页码总数

  10. laypage.render({

  11. elem: 'laypage-main'

  12. ,count: ${data.total} //数据总数

  13. ,curr: ${data.current}

  14. ,limit: ${data.size}

  15. ,jump: function(obj, first){

  16. console.log(obj)


  17. //首次不执行

  18. if(!first){

  19. var url = window.location.href;

  20. location.href = "?pn=" + obj.curr;

  21. }

  22. }

  23. });

  24. });

  25. });


  26. </script>

  27. </#macro>

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

  1. <div style="text-align: center">

  2. <@page pageData></@page>

  3. </div>

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

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

导航分类

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

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

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

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

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

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

  2. <a href="/">首页</a>

  3. </li>


  4. <#list categorys as category>

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

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

  7. </li>

  8. </#list>

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

分类详情

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

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

  2. public String category(@PathVariable Long id) {

  3. Page page = getPage();


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

  5. req.setAttribute("pageData", pageData);

  6. req.setAttribute("currentCategoryId", id);


  7. return "post/category";

  8. }

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

  • templates/post/category.ftl

  1. <ul class="fly-list">

  2. <#list pageData.records as post>

  3. <@listing post></@listing>

  4. </#list>

  5. </ul>


  6. <!-- <div class="fly-none">没有相关数据</div> -->


  7. <div style="text-align: center">

  8. <@page pageData></@page>

  9. </div>

博客详情

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

  • com.example.controller.PostController

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

  2. public String view(@PathVariable Long id) {

  3. QueryWrapper wrapper = new QueryWrapper<Post>()

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

  5. PostVo vo = postService.selectOne(wrapper);

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


  7. req.setAttribute("post", vo);

  8. req.setAttribute("pageData", commentPage);

  9. return "post/view";

  10. }

上面我写了两个service方法

  • postService.selectOne

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

  1. <select id="selectOne" resultType="com.example.vo.PostVo">

  2. select p.*

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

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

  5. from post p

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

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


  8. ${ew.customSqlSegment}


commentService.paging

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

  1. <select id="selectComments" resultType="com.example.vo.CommentVo">

  2. select c.*

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

  4. from comment c

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


  6. ${ew.customSqlSegment}

  7. </select>

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

  1. <#list pageData.records as comment>

  2. ...

  3. </#list>

  4. <!--分页-->

  5. <div style="text-align: center">

  6. <@page pageData></@page>

  7. </div>

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

2、用户状态

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

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

登录逻辑

  • com.example.controller.IndexController

  1. @GetMapping("/login")

  2. public String login() {

  3. return "auth/login";

  4. }


  5. @ResponseBody

  6. @PostMapping("/login")

  7. public Result doLogin(String email, String password) {


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

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

  10. }

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


  12. try {


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

  14. SecurityUtils.getSubject().login(token);


  15. }catch (AuthenticationException e) {

  16. if (e instanceof UnknownAccountException) {

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

  18. } else if (e instanceof LockedAccountException) {

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

  20. } else if (e instanceof IncorrectCredentialsException) {

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

  22. } else {

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

  24. }

  25. }

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

  27. }


  28. @GetMapping("/register")

  29. public String register() {

  30. return "auth/register";

  31. }


  32. @ResponseBody

  33. @PostMapping("/register")

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


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

  36. if(!kaptcha.equalsIgnoreCase(captcha)) {

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

  38. }


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

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

  41. }


  42. Result result = userService.register(user);

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

  44. return result;

  45. }


  46. @GetMapping("/logout")

  47. public String logout() throws IOException {

  48. SecurityUtils.getSubject().logout();

  49. return "redirect:/";

  50. }

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

  1. SecurityUtils.getSubject().login(token);

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

  3. @Slf4j

  4. @Component

  5. public class AccountRealm extends AuthorizingRealm {


  6. @Autowired

  7. UserService userService;


  8. @Override

  9. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {


  10. return null;

  11. }


  12. @Override

  13. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

  14. UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;


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

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


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


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

  19. return info;

  20. }

  21. }

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

  1. @Override

  2. public AccountProfile login(String username, String password) {

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


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

  5. if(user == null) {

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

  7. }


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

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

  10. }


  11. //更新最后登录时间

  12. user.setLasted(new Date());

  13. this.updateById(user);


  14. AccountProfile profile = new AccountProfile();


  15. BeanUtil.copyProperties(user, profile);


  16. return profile;

  17. }

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

注册逻辑

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

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

  1. <!--验证码-->

  2. <dependency>

  3. <groupId>com.github.axet</groupId>

  4. <artifactId>kaptcha</artifactId>

  5. <version>0.0.9</version>

  6. </dependency>

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

  1. @Configuration

  2. public class WebMvcConfig {


  3. @Bean

  4. public DefaultKaptcha producer () {

  5. Properties propertis = new Properties();

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

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

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

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

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

  11. Config config = new Config(propertis);

  12. DefaultKaptcha defaultKaptcha = new DefaultKaptcha();

  13. defaultKaptcha.setConfig(config);


  14. return defaultKaptcha;

  15. }

  16. }

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

  1. @Autowired

  2. private Producer producer;

  3. 然后com.example.controller.IndexController中

  4. @GetMapping("/capthca.jpg")

  5. public void captcha(HttpServletResponse response) throws IOException {

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

  7. response.setContentType("image/jpeg");


  8. //生成文字验证码

  9. String text = producer.createText();

  10. //生成图片验证码

  11. BufferedImage image = producer.createImage(text);

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

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


  14. ServletOutputStream outputStream = response.getOutputStream();

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

  16. }

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

  1. <img id="capthca" src="/capthca.jpg">

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

  1. @ResponseBody

  2. @PostMapping("/register")

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


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

  5. if(!kaptcha.equalsIgnoreCase(captcha)) {

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

  7. }

  8. ...


  9. return result;

  10. }

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

  1. Result result = userService.register(user);

我们看下里面的逻辑

  1. @Override

  2. public Result register(User user) {

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

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

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

  6. }


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

  8. if(po != null) {

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

  10. }


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


  12. po = new User();

  13. po.setEmail(user.getEmail());

  14. po.setPassword(passMd5);

  15. po.setCreated(new Date());

  16. po.setUsername(user.getUsername());

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

  18. po.setPoint(0);


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

  20. }

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

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

  • com.example.common.lang.Result

  1. @Data

  2. public class Result implements Serializable {


  3. private Integer code;

  4. private Integer status;

  5. private String msg;

  6. private Object data;

  7. private String action;

  8. ...

  9. }

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

  1. Result result = userService.register(user);

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

  3. return result;

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

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

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

  • static/res/mods/index.js

  1. //表单提交

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

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

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

  5. var end = function(){

  6. if(res.action){

  7. location.href = res.action;

  8. } else {

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

  10. }

  11. };

  12. if(res.status == 0){

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

  14. icon: 1,

  15. time: 10*1000,

  16. end: end

  17. }) : end();

  18. };

  19. });

  20. return false;

  21. });

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

  • templates/auth/register.ftl

  1. <script>

  2. $(function () {

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

  4. this.src="/capthca.jpg";

  5. });

  6. });

  7. </script>

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

shiro页面标签

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

  1. <dependency>

  2. <groupId>net.mingsoft</groupId>

  3. <artifactId>shiro-freemarker-tags</artifactId>

  4. <version>0.1</version>

  5. </dependency>

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

  • com.example.config.FreemarkerConfig

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

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

  • templates/inc/header.ftl

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

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

<@*shiro.guest*>

<@*shiro.user*>

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

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

  1. <ul class="layui-nav fly-nav-user">

  2. <@shiro.guest>

  3. <!-- 未登入的状态 -->

  4. <li class="layui-nav-item">

  5. <a class="iconfont icon-touxiang layui-hide-xs" href="/login"></a>

  6. </li>

  7. <li class="layui-nav-item">

  8. <a href="/login">登入</a>

  9. </li>

  10. <li class="layui-nav-item">

  11. <a href="/register">注册</a>

  12. </li>

  13. </@shiro.guest>

  14. <@shiro.user>

  15. <!-- 登入后的状态 -->

  16. <li class="layui-nav-item">

  17. <a class="fly-nav-avatar" href="javascript:;">

  18. <cite class="layui-hide-xs"><@shiro.principal property="username" /></cite>

  19. <i class="iconfont icon-renzheng layui-hide-xs" title="认证信息:layui 作者"></i>

  20. <i class="layui-badge fly-badge-vip layui-hide-xs">VIP<@shiro.principal property="vipLevel" /></i>

  21. <img src="<@shiro.principal property="avatar" />">

  22. </a>

  23. <dl class="layui-nav-child">

  24. <dd><a href="user/set.html"><i class="layui-icon"></i>基本设置</a></dd>

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

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

  27. <hr style="margin: 5px 0;">

  28. <dd><a href="/logout" style="text-align: center;">退出</a></dd>

  29. </dl>

  30. </li>

  31. </@shiro.user>

  32. </ul>

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

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

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



 给eblog一个star,感谢支持哈

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存