简介这份资源是面向计算机专业学生与Java初学者的一套智能二维码门禁管理系统毕业设计完整方案涵盖可运行源码与配套论文资料适合用于毕业设计、期末大作业或课程设计等场景难度适中便于上手与二次开发。压缩包共356个文件约3.37MB包含26个Java源文件、9个JSP页面、38个JavaScript脚本以及大量CSS、PNG、WXSS、WXML等前端与小程序样式资源另有JSON、XML配置文件和DOCX论文文档整体结构清晰覆盖后端控制层、业务实现与前端展示多个层次。已有162人学习下载说明该方案在同类选题中具有一定参考价值。读者可获得经过本地编译验证、评审得分98分的完整项目代码以及助教老师审定的论文材料便于快速理解二维码门禁的识别、权限校验与用户管理逻辑并在此基础上完成自己的设计与答辩准备。1. 智能二维码门禁管理系统从扫码到开锁一个Java毕业设计到底要打通哪些环节宿舍楼下那台门禁刷卡反应慢半拍忘带卡就得等别人进出时蹭进去。很多计算机毕业设计选题就盯上了这个场景——把刷卡换成二维码手机亮码、闸机开锁后台还能查记录。这个标题里的「智能二维码门禁管理系统」本质是一套用Java做服务端、二维码做身份凭证、硬件或模拟设备做执行端的完整链路。它解决的是传统门禁凭证易复制、记录难追溯、访客不好管的问题适合做计算机毕业设计、课程设计也适合想练手Spring Boot全栈的开发者。源码和论文资料是交付物但真正值钱的是你能不能把「生成码→验码→开门→留痕」这条链路跑通而不是把代码下载下来改个名字交差。2. 二维码门禁的凭证设计为什么不能直接把用户ID塞进二维码2.1 二维码里到底该放什么新手最容易犯的错是生成二维码时直接把用户ID或者门禁卡号明文写进去。这样做扫码端拿到码就能开锁但任何人用手机扫一下你的码就能拿到用户ID自己再生成一个同样的码门禁形同虚设。常见做法是放一个一次性令牌token这个token由服务端生成、带过期时间、和用户绑定扫码端拿到token后回传服务端校验校验通过才下发开锁指令。具体设计上二维码内容可以是一个URL形如https://your-domain/qr/verify?tokenxxxxx也可以是一段JSON字符串。URL方案的好处是手机自带相机扫码就能跳转不需要额外AppJSON方案则需要专门的扫码端解析。毕业设计里我一般推荐URL方案因为演示方便老师拿手机一扫就能看到效果。token的生成规则要包含几个要素用户标识、时间戳、随机串、签名。签名用服务端持有的密钥做HMAC防止token被篡改。过期时间建议设短30秒到2分钟足够过期后扫码端提示「二维码已失效请刷新」。2.2 数据库表怎么建才够用门禁系统的核心表不多但字段设计直接影响后面能不能扩展。下面这套表结构是我在多个类似项目里沉淀下来的直接可用。-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt加密, real_name VARCHAR(64) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 手机号, role VARCHAR(20) DEFAULT USER COMMENT USER/ADMIN, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 门禁设备表 CREATE TABLE door_device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_no VARCHAR(64) NOT NULL UNIQUE COMMENT 设备编号, location VARCHAR(128) COMMENT 安装位置, secret_key VARCHAR(128) COMMENT 设备通信密钥, status TINYINT DEFAULT 1 COMMENT 1在线 0离线, last_heartbeat DATETIME COMMENT 最后心跳时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 通行记录表 CREATE TABLE access_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, device_id BIGINT NOT NULL, token VARCHAR(256) COMMENT 本次使用的令牌, result TINYINT COMMENT 1通过 0拒绝, reason VARCHAR(128) COMMENT 拒绝原因, scan_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, scan_time), INDEX idx_device_time (device_id, scan_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 权限表用户和设备的通行关系 CREATE TABLE user_device_auth ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, device_id BIGINT NOT NULL, start_time DATETIME COMMENT 权限生效时间, end_time DATETIME COMMENT 权限失效时间, UNIQUE KEY uk_user_device (user_id, device_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;access_log表里我特意加了token字段方便排查「同一个码被扫了两次」这类问题。user_device_auth表用联合唯一索引防止重复授权。door_device表的secret_key用于设备和服务端之间的通信鉴权后面讲设备模拟时会用到。注意password字段长度至少128因为BCrypt加密后的字符串是60位留足余量。不要用MD5毕业设计答辩时老师一问加密方式就露馅。2.3 令牌生成与校验的代码实现下面这段代码是令牌生成和校验的核心逻辑用Java写依赖javax.crypto和java.util.Base64不需要额外引入第三方库。import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class TokenUtil { // 服务端持有的签名密钥实际项目中应从配置中心读取 private static final String SECRET your-256-bit-secret-key-here; // 令牌有效期90秒 private static final long EXPIRE_MS 90 * 1000L; /** * 生成令牌 * param userId 用户ID * param deviceId 设备ID * return Base64编码的令牌字符串 */ public static String generate(Long userId, Long deviceId) { long timestamp System.currentTimeMillis(); // 随机串防止同一毫秒内生成的令牌重复 String nonce java.util.UUID.randomUUID().toString().replace(-, ).substring(0, 8); String raw userId : deviceId : timestamp : nonce; String sign hmacSha256(raw); // 令牌格式raw . sign return Base64.getUrlEncoder().withoutPadding() .encodeToString((raw . sign).getBytes(StandardCharsets.UTF_8)); } /** * 校验令牌 * param token 扫码端回传的令牌 * return 校验通过返回用户ID和设备ID失败返回null */ public static Long[] verify(String token) { try { String decoded new String(Base64.getUrlDecoder().decode(token), StandardCharsets.UTF_8); int dotIndex decoded.lastIndexOf(.); if (dotIndex 0) return null; String raw decoded.substring(0, dotIndex); String sign decoded.substring(dotIndex 1); // 验签 if (!hmacSha256(raw).equals(sign)) return null; String[] parts raw.split(:); if (parts.length ! 4) return null; long timestamp Long.parseLong(parts[2]); // 检查过期 if (System.currentTimeMillis() - timestamp EXPIRE_MS) return null; return new Long[]{Long.parseLong(parts[0]), Long.parseLong(parts[1])}; } catch (Exception e) { return null; } } private static String hmacSha256(String data) { try { Mac mac Mac.getInstance(HmacSHA256); mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), HmacSHA256)); byte[] bytes mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } catch (Exception e) { throw new RuntimeException(HMAC计算失败, e); } } }这段代码的逻辑很直白generate方法把用户ID、设备ID、时间戳、随机串拼成一个原始串再用HMAC-SHA256签名最后Base64编码。verify方法反过来解码、验签、检查过期时间。关键参数是EXPIRE_MS设成90秒是为了兼顾演示体验和安全性——太短了用户还没走到闸机就过期太长了被截屏复用的风险就高。SECRET这个密钥不要硬编码在代码里实际部署时放到application.yml或者环境变量里。毕业设计里如果只在本机跑硬编码也能接受但答辩时最好提一句「生产环境应使用配置中心管理密钥」这是加分项。3. 用Spring Boot搭服务端扫码验证接口和开锁指令怎么串起来3.1 项目依赖和分层结构服务端用Spring Boot 2.7.x3.x也可以但要注意JDK版本依赖只需要四个spring-boot-starter-web、spring-boot-starter-jdbc或mybatis-spring-boot-starter、mysql-connector-java、spring-boot-starter-validation。二维码生成用Google的ZXing加一个com.google.zxing:core:3.5.1和javase:3.5.1就够了。分层结构按Controller→Service→Mapper走不要把所有逻辑塞在Controller里。下面这张表是我建议的包结构照着建不会乱。包名职责关键类controller接收HTTP请求参数校验QrCodeController、AccessControllerservice业务逻辑令牌校验、权限判断QrCodeService、AccessServicemapper数据库操作UserMapper、AccessLogMapperentity数据库实体User、DoorDevice、AccessLogdto请求/响应对象ScanRequest、ScanResponseutil工具类TokenUtil、QrCodeUtilconfig配置类WebConfig、MybatisConfig3.2 扫码验证接口的完整实现扫码端可以是手机App、微信小程序也可以是模拟器拿到二维码里的token后向服务端发一个POST请求服务端校验token、检查权限、记录日志、返回开锁指令。下面是从Controller到Service的完整代码。// ScanRequest.java public class ScanRequest { NotBlank(message token不能为空) private String token; NotNull(message deviceId不能为空) private Long deviceId; // getter/setter省略 } // QrCodeController.java RestController RequestMapping(/api/qr) public class QrCodeController { Autowired private QrCodeService qrCodeService; PostMapping(/scan) public ResultScanResponse scan(RequestBody Valid ScanRequest request) { return Result.ok(qrCodeService.verifyAndOpen(request)); } } // QrCodeService.java Service public class QrCodeService { Autowired private UserDeviceAuthMapper authMapper; Autowired private AccessLogMapper accessLogMapper; Autowired private DoorDeviceMapper deviceMapper; public ScanResponse verifyAndOpen(ScanRequest request) { // 1. 校验令牌 Long[] ids TokenUtil.verify(request.getToken()); if (ids null) { saveLog(null, request.getDeviceId(), request.getToken(), 0, 令牌无效或已过期); throw new BizException(二维码无效或已过期请刷新); } Long userId ids[0]; Long tokenDeviceId ids[1]; // 2. 检查令牌里的设备ID和请求里的设备ID是否一致 if (!tokenDeviceId.equals(request.getDeviceId())) { saveLog(userId, request.getDeviceId(), request.getToken(), 0, 设备不匹配); throw new BizException(二维码与当前设备不匹配); } // 3. 检查用户是否有该设备的通行权限 UserDeviceAuth auth authMapper.selectByUserAndDevice(userId, request.getDeviceId()); if (auth null) { saveLog(userId, request.getDeviceId(), request.getToken(), 0, 无通行权限); throw new BizException(您没有该门禁的通行权限); } // 检查权限有效期 Date now new Date(); if (auth.getStartTime() ! null now.before(auth.getStartTime())) { saveLog(userId, request.getDeviceId(), request.getToken(), 0, 权限未生效); throw new BizException(通行权限尚未生效); } if (auth.getEndTime() ! null now.after(auth.getEndTime())) { saveLog(userId, request.getDeviceId(), request.getToken(), 0, 权限已过期); throw new BizException(通行权限已过期); } // 4. 检查设备是否在线 DoorDevice device deviceMapper.selectById(request.getDeviceId()); if (device null || device.getStatus() ! 1) { saveLog(userId, request.getDeviceId(), request.getToken(), 0, 设备离线); throw new BizException(门禁设备离线请联系管理员); } // 5. 记录通行成功日志 saveLog(userId, request.getDeviceId(), request.getToken(), 1, null); // 6. 返回开锁指令 ScanResponse response new ScanResponse(); response.setOpen(true); response.setMessage(验证通过门禁已开启); response.setUserName(deviceMapper.selectUserName(userId)); return response; } private void saveLog(Long userId, Long deviceId, String token, int result, String reason) { AccessLog log new AccessLog(); log.setUserId(userId); log.setDeviceId(deviceId); log.setToken(token); log.setResult(result); log.setReason(reason); log.setScanTime(new Date()); accessLogMapper.insert(log); } }这段代码的校验顺序是有讲究的先验令牌再验设备匹配再验权限最后验设备在线。为什么把设备匹配放在权限之前因为令牌里的设备ID是生成时就绑定的如果扫码端请求的设备ID和令牌里的不一致说明这个码被拿到别的设备上用了属于异常行为应该优先拦截。saveLog方法在每次校验失败时也会记录这样管理员在后台能看到「谁在什么时候试图开哪个门被拒绝了原因是什么」。这个细节在论文里可以写成「安全审计功能」答辩时是个亮点。3.3 二维码生成接口和前端展示用户在自己的手机上打开页面点击「生成通行码」服务端生成token并返回一个二维码图片。下面是用ZXing生成二维码的工具类。import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.client.j2se.MatrixToImageWriter; import java.io.ByteArrayOutputStream; import java.util.HashMap; import java.util.Map; public class QrCodeUtil { /** * 生成二维码PNG字节数组 * param content 二维码内容通常是带token的URL * param size 图片边长单位像素 */ public static byte[] generate(String content, int size) throws Exception { MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); // 纠错级别M级约15%容错适合屏幕展示 hints.put(EncodeHintType.ERROR_CORRECTION, com.google.zxing.qrcode.decoder.ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 1); BitMatrix matrix new MultiFormatWriter() .encode(content, BarcodeFormat.QR_CODE, size, size, hints); ByteArrayOutputStream out new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(matrix, PNG, out); return out.toByteArray(); } }ERROR_CORRECTION设成M级就够了屏幕展示不需要H级那么高的容错M级生成的二维码图案更简洁手机识别更快。MARGIN设1是留白边ZXing默认是4留白太多二维码在手机上显示会偏小。Controller层暴露一个GET /api/qr/generate?deviceIdxxx接口从当前登录用户session里取userId调用TokenUtil.generate生成token拼成URL再调QrCodeUtil.generate返回图片流。前端用img src/api/qr/generate?deviceId1直接展示。提示二维码内容里的URL域名要和实际部署的域名一致否则手机扫码后跳转失败。本地开发时可以用局域网IP比如http://192.168.1.100:8080/qr/verify?tokenxxx手机连同一个WiFi就能访问。4. 设备端模拟与联调没有真实闸机怎么把流程跑通4.1 用Java写一个模拟扫码端真实门禁设备通常是嵌入式Linux或者单片机毕业设计里不可能人手一台。常见做法是写一个Java Swing小工具或者一个简单的HTTP客户端模拟扫码端的行为发送token到服务端收到opentrue就弹窗提示「门已开」。import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class DeviceSimulator { private static final String SERVER http://localhost:8080; private static final Long DEVICE_ID 1L; public static void main(String[] args) throws Exception { // 模拟扫码端拿到token后回传服务端 String token args.length 0 ? args[0] : test-token; String json String.format({\token\:\%s\,\deviceId\:%d}, token, DEVICE_ID); HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(SERVER /api/qr/scan)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(服务端返回 response.body()); if (response.body().contains(\open\:true)) { System.out.println( 门禁已开启继电器动作 ); } else { System.out.println( 验证失败门禁保持关闭 ); } } }这个模拟器跑起来后你可以在浏览器里生成二维码用手机扫码拿到token再把token复制到模拟器参数里运行。虽然多了一步复制粘贴但整个链路——生成码、扫码、验码、开锁、记录——全部打通了。答辩演示时老师看到手机扫码后电脑上弹出「门禁已开启」效果比单纯讲代码好得多。4.2 用Postman做接口联调的顺序在写前端之前先用Postman把接口逐个调通顺序如下POST /api/user/login拿到session或tokenGET /api/qr/generate?deviceId1拿到二维码图片用手机扫出里面的tokenPOST /api/qr/scanbody里带上token和deviceId检查返回opentrue故意传一个过期的token检查返回「二维码无效或已过期」故意传一个没有权限的deviceId检查返回「您没有该门禁的通行权限」这五步走完服务端的核心逻辑就验证完了。很多同学跳过这一步直接写前端结果前端调不通又回头查服务端来回折腾。先Postman后前端是省时间的做法。4.3 设备心跳和在线状态维护door_device表里有last_heartbeat字段设备端每隔30秒向服务端发一个心跳请求服务端更新这个字段。后台管理页面根据last_heartbeat和当前时间的差值判断设备是否在线超过90秒没心跳就标记为离线。PostMapping(/heartbeat) public Result? heartbeat(RequestParam String deviceNo, RequestParam String sign) { DoorDevice device deviceMapper.selectByDeviceNo(deviceNo); if (device null) return Result.fail(设备未注册); // 用设备密钥验签防止伪造心跳 String expected DigestUtils.md5Hex(deviceNo device.getSecretKey()); if (!expected.equals(sign)) return Result.fail(签名错误); deviceMapper.updateHeartbeat(device.getId(), new Date()); return Result.ok(); }心跳接口的签名用MD5就够了因为心跳不涉及敏感数据主要作用是防止随便一个请求就把设备标记成在线。secret_key在设备注册时由服务端生成并下发模拟器里硬编码同一个值即可。5. 避坑与排查二维码门禁系统最容易翻车的五个地方5.1 二维码扫出来是乱码现象手机扫码后显示一串乱码或者跳转到一个空白页面。原因二维码内容里包含了中文或者特殊字符生成时没有指定UTF-8编码。ZXing默认用ISO-8859-1中文会乱码。解决在EncodeHintType.CHARACTER_SET里显式指定UTF-8如3.3节代码所示。另外如果二维码内容是URLURL里的token参数要做URLEncode避免、/、这些Base64字符在URL里被转义。5.2 同一个二维码被扫两次都能开门现象用户生成一个码扫一次开门再扫一次还能开门。原因token校验只检查了签名和过期时间没有检查「是否已被使用」。如果过期时间设了90秒这90秒内同一个码可以被无限次使用。解决在Redis里存一个token:used的键第一次校验通过后写入设置过期时间和token一致。第二次校验时先查这个键存在就拒绝。毕业设计里如果不想引入Redis可以用access_log表查——同一个token在access_log里已经有result1的记录就拒绝。但查库比查Redis慢并发高了会有问题。建议至少提一下Redis方案体现你知道生产环境怎么做。5.3 手机扫码后跳转的域名打不开现象手机扫二维码后浏览器提示「无法访问此网站」。原因二维码里的URL用了localhost或者127.0.0.1手机访问的是自己的回环地址当然打不开。解决把URL里的域名换成电脑的局域网IP比如192.168.1.100并确保手机和电脑在同一个WiFi下。Windows上用ipconfig查IPMac/Linux上用ifconfig或ip addr。防火墙要放行8080端口否则手机请求会被拦截。5.4 通行记录里user_id是null现象access_log表里有些记录的user_id字段是空的。原因令牌校验失败时TokenUtil.verify返回nullsaveLog方法传进去的userId就是null。这是设计如此——令牌无效时确实不知道是谁。但如果你的access_log表user_id字段设了NOT NULL插入就会报错。解决把user_id字段改成允许NULL或者在saveLog里对null做处理存0表示未知用户。我一般把user_id设成BIGINT DEFAULT 0插入时null转0查询时过滤掉0的记录。5.5 答辩时被问「你的系统和直接刷卡有什么区别」现象老师觉得二维码门禁和刷卡没本质区别创新点不足。原因你只做了「扫码开门」没有体现出二维码相比IC卡的优势。解决从三个角度回答。第一二维码可以动态刷新截图无效IC卡可以被复制第二二维码可以远程下发访客不用到现场办卡管理员在后台授权后访客手机就能收到码第三二维码通行记录和用户账号绑定能追溯到具体的人IC卡只能追溯到卡号。如果能在系统里加上「访客临时授权」功能——管理员设置访客姓名、有效期、可通行门禁系统生成一个临时二维码发给访客——这个创新点就立住了。6. 从毕业设计到能跑的小系统三个让代码更值钱的改动6.1 把令牌从「一次性」升级为「可刷新」前面5.2节提到同一个码被扫两次的问题用Redis标记已使用是一种方案。但更优雅的做法是二维码页面每隔60秒自动刷新一次旧token在生成新token时主动失效。这样用户不需要手动点刷新体验更好安全性也更高。实现方式是在QrCodeService里加一个refresh方法生成新token的同时把旧token写入Redis黑名单过期时间设为旧token的剩余有效期。校验时先查黑名单命中就拒绝。前端用setInterval每60秒调一次/api/qr/refresh拿到新的二维码图片替换img的src。// 前端自动刷新二维码 let refreshTimer setInterval(async () { const resp await fetch(/api/qr/refresh?deviceId deviceId); const blob await resp.blob(); document.getElementById(qrImg).src URL.createObjectURL(blob); }, 60000);这个改动不大但答辩时可以说「实现了动态二维码截图无效」比静态二维码高一个档次。6.2 加一个简单的管理后台毕业设计如果只有扫码接口演示效果很单薄。加一个管理后台用Thymeleaf或者VueElement UI都行至少包含三个页面用户管理增删改查、授权门禁、设备管理查看在线状态、心跳时间、通行记录按时间、用户、设备筛选。通行记录页面用分页查询SQL里用LIMIT和OFFSET不要一次性查全部。数据量大了之后access_log表要按月份分表或者加索引idx_user_time和idx_device_time这两个索引就是为查询准备的。6.3 论文里该写什么、不该写什么论文资料是标题里提到的交付物之一但很多同学把论文写成代码说明书这是大忌。论文的重点应该是需求分析为什么做这个、系统设计架构图、表结构、接口设计、关键算法令牌生成与校验、权限判断、测试功能测试、安全测试。代码实现放在附录里正文里只贴核心片段。安全测试部分可以写用过期token请求、用其他设备的token请求、用无权限用户的token请求、用篡改过的token请求四种情况系统都能正确拒绝。这比写「系统运行稳定」有说服力得多。我自己的习惯是每做完一个模块就在README里记一条「这个模块解决了什么问题、关键参数是什么、踩了什么坑」。等到写论文的时候这些记录直接就是素材不用从头回忆。毕业设计做完不是终点把代码整理成能跑、能讲、能复现的项目才是真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取