【任务9】注册与登录:拥有登录功能(初级)

打卡要求如下:
1、新建一个登录页面,命名为 login.html
2、新建一个校验登录的接口,命名为 Login,用户登录成功后跳转到首页。
3、你是如何保存用户登录态的?请把你的大致逻辑写一下。

本问题对应的项目链接:从0设计一个web项目
PS:每次在最后,都要给出你的GitHub链接哦,方便我打开

共以下 7 个回答

  • 星河揽我入梦 永久会员 2024年7月4日 下午2:27

    使用session保持用户登录态
    https://github.com/CurryisCoat/community/tree/dev
    public String login(HttpServletRequest request,HttpServletResponse response) throws IOException {
    String username = request.getParameter(“username”);
    User user = userService.login(username);
    response.setContentType(“text/html;charset=utf-8”);
    PrintWriter writer = response.getWriter();

        if(user != null){
            if(md5s.md5(request.getParameter("password")+user.getSalt()).equals(user.getPassword())){
                HttpSession session = request.getSession();
                String sessionAttribute = md5s.GenerateUUID();
                session.setAttribute("LoginStatus",sessionAttribute);
                session.setMaxInactiveInterval(24 * 60 * 60);
                return "index";
            }else{
              return "login";
            }
        }else{
           return "register";
        }
    
    0 赞同 0 条回复
  • So 普通 2022年10月30日 下午6:45

    使用session保持用户登录状态

    https://github.com/Egofir/ferry

    0 赞同 0 条回复
  • 豆子彩虹 普通 2022年10月26日 下午2:12

    保存用户登陆态的逻辑

    @PostMapping("/Login")
        public R login(@Valid @RequestBody User user, HttpServletRequest request) {
            String userName = user.getUserName();
            String userPwd = user.getUserPwd();
            String realPwd = userService.getPwd(userName);
            if (realPwd == null) {
                return R.error("用户不存在");
            }
            boolean check = MyUtils.checkBcrypt(userPwd, realPwd);
            if (check) {
                HttpSession session = request.getSession();
                String sessionAttribute = MyUtils.getRandom();
                session.setAttribute("LoginStatus", sessionAttribute);
                session.setMaxInactiveInterval(24 * 60 * 60);
                redisTemplate.opsForValue().set(sessionAttribute, userName, 1, TimeUnit.DAYS);
                return R.ok();
            } else {
                return R.error("用户名或密码错误");
            }
        }
    

    在登陆时向session存入随机生成的16位id,在redis中存入随机生成的16位id和userName,在访问主页时检验session和redis中是否存在相应的数据,进行登陆态的判断。
    之前自己实现的时候没有考虑拦截其他页面请求的问题,查看评论区之后对自己的代码进行了补充完善,加入拦截器。

    @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession();
            String sessionAttribute = (String) session.getAttribute("LoginStatus");
            if (sessionAttribute != null) {
                String userName = redisTemplate.opsForValue().get(sessionAttribute);
                if (userName != null) {
                    return true;
                }
            } else {
                try {
                    //TODO: 上线前更改重定向url
                    response.sendRedirect("http://localhost:8080/FromZerotoExpert/login.html");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            return false;
        }
    

    github地址:https://github.com/lei342345795/FromZerotoExpert

    0 赞同 0 条回复
  • lucca 普通 2022年10月11日 上午3:29

    Q1 如何维护登录态?

    专门去看了黑马Redis实战的前两节,借鉴了其思想来完善项目。

    一开始使用的是 cookie + session 的模式。

    tomcat 会将每个请求对应的 session 存到自己的内存中,并将 sessionId 返回给前端,自动存到 cookie,下次访问会自动携带 sessionId,
    因此下次访问仍然可以获取到上次请求的 session,从而解决 http 无状态的问题

    以上过程对开发者是透明的,由 tomcat 维护,直接用就可以。

    接下来是逻辑流程:

    用户登陆后,会将用户信息存储到 session 中。

    编写配置拦截器,下一次访问需要验证登录态的接口时,会进入拦截器

    拦截器负责获取用户信息,如果存在即登录态还在,反之则过期

    同时如果没过期,用户访问时要刷新过期时间,防止登录后操作了,但是还是被判定为下线情况

    同时为了保证线程安全,使用 ThreadLocal 存储用户信息(脱敏)

    如果是单机模式,目前已经足以解决问题,但 session 无法在多台机器中共享(或者说很麻烦),因此引入分布式缓存 redis 来代替 cookie 和 session

    更换成 redis 之后,有几个问题需要注意。

    Q2: redis 没有了自动维护的 session id,如何找到自己的信息

    其实就是 redis 的 key 的选择,这边借鉴黑马 redis 的思路,用户登录后生成token,发送给前端

    前端以后每次请求都携带 token,下次拦截器用 token 去 redis 里面拿

    同时注意,要刷新数据过期时间

    Q3: 非登录态接口无法刷新过期时间

    用户只有访问需要检测登录态的页面才能刷新过期时间

    如果登录后访问了不需要登录态的页面,一段时间后仍然被判定为下线

    因此要对所有的页面都能刷新登录态过期时间,所以再抽一层,做一个拦截器

    github地址(点击即可)

    0 赞同 2 条回复
    • shuaidi 永久会员 @ lucca 2022年10月11日 上午11:35

      你的项目,有一些小 Bug,比如我登录之后,自己手动修改了一下 token 的值,那么就会打不开首页了(打开时一片空白),而且也没有提示我自动跳转去登录。这样就会导致我一直无法打开(除非我记得登录的地址)

    • lucca 普通 @ shuaidi 2022年10月12日 上午12:58

      确实考虑的不严谨,谢谢帅地哥指正,已更新

  • 小肸 永久会员 2022年10月9日 下午4:18

    我之前做的项目都是基于Vue加上SpringBoot,而黑马的教程好像是基于JSP的,看了Second和季两位的代码,对JSP构建Web应用有些了解,我还是给出Vue+SpringBoot实现的思路。我只实现了后端相关的,前端Vue的使用还有些不熟悉,后面再补补。

    • 后端其实就是服务端,之前可能对session和cookie还有些误解,通过浏览器的开发工具和postman对一个后端接口发送请求,返回的响应请求中的头部有个JSESSIONID,这个应该就是后端给客户端的网络连接的标识符,session就是会话的意思。保存用户登录态其实就是用户登录后,进行其它请求要判断用户是否登录,如果是登录的,就执行(还可以进行权限拦截,我之前做的星球中的博客项目中的shiro框架就是用来对用户进行登录验证和权限拦截,当时可能只理解了个大概,现在理解更深了一点,它应该是用来token令牌作为用户身份的标识符,然后在需要进行身份验证的接口上加上一个注解来拦截请求并验证token令牌)。
    • 对于登录,我用了两个接口,一个接口是login,用来给用户登录,一个接口是isLogin用来判断用户是否登录。
      login接口实现思路:首先验证登录参数的合法性,后面根据登录账号查找数据库中的账号对应的加密密码,与加密后的登录密码对比,如果匹配成功,则登录成功,为了保持用户登录态,生成一个accountId+sessionId的键值对(accountId就是根据用户账号生成的,与用户账号对应,因为sessionId要与账号对应,但后面要将这个用户账号以cookie的形式返回给用户,直接返回账号可能有些不安全,就用个当前时间+随机数+账号组合下返回),用redis缓存(redis我之前了解了下原理,这次去网上搜了下怎么使用),设置过期时间为24小时,因为要保持用户登录态24个小时,然后返回键为”accountId”值为生成的accountId给用户。而这个cookie就是用户保证登录态的关键。
      isLogin接口实现思路:这个接口就是判断用户是否位于登录态,用户第一次登录成功后,(同一次会话)下次请求中就会带上一个键为”accountId”的cookie,然后我们以这个cookie的值在redis缓存中找,如果找到了一个sessionId与当前请求的session的会话sessionId相同,说明用户位于登录态,不需要重新登录,否则需要重新登录。

    登录接口代码

    @PostMapping("/login")
        public ResultVO login(@RequestBody @Valid LoginForm loginForm, BindingResult bindingResult,
                              HttpServletRequest request, HttpServletResponse response) {
            HttpSession session = request.getSession();
            if (bindingResult.hasErrors()) {
                return ResultVOUtil.fail(100, "登录参数不正确");
            }
            User user = userService.getOneByAccount(loginForm.getLoginAccount());
            if (user == null) {
                return ResultVOUtil.fail(101, "账号不存在");
            }
            // 根据登录密码生成加密密码
            String entryptPassword = PasswordUtil.getPBKDF2(loginForm.getLoginPassword(), user.getUserSalt());
            if (!user.getUserPassword().equals(entryptPassword)) {
                return ResultVOUtil.fail(102, "密码错误");
            }
            // 取得会话的sessionId
            String sessionId = session.getId();
            // 根据账号生成accountId
            String accountId = AccountIdUtil.createAccountId(loginForm.getLoginAccount());
            // 将accountId 与 sessionId 绑定
            redisTemplate.opsForValue().set(accountId, sessionId);
            redisTemplate.expire(accountId, 60*60*24, TimeUnit.SECONDS);
            Cookie cookie = new Cookie("accountId",accountId);
            response.addCookie(cookie);
            return ResultVOUtil.success("登录");
        }
        @GetMapping("/isLogin")
        public ResultVO isLogin(HttpServletRequest request, HttpServletResponse response) {
            if (request.getCookies() != null) {
                for (Cookie cookie : request.getCookies()) {
                    if (cookie.getName().equals("accountId")) {
                        String accountId = cookie.getValue();
                        String sessionId = (String) redisTemplate.opsForValue().get(accountId);
                        if (sessionId == null) {
                            return ResultVOUtil.fail(430, "请重新登录");
                        }
                        if (sessionId.equals(request.getSession().getId())) {
                            return ResultVOUtil.success();
                        }
                    }
                }
            }
            return ResultVOUtil.fail(430, "请重新登录");
        }
    

    生成accountId代码

    public class AccountIdUtil {
        /**
         * 生成accountId(随机数+时间+account)
         * @param account
         * @return
         */
        public static String createAccountId(String account) {
            String random = String.valueOf((new Random().nextInt(900000) + 100000));
            return random + System.currentTimeMillis() + account;
        }
    }
    
    • 至于页面的跳转,可以交给前端来做,前端配个路由拦截器文件,比如:Vue中首页路由路径是/fromzerotoexpert,要跳转到这个路由,首先向isLogin发送请求判断用户是否位于登录态,如果位于就跳转,否则跳转到/login,就是登录页面。

    GitHub链接:https://github.com/shuaishuaixi03/FromZerotoExpert/tree/dev

    0 赞同 0 条回复
  • Second(S) 普通 2022年9月27日 下午12:55

    我的实现方式如下:
    1.设置拦截器,拦截所有路径的请求,只放行与登录注册相关的接口和静态资源

        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(
                    "/redirect_login",
                    "/FromZerotoExpert/redirect_login",
                    "/FromZerotoExpert/login/**",
                    "/FromZerotoExpert/register",
                    "/FromZerotoExpert/checkUsername",
                    "/FromZerotoExpert/checkPassword",
                    "/FromZerotoExpert/checkEmail",
                    "/FromZerotoExpert/checkAll",
                    "/FromZerotoExpert/save",
                    "/**/*.html",                //html静态资源
                    "/**/*.js",                  //js静态资源
                    "/**/*.css"                  //css静态资源
            );
    

    2.自定义登录拦截的拦截器,检测是否登录,若检测已登录过则刷新redis数据的存储时间,并放行访问,否则未登录过,跳转至登录页面进行登录。

     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            //若能在redis数据库中查询到对应的sessionId中确实存在username的记录,则拦截器放行访问
            try {
                String sessionId = (String) request.getSession().getAttribute("user");
                if(null != sessionId && !"".equals(sessionId)){
                    String username = stringRedisTemplate.opsForValue().get(sessionId);
                    if(null != username && !"".equals(username)){
           
                        request.getSession().setMaxInactiveInterval(60);        //重置session过期时间
                        stringRedisTemplate.expire(sessionId, 60, TimeUnit.SECONDS);
                        stringRedisTemplate.expire(username, 60, TimeUnit.SECONDS);
                        return true;
                    }
                }
    
            }catch (Exception e){
                e.printStackTrace();
            }
    
    
            response.sendRedirect("/redirect_login");
            //查询sessionId失败(未找到或异常),则重定向至登录页面
            return false;
    

    在这里,我们利用session存储的sessionId和redis数据库访问用户信息,从而检测是否已登录过,具体细节在登录检测中会谈到。
    3.登录检测(未进行登录过的访问请求只能跳转至登录界面进行登录,并进行登录检测)

        public RegisterResult checkUser(User user, HttpServletRequest request, HttpServletResponse response) {
            //检测用户名是否存在
            if(null == user) return new RegisterResult(false, "该用户不存在!");
            String username = user.getUsername();
            String input_password = user.getPassword();
            User res = userDao.selectByUsername(username);
            if(null == res) return new RegisterResult(false, "该用户不存在!");
    
            //校验密码是否正确
            String salt = res.getSalt();
            String password = res.getPassword();
            String check_password = RegisterUtils.encryptPassword(input_password, salt);
            //密码错误,则在前端做出提示
            if(!password.equals(check_password)) return new RegisterResult(false, "用户名或密码错误!");
    
    
            //密码正确,先检查之前的登录是否过期
            // 看到了下节预告,先进行了一点简单的实现
            String oldSessionId = stringRedisTemplate.opsForValue().get(username);
            if(null != oldSessionId && !"".equals(oldSessionId)){
                //未过期,则手动设置过期
                stringRedisTemplate.expire(oldSessionId, 0, TimeUnit.SECONDS);
                stringRedisTemplate.expire(username, 0, TimeUnit.SECONDS);
            }
    
    
            //密码正确,生成sessionId,并将sessionId和username互作为键值存入redis数据库中
            String sessionId = IdUtil.simpleUUID();
            request.getSession().setAttribute("user", sessionId);
            request.getSession().setMaxInactiveInterval(60);        //设置session过期时间
            stringRedisTemplate.opsForValue().set(sessionId, username);
            stringRedisTemplate.opsForValue().set(username, sessionId);
            //设置redis键值对过期时间
            stringRedisTemplate.expire(sessionId, 60, TimeUnit.SECONDS);
            stringRedisTemplate.expire(username, 60, TimeUnit.SECONDS);
    
    
    
            //存储用户名称,重定向至主页时防止数据丢失
            request.getSession().setAttribute("username", username);
    
            return new RegisterResult(true);
        }
    

    在这里首先对登录的基本需求进行检测,如用户名是否存在,用户账号密码是否正确等。若登录失败,则返回给前端做出失败的提示。若检测能够进行登录成功,则生成一个UUID当作sessionId,然后将此sessionId和username互为键值存放在redis数据库中(存两个键值对,方便相互查询),这样拦截器通过查询session中的sessionId,并在redis中查询这个sessionId检测是否能获取到用户名,便可以检测是否登录成功过。然后因为看到了下节预告,所以在使用session和redis之前,先用redis数据库检测该用户是否在别的地方登陆过,若有,则将之前在redis中存放的相关键值对失效,然后再存入新的内容,这样就能简单的实现仅后者在线了。最后,将登录检测的结果,返回给前端,告诉前端登录成功/失败,前端进行处理。
    注:在这里设置过期时间是60S,方便能体会到用户登录信息失效过期的场景,不然要等一天才过期,有点不好测试自己的代码。
    4.前端检测登录状态

        //发生登录点击事件时,进行异步提交,检测登录状态
        $("#login_submit").click(function (){
            if(check_loginUsername() && check_loginPassword()){
                $.post("login/user", {"username": $("#login_username").val(),
                    "password": $("#login_password").val()}, function (data){
                    if(data.qualified){
                        //登录成功,取消提示信息,跳转至主页面
                        login_message.innerHTML = "";
                        $(location).attr("href", "/FromZerotoExpert/main");
                    }else{
                        //登录失败,显示提示信息,给用户反馈
                        login_message.innerHTML = data.message;
                        $(login_message).css('color', 'red');
                    }
                })
            }else{
                login_usernameCheck();
                login_passwordCheck();
            }
    

    5.登录成功,后端在返回主页页面之前,将用户名存储在cookie中供前端调用,动态显示”xxx已登录”

        @RequestMapping("/FromZerotoExpert/main")
        public String mainPage(HttpServletRequest request, HttpServletResponse response){
            String username = (String) request.getSession().getAttribute("username");
            Cookie cookie = new Cookie("username", username);
            response.addCookie(cookie);
            return "main.html";
        }
    

    gitHub地址:https://github.com/Second6s9/FromZerotoExpert/tree/dev
    gitHub防覆盖地址:https://github.com/Second6s9/FromZerotoExpert/tree/day09
    服务器地址:http://123.57.142.33:8088/FromZerotoExpert/

    0 赞同 0 条回复
  • 秊 永久会员 2022年9月26日 下午6:22
    1. 我的思路是:前端使用 ajaxForm 接受后端接口的返回值,通过判断进行跳转
    $("#login").ajaxForm(function (res) {
        if (res.success === true) {
            window.location.href = "/FromZerotoExpert";
        }
    });
    
      • 首次登录: 登录时生成Cookie: fzteUser=sessionId, 同时在 redis 中以哈希形式存储: key: session field:name value:userName
    • 二次登录: 登录时检测Cookie,
      • 若有:若这次登录的账号和上次相同更新 Cooike 和 redis 中的键有效期;若不相同,删除上次的用户信息,创建新账号的信息
      • 若无, 进行1.
    • 访问首页: 根据是否有Cookie, 且 Cookie 中的 sessionId 在 redis 中是否过期(防止用户手动更改 Cookie 有效期)实现自动登录
    /**
         * 添加用户登录凭证
         */
        public void addLoginCert(String userName, HttpServletResponse response) {
            // 设置sessionID
            String sessionId = Long.toString(new SnowFlakeIdWorker(0, 0).nextId());
            // 创建Cookie
            Cookie cookie = new Cookie("fzteUser", sessionId);
            cookie.setMaxAge(60 * 60 * 24);
            response.addCookie(cookie);
            // redis中添加
            jedis.hset(sessionId, "name", userName);
            jedis.expire(sessionId, 60 * 60 * 24);
        }
    
    /**
         * 更新用户登录凭证
         * @param sessionId Cookie 中 "fzteUser" 的值
         */
        public boolean updateLoginCert(String sessionId, String userName, HttpServletResponse response) {
            // 更新 sessionId
            if (jedis.expire(sessionId, 60 * 60 * 24) == 0) {
                // sessionId失效, 即找不到这个key
                return false;
            }
    
            //  此时登录的用户名和redis中记录的不同
            if (!Objects.equals(jedis.hget(sessionId, "name"), userName)) {
                // 删除原来的用户记录, 减少服务器空间消耗
                jedis.expire(sessionId, 0);
                return false;
            }
    
            // 更新 Cookie
            Cookie cookie = new Cookie("fzteUser", sessionId);
            cookie.setMaxAge(60 * 60 * 24);
            response.addCookie(cookie);
            return true;
        }
    

    访问首页时的前端判断为:

    $.ajax({
        url: '/welcome',
        method: 'get',
        success: function (res) {
            if (res.success === true) {
                document.write("

    嗨, " + res.content + ", 欢迎您来到 from zero to expert"); } else { window.location.href = "/Login"; } } })

    @GetMapping("/welcome")
        @ResponseBody
        public CommonResp welcome(@CookieValue(value = "fzteUser", required = false) String sessionId) {
            CommonResp resp = new CommonResp<>();
            // 根据是否有Cookie, 且 Cookie 中的 sessionId 在 redis 中是否过期(防止用户手动更改 Cookie 有效期)
            if (sessionId == null || Objects.equals(userService.jedis.hget(sessionId, "name"), null)) {
                resp.setSuccess(false);
            } else {
                resp.setContent(userService.jedis.hget(sessionId, "name"));
            }
            return resp;
        }
    

    对于用Cookie存储用户登录态我有点疑问: 不管我在 Cookie上怎么加密,假如有人通过一些手段获取到了我的 Cookie,那么他也可以在他的电脑上添加这个 Cookie 并实现自动登录。应该有更高级的存储用户登录态方式?

    服务器链接:http://124.223.184.187:8088/FromZerotoExpert
    Github仓库链接:https://github.com/jiangyklala/FromZerotoExpert/tree/dev

    0 赞同 4 条回复
    • shuaidi 永久会员 @ 秊 2022年9月26日 下午8:19

      你的疑问:
      1、一般不考虑cookie这些被人拿,如果被人打开了浏览器,并且把信息拿了,那别人确实可以登录了,我们不考虑这个,目前安全性较高的网站,cooki时间都比较短。如果考虑被人拿,那浏览器自动保存密码,也会被人拿了。

      2、那有人会说,我能否暴力刷登录接口?有些网站是不提供api接口给你访问的,只能通过浏览器的前端网页,因为后端接口有网关限制,只对特定的IP开放。不过有些网站也会专门提供接口给别人用,你可以自己去生产token,比如腾讯云,阿里云这些都可以,这个时候的token泄漏了那就没办法了,这就如同你的账户密码也泄口了。

      总的来说就是,关于session/cookie这些,我们不考虑安全性,安全性交给其他层面的去维护。

    • 秊 永久会员 @ shuaidi 2022年9月26日 下午8:42

      好嘞,谢谢地哥

    • shuaidi 永久会员 @ 秊 2022年9月26日 下午9:04

      你的程序有个Bug,比如我登录成功之后,自己在浏览器修改了cookie的值,那么就一直无法登录了。

    • 秊 永久会员 @ shuaidi 2022年9月26日 下午10:25

      已修复?

# 回答此问题

后才能回答