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() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user