Skip to content

SpringMVC教程 - 20 登录功能和访问拦截(传统项目)

我们的教程到这里要结束了。

最后我们实现一个简单的 demo,实现:登录 + 注册 + 一个学生列表功能,学生列表在上一个章节已经实现了,现在还需要实现如下功能:

  1. 实现用户注册功能;
  2. 实现用户登录功能;
  3. 登录后跳转到学生列表页面;
  4. 如果没有登录,就通过 URL 访问学生列表,则自动跳转到登录页面。

这一章需要在 18-全注解开发19-SSM整合 两个章节的基础上继续完成。

20.1 创建数据库表

首先创建数据库表,用户表,用来注册和登录。

sql
CREATE TABLE `tb_user` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `username` VARCHAR(50) NULL comment '用户名',
  `password` VARCHAR(50) NULL comment '密码',
  `create_time` DATETIME NULL comment '创建时间',
  `update_time` DATETIME NULL comment '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

20.2 实体类

创建用户实体类 User.java,与数据库表对应:

java
package com.foooor.helloweb.pojo;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  • 如果之前已经有了 User 类,则修改一下字段。

20.3 Mapper接口

创建用户Mapper接口 UserMapper.java

java
package com.foooor.helloweb.mapper;

import com.foooor.helloweb.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper {

    /**
     * 根据用户名查询用户
     */
    @Select("SELECT * FROM tb_user WHERE username = #{username}")
    User findByUsername(String username);

    /**
     * 注册用户
     */
    @Insert("INSERT INTO tb_user (username, password, create_time, update_time) VALUES (#{username}, #{password}, NOW(), NOW())")
    int insert(User user);
}
  • 一个根据用户名查询用户,在登录的时候使用;
  • 一个插入用户,在注册的时候使用;

20.4 工具类

添加MD5密码加密工具类 MD5Util.java ,工具类提供一个加密方法,主要用来对密码进行加密。

在注册的时候页面传递密码到后端,后端使用加密算法对密码进行加密后,然后保存到数据库。在登录的时候,页面传递密码到后端,使用同样的方式将密码加密,加密后,在和数据库的数据进行比较(因为数据库的也是加密的),如果相同,则说明密码正确。

java
package com.foooor.helloweb.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {
    /**
     * 对字符串进行MD5加密
     */
    public static String encrypt(String input) {
        try {
            // 创建MD5消息摘要实例
            MessageDigest md = MessageDigest.getInstance("MD5");

            // 更新摘要
            md.update(input.getBytes());

            // 获取加密后的字节数组
            byte[] byteData = md.digest();

            // 转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : byteData) {
                sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
            }

            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5加密失败", e);
        }
    }
}
  • 这里使用最简单的 MD5 加密算法,准确的说 MD5 并不是一个加密算法,因为加密意味着解密,能解密才是加密算法。而 MD5 是不可逆的,是一个摘要算法,通过算法对内容提取摘要得到的一个 32 个字符(16个字节)的16进制字符串结果。对于一个很大的文件也可以计算一个 MD5 码,我们不可能通过一个 MD5 码反推得到一个很大的文件,那么这将是多么牛逼的压缩算法,可惜 MD5 是不可逆的。有很多网站说破解 MD5,其实是暴力破解,也就是给一个 MD5 码看一下数据库有没有,数据库存储了很多字符串对应的 MD5 码,然后去数据库查一下有没有,你搞一个文件的 MD5 码给他,一查一个不吱声。

20.5 Service层

创建用户服务接口IUserService.java

java
package com.foooor.helloweb.service;

import com.foooor.helloweb.pojo.User;

public interface IUserService {

    /**
     * 用户注册
     */
    boolean register(User user);

    /**
     * 用户登录
     */
    User login(String username, String password);

}
  • 提供一个注册方法和一个登录方法。

创建用户服务实现类 UserServiceImpl.java

java
package com.foooor.helloweb.service.impl;

import com.foooor.helloweb.common.BizException;
import com.foooor.helloweb.common.ErrorCode;
import com.foooor.helloweb.mapper.UserMapper;
import com.foooor.helloweb.pojo.User;
import com.foooor.helloweb.service.IUserService;
import com.foooor.helloweb.util.MD5Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@Transactional
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public boolean register(User user) {
        User dbUser = userMapper.findByUsername(user.getUsername());
        // 检查用户名是否已存在
        if (dbUser != null) {
            log.warn("用户名已存在");
            throw new BizException(ErrorCode.USER_REGISTERED);
        }

        // 对密码进行加密处理
        String encryptedPassword = MD5Util.encrypt(user.getPassword());
        user.setPassword(encryptedPassword);

        // 保存用户
        return userMapper.insert(user) > 0;
    }

    @Override
    public User login(String username, String password) {
        // 查询用户
        User user = userMapper.findByUsername(username);
        if (user != null) {
            // 这里使用简单的MD5加密示例
            String encryptedPassword = MD5Util.encrypt(password);
            // 和数据库中的密码进行比较
            if (user.getPassword().equals(encryptedPassword)) {
                return user;
            }
        }

        return null;
    }
}

20.6 Controller层

创建登录Controller ,这里将登录和注册都放在一个 Controller 中了。

LoginController.java

java
package com.foooor.helloweb.controller;

import com.foooor.helloweb.pojo.User;
import com.foooor.helloweb.service.IUserService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/views")
public class LoginController {

    @Autowired
    private IUserService userService;

    /**
     * 跳转到注册页面
     */
    @GetMapping("/to-register")
    public String toRegister() {
        return "register";
    }

    /**
     * 跳转到登录页面
     */
    @GetMapping("/to-login")
    public String toLogin() {
        return "login";
    }

    /**
     * 处理登录请求
     */
    @PostMapping("/login")
    public String login(@RequestParam String username,
                        @RequestParam String password,
                        HttpSession session, Model model) {

        // 调用Service层的登录方法进行验证
        User user = userService.login(username, password);

        if (user != null) {
            // 将用户信息存入Session
            session.setAttribute("loginUser", user);

            // 登录成功,重定向到学生列表页面
            return "redirect:/views/students";
        } else {
            // 登录失败,返回登录页面并显示错误信息
            model.addAttribute("error", "用户名或密码错误");
            return "login";
        }
    }

    /**
     * 处理注册请求
     */
    @PostMapping("/register")
    public String register(@RequestParam String username,
                           @RequestParam String password,
                           @RequestParam String confirmPassword,
                           Model model) {

        // 验证密码是否一致
        if (!password.equals(confirmPassword)) {
            model.addAttribute("error", "两次输入的密码不一致");
            return "register";
        }

        // 创建用户对象
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);

        // 注册用户
        boolean success = userService.register(user);
        if (success) {
            // 注册成功,重定向到登录页面
            return "redirect:/views/to-login";
        } else {
            // 注册失败,返回注册页面并显示错误信息
            model.addAttribute("error", "用户名已存在");
            return "register";
        }
    }

    /**
     * 处理退出登录
     */
    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 销毁Session
        session.invalidate();

        // 重定向到登录页面
        return "redirect:/views/to-login";
    }
}
  • 在其中分别处理登录、注册、退出功能;
  • 注册后重定向到登录页面;
  • 登录后重定向到学生列表页面。

20.7 Thymeleaf模板

下面再创建两个页面,一个是注册页面,一个是登录页面。还是在模板目(webapp/WEB-INF/template/)下创建。

创建注册页面模板 register.html

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 400px;
            margin: 100px auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        h1 {
            text-align: center;
        }
        .error {
            color: red;
            text-align: center;
            margin-bottom: 20px;
        }
        div {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
        }
        input[type="text"], input[type="password"] {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
        }
        input[type="submit"] {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        input[type="submit"]:hover {
            background-color: #45a049;
        }
        .login-link {
            text-align: center;
            margin-top: 15px;
        }
    </style>
</head>
<body>
<h1>用户注册</h1>

<!-- 显示错误信息 -->
<p class="error" th:if="${error}">[[${error}]]</p>

<form th:action="@{/views/register}" method="post">
    <div>
        <label>用户名:</label>
        <input type="text" name="username" required>
    </div>
    <div>
        <label>密码:</label>
        <input type="password" name="password" required>
    </div>
    <div>
        <label>确认密码:</label>
        <input type="password" name="confirmPassword" required>
    </div>
    <div>
        <input type="submit" value="注册">
    </div>
</form>

<div class="login-link">
    <span>已有账号?</span>
    <a th:href="@{/views/to-login}">立即登录</a>
</div>
</body>
</html>

页面显示效果如下:

创建登录页面模板 login.html

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 400px;
            margin: 100px auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        h1 {
            text-align: center;
        }
        .error {
            color: red;
            text-align: center;
            margin-bottom: 20px;
        }
        div {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
        }
        input[type="text"], input[type="password"] {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
        }
        input[type="submit"] {
            width: 100%;
            padding: 10px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        input[type="submit"]:hover {
            background-color: #45a049;
        }
        .register-link {
            text-align: center;
            margin-top: 15px;
        }
    </style>
</head>
<body>
<h1>用户登录</h1>

<!-- 显示错误信息 -->
<p class="error" th:if="${error}">[[${error}]]</p>

<form th:action="@{/views/login}" method="post">
    <div>
        <label>用户名:</label>
        <input type="text" name="username" required>
    </div>
    <div>
        <label>密码:</label>
        <input type="password" name="password" required>
    </div>
    <div>
        <input type="submit" value="登录">
    </div>
</form>

<div class="register-link">
    <span>还没有账号?</span>
    <a th:href="@{/views/to-register}">立即注册</a>
</div>
</body>
</html>

显示效果如下:

20.8 登录拦截器

项目到这里,是已经可以注册用户和实现登录了。

但是现在还没有实现对学生列表进行访问控制,也就是不管登没登录,通过 http://localhost:8080/view/students 都可以访问学生列表页面。下面就来实现访问拦截功能。

创建登录拦截器 LoginInterceptor.java

java
package com.foooor.helloweb.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取Session
        HttpSession session = request.getSession();

        // 检查Session中是否有登录用户信息
        Object user = session.getAttribute("loginUser");

        if (user == null) {
            // 如果没有登录,重定向到登录页面
            response.sendRedirect("/views/to-login");
            return false;
        }

        // 如果已经登录,继续处理请求
        return true;
    }
}
  • 在拦截器中,从 session 中取出登录信息,也就是在登录的时候放进去的,来判断用户有没有登录,如果登录了,则放行;如果没有登录,就跳转到登录页面。

20.9 拦截器配置

在 SpringMvcConfig 类中配置拦截器,并设置例外,需要注意,不要拦截登录和注册相关的接口,否则登录功能和注册功能会失效。

还可能造成循环,没登录跳转到登录,但是因为拦截了登录,导致又跳转到登录,死循环了。

SpringMvcConfig 配置类中注入并配置拦截器:

java
@Configuration
@ComponentScan(basePackages = "com.foooor.helloweb.controller")
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {

    // 注入登录拦截器
    @Autowired
    private LoginInterceptor loginInterceptor;

    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 其他拦截器配置,略...

        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")  // 拦截所有请求
                // 添加例外,不拦截登录和注册页面
                .excludePathPatterns(
                        "/views/to-login", 
                        "/views/login", 
                        "/views/to-register", 
                        "/views/register"
                ); 
    }

    // 其他,略...
}

20.10 测试

现在项目基本完成,可以进行测试了。

  1. 可以进行用户的注册和登录。

  2. 登录成功后,会跳转到学生列表页面。

  3. 如果没登录,直接访问学生列表会跳转到登录页面。如果登录了就没问题了。

你可以在学生列表添加一个超链接,用于用户退出:

html
<a th:href="@{/views/logout}">退出</a>

现在整个功能是基本完成了,只是比较粗糙,很多细节需要完善,例如用户名和密码的校验规则等等,这里只是演示一下实现的流程。


内容未完......