从零开始写项目第二篇【登陆注册、聊天、收藏夹模块】(三)

简介: 笔记

大致的错误代码如下:

/**
     * 用户登陆,Shiro从Reaml中验证,返回JSON提示用户
     *
     * @param request
     * @return
     * @throws Exception
     */
    @RequestMapping("/login.do")
    @ResponseBody
    public Map<String, Object> login(HttpServletRequest request) throws Exception {
        Map<String, Object> resultMap = new LinkedHashMap<String, Object>();
        //如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
        String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
        //根据shiro返回的异常类路径判断,抛出指定异常信息
        if (exceptionClassName != null) {
            if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
                resultMap.put("message", "账号不存在");
            } else if (IncorrectCredentialsException.class.getName().equals(
                    exceptionClassName)) {
                resultMap.put("message", "用户名/密码错误");
            } else if ("captchaCodeError".equals(exceptionClassName)) {
                resultMap.put("message", "验证码错误");
            } else {
                throw new Exception();//最终在异常处理器生成未知错误
            }
        } else {
            resultMap.put("message", "登陆成功");
            //把用户信息写到session中
            Subject subject = SecurityUtils.getSubject();
            ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
            request.getSession().setAttribute("activeUser", activeUser);
        }
        return resultMap;
    }
$.ajax({
                url: path + "/user/login.do",
                type: "post",
                async: false,
                data: $("#loginForm").serialize(),
                success: function (responseText) {
                    console.log(responseText);
                    if(responseText.message=="验证码错误"){
                        alert("验证码错误");
                    }else if(responseText.message=="账号不存在") {
                        alert("账号不存在");
                    }else if(responseText.message=="用户名/密码错误") {
                        alert("用户名/密码错误");
                    }else if(responseText.message=="登陆成功") {
                        window.location.href = path + "/index.html";
                    }else {
                        console.log(responseText);
                        alert("未知错误");
                    }
                },
                error: function () {
                    alert("系统错误");
                }
            })

在测试的时候就有非常怪异的想象:登陆的时候,有时可以返回正常JSON的信息、有的时候直接不调用ajax(后台打断点并没有进入后台),但是能够经过sucess方法返回一个页面的内容。

这就令我感到非常惊奇了,于是乎,我一直在搜索“为什么ajax不调用、success方法却回调了”、”sucess回调方法返回一个页面“、”ajax常见错误“。显然,我一直认为是ajax的问题,并没有怀疑Shiro的认证流程。在此方面花费我很多的时间,我怀疑过jquery的版本,还有其他方面的冲突…..

直到后来我就在想:为什么有的时候JSON返回一个页面的内容呢???此时我想起Shiro的认证流程了。如果认证不通过,Shiro默认返回给login.do处理,如果验证通过,shiro默认返回上一级请求的url。

也就是说:我在login.do中返回一个JSON是不合理的。因为如果没有认证的话,Shiro默认是需要返回登陆界面的,而我擅自修改成JSON了。于是就造成了奇怪的现象了。

那问题又来了,如果认证失败的话,为了做到更好的用户体验是需要实时地告诉用户哪里错了,而不是直接返回页面内容。用户不知道还会一脸懵逼

因为login.do是专门处理异常的信息的,因此我们可以使用统一处理异常的方式去处理:

if (exceptionClassName != null) {
            if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
                throw new UserException("账号不存在");
            } else if (IncorrectCredentialsException.class.getName().equals(
                    exceptionClassName)) {
                throw new UserException("密码错误了");
            } else if ("captchaCodeError".equals(exceptionClassName)) {
                throw new UserException("验证码错误了");
            } else {
                throw new Exception();//最终在异常处理器生成未知错误
            }
        }
        return "redirect:/goURL/user/toLogin.do";

统一异常处理器:

/**
 * 统一异常处理类
 */
public class SysException implements HandlerExceptionResolver {
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception ex) {
        //输出异常
        ex.printStackTrace();
        String message = null;
        UserException userException = null;
        //如果ex是系统 自定义的异常,直接取出异常信息
        if (ex instanceof UserException) {
            userException = (UserException) ex;
        } else {
            //针对非UserException异常,对这类重新构造成一个UserException,异常信息为“未知错误”
            userException = new UserException("未知错误");
        }
        message = userException.getMessage();
        request.setAttribute("message", message);
        try {
            //根据模版生成页面,重定向到页面中
            Map<String, Object> map = new HashedMap();
            map.put("title", "错误页面");
            map.put("content", message);
            map.put("subject", "出错啦");
            map.put("path", ReadPropertiesUtil.readProp("projectPath"));
            FreeMarkerUtils markerUtils = new FreeMarkerUtils();
            markerUtils.ouputFile("promptPages.ftl", "promptPages.html", map);
            request.getRequestDispatcher("/promptPages.html").forward(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

上面已经解决了提示错误信息的问题了。可是我觉得不够好,因为错误信息跳转到别的页面了,用户需要重新回到登陆页面进行注册,这个也太麻烦了吧。ps(要是我登陆错误搞这么一个东西,我就认为这个是破网站…)。

于是乎,我就想在怎么实时把错误信息返回给登陆页面呢??ajax是否还能用呢??login方法是一定要返回一个页面的了。试了很多无用的方法,在网上也找不到相关的方法,当时搜索关键字”Shiro返回错误信息“…..。

此时,我就在想ajax和Shiro是否能结合起来…后来去搜索了一下”ajax和Shiro“才知道网上也有人遇到我这种情况。

参考资料:

http://blog.csdn.net/haiyang0735/article/details/51278387

https://my.oschina.net/WMSstudio/blog/162594

大致的思路就知道了:重写自定义表单过滤器的方法,判断是否为ajax请求来进行处理

期间找了很多相关的资料,每个人的实现都参差不齐。表单过滤器方法中的retrun truereturn false也把我搞得一头雾水。最后,回归到搜索关键字“Shiro认证流程“,找到了一篇解决我问题的博文:

http://www.cnblogs.com/leechenxiang/p/7070229.html

经过上面的资料和阅读了Shiro相关的源码,我基本能知道shiro认证流程了,下面是我画的一张流程图:

17.jpg

根据流程可以判断在验证码失败时如果是ajax请求返回JSON数据。如果登陆失败,重写onLoginFailure方法,也判断是否为ajax请求。,

package zhongfucheng.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import zhongfucheng.entity.ActiveUser;
import zhongfucheng.utils.ReadPropertiesUtil;
import zhongfucheng.utils.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * Created by ozc on 2017/10/27.
 */
/**
 * 自定义一个表单过滤器的目的就是认证流程由自己控制
 */
public class UserFormAuthenticationFilter extends FormAuthenticationFilter {
    /**
     * 只要请求地址不是post请求和不是user/login(处理登陆的url),那么就返回登陆页面上
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //判断是否是登陆页面地址请求地址、如果不是那么重定向到controller的方法中
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                //在提交给realm查询前,先判断验证码
                if (WebUtils.validateCaptcha(httpRequest)) {
                    return executeLogin(request, response);
                } else {
                    if (isAjax(httpRequest)) {
                        //这里要使用标准的JSON格式
                        WebUtils.printCNJSON("{\"message\":\"验证码错误\"}", httpServletResponse);
                        return false;
                    } else {
                        // 放行 allow them to see the login page ;)
                        httpRequest.setAttribute("shiroLoginFailure", "captchaCodeError");
                        return true;
                    }
                }
            } else {
                // 放行 allow them to see the login page ;)
                return true;
            }
        } else {
            // TODO AJAX请求用户扩展。以后再补
            if (isAjax(httpRequest)) {
                //httpServletResponse.sendError(ShiroFilterUtils.HTTP_STATUS_SESSION_EXPIRE);
                return false;
            } else {
                //返回配置的user/login.do,该方法会重定向到登陆页面地址,再次发送请求给本方法
                saveRequestAndRedirectToLogin(request, response);
            }
            return false;
        }
    }
    /**
     * 认证成功,把用户认证信息保存在session中,判断是否为ajax请求
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        //在跳转前将数据保存到session中
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
        WebUtils.setValue2Session(httpRequest, "activeUser", activeUser);
        //如果是ajax请求,那么我们手动跳转
        //如果不是ajax请求,那么由Shiro帮我们跳转
        if (isAjax(httpRequest)) {
            WebUtils.printCNJSON("{\"message\":\"登陆成功\"}", httpServletResponse);
        } else {
            //设置它跳转到首页路径,如果不设置它还会停留在登陆页面。
            String indexPath = ReadPropertiesUtil.readProp("projectPath") + "/index.html";
            org.apache.shiro.web.util.WebUtils.redirectToSavedRequest(request, response, indexPath);
        }
        return false;
    }
    /**
     * 认证失败、如果ajax请求则返回json数据
     * 如果非ajax请求、则默认返回给login.do处理异常
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        // 不是ajax请求,就按照源码的方式去干(返回异常给controller,controller处理异常)
        if (!isAjax(httpServletRequest)) {
            setFailureAttribute(request, e);
            return true;
        }
        //是ajax请求,我们返回json给浏览器
        String message = e.getClass().getSimpleName();
        if ("IncorrectCredentialsException".equals(message)) {
            WebUtils.printCNJSON("{\"message\":\"密码错误\"}", httpServletResponse);
        } else if ("UnknownAccountException".equals(message)) {
            WebUtils.printCNJSON("{\"message\":\"账号不存在\"}", httpServletResponse);
        } else if ("captchaCodeError".equals(message)) {
            WebUtils.printCNJSON("{\"message\":\"验证码错误\"}", httpServletResponse);
        } else {
            WebUtils.printCNJSON("{\"message\":\"未知错误\"}", httpServletResponse);
        }
        return false;
    }
    /**
     * 判断ajax请求
     *
     * @param request
     * @return
     */
    boolean isAjax(HttpServletRequest request) {
        return (request.getHeader("X-Requested-With") != null && "XMLHttpRequest".equals(request.getHeader("X-Requested-With").toString()));
    }
}

值得一提的是:手动返回JSON格式数据、要是标准的JSON格式。否则会出错

参考资料:

http://www.cnblogs.com/54td/p/6074456.html

login.do代码:

@RequestMapping("/login.do")
    public String login(HttpServletRequest request, HttpServletResponse response) throws Exception {
        /**
         * 如果在shiro配置文件中配置了authc的话,那么所点击的url就需要认证了才可以访问
         * a:如果url是登陆请求地址(user/login.do),不是post请求的话,流程是不会去Realm中的。那么会返回到该方法中,也就是会返回登陆页面
         * b:如果url是登陆页面地址,是post请求的话,那么去realm中对比,如果成功了那么跳转到在表单过滤器中配置的url中
         *
         * c:如果url不是登陆页面地址,那么表单过滤器会重定向到此方法中,该方法返回登陆页面地址。并记住是哪个url被拦截住了
         * d:用户填写完表单之后,会进入b环节,此时登陆成功后跳转的页面是c环节记住的url。
         */
        //如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
        String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
        //根据shiro返回的异常类路径判断,抛出指定异常信息
        if (exceptionClassName != null) {
            if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
                throw new UserException("账号不存在");
            } else if (IncorrectCredentialsException.class.getName().equals(
                    exceptionClassName)) {
                throw new UserException("密码错误了");
            } else if ("captchaCodeError".equals(exceptionClassName)) {
                throw new UserException("验证码错误了");
            } else {
                throw new Exception();//最终在异常处理器生成未知错误
            }
        }
        return "redirect:/goURL/user/toLogin.do";
    }


忘记密码


对于忘记密码这个功能并不难,就跳转到忘记密码页面,让用户输入邮箱,去邮箱所给的超链接修改密码就行了。

18.png


记住我功能


本来我是想在登陆的时候勾选“记住我”,那么下次访问的时候,就可以直接访问了(因为我设置了一些链接是需要认证后才能访问的)。

原本是想使用Shiro的rememberMe这个功能的。发现它其实并没有为我们自动实现登陆

具体的参考链接如下:

http://blog.csdn.net/nsrainbow/article/details/36945267

查了挺多资料的,发现Shiro的rememberMe这个功能并不好用,但我的系统是基于Shiro来进行开发的。

根据源码的文档说明:

public interface RememberMeAuthenticationToken extends AuthenticationToken {
    /**
     * Returns {@code true} if the submitting user wishes their identity (principal(s)) to be remembered
     * across sessions, {@code false} otherwise.
     *
     * @return {@code true} if the submitting user wishes their identity (principal(s)) to be remembered
     *         across sessions, {@code false} otherwise.
     */
    boolean isRememberMe();
}

只有在当前用户的session(Shiro的Session)存储了已认证的用户,那么才承认当前的用户是已登陆的。

我也想要实现rememberMe这个功能,那么只能手动去弄了

我是通过拦截器来实现的:

public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private UserService userService;
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        Subject currentUser = SecurityUtils.getSubject();
        //如果 isAuthenticated 为 false 证明不是登录过的,同时 isRememberd 为true 证明是没登陆直接通过记住我功能进来的
        if (!currentUser.isAuthenticated() && currentUser.isRemembered()) {
            ActiveUser activeUser = (ActiveUser) SecurityUtils.getSubject().getPrincipal();
            //获取session看看是不是空的
            Session session = currentUser.getSession();
            if (session.getAttribute("currentUser") == null) {
                User user = userService.validateUserExist(activeUser.getUsercEmail());
                UsernamePasswordToken token = new UsernamePasswordToken(user.getUserEmail(), activeUser.getPassword(), currentUser.isRemembered());
                //把当前用户放入session
                currentUser.login(token);
                session.setAttribute("currentUser",user);
                //设置会话的过期时间--ms,默认是30分钟,设置负数表示永不过期
                session.setTimeout(-1000l);
                //这是httpSession、用户页面获取数据的。
                httpServletRequest.getSession().setAttribute("activeUser", activeUser);
            }
        }
        return true;
    }
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

那么就可以实现当用户即使关闭了浏览器之后,再次打开浏览器访问我们的页面还是登陆状态!


对登陆注册优化


最开始在页面使用路径都是自己手写:http://localhost:8080/zhongfucheng/这样子的。这样并不通用。想要做得通用的话,那么就要使用变量了。而我又不想使用JSP页面。于是我的项目统一使用freemarker

跳转到某个url的逻辑其实是一样的,没必要为每个跳转都写一个方法。于是我将其抽取出来:

/**
     * 通用的页面跳转
     *
     * @param folder 模块
     * @param file   具体文件
     * @return
     */
    @RequestMapping("/goURL/{folder}/{file}.do")
    public String goURL(@PathVariable("folder") String folder, @PathVariable("file") String file) {
        return "/" + folder + "/" + file + ".ftl";
    }

对于首页而言我们可以使用监听器来对其进行初始化

public class ProjectListener implements ServletContextListener {
    /**
     * 当项目启动的时候,我们就根据模版生成我们的index页面
     *
     * @param servletContextEvent
     */
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println(servletContextEvent.getServletContext().getRealPath("/"));
        try {
            FreeMarkerUtils markerUtils = new FreeMarkerUtils();
            Map map = new HashMap();
            map.put("path", ReadPropertiesUtil.readProp("projectPath"));
            markerUtils.ouputFile("index.ftl", "index.html", map);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
}

在JavaScript中可能也需要用到http://localhost:8080/zhongfucheng/路径,我们可以使用JavaScript来获取项目的该路径,而不用写死:

<!--使用JS获取项目根路径-->
    <script>
        var path = "";
        $(function () {
            var strFullPath = window.document.location.href;
            var strPath = window.document.location.pathname;
            var pos = strFullPath.indexOf(strPath);
            var prePath = strFullPath.substring(0, pos);
            var postPath = strPath.substring(0, strPath.substr(1).indexOf('/') + 1);
            path = prePath + postPath;
        });
    </script>

比如我们在获取用户信息的时候就很方便了。

19.jpg

在代码会重复的情况下封装一些常用的Utils,或者使用别人写好的Utils

20.png

目录
相关文章
|
移动开发 JavaScript 前端开发
三分钟学会 H5 聊天机器人开发(附源码和在线演示)
【学习目标】 熟悉和掌握 HTML结构和CSS的相关知识 学会使用HBuilder进行APP打包 熟悉JavaScript的基本用法和jQuery的使用(提前预习)
398 0
|
小程序
视频号主页,实现一键添加个人微信功能,留客更方便,真香
在视频号主页放置添加微信的按钮,其实微信已经支持了,只不过只支持企业微信,不支持个人微信,那怎么办,只能自己实现了。
458 0
视频号主页,实现一键添加个人微信功能,留客更方便,真香
|
前端开发 开发者 微服务
项目第七天内容介绍 | 学习笔记
快速学习 项目第七天内容介绍
接口测试平台代码实现番外:主页改版-9
上节,我们搞定了 首页搜索的功能的mock版本,就是写死了返回值的假版本。本节课就来搞定真实的搜索吧。
接口测试平台代码实现番外:主页改版-9
|
前端开发 JavaScript 测试技术
接口测试平台代码实现番外:主页改版-3
接口测试平台代码实现番外:主页改版-3
接口测试平台代码实现番外:主页改版-3
|
前端开发 JavaScript 测试技术
接口测试平台代码实现番外:主页改版-8
接口测试平台代码实现番外:主页改版-8
接口测试平台代码实现番外:主页改版-8
|
JavaScript 前端开发 测试技术
接口测试平台代码实现番外:主页改版-5
上节之后有粉丝私聊觉得,平台右上角的“主页/退出” 按钮已经过时。所以我们本节首先来优化下。
接口测试平台代码实现番外:主页改版-5
|
前端开发 JavaScript 测试技术
接口测试平台代码实现番外:主页改版-6
本节我们来实现下那三个饼形图的后台逻辑,不过我这里只做其中一个的,其他俩个留着日后再用。 也就是用户的 资源占平台总的比。资源暂时定为项目数比 接口数比 用例数 。 这里大家可以自行设计,本教程只演示如何实现这个流程。
接口测试平台代码实现番外:主页改版-6
|
JavaScript 测试技术
接口测试平台代码实现番外:主页改版-2
接口测试平台代码实现番外:主页改版-2
接口测试平台代码实现番外:主页改版-2
|
前端开发 测试技术 Python
接口测试平台代码实现番外:主页改版-4
接口测试平台代码实现番外:主页改版-4
接口测试平台代码实现番外:主页改版-4

热门文章

最新文章