Initial commit
This commit is contained in:
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
@@ -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 |
Reference in New Issue
Block a user