Initial commit

This commit is contained in:
Zzzz
2026-04-27 18:40:30 +08:00
commit 2120774b05
112 changed files with 12308 additions and 0 deletions
@@ -0,0 +1,26 @@
package com.mzh.library.controller;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class DashboardServlet extends HttpServlet {
private static final String DASHBOARD_JSP = "/WEB-INF/jsp/dashboard.jsp";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
AuthenticatedUser user = session == null
? null
: (AuthenticatedUser) session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
request.setAttribute("currentUser", user);
request.getRequestDispatcher(DASHBOARD_JSP).forward(request, response);
}
}
@@ -0,0 +1,100 @@
package com.mzh.library.controller;
import com.mzh.library.dao.impl.JdbcUserDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.service.AuthService;
import com.mzh.library.service.AuthenticationResult;
import com.mzh.library.service.impl.AuthServiceImpl;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginServlet extends HttpServlet {
private static final String LOGIN_JSP = "/WEB-INF/jsp/auth/login.jsp";
private static final String DASHBOARD_PATH = "/dashboard";
private static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
private AuthService authService;
@Override
public void init() {
this.authService = new AuthServiceImpl(new JdbcUserDao());
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if (isAuthenticated(request)) {
response.sendRedirect(request.getContextPath() + DASHBOARD_PATH);
return;
}
request.setAttribute("redirect", safeRedirect(request.getParameter("redirect")));
request.getRequestDispatcher(LOGIN_JSP).forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = trim(request.getParameter("username"));
String password = request.getParameter("password");
String redirect = safeRedirect(request.getParameter("redirect"));
AuthenticationResult result = authService.authenticate(username, password);
if (!result.isAuthenticated()) {
request.setAttribute("errorMessage", result.getMessage());
request.setAttribute("username", username);
request.setAttribute("redirect", redirect);
request.getRequestDispatcher(LOGIN_JSP).forward(request, response);
return;
}
resetSession(request, result.getUser());
response.sendRedirect(resolveRedirect(request, redirect));
}
private boolean isAuthenticated(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session != null && session.getAttribute(SessionAttributes.AUTHENTICATED_USER) != null;
}
private void resetSession(HttpServletRequest request, AuthenticatedUser user) {
HttpSession existingSession = request.getSession(false);
if (existingSession != null) {
existingSession.invalidate();
}
HttpSession session = request.getSession(true);
session.setMaxInactiveInterval(SESSION_TIMEOUT_SECONDS);
session.setAttribute(SessionAttributes.AUTHENTICATED_USER, user);
session.setAttribute(SessionAttributes.USER_ROLE, user.getRole().getCode());
session.setAttribute(SessionAttributes.USER_PERMISSIONS, user.getPermissionCodes());
}
private String resolveRedirect(HttpServletRequest request, String redirect) {
if (redirect.isEmpty() || "/login".equals(redirect) || "/logout".equals(redirect)) {
return request.getContextPath() + DASHBOARD_PATH;
}
return request.getContextPath() + redirect;
}
private String safeRedirect(String value) {
String redirect = trim(value);
if (redirect.startsWith("/")
&& !redirect.startsWith("//")
&& !redirect.contains("\r")
&& !redirect.contains("\n")) {
return redirect;
}
return "";
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,36 @@
package com.mzh.library.controller;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LogoutServlet extends HttpServlet {
private static final Logger LOGGER = Logger.getLogger(LogoutServlet.class.getName());
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticatedUser user = currentUser(session);
if (user != null) {
LOGGER.info("Logout userId=" + user.getId() + " role=" + user.getRole().getCode());
}
session.invalidate();
}
response.sendRedirect(request.getContextPath() + "/login");
}
private AuthenticatedUser currentUser(HttpSession session) {
Object value = session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
}
}
@@ -0,0 +1,29 @@
package com.mzh.library.controller;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class RoleAreaServlet extends HttpServlet {
private static final String ROLE_HOME_JSP = "/WEB-INF/jsp/role-home.jsp";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String servletPath = request.getServletPath();
if (servletPath.startsWith("/admin")) {
request.setAttribute("areaName", "Administration");
request.setAttribute("areaSummary", "Account, role, permission, and system-maintenance entry point.");
} else if (servletPath.startsWith("/librarian")) {
request.setAttribute("areaName", "Librarian Workspace");
request.setAttribute("areaSummary", "Book, reader, borrowing, return, renewal, and overdue entry point.");
} else {
request.setAttribute("areaName", "Reader Center");
request.setAttribute("areaSummary", "Catalog search and reader self-service entry point.");
}
request.getRequestDispatcher(ROLE_HOME_JSP).forward(request, response);
}
}
@@ -0,0 +1,18 @@
package com.mzh.library.controller;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UnauthorizedServlet extends HttpServlet {
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
}
}
@@ -0,0 +1,9 @@
package com.mzh.library.dao;
import com.mzh.library.entity.User;
import java.util.Optional;
public interface UserDao {
Optional<User> findActiveByUsername(String username);
}
@@ -0,0 +1,49 @@
package com.mzh.library.dao.impl;
import com.mzh.library.dao.UserDao;
import com.mzh.library.entity.Role;
import com.mzh.library.entity.User;
import com.mzh.library.exception.DaoException;
import com.mzh.library.util.JdbcUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
public class JdbcUserDao implements UserDao {
private static final String FIND_ACTIVE_BY_USERNAME = ""
+ "SELECT id, username, password_hash, display_name, role_code, active "
+ "FROM users "
+ "WHERE username = ? AND active = 1";
@Override
public Optional<User> findActiveByUsername(String username) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_ACTIVE_BY_USERNAME)) {
statement.setString(1, username);
try (ResultSet resultSet = statement.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
return Optional.of(mapUser(resultSet));
}
} catch (SQLException | IllegalArgumentException ex) {
throw new DaoException("Unable to load active user by username", ex);
}
}
private User mapUser(ResultSet resultSet) throws SQLException {
User user = new User();
user.setId(resultSet.getLong("id"));
user.setUsername(resultSet.getString("username"));
user.setPasswordHash(resultSet.getString("password_hash"));
user.setDisplayName(resultSet.getString("display_name"));
user.setRole(Role.fromCode(resultSet.getString("role_code")));
user.setActive(resultSet.getBoolean("active"));
return user;
}
}
@@ -0,0 +1,61 @@
package com.mzh.library.entity;
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class AuthenticatedUser implements Serializable {
private static final long serialVersionUID = 1L;
private final long id;
private final String username;
private final String displayName;
private final Role role;
private final Set<Permission> permissions;
public AuthenticatedUser(long id, String username, String displayName, Role role, Set<Permission> permissions) {
this.id = id;
this.username = username;
this.displayName = displayName;
this.role = role;
this.permissions = Collections.unmodifiableSet(new LinkedHashSet<>(permissions));
}
public static AuthenticatedUser from(User user, Set<Permission> permissions) {
return new AuthenticatedUser(
user.getId(),
user.getUsername(),
user.getDisplayName(),
user.getRole(),
permissions
);
}
public long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getDisplayName() {
return displayName;
}
public Role getRole() {
return role;
}
public Set<Permission> getPermissions() {
return permissions;
}
public Set<String> getPermissionCodes() {
return permissions.stream()
.map(Permission::getCode)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
}
@@ -0,0 +1,22 @@
package com.mzh.library.entity;
public enum Permission {
MANAGE_USERS("manage_users"),
MANAGE_BOOKS("manage_books"),
MANAGE_READERS("manage_readers"),
MANAGE_BORROWING("manage_borrowing"),
VIEW_REPORTS("view_reports"),
VIEW_SYSTEM_LOGS("view_system_logs"),
VIEW_CATALOG("view_catalog"),
BORROW_BOOKS("borrow_books");
private final String code;
Permission(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
@@ -0,0 +1,40 @@
package com.mzh.library.entity;
import java.util.Locale;
public enum Role {
ADMINISTRATOR("administrator", "Administrator"),
LIBRARIAN("librarian", "Librarian"),
READER("reader", "Reader");
private final String code;
private final String displayName;
Role(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public static Role fromCode(String code) {
if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("Role code is required");
}
String normalized = code.trim().toLowerCase(Locale.ROOT);
for (Role role : values()) {
if (role.code.equals(normalized)) {
return role;
}
}
throw new IllegalArgumentException("Unsupported role code: " + code);
}
}
@@ -0,0 +1,58 @@
package com.mzh.library.entity;
public class User {
private long id;
private String username;
private String passwordHash;
private String displayName;
private Role role;
private boolean active;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
@@ -0,0 +1,7 @@
package com.mzh.library.exception;
public class DaoException extends RuntimeException {
public DaoException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,73 @@
package com.mzh.library.filter;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AuthenticationFilter implements Filter {
private static final Set<String> PUBLIC_PATHS = new HashSet<>(Arrays.asList(
"",
"/",
"/login",
"/unauthorized",
"/favicon.ico"
));
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String path = relativePath(httpRequest);
if (isPublic(path)) {
chain.doFilter(request, response);
return;
}
HttpSession session = httpRequest.getSession(false);
Object user = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
if (user != null) {
chain.doFilter(request, response);
return;
}
String redirect = encode(buildRelativeUrl(httpRequest, path));
httpResponse.sendRedirect(httpRequest.getContextPath() + "/login?redirect=" + redirect);
}
private boolean isPublic(String path) {
return PUBLIC_PATHS.contains(path)
|| path.startsWith("/static/");
}
private String relativePath(HttpServletRequest request) {
String contextPath = request.getContextPath();
String requestUri = request.getRequestURI();
return requestUri.substring(contextPath.length());
}
private String buildRelativeUrl(HttpServletRequest request, String path) {
String query = request.getQueryString();
return query == null || query.isEmpty() ? path : path + "?" + query;
}
private String encode(String value) throws UnsupportedEncodingException {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
}
@@ -0,0 +1,101 @@
package com.mzh.library.filter;
import com.mzh.library.dao.impl.JdbcUserDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.service.AuthService;
import com.mzh.library.service.impl.AuthServiceImpl;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AuthorizationFilter implements Filter {
private static final Logger LOGGER = Logger.getLogger(AuthorizationFilter.class.getName());
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
private static final List<PathRule> RULES = Arrays.asList(
new PathRule("/admin", Permission.MANAGE_USERS),
new PathRule("/librarian", Permission.MANAGE_BORROWING),
new PathRule("/reader", Permission.VIEW_CATALOG)
);
private final AuthService authService = new AuthServiceImpl(new JdbcUserDao());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String path = relativePath(httpRequest);
Permission requiredPermission = requiredPermission(path);
if (requiredPermission == null) {
chain.doFilter(request, response);
return;
}
AuthenticatedUser user = currentUser(httpRequest.getSession(false));
if (authService.hasPermission(user, requiredPermission)) {
chain.doFilter(request, response);
return;
}
logDeniedAccess(user, requiredPermission, path);
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
request.setAttribute("errorMessage", "You do not have permission to access this page.");
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
}
private void logDeniedAccess(AuthenticatedUser user, Permission requiredPermission, String path) {
String actor = user == null
? "anonymous"
: "userId=" + user.getId() + " role=" + user.getRole().getCode();
LOGGER.warning("Permission denied path=" + path
+ " requiredPermission=" + requiredPermission.getCode()
+ " actor=" + actor);
}
private Permission requiredPermission(String path) {
for (PathRule rule : RULES) {
if (path.equals(rule.prefix) || path.startsWith(rule.prefix + "/")) {
return rule.permission;
}
}
return null;
}
private AuthenticatedUser currentUser(HttpSession session) {
if (session == null) {
return null;
}
Object value = session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
}
private String relativePath(HttpServletRequest request) {
String contextPath = request.getContextPath();
String requestUri = request.getRequestURI();
return requestUri.substring(contextPath.length());
}
private static final class PathRule {
private final String prefix;
private final Permission permission;
private PathRule(String prefix, Permission permission) {
this.prefix = prefix;
this.permission = permission;
}
}
}
@@ -0,0 +1,30 @@
package com.mzh.library.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class CharacterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) {
String configuredEncoding = filterConfig.getInitParameter("encoding");
if (configuredEncoding != null && !configuredEncoding.trim().isEmpty()) {
encoding = configuredEncoding.trim();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding(encoding);
response.setCharacterEncoding(encoding);
chain.doFilter(request, response);
}
}
@@ -0,0 +1,10 @@
package com.mzh.library.service;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
public interface AuthService {
AuthenticationResult authenticate(String username, String password);
boolean hasPermission(AuthenticatedUser user, Permission permission);
}
@@ -0,0 +1,35 @@
package com.mzh.library.service;
import com.mzh.library.entity.AuthenticatedUser;
public class AuthenticationResult {
private final boolean authenticated;
private final AuthenticatedUser user;
private final String message;
private AuthenticationResult(boolean authenticated, AuthenticatedUser user, String message) {
this.authenticated = authenticated;
this.user = user;
this.message = message;
}
public static AuthenticationResult success(AuthenticatedUser user) {
return new AuthenticationResult(true, user, null);
}
public static AuthenticationResult failure(String message) {
return new AuthenticationResult(false, null, message);
}
public boolean isAuthenticated() {
return authenticated;
}
public AuthenticatedUser getUser() {
return user;
}
public String getMessage() {
return message;
}
}
@@ -0,0 +1,40 @@
package com.mzh.library.service;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Role;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
public class PermissionPolicy {
public Set<Permission> permissionsFor(Role role) {
if (role == null) {
return Collections.emptySet();
}
switch (role) {
case ADMINISTRATOR:
return Collections.unmodifiableSet(EnumSet.allOf(Permission.class));
case LIBRARIAN:
return Collections.unmodifiableSet(EnumSet.of(
Permission.MANAGE_BOOKS,
Permission.MANAGE_READERS,
Permission.MANAGE_BORROWING,
Permission.VIEW_REPORTS,
Permission.VIEW_CATALOG
));
case READER:
return Collections.unmodifiableSet(EnumSet.of(
Permission.VIEW_CATALOG,
Permission.BORROW_BOOKS
));
default:
return Collections.emptySet();
}
}
public boolean allows(Role role, Permission permission) {
return permission != null && permissionsFor(role).contains(permission);
}
}
@@ -0,0 +1,69 @@
package com.mzh.library.service.impl;
import com.mzh.library.dao.UserDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.User;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.AuthService;
import com.mzh.library.service.AuthenticationResult;
import com.mzh.library.service.PermissionPolicy;
import com.mzh.library.util.PasswordHasher;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
public class AuthServiceImpl implements AuthService {
private static final Logger LOGGER = Logger.getLogger(AuthServiceImpl.class.getName());
private static final String REQUIRED_MESSAGE = "Username and password are required.";
private static final String INVALID_MESSAGE = "Invalid username or password.";
private static final String UNAVAILABLE_MESSAGE = "Login service is temporarily unavailable. Please try again later.";
private final UserDao userDao;
private final PermissionPolicy permissionPolicy;
public AuthServiceImpl(UserDao userDao) {
this(userDao, new PermissionPolicy());
}
public AuthServiceImpl(UserDao userDao, PermissionPolicy permissionPolicy) {
this.userDao = userDao;
this.permissionPolicy = permissionPolicy;
}
@Override
public AuthenticationResult authenticate(String username, String password) {
String normalizedUsername = normalizeUsername(username);
if (normalizedUsername.isEmpty() || password == null || password.trim().isEmpty()) {
return AuthenticationResult.failure(REQUIRED_MESSAGE);
}
try {
Optional<User> user = userDao.findActiveByUsername(normalizedUsername);
if (!user.isPresent() || !PasswordHasher.verify(password, user.get().getPasswordHash())) {
LOGGER.info("Login failed for username=" + normalizedUsername);
return AuthenticationResult.failure(INVALID_MESSAGE);
}
User authenticated = user.get();
Set<Permission> permissions = permissionPolicy.permissionsFor(authenticated.getRole());
AuthenticatedUser sessionUser = AuthenticatedUser.from(authenticated, permissions);
LOGGER.info("Login success userId=" + authenticated.getId() + " role=" + authenticated.getRole().getCode());
return AuthenticationResult.success(sessionUser);
} catch (DaoException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Login service error for username=" + normalizedUsername, ex);
return AuthenticationResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public boolean hasPermission(AuthenticatedUser user, Permission permission) {
return user != null && permissionPolicy.allows(user.getRole(), permission);
}
private String normalizeUsername(String username) {
return username == null ? "" : username.trim();
}
}
@@ -0,0 +1,57 @@
package com.mzh.library.util;
import com.mzh.library.exception.DaoException;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public final class JdbcUtil {
private static final String CONFIG_FILE = "db.properties";
private static final String DEFAULT_DRIVER = "com.mysql.cj.jdbc.Driver";
private JdbcUtil() {
}
public static Connection getConnection() {
Properties properties = loadProperties();
String driver = properties.getProperty("db.driver", DEFAULT_DRIVER);
String url = required(properties, "db.url");
String username = required(properties, "db.username");
String password = required(properties, "db.password");
try {
Class.forName(driver);
return DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException | SQLException ex) {
throw new DaoException("Unable to open database connection", ex);
}
}
private static Properties loadProperties() {
try (InputStream inputStream = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(CONFIG_FILE)) {
if (inputStream == null) {
throw new DaoException("Missing database configuration file: " + CONFIG_FILE, null);
}
Properties properties = new Properties();
properties.load(inputStream);
return properties;
} catch (IOException ex) {
throw new DaoException("Unable to read database configuration", ex);
}
}
private static String required(Properties properties, String key) {
String value = properties.getProperty(key);
if (value == null || value.trim().isEmpty()) {
throw new DaoException("Missing database configuration value: " + key, null);
}
return value.trim();
}
}
@@ -0,0 +1,63 @@
package com.mzh.library.util;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
public final class PasswordHasher {
private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
private static final String PREFIX = "pbkdf2_sha256";
private static final int DEFAULT_ITERATIONS = 60000;
private static final int SALT_BYTES = 16;
private static final int HASH_BYTES = 32;
private PasswordHasher() {
}
public static String hash(String password) {
byte[] salt = new byte[SALT_BYTES];
new SecureRandom().nextBytes(salt);
byte[] hash = derive(password, salt, DEFAULT_ITERATIONS);
return PREFIX + "$" + DEFAULT_ITERATIONS + "$"
+ Base64.getEncoder().encodeToString(salt) + "$"
+ Base64.getEncoder().encodeToString(hash);
}
public static boolean verify(String password, String storedHash) {
if (password == null || storedHash == null || storedHash.trim().isEmpty()) {
return false;
}
String[] parts = storedHash.split("\\$");
if (parts.length != 4 || !PREFIX.equals(parts[0])) {
return false;
}
try {
int iterations = Integer.parseInt(parts[1]);
byte[] salt = Base64.getDecoder().decode(parts[2]);
byte[] expected = Base64.getDecoder().decode(parts[3]);
byte[] actual = derive(password, salt, iterations);
return MessageDigest.isEqual(expected, actual);
} catch (IllegalArgumentException ex) {
return false;
}
}
private static byte[] derive(String password, byte[] salt, int iterations) {
char[] passwordChars = password.toCharArray();
try {
PBEKeySpec spec = new PBEKeySpec(passwordChars, salt, iterations, HASH_BYTES * 8);
return SecretKeyFactory.getInstance(ALGORITHM).generateSecret(spec).getEncoded();
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("Unable to hash password", ex);
} finally {
Arrays.fill(passwordChars, '\0');
}
}
}
@@ -0,0 +1,10 @@
package com.mzh.library.util;
public final class SessionAttributes {
public static final String AUTHENTICATED_USER = "authenticatedUser";
public static final String USER_ROLE = "userRole";
public static final String USER_PERMISSIONS = "userPermissions";
private SessionAttributes() {
}
}
+4
View File
@@ -0,0 +1,4 @@
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mzh_library?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
db.username=library_user
db.password=change_me
+103
View File
@@ -0,0 +1,103 @@
CREATE DATABASE IF NOT EXISTS mzh_library
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE mzh_library;
CREATE TABLE IF NOT EXISTS roles (
code VARCHAR(32) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
description VARCHAR(255) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS permissions (
code VARCHAR(64) PRIMARY KEY,
name VARCHAR(96) NOT NULL,
description VARCHAR(255) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS role_permissions (
role_code VARCHAR(32) NOT NULL,
permission_code VARCHAR(64) NOT NULL,
PRIMARY KEY (role_code, permission_code),
CONSTRAINT fk_role_permissions_role
FOREIGN KEY (role_code) REFERENCES roles (code),
CONSTRAINT fk_role_permissions_permission
FOREIGN KEY (permission_code) REFERENCES permissions (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100) NOT NULL,
role_code VARCHAR(32) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_users_username (username),
KEY idx_users_role_code (role_code),
CONSTRAINT fk_users_role
FOREIGN KEY (role_code) REFERENCES roles (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS system_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
operator_id BIGINT NULL,
operator_role VARCHAR(32) NULL,
operation_type VARCHAR(64) NOT NULL,
target_table VARCHAR(64) NULL,
target_id VARCHAR(64) NULL,
result_status VARCHAR(32) NOT NULL,
message VARCHAR(500) NULL,
request_ip VARCHAR(64) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_system_logs_operator_id (operator_id),
KEY idx_system_logs_operation_type (operation_type),
KEY idx_system_logs_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO roles (code, name, description) VALUES
('administrator', 'Administrator', 'Full system administration role'),
('librarian', 'Librarian', 'Library operation and borrowing management role'),
('reader', 'Reader', 'Reader self-service role')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
description = VALUES(description);
INSERT INTO permissions (code, name, description) VALUES
('manage_users', 'Manage users', 'Create and maintain administrator, librarian, and reader accounts'),
('manage_books', 'Manage books', 'Create, update, delete, and categorize books'),
('manage_readers', 'Manage readers', 'Maintain reader profiles and eligibility'),
('manage_borrowing', 'Manage borrowing', 'Borrow, return, renew, and process overdue records'),
('view_reports', 'View reports', 'View search, ranking, inventory, and overdue reports'),
('view_system_logs', 'View system logs', 'View system maintenance and exception logs'),
('view_catalog', 'View catalog', 'Search and view library books'),
('borrow_books', 'Borrow books', 'Borrow and renew own books')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
description = VALUES(description);
INSERT IGNORE INTO role_permissions (role_code, permission_code) VALUES
('administrator', 'manage_users'),
('administrator', 'manage_books'),
('administrator', 'manage_readers'),
('administrator', 'manage_borrowing'),
('administrator', 'view_reports'),
('administrator', 'view_system_logs'),
('administrator', 'view_catalog'),
('administrator', 'borrow_books'),
('librarian', 'manage_books'),
('librarian', 'manage_readers'),
('librarian', 'manage_borrowing'),
('librarian', 'view_reports'),
('librarian', 'view_catalog'),
('reader', 'view_catalog'),
('reader', 'borrow_books');
-- Demo accounts for local scaffold verification only. Change or remove them
-- before using a non-local database.
INSERT IGNORE INTO users (username, password_hash, display_name, role_code, active) VALUES
('admin', 'pbkdf2_sha256$60000$bXpoLWFkbWluLWRlbW8tc2FsdA==$RwBCvhf3Wsc0jemnHlir4mdNZF4ZhHjrfHx/b1Bera0=', 'System Administrator', 'administrator', 1),
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1),
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1);
@@ -0,0 +1,49 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body class="auth-page">
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="auth-shell">
<section class="login-panel" aria-labelledby="login-title">
<div>
<p class="eyebrow">Library Management</p>
<h1 id="login-title">Sign in</h1>
</div>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
<label for="username">Username</label>
<input id="username"
name="username"
type="text"
value="${fn:escapeXml(username)}"
autocomplete="username"
required>
<label for="password">Password</label>
<input id="password"
name="password"
type="password"
autocomplete="current-password"
required>
<button class="button button-primary" type="submit">Sign in</button>
</form>
</section>
</main>
</body>
</html>
@@ -0,0 +1,28 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Unauthorized - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="notice-panel" aria-labelledby="unauthorized-title">
<h1 id="unauthorized-title">Access denied</h1>
<p>
<c:choose>
<c:when test="${not empty errorMessage}">
<c:out value="${errorMessage}" />
</c:when>
<c:otherwise>You do not have permission to access this page.</c:otherwise>
</c:choose>
</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">Back to dashboard</a>
</section>
</main>
</body>
</html>
@@ -0,0 +1,20 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<header class="app-header">
<a class="brand" href="${pageContext.request.contextPath}/dashboard">MZH Library</a>
<c:if test="${not empty sessionScope.authenticatedUser}">
<nav class="top-nav" aria-label="Primary">
<a href="${pageContext.request.contextPath}/dashboard">Dashboard</a>
<c:if test="${sessionScope.userRole == 'administrator'}">
<a href="${pageContext.request.contextPath}/admin/home">Admin</a>
</c:if>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
</c:if>
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
<span class="user-pill">
<c:out value="${sessionScope.authenticatedUser.displayName}" />
</span>
<a class="button button-secondary" href="${pageContext.request.contextPath}/logout">Logout</a>
</nav>
</c:if>
</header>
+47
View File
@@ -0,0 +1,47 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero" aria-labelledby="dashboard-title">
<p class="eyebrow">
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
</p>
<h1 id="dashboard-title">Dashboard</h1>
<p>Signed in as <strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong>.</p>
</section>
<section class="card-grid" aria-label="Role workspaces">
<c:if test="${sessionScope.userRole == 'administrator'}">
<article class="workspace-card">
<h2>Administration</h2>
<p>Account, role, permission, and system-maintenance entry point.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">Open</a>
</article>
</c:if>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<article class="workspace-card">
<h2>Librarian Workspace</h2>
<p>Book, reader, borrowing, return, renewal, and overdue entry point.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">Open</a>
</article>
</c:if>
<article class="workspace-card">
<h2>Reader Center</h2>
<p>Catalog search and reader self-service entry point.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">Open</a>
</article>
</section>
</main>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><c:out value="${areaName}" /> - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="notice-panel" aria-labelledby="area-title">
<p class="eyebrow">
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
</p>
<h1 id="area-title"><c:out value="${areaName}" /></h1>
<p><c:out value="${areaSummary}" /></p>
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">Back to dashboard</a>
</section>
</main>
</body>
</html>
+90
View File
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>MZH Library Management</display-name>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.mzh.library.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>AuthenticationFilter</filter-name>
<filter-class>com.mzh.library.filter.AuthenticationFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>AuthorizationFilter</filter-name>
<filter-class>com.mzh.library.filter.AuthorizationFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthorizationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.mzh.library.controller.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>LogoutServlet</servlet-name>
<servlet-class>com.mzh.library.controller.LogoutServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LogoutServlet</servlet-name>
<url-pattern>/logout</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>DashboardServlet</servlet-name>
<servlet-class>com.mzh.library.controller.DashboardServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DashboardServlet</servlet-name>
<url-pattern>/dashboard</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>RoleAreaServlet</servlet-name>
<servlet-class>com.mzh.library.controller.RoleAreaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RoleAreaServlet</servlet-name>
<url-pattern>/admin/home</url-pattern>
<url-pattern>/librarian/home</url-pattern>
<url-pattern>/reader/home</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>UnauthorizedServlet</servlet-name>
<servlet-class>com.mzh.library.controller.UnauthorizedServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UnauthorizedServlet</servlet-name>
<url-pattern>/unauthorized</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
+3
View File
@@ -0,0 +1,3 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:redirect url="/login" />
+263
View File
@@ -0,0 +1,263 @@
:root {
color-scheme: light;
--color-ink: #202124;
--color-muted: #5f6368;
--color-border: #d9dde3;
--color-panel: #ffffff;
--color-page: #f5f7fb;
--color-primary: #256f6c;
--color-primary-strong: #1b5654;
--color-accent: #b54238;
--shadow-panel: 0 18px 45px rgba(28, 39, 49, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--color-ink);
background: var(--color-page);
font-family: Arial, "Microsoft YaHei", sans-serif;
line-height: 1.5;
}
a {
color: inherit;
}
.app-header {
min-height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 0 32px;
border-bottom: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.96);
}
.brand {
color: var(--color-primary-strong);
font-weight: 700;
text-decoration: none;
}
.top-nav {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
color: var(--color-muted);
font-size: 14px;
}
.top-nav a {
text-decoration: none;
}
.user-pill {
max-width: 220px;
padding: 6px 10px;
overflow: hidden;
color: var(--color-ink);
border: 1px solid var(--color-border);
border-radius: 6px;
text-overflow: ellipsis;
white-space: nowrap;
}
.auth-page {
background:
linear-gradient(rgba(245, 247, 251, 0.86), rgba(245, 247, 251, 0.92)),
url("../images/library-login.svg") center / cover no-repeat;
}
.auth-shell,
.page-shell {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
}
.auth-shell {
min-height: calc(100vh - 64px);
display: grid;
align-items: center;
padding: 48px 0;
}
.login-panel,
.notice-panel,
.dashboard-hero,
.workspace-card {
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-panel);
box-shadow: var(--shadow-panel);
}
.login-panel {
width: min(420px, 100%);
padding: 32px;
}
.eyebrow {
margin: 0 0 6px;
color: var(--color-primary);
font-size: 13px;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 18px;
font-size: 32px;
line-height: 1.15;
}
h2 {
margin-bottom: 10px;
font-size: 20px;
}
.login-form {
display: grid;
gap: 10px;
}
.login-form label {
color: var(--color-muted);
font-size: 14px;
font-weight: 700;
}
.login-form input {
width: 100%;
min-height: 44px;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font: inherit;
}
.login-form input:focus {
outline: 3px solid rgba(37, 111, 108, 0.18);
border-color: var(--color-primary);
}
.button {
display: inline-flex;
min-height: 40px;
align-items: center;
justify-content: center;
padding: 9px 14px;
border: 1px solid transparent;
border-radius: 6px;
font: inherit;
font-weight: 700;
text-decoration: none;
cursor: pointer;
}
.button-primary {
margin-top: 12px;
color: #ffffff;
background: var(--color-primary);
}
.button-primary:hover {
background: var(--color-primary-strong);
}
.button-secondary {
color: var(--color-primary-strong);
border-color: rgba(37, 111, 108, 0.35);
background: #ffffff;
}
.message {
margin-bottom: 16px;
padding: 10px 12px;
border-radius: 6px;
font-size: 14px;
}
.message-error {
color: #7a211a;
border: 1px solid rgba(181, 66, 56, 0.3);
background: #fff0ee;
}
.page-shell {
padding: 36px 0 56px;
}
.dashboard-hero {
padding: 28px;
margin-bottom: 24px;
}
.dashboard-hero p:last-child,
.workspace-card p:last-child,
.notice-panel p:last-child {
margin-bottom: 0;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
}
.workspace-card {
min-height: 190px;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 24px;
}
.workspace-card p {
color: var(--color-muted);
}
.workspace-card .button {
margin-top: auto;
}
.notice-panel {
max-width: 680px;
padding: 28px;
}
@media (max-width: 720px) {
.app-header {
align-items: flex-start;
flex-direction: column;
padding: 16px;
}
.top-nav {
width: 100%;
}
h1 {
font-size: 28px;
}
.login-panel,
.notice-panel,
.dashboard-hero,
.workspace-card {
box-shadow: none;
}
}
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960" role="img" aria-label="Library shelves">
<rect width="1440" height="960" fill="#e8edf1"/>
<rect x="0" y="705" width="1440" height="255" fill="#d4ddd8"/>
<g opacity="0.92">
<rect x="760" y="145" width="390" height="560" rx="8" fill="#f9fbfc" stroke="#bfcbc9" stroke-width="8"/>
<rect x="800" y="205" width="310" height="28" fill="#256f6c"/>
<rect x="800" y="270" width="310" height="28" fill="#b54238"/>
<rect x="800" y="335" width="310" height="28" fill="#4b6572"/>
<rect x="800" y="400" width="310" height="28" fill="#d7a441"/>
<rect x="800" y="465" width="310" height="28" fill="#256f6c"/>
<rect x="800" y="530" width="310" height="28" fill="#7f8d95"/>
<rect x="800" y="595" width="310" height="28" fill="#b54238"/>
</g>
<g opacity="0.84">
<rect x="1210" y="245" width="92" height="460" rx="8" fill="#ffffff" stroke="#bfcbc9" stroke-width="7"/>
<rect x="1232" y="282" width="48" height="385" fill="#256f6c"/>
<rect x="1232" y="282" width="48" height="65" fill="#d7a441"/>
</g>
<circle cx="1095" cy="128" r="36" fill="#d7a441" opacity="0.75"/>
<rect x="0" y="730" width="1440" height="8" fill="#bfcbc9"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,76 @@
package com.mzh.library.service;
import com.mzh.library.dao.UserDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Role;
import com.mzh.library.entity.User;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.AuthServiceImpl;
import com.mzh.library.util.PasswordHasher;
import java.util.Optional;
public final class AuthServiceCheck {
private static final String REQUIRED_MESSAGE = "Username and password are required.";
private static final String INVALID_MESSAGE = "Invalid username or password.";
private static final String UNAVAILABLE_MESSAGE =
"Login service is temporarily unavailable. Please try again later.";
private AuthServiceCheck() {
}
public static void main(String[] args) {
User admin = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, "correct-password");
AuthService authService = new AuthServiceImpl(username -> "admin".equals(username)
? Optional.of(admin)
: Optional.empty());
requireMessage(authService.authenticate("", "correct-password"), REQUIRED_MESSAGE);
requireMessage(authService.authenticate("admin", ""), REQUIRED_MESSAGE);
requireMessage(authService.authenticate("missing", "correct-password"), INVALID_MESSAGE);
requireMessage(authService.authenticate("admin", "wrong-password"), INVALID_MESSAGE);
AuthenticationResult result = authService.authenticate(" admin ", "correct-password");
require(result.isAuthenticated(), "valid credentials should authenticate");
AuthenticatedUser authenticatedUser = result.getUser();
require(authenticatedUser != null, "authenticated result should include session-safe user");
require(authenticatedUser.getRole() == Role.ADMINISTRATOR, "admin should resolve to administrator");
require(authenticatedUser.getPermissionCodes().contains(Permission.MANAGE_USERS.getCode()),
"administrator should receive manage-users permission code");
require(authService.hasPermission(authenticatedUser, Permission.VIEW_SYSTEM_LOGS),
"administrator should have all scaffold permissions");
AuthService failingService = new AuthServiceImpl(new FailingUserDao());
requireMessage(failingService.authenticate("admin", "correct-password"), UNAVAILABLE_MESSAGE);
}
private static User user(long id, String username, String displayName, Role role, String password) {
User user = new User();
user.setId(id);
user.setUsername(username);
user.setDisplayName(displayName);
user.setRole(role);
user.setPasswordHash(PasswordHasher.hash(password));
user.setActive(true);
return user;
}
private static void requireMessage(AuthenticationResult result, String message) {
require(!result.isAuthenticated(), "result should not be authenticated");
require(message.equals(result.getMessage()), "expected message: " + message);
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
private static final class FailingUserDao implements UserDao {
@Override
public Optional<User> findActiveByUsername(String username) {
throw new DaoException("Simulated DAO failure", null);
}
}
}
@@ -0,0 +1,25 @@
package com.mzh.library.service;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Role;
public final class PermissionPolicyCheck {
private PermissionPolicyCheck() {
}
public static void main(String[] args) {
PermissionPolicy policy = new PermissionPolicy();
require(policy.allows(Role.ADMINISTRATOR, Permission.MANAGE_USERS), "administrator should manage users");
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_BORROWING), "librarian should manage borrowing");
require(!policy.allows(Role.LIBRARIAN, Permission.MANAGE_USERS), "librarian should not manage users");
require(policy.allows(Role.READER, Permission.VIEW_CATALOG), "reader should view catalog");
require(!policy.allows(Role.READER, Permission.MANAGE_BOOKS), "reader should not manage books");
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
}