接續應用系統專案(8-1) - Spring Boot Security 設定與認證-基礎準備 (URL: https://ithelp.ithome.com.tw/articles/10398800) & 應用系統專案(8-2) - Spring Boot Security 設定與認證 - JWT 及 OAuth2 認證(URL: https://ithelp.ithome.com.tw/articles/10398837),, 這裡介紹 Spring boot Srcurity 的客製化多元認證設定與範例,同樣以程式內的註解說明該程式的功能。
主要目錄與程式
├── WebSecurityConfig (Spring Security 主設定)
├── CustomAuthenticationProvider (客製化多來源認證器)
├── DataBaseAuthentication(資料庫認證)
├── ActiveDirectoryAuthentication(AD/LDAP認證)
├── MemoryAuthentication(記憶體認證,開發測試用)
├── CSPNonceFilter(Content Security Policy 過濾器)
├── SecurityConstants(Security Policy 相關常變數)
前端URL相關控制處理
tw.lewishome.webapp.page
└── HomePageController (登入相關 Endpoint URL)
前端URL相關 Html相關文件(主要使用thymeleaf template)
src/main/resources
├── mybatis (JPA Mybatis Mapper,SysAccessLog的mybaticMapper暫時不用)
│
├── static/ (一般靜態資料)
│ └── css (登入相關頁面)
│ └── img (登入相關頁面)
│ └── js (登入相關頁面)
│
└── templates/ (thymeleaf template)
└── home (登入相關頁面)
├── homePage.html (登入頁面)
└── landingPage.html (登入成功頁面)
位置:src/main/java/tw/lewishome/webapp/base/security/DataBaseAuthentication.java
package tw.lewishome.webapp.base.security;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tw.lewishome.webapp.database.primary.entity.SysUserProfileEntity;
import tw.lewishome.webapp.database.primary.repository.SysUserProfileRepository;
import tw.lewishome.webapp.base.utility.common.SM3Utils;
/**
*
* DataBaseAuthentication 負責以資料庫中的 SysUserProfileEntity 資料進行使用者認證。
* 主要功能為驗證使用者帳號與密碼,並回傳授權角色資訊。
*
* 認證流程說明:
* <ul>
* <li>根據傳入的使用者名稱 (username) 查詢對應的 SysUserProfileEntity。</li>
* <li>若查無資料、密碼為空或資料庫密碼為空,則認證失敗。</li>
* <li>將傳入的密碼進行 SM3 加密後,與資料庫儲存的密碼比對。</li>
* <li>比對成功則回傳授權角色 "DB_User"。</li>
* <li>若任一步驟失敗,則回傳空的角色列表。</li>
* </ul>
*
* 注意事項:
* <ul>
* <li>本類別僅負責資料庫層級的認證,不包含其他授權邏輯。</li>
* <li>角色名稱 "DB_User" 會由系統 (CustomAuthenticationProvider) 自動加上 "ROLE_" 前綴。</li>
* <li>如發生例外狀況,僅會印出錯誤堆疊,並回傳空角色列表。</li>
* </ul>
*
* @author Lewis 開發團隊
* @since 2024
*/
@Component
public class DataBaseAuthentication {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new DataBaseAuthentication instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*
*/
public DataBaseAuthentication() {
// Constructor body (can be empty)
}
@Autowired
private SysUserProfileRepository sysUserProfileRepository;
/**
* 以SysUserProfile DB 資料認證
*
* @param username login user
* @param password password
* @return ArrayList user roles
*/
public List<String> authenticateUser(String username, String password) {
// 若授權失敗,回傳空的 userRoles。
List<String> userRoles = new ArrayList<>();
try {
if (username == null) {
return userRoles;
}
// 取得 SysUserProfileEntity 資料
SysUserProfileEntity oneSysUserProfileEntity = sysUserProfileRepository.findByDataKey_UserId(username);
if (oneSysUserProfileEntity == null) {
return userRoles;
}
if (StringUtils.isBlank(password)) {
return userRoles;
}
// 資料庫應該使用 SM3 加密的密碼,安全上建議使用不可以逆向的加密方式
String dbUserPasswd = oneSysUserProfileEntity.getUserPassword().trim();
if (StringUtils.isBlank(dbUserPasswd)) {
return userRoles;
}
// 這裡使用 SM3 加密比對 ,SM3不可以逆向解密,所以無法明文密碼比對
String encryptedPasswd = SM3Utils.sm3Encrypt(password);
if (Boolean.FALSE.equals(encryptedPasswd.equals(dbUserPasswd))) {
return userRoles;
}
// 系統(CustomAuthenticationProvider)會自動加 ROLE_
userRoles.add("DB_User");
} catch (Exception ex) {
ex.printStackTrace();
}
return userRoles;
}
}
位置:src/main/java/tw/lewishome/webapp/base/security/ActiveDirectoryAuthentication.java
package tw.lewishome.webapp.base.security;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.utility.common.NetUtils;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
/**
* ActiveDirectoryAuthentication 負責與 Active Directory (AD) 進行使用者認證,
* 並取得使用者所屬的羣組資訊(Roles)。
*
* 此元件會根據設定檔中的 AD 伺服器主機名稱、連接埠、網域等資訊,
* 連線至 AD 伺服器,並以 LDAP 協定查詢使用者資訊。
* 支援多個 AD IP,會自動檢查可用的主機 IP。
*
* 主要功能:
* <ul>
* <li>驗證使用者帳號密碼是否正確</li>
* <li>取得使用者所屬的 AD 羣組(Roles)</li>
* <li>支援自訂 AD 屬性查詢</li>
* </ul>
*
* 主要方法說明:
* <ul>
* <li>{@link #authenticateUser(String, String)}:以 AD 認證使用者,回傳使用者所屬羣組清單</li>
* <li>{@link #validHostIp(String, int)}:檢查 AD 主機名稱或 IP 是否有效,並回傳可用 IP</li>
* <li>{@link #getAdAttributes(LdapContext, String, String, SearchControls)}:查詢
* AD 並取得指定屬性</li>
* <li>{@link #getAdRoles(Attribute)}:解析 AD 屬性,取得使用者所屬羣組名稱</li>
* <li>{@link #getCN(String)}:由 AD 羣組 DN 取得 CN 名稱</li>
* </ul>
*
* 注意事項:
* <ul>
* <li>若認證失敗或查詢不到資料,會回傳空的羣組清單</li>
* <li>系統會自動加上 "AD_User" 角色</li>
* <li>LDAP 查詢時,請確認 AD 屬性名稱與網域設定正確</li>
* </ul>
*
* 適用場景:需要與 AD 整合,進行使用者登入驗證及權限控管的 Spring Boot 應用程式。
*
* @author Lewis
* @since 2024
*/
@Component
@SuppressWarnings("unused")
@Slf4j
public class ActiveDirectoryAuthentication {
// @Autowired
SystemEnvReader systemEnvReader = new SystemEnvReader();
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new ActiveDirectoryAuthentication instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public ActiveDirectoryAuthentication() {
// Constructor body (can be empty)
}
// AD Server Name
private String activeDirectoryServerHostName = systemEnvReader.getProperty("AD_SERVER_HOST_NAME", SecurityConstants.AD_SERVER_HOST_NAME) ;
// AD Port
private String activeDirectoryServerHostPort = systemEnvReader.getProperty("AD_SERVER_HOST_PORT", SecurityConstants.AD_SERVER_HOST_PORT) ;
// AD domain Name
private String activeDirectoryServerDomain = systemEnvReader.getProperty("AD_SERVER_DOMAIN",SecurityConstants.AD_SERVER_DOMAIN) ;
private static final String AD_ATTR_NAME_TOKEN_GROUPS = "tokenGroups";
private static final String AD_ATTR_NAME_OBJECT_CLASS = "objectClass";
private static final String AD_ATTR_NAME_OBJECT_CATEGORY = "objectCategory";
private static final String AD_ATTR_NAME_MEMBER = "member";
private static final String AD_ATTR_NAME_MEMBER_OF = "memberOf";
private static final String AD_ATTR_NAME_DESCRIPTION = "description";
private static final String AD_ATTR_NAME_OBJECT_GUID = "objectGUID";
private static final String AD_ATTR_NAME_OBJECT_SID = "objectSid";
private static final String AD_ATTR_NAME_DISTINGUISHED_NAME = "distinguishedName";
private static final String AD_ATTR_NAME_CN = "cn";
private static final String AD_ATTR_NAME_SN = "sn";
private static final String AD_ATTR_NAME_USER_PRINCIPAL_NAME = "userPrincipalName";
private static final String AD_ATTR_NAME_USER_EMAIL = "mail";
private static final String AD_ATTR_NAME_GROUP_TYPE = "groupType";
private static final String AD_ATTR_NAME_SAM_ACCOUNT_TYPE = "sAMAccountType";
private static final String AD_ATTR_NAME_SAM_ACCOUNT_NAME = "sAMAccountName";
private static final String AD_ATTR_NAME_USER_ACCOUNT_CONTROL = "userAccountControl";
private static final String AD_ATTR_NAME_GIVEN_NAME = "givenName";
/**
* 以AD 系統認證
*
* @param username
* login user
* @param password
* password
* @return ArrayList user roles
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public List<String> authenticateUser(String username, String password) {
// 若授權失敗,回傳空的 userRoles。
List<String> userRoles = new ArrayList<>();
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return userRoles;
}
log.info("perform ActiveDirectoryAuthentication : username = {}", username);
String activeDirectoryServerHostAndPort = null;
int adPort = Integer.parseInt(activeDirectoryServerHostPort);
// 因為 AD IP 可以註冊多個,所以找出可用的 IP。
String adHostIP = validHostIp(activeDirectoryServerHostName, adPort);
if (StringUtils.isBlank(adHostIP)) {
return userRoles;
}
try {
// 依據 AD的設定狀況,決定取那些資料
String returnedAtts[] = { AD_ATTR_NAME_SN,
AD_ATTR_NAME_GIVEN_NAME,
AD_ATTR_NAME_USER_EMAIL,
AD_ATTR_NAME_MEMBER_OF };
String searchFilter = "(&(objectClass=user)(" + AD_ATTR_NAME_SAM_ACCOUNT_NAME + "=" + username + "))";
// Create the search controls
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(returnedAtts);
// Specify the search scope
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String[] arrayDn = activeDirectoryServerDomain.split("\\.");
if (arrayDn.length == 0) {
return userRoles;
}
String searchBase = "DC=" + arrayDn[0];
for (int i = 1; i < arrayDn.length; i++) {
searchBase = searchBase + ",";
searchBase = searchBase + "DC=" + arrayDn[i];
}
activeDirectoryServerHostAndPort = "ldap://" + adHostIP + ":" + activeDirectoryServerHostPort;
Hashtable environment = new Hashtable();
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.PROVIDER_URL, activeDirectoryServerHostAndPort);
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
environment.put(Context.SECURITY_PRINCIPAL, username + "@" + activeDirectoryServerDomain);
environment.put(Context.SECURITY_CREDENTIALS, password);
LdapContext ctxGC = new InitialLdapContext(environment, null);
// Search for objects in the GC using the filter
Attributes attributes = getAdAttributes(ctxGC, searchBase, searchFilter, searchControls);
// 從 AD 取得 Roles(例如部門等等);
if (attributes != null) {
Attribute memberOf = attributes.get(AD_ATTR_NAME_MEMBER_OF);
userRoles = getAdRoles(memberOf);
}
// 系統(CustomAuthenticationProvider)會自動加 ROLE_
userRoles.add("AD_User");
} catch (Exception ex) {
ex.printStackTrace();
}
// 系統(CustomAuthenticationProvider)會自動加 ROLE_
return userRoles;
}
/**
* 檢核 AD Host Name(IP) 是否有效
*
* @param activeDirectoryServerHostName
* AD server
* @param adPort
* Ad port
* @return String AD IP
*/
private String validHostIp(String activeDirectoryServerHostName, int adPort) {
String adHostIp = "";
try {
InetAddress[] addresses = InetAddress.getAllByName(activeDirectoryServerHostName);
for (int i = 0; i < addresses.length; i++) {
if (NetUtils.checkSocket(addresses[i].getHostAddress(), adPort, 10)) {
adHostIp = addresses[i].getHostAddress();
break;
}
}
} catch (Exception ex) {
log.warn("HostIp & Port is Not valid ");
// ex.printStackTrace();
}
return adHostIp;
}
/**
* 取的 AD Attributes (內有 searchFilter 指定的資料)
*
* @param ctxGC
* LdapContext
* @param searchBase
* searchBase
* @param searchFilter
* searchFilter
* @param searchCtls
* searchCtls
* @return
*/
@SuppressWarnings({ "rawtypes" })
private Attributes getAdAttributes(LdapContext ctxGC, String searchBase, String searchFilter,
SearchControls searchCtls) {
Attributes adAttributes = null;
try {
NamingEnumeration answer = ctxGC.search(searchBase, searchFilter, searchCtls);
while (answer.hasMoreElements()) {
SearchResult sr = (SearchResult) answer.next();
adAttributes = sr.getAttributes();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return adAttributes;
}
/**
* 從 Attributes (CN) 取得Roles。
*
* @param memberOf
* AD Attribute
* @return ArrayList AD roles
*/
private List<String> getAdRoles(Attribute memberOf) {
List<String> userAdRoles = new ArrayList<>();
try {
if (memberOf != null) {
for (NamingEnumeration<?> e1 = memberOf.getAll(); e1.hasMoreElements();) {
String userGroupDN = e1.nextElement().toString();
String userGroupCN = getCN(userGroupDN);
userAdRoles.add(userGroupCN.substring(3));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return userAdRoles;
}
/**
* 取得 user group CN 。
*
* @param userGroupDN
* AD GroupDN
* @return String cnName
*/
private static String getCN(String userGroupDN) {
String cnName = null;
if (userGroupDN != null && userGroupDN.toUpperCase().startsWith("CN=")) {
cnName = userGroupDN.substring(3);
int position = userGroupDN.indexOf(',');
if (position == -1) {
return cnName;
} else {
cnName = userGroupDN.substring(0, position);
return cnName;
}
}
return "";
}
}
位置:src/main/java/tw/lewishome/webapp/base/security/MemoryAuthentication.java
package tw.lewishome.webapp.base.security;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
/**
*
* MemoryAuthentication 類別提供一個以記憶體方式儲存使用者帳號與密碼的簡易授權機制。
* 主要用途為在不需連接資料庫的情境下,快速驗證使用者身分,適合用於測試或開發階段。
*
*
*
* 內部以 Map 結構保存使用者帳號與密碼配對,並提供 authenticateUser 方法進行授權檢查。
* 若驗證成功,回傳包含 "MEM_User" 權限的角色清單;若失敗則回傳空集合。
*
*
*
* 注意:此類別僅適用於簡易驗證場景,請勿用於正式環境或敏感資料保護。
*
*
* @author Lewis
* @since 1.0
*/
@Component
@Slf4j
public class MemoryAuthentication {
@Autowired
SystemEnvReader systemEnvReader;
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
*
* Constructs a new MemoryAuthentication instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public MemoryAuthentication() {
// Constructor body (can be empty)
}
// Memory User, password map
private Map<String, String> MapUserAuth = Stream
.of(new String[][] { { "AAA", "AAA" }, { "BBB", "BBB" }, { "CCC", "CCC" }
}).collect(Collectors.toMap(data -> data[0], data -> data[1]));
/**
* 以 Memory User, password map 確認授權
*
* @param username login user
* @param password password
* @return ArrayList user roles
*/
public List<String> authenticateUser(String username, String password) {
List<String> userRoles = new ArrayList<>();
String activeProfile = systemEnvReader.getProperty("spring.profiles.active","dev");
// only effective on dev environment
if (activeProfile == "dev"){
return userRoles;
}
// 若授權失敗,回傳空的 userRoles。
log.info("perform MemoryAuthentication : username = {}", username);
if (StringUtils.isBlank(username)) {
return userRoles;
}
String dbPasscode = MapUserAuth.get(username.trim().toUpperCase());
if (StringUtils.isBlank(dbPasscode) || Boolean.FALSE.equals(dbPasscode.equals(password))) {
return userRoles;
}
userRoles.add("MEM_User");
return userRoles;
}
}
位置:src/main/java/tw/lewishome/webapp/base/security/CustomAuthenticationProvider.java
package tw.lewishome.webapp.base.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.database.primary.service.SysUserProfileService;
/**
*
* CustomAuthenticationProvider 是自訂的 Spring Security 認證提供者,負責根據使用者的帳號與密碼,
* 依照指定的認證順序(如資料庫認證、Active Directory 認證、記憶體認證)進行身份驗證與授權。
* 此類別可擴充多種認證方式,並於認證成功後更新使用者最後一次成功認證的方法至資料庫。
*
* 主要功能說明:
* <ul>
* <li>整合多種認證方式(資料庫、Active Directory、記憶體),可依環境或需求調整認證順序。</li>
* <li>於認證成功時,將使用者角色資訊加入 Spring Security 的權限管理。</li>
* <li>自動更新或新增使用者最後一次成功認證方式至 SysUserProfile 資料表。</li>
* <li>支援 DEV 環境下的記憶體認證,方便開發測試。</li>
* </ul>
*
* 主要成員說明:
* <ul>
* <li><b>dataBaseAuthentication</b>:資料庫認證服務。</li>
* <li><b>activeDirectoryAuthentication</b>:Active Directory 認證服務。</li>
* <li><b>memoryAuthentication</b>:記憶體認證服務(僅限開發環境)。</li>
* <li><b>sysUserProfileRepository</b>:用於查詢及更新使用者認證資訊的資料庫存取物件。</li>
* <li><b>sysUserProfileService</b>:取得使用者認證順序的服務。</li>
* </ul>
*
* 主要方法說明:
* <ul>
* <li><b>authenticate</b>:依據使用者輸入的帳號密碼進行認證,並回傳認證結果。</li>
* <li><b>supports</b>:判斷是否支援指定的認證型別。</li>
* <li><b>doAuthenticateUser</b>:依據認證順序逐一嘗試認證,並回傳使用者角色。</li>
* <li><b>updateSysUser</b>:更新或新增使用者最後一次成功認證方式至資料庫。</li>
* </ul>
*
* 注意事項:
* <ul>
* <li>此類別需搭配 Spring Security 框架使用。</li>
* <li>請確保各認證服務已正確注入並設定。</li>
* <li>資料庫操作請注意交易管理與例外處理。</li>
* </ul>
*
*
* @author Lewis
* @version 1.0
* @since 2024-06
*/
@Component
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
/**
*
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new CustomAuthenticationProvider instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public CustomAuthenticationProvider() {
// Constructor body (can be empty)
}
@Autowired
private DataBaseAuthentication dataBaseAuthentication;
@Autowired
private ActiveDirectoryAuthentication activeDirectoryAuthentication;
@Autowired
private MemoryAuthentication memoryAuthentication = new MemoryAuthentication();
@Autowired
private SysUserProfileService sysUserProfileService;
/**
* 客製化登入認證
*/
@SuppressWarnings("null")
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.info("[CustomAuthenticationProvider] authenticate() called with principal = {} ",
(authentication == null ? "null" : authentication.getName()));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, null);
try {
// 從Spring Securty authentication 取得 UserName 以及 Password
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName().toUpperCase().trim().toUpperCase();
String password = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getCredentials().toString().trim();
log.info("CustomAuthenticationProvider strting.....");
// 利用UserName 以及 Password 做自行認證 (以後這裡可以會加入任何認證方式,如 DB認證,AD認證等等)
List<String> userRoles = doAuthenticateUser(username, password);
if (userRoles.isEmpty()) {
// 認證失敗
throw new BadCredentialsException("invalid login details");
} else {
List<GrantedAuthority> authorities = new ArrayList<>();
for (int i = 0; i < userRoles.size(); i++) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + userRoles.get(i)));
}
// Create authenticated token with authorities; framework will set
// SecurityContext
token = new UsernamePasswordAuthenticationToken(
username.toUpperCase(),
authentication.getCredentials(), authorities);
token.setDetails(authentication.getDetails());
log.info("CustomAuthenticationProvider authenticate successed for user: {}", username);
}
} catch (AuthenticationException ex) {
// Authentication failures should be propagated so that Spring Security
// triggers the failure handler. Don't swallow BadCredentialsException.
throw new BadCredentialsException("invalid login details");
}
log.info("[CustomAuthenticationProvider] authenticate returning token principal= {} , authenticated= {}",
token.getPrincipal() , token.isAuthenticated());
return token;
}
/** */
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
/**
* 依認證順序,進行認證授權
*
* @param username
* username
* @param password
* password
* @return ArrayList user roles
*/
public List<String> doAuthenticateUser(String username, String password) {
List<String> userRoles = new ArrayList<>();
List<String> doAuthSeq = sysUserProfileService.getSysUserLastAuth(username);
String lastAuthMethod = "";
// 依據 認證方法順序,執行認證最後成功認證方法 (只有 DEV 環境做Memory認證)
for (int i = 0; i < doAuthSeq.size(); i++) {
String oneAuthMethod = doAuthSeq.get(i);
if (oneAuthMethod.equalsIgnoreCase("activeDirectory")) {
lastAuthMethod = "activeDirectory";
userRoles = activeDirectoryAuthentication.authenticateUser(username, password);
} else if (oneAuthMethod.equalsIgnoreCase("dataBase")) {
lastAuthMethod = "dataBase";
userRoles = dataBaseAuthentication.authenticateUser(username, password);
} else if (oneAuthMethod.equalsIgnoreCase("memory")) { // 只有 DEV 環境做Memory認證
lastAuthMethod = "memory";
userRoles = memoryAuthentication.authenticateUser(username, password);
}
if (userRoles.isEmpty()) {
continue;
} else {
log.info("User: {} , AuthMethod: {} , Authenticate Success", username, oneAuthMethod);
userRoles.add(username.toUpperCase().trim());
// 更新最後認證成功的方法
sysUserProfileService.updateSysUserLastAuth(username, lastAuthMethod);
break;
}
}
return userRoles;
}
}
位置:src/main/java/tw/lewishome/webapp/base/security/CSPNonceFilter.java
package tw.lewishome.webapp.base.security;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.utility.common.FileUtils;
/**
*
* CSPNonceFilter 是一個 Spring Boot Security 過濾器,主要用於防止 Content Security Policy
* (CSP) 相關的安全弱點。
* 此過濾器會在每一次 HTTP 請求時自動產生一組隨機的 nonce(一次性令牌),並將其設置於 request 的屬性中,
* 以便於前端(如 Thymeleaf)在產生 script 標籤時能夠動態插入 nonce,強化 CSP 的防護效果。
*
*
*
* 主要功能說明:
* <ul>
* <li>每次請求時產生一組長度為 32 bytes 的隨機 nonce,並以 Base64 編碼。</li>
* <li>將 nonce 設置於 request 屬性 "cspNonce",供後續使用。</li>
* <li>僅針對系統定義的 ENDPOINT(由 BaseSystemConstants.ListSysEndPoint 控管)才會產生
* nonce。</li>
* <li>透過 CSPNonceResponseWrapper,於回應標頭 "Content-Security-Policy" 中自動將 "{nonce}"
* 字串替換為實際 nonce 值。</li>
* </ul>
*
*
*
* 使用注意事項:
* <ul>
* <li>此過濾器僅適用於需要 CSP nonce 的系統 ENDPOINT。</li>
* <li>請確保前端模板(如 Thymeleaf)能正確取得並使用 cspNonce 屬性。</li>
* <li>若回應標頭中包含 "{nonce}" 字串,將自動替換為本次請求產生的 nonce。</li>
* </ul>
*
*
* @author Lewis
* @version 1.0
* @since 2024
*/
@Component
@Slf4j
public class CSPNonceFilter extends OncePerRequestFilter {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new CSPNonceFilter instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public CSPNonceFilter() {
// Constructor body (can be empty)
}
private static final int NONCE_SIZE = 32; // recommended is at least 128 bits/16 bytes
private static final String CSP_NONCE_ATTRIBUTE = "cspNonce"; // Session 變數名稱
private static String nonce = ""; // Session 變數內容
private SecureRandom secureRandom = new SecureRandom(); // 隨機產生數列
/**
*
* 系統Spring Boot Security相關程式 避免弱點 Content Security Policy, Thymeleaf 增加Nonce變數
*/
@Override
public void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
try {
byte[] nonceArray = new byte[NONCE_SIZE];
secureRandom.nextBytes(nonceArray);
nonce = Base64.getEncoder().encodeToString(nonceArray);
// 設定於 request的Session變數 cspNonce
request.setAttribute(CSP_NONCE_ATTRIBUTE, nonce);
filterChain.doFilter(request, new CSPNonceResponseWrapper(response, nonce));
} catch (Exception ex) {
log.error("CSPNonceFilter Exception: requestUrl={}", request.getRequestURI(), ex);
}
}
/**
*
*
* 只有系統 ENDPOINT 需要產生 nonce (Thymeleaf)
*/
@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) throws ServletException {
// filter by extension list
String requestNoPathUrl = request.getRequestURI().substring(request.getContextPath().length());
String extension = FileUtils.getFileNameSuffix(requestNoPathUrl);
if (extension != null && SecurityConstants.DO_NOT_CSP_NONCE_Filter_EXTENSION.contains(extension.replace(".", ""))) {
return true;
}
// // 只有系統 ENDPOINT 需要產生 nonce (Thymeleaf)
// String requestURI = request.getRequestURI();
// if (SecurityConstants.LIST_SYSTEM_ENDPOINT.size() > 0 && SecurityConstants.LIST_SYSTEM_ENDPOINT.contains(requestURI)) {
// return false;
// }
return false;
}
/**
* 產生 nonce
* Wrapper to fill the nonce value
*/
public static class CSPNonceResponseWrapper extends HttpServletResponseWrapper {
/**
* CSPNonceResponseWrapper supper response
*
* @param response response
* @param nonce nonce
*/
public CSPNonceResponseWrapper(HttpServletResponse response, String nonce) {
super(response);
// nonce = nonce;
}
@Override
public void setHeader(String name, String value) {
try {
// Header名包含"Content-Security-Policy"時,將 "{nonce}" 字串以 新產生的 nonce變數內容取代
if (name.equals("Content-Security-Policy") && StringUtils.isNotBlank(value)) {
super.setHeader(name, value.replace("{nonce}", nonce));
} else {
super.setHeader(name, value);
}
} catch (Exception ex) {
log.warn(" Cannot set header");
}
}
@Override
public void addHeader(String name, String value) {
// Header名包含"Content-Security-Policy"時,將 "{nonce}" 字串以 新產生的 nonce變數內容取代
if (name.equals("Content-Security-Policy") && StringUtils.isNotBlank(value)) {
super.addHeader(name, value.replace("{nonce}", nonce));
} else {
super.addHeader(name, value);
}
}
}
}
package tw.lewishome.webapp.base.security;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import tw.lewishome.webapp.base.security.audit.LoginFailureLogger;
import tw.lewishome.webapp.base.security.audit.LoginSuccessLogger;
import tw.lewishome.webapp.base.security.audit.SignOffSuccessLogger;
import tw.lewishome.webapp.base.security.jwttoken.JwtAuthenticationFilter;
import tw.lewishome.webapp.base.security.oauth2.OAuth2FailureHandler;
import tw.lewishome.webapp.base.security.oauth2.OAuth2SuccessHandler;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
/**
*
* WebSecurityConfig 類別負責設定 Spring Security 的網頁安全性相關配置。
*
* 主要功能包含:
*
* <ul>
* <li>設定 HTTP Header 安全性(如 XSS 防護、內容安全政策 CSP、HSTS、Referrer Policy 等)。</li>
* <li>定義不需登入認證的白名單端點(ENDPOINTS_WHITELIST)。</li>
* <li>定義不需 CSRF 檢查的端點(CSRF_ENDPOINTS_WHITELIST)。</li>
* <li>設定內容安全政策(Content Security Policy, CSP),強制所有 JS 需由 Server 端提供且不可被竄改,禁止
* Inline Style 及 Script。</li>
* <li>自訂登入表單路徑、參數名稱、登入成功/失敗處理器、登出成功處理器。</li>
* <li>加入 CSP Nonce 過濾器,確保每次請求的 Script 都有唯一的 Nonce。</li>
* <li>加入 JWT 驗證過濾器,於登入表單前執行 Token 驗證。</li>
* <li>限制同一 Session 同時僅允許一個使用者登入,並可自訂 Session 過期後的導向頁面。</li>
* <li>提供自訂的認證提供者(CustomAuthenticationProvider),可擴充登入驗證邏輯。</li>
* <li>提供 AuthenticationManager Bean,供 Spring Security 使用。</li>
* </ul>
*
*
* 此類別透過多個 Bean 及 Filter 的配置,強化 Web 應用程式的安全性,並支援客製化的登入、登出及驗證流程。
*
*
* @author Lewis
* @version 1.0
* @since 2024-06
*/
@Configuration // 系統設定類
@EnableWebSecurity // 啟用 webSecurity
@EnableMethodSecurity(prePostEnabled = true) // 啟用 MethodSecurity (主要檢查Role)
@Slf4j // logger
public class WebSecurityConfig {
/**
* For Fix javadoc warning :
* use of default constructor, which does not provide a comment
*
* Constructs a new WebSecurityConfig instance.
* This is the default constructor, implicitly provided by the compiler
* and can be used to create a new instance of the class.
*/
public WebSecurityConfig() {
// Constructor body (can be empty)
}
@Autowired
LoginSuccessLogger loginSuccessLogger;
@Autowired
LoginFailureLogger loginFailureLogger;
@Autowired
SignOffSuccessLogger signOffSuccessLogger;
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Autowired(required = false)
OAuth2SuccessHandler oauth2SuccessHandler;
@Autowired(required = false)
OAuth2FailureHandler oauth2FailureHandler;
@Autowired
SystemEnvReader systemEnvReader;
// 系統環境設定或預設值 - 將在 @PostConstruct 中初始化
private String systemEndPointWhiteList;
private List<String> listEndPointWhiteList;
private String[] ENDPOINTS_WHITELIST;
private String systemCSRFEndPointWhiteList;
private List<String> listCSRFEndPointWhiteList;
private String[] CSRF_ENDPOINTS_WHITELIST;
/**
* 保護網頁的 contentSecurityPolicyString ,這裡適用 CSP 2.0。
* 主要的難處是希望所有的 JS 是從Server端的程式碼,未被串改過
* 不可使用Inline style ,需使用 link href= ,
* JS 建議最好是 include (src=) 進檔案,避免Inline script
*
*/
public String CONTENT_SECURITY_POLICY;
@PostConstruct
private void initializeSecurityConfig() {
this.systemEndPointWhiteList = systemEnvReader.getProperty("ENDPOINT_WHITE_LIST",
SecurityConstants.DEFAULT_ENDPOINT_WHITE_LIST);
this.listEndPointWhiteList = CommUtils.splitDelimiter(systemEndPointWhiteList, ";");
this.ENDPOINTS_WHITELIST = listEndPointWhiteList.toArray(new String[0]);
this.systemCSRFEndPointWhiteList = systemEnvReader.getProperty("CSRF_ENDPOINT_WHITE_LIST",
SecurityConstants.DEFAULT_CSRF_ENDPOINT_WHITE_LIST);
this.listCSRFEndPointWhiteList = CommUtils.splitDelimiter(systemCSRFEndPointWhiteList, ";");
this.CSRF_ENDPOINTS_WHITELIST = listCSRFEndPointWhiteList.toArray(new String[0]);
this.CONTENT_SECURITY_POLICY = systemEnvReader.getProperty("CONTENT_SECURITY_POLICY",
SecurityConstants.DEFAULT_CONTENT_SECURITY_POLICY);
}
/**
* 過濾 http security
*
* @param http
* HttpSecurity
* @return SecurityFilterChain SecurityFilterChain
* @throws java.lang.Exception
* java.lang.Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 設定 Http Header 安全資訊
http.headers(headersConfig -> {
headersConfig.xssProtection(
xss -> xss.headerValue(
XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK));
headersConfig.contentSecurityPolicy(
cps -> cps.policyDirectives(CONTENT_SECURITY_POLICY));
headersConfig.httpStrictTransportSecurity(
httpStrictTransportSecurity -> {
httpStrictTransportSecurity.includeSubDomains(true);
httpStrictTransportSecurity.maxAgeInSeconds(315360000);
});
headersConfig.cacheControl(cacheControl -> cacheControl.disable());
headersConfig.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable());
headersConfig.referrerPolicy(
referrerPolicy -> referrerPolicy.policy(ReferrerPolicy.STRICT_ORIGIN));
headersConfig.frameOptions(frameOptions -> frameOptions.sameOrigin());
});
// 關閉瀏覽器的跨來源資源共享(CORS)功能
http.cors(cors -> cors.disable());
// CSRF(Cross-Site Request Forgery,跨站請求偽造)保護設定
http.csrf(csrf -> {
// 在PageConstants.CSRF_ENDPOINTS_WHITELIST 白名單定義,不需 CSRF 檢查
csrf.ignoringRequestMatchers(CSRF_ENDPOINTS_WHITELIST);
});
// 不需要認證白名單定義 (例如 css、javascript、image、login home page 以及 jwtAuth 授權Page 等)
http.authorizeHttpRequests(authorize -> {
// 在PageConstants.ENDPOINTS_WHITELIST 白名單定義,不需登入認證
authorize.requestMatchers(ENDPOINTS_WHITELIST).permitAll();
// 其他請求都需要認證
authorize.anyRequest().authenticated();
});
String loginPageUrl = systemEnvReader.getProperty("LOGIN_PAGE_URL",SecurityConstants.LOGIN_PAGE_URL);
String userNameParameter = systemEnvReader.getProperty("USER_NAME_PARAMETER",SecurityConstants.USER_NAME_PARAMETER);
String userPasswordParameter = systemEnvReader.getProperty("USER_PASSWORD_PARAMETER",SecurityConstants.USER_PASSWORD_PARAMETER);
// Login 表單登入相關處裡
http.formLogin(login -> {
// 登入頁面必須要於 controller 有 Get method
login.loginPage(loginPageUrl);
// 登入頁面欄位名稱 ==>給 spring security 認證的username變數
login.usernameParameter(userNameParameter);
// 登入頁面 欄位名稱==>給 spring security 認證的password變數
// Spring security 將Username與Password 傳送給後續的 (底下AuthenticationProvider)
login.passwordParameter(userPasswordParameter);
// loginProcessingUrl 會被 Spring security 攔截,不需要有這個 ENDPOINT
login.loginProcessingUrl("/loginSubmit");
// 登入成功處裡程序 有設.defaultSuccessUrl 會 沒有作用
login.successHandler(loginSuccessLogger);
// 登入失敗時處裡程序,當設定了 failureHandler 則 .failureUrl 會無效,
// failureHandler 會有 Redirect URL
// failureHandler 沒有設定 Redirect URL,
// Default使用 .loginPage 加上 ?error ==>("/home?error")
// failureHandler 有 Redirect URL ("/home?retry") , 這 /home 需要有列入白名單
login.failureHandler(loginFailureLogger);
// .defaultSuccessUrl("/landing", true) // spring security 認證後,強迫 landing的 URL
login.permitAll();
});
// Login 表單登出相關處裡
http.logout(logout -> {
// 登出成功時處裡程序
logout.logoutSuccessHandler(signOffSuccessLogger);
// .logoutUrl 會被 Spring security 攔截 但必須值定一個Endpoint URL
// 所以controller 不需要有 /logoutSubmit 的 post/get request method
logout.logoutUrl("/logoutSubmit");
// 清除授權資料
logout.clearAuthentication(true);
// 清除Session資料
logout.invalidateHttpSession(true);
// 清除Cookie資料
logout.deleteCookies("JSESSIONID");
});
// OAuth2.0 登入設定 - Accept /callback as the OAuth2 redirection endpoint
http.oauth2Login(oauth2 -> {
log.info("Configuring OAuth2 Login with /callback endpoint...");
// Authorization endpoint: where user is redirected TO for OAuth2 authorization
String oauth2AuthorizationBaseUri = systemEnvReader.getProperty("oauth2AuthorizationBaseUri",
"/oauth2/authorization");
oauth2.authorizationEndpoint(auth -> auth.baseUri(oauth2AuthorizationBaseUri));
// Redirection endpoint: where OAuth2 provider redirects BACK to with auth code
String oauthRedirectionUri = systemEnvReader.getProperty("oauth2RedirectionUri", "/callback");
oauth2.redirectionEndpoint(redirect -> {
log.info("Setting OAuth2 redirection endpoint to /callback");
redirect.baseUri(oauthRedirectionUri);
});
if (oauth2SuccessHandler != null) {
log.info("✓ Setting OAuth2SuccessHandler");
oauth2.successHandler(oauth2SuccessHandler);
} else {
log.warn("✗ OAuth2SuccessHandler is NULL");
}
if (oauth2FailureHandler != null) {
log.info("✓ Setting OAuth2FailureHandler");
oauth2.failureHandler(oauth2FailureHandler);
} else {
log.warn("✗ OAuth2FailureHandler is NULL");
}
oauth2.permitAll();
});
// Http HeaderWriter (加入Nonce變數,讓 Thymeleaf 處理)
http.addFilterBefore(new CSPNonceFilter(), HeaderWriterFilter.class);
// Jwt Token 需要於 Login From 執行前檢核,所以需要 Http request 增加 Filter Before
http.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// Sission 相關設定 (例如同時可以登入 1 個)
http.sessionManagement(session -> session
.sessionConcurrency((concurrency) -> concurrency
// 相同Sission 同時可以登入 1 個
.maximumSessions(1)
// 超過登入Session數, 允許登入,但之前登入的會失效
.maxSessionsPreventsLogin(false)
// 超過登入Session數, PreventsLogin = true: 不允許登入
// .maxSessionsPreventsLogin(true)
.expiredUrl("/home?expired")));
// return http 過濾設定
return http.build();
} // end SecurityFilterChain
/**
* 用於表單登入驗證的 AuthenticationManager bean。
* 包含 CustomAuthenticationProvider 以進行使用者名稱/密碼驗證。
* OAuth2 驗證由 Spring Security 的 OAuth2 過濾器單獨處理。
*
* 當有定義 @Bean AuthenticationManager 時,Spring Security 會使用它進行驗證,
* 而不是自動探索其他提供者。
*/
@Bean
public AuthenticationManager authenticationManager() {
List<AuthenticationProvider> providers = new ArrayList<>();
// Add CustomAuthenticationProvider for form login (username/password)
if (customAuthenticationProvider != null) {
log.info("✓ CustomAuthenticationProvider added to AuthenticationManager for form login");
providers.add(customAuthenticationProvider);
} else {
log.warn("✗ CustomAuthenticationProvider is NULL - form login will fail!");
}
// Create and return the ProviderManager
// If providers list is empty, this will cause authentication to fail
ProviderManager manager = new ProviderManager(providers);
// Don't hide exceptions - report them clearly
manager.setEraseCredentialsAfterAuthentication(true);
return manager;
}
}
位置:src/main/java/tw/lewishome/webapp/page/HomePageController.java
package tw.lewishome.webapp.page;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import tw.lewishome.webapp.base.security.audit.SysSecurityAuditLogService;
/**
* <pre>
* 系統跟目錄(Home)的控制服務程式
*
* 系統跟目錄(Home)的控制服務程式,包含以下主要功能 <br/>
* </pre>
*
* @author Lewis
* @version $Id: $Id
*/
@Controller // 宣告是 Controller 類別
@Slf4j
public class HomePageController {
@Autowired
SysSecurityAuditLogService sysSecurityAuditLogService;
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new HomePageController instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public HomePageController() {
// Constructor body (can be empty)
}
private static String authorizationRequestBaseUri = "oauth2/authorization";
Map<String, String> oauth2AuthenticationUrls = new HashMap<>();
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
/**
* <pre>
* 接受前端 URL ==> "/home" or "/index" or "/的 Get request
* 顯示前端 /home/homePage.html
* </pre>
*
* @param model Session model
* @return String 前端Html
*/
@SuppressWarnings({ "unchecked", "null" })
@GetMapping({ "/home", "/index", "/" })
public String getHomeForm(Model model, HttpServletRequest request) {
model.addAttribute("nonce", request.getAttribute("cspNonce"));
// 回覆前端的 html ()
Iterable<ClientRegistration> clientRegistrations = null;
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
.as(Iterable.class);
if (type != ResolvableType.NONE &&
ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
}
clientRegistrations.forEach(registration -> oauth2AuthenticationUrls.put(registration.getClientName(),
authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
model.addAttribute("urls", oauth2AuthenticationUrls);
return "/home/homePage.html";
}
/**
* 接受Login認證後 導向 "/landing" 的 getRequest
*
* @param model page Model
* @return String /home/landingPage.html
*/
@GetMapping({ "/landing", "/loginSubmit" })
public String doLandingForm(Model model, HttpServletRequest request) throws Exception {
model.addAttribute("menuFunc", "landing");
model.addAttribute("menuUrl", "landing");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
log.info("Authentication: {}", authentication.getName());
if (authentication.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
System.out.println("OAuth2User Attributes: " + oauth2User.getAttributes());
log.info("OAuth2User: {}", oauth2User);
}
}
return "/home/landingPage.html";
}
/**
* 處理 Menu點選的 URL (統一Menu入口,先處理Menu共同需要的程序)
*
* @param model Page Model
* @param request HttpServletRequest
* @param menuUrl 轉向 Menu 指定 URL
* @param menuDesc Menu說明 (功能 Title)
* @param menuFunc Menu功能 (查詢/維護)
* @return String menuUrl
*/
@GetMapping("doMenu")
public String doRedirectMenuUrl(Model model, HttpServletRequest request,
@RequestParam("Url") String menuUrl,
@RequestParam("Desc") String menuDesc,
@RequestParam("Func") String menuFunc) {
// html 會使用
model.addAttribute("menuDesc", menuDesc);
// html 會使用 在Post Endpoint時傳給後端使用
// model.addAttribute("isMenuUrl", true);
model.addAttribute("menuFunc", menuFunc);
model.addAttribute("menuUrl", menuUrl);
// headerDataString
model.addAttribute("headerDataString", "");
// SysAccessLogS
String auditSctionString = "Click Menu " + menuDesc + " " + menuFunc;
sysSecurityAuditLogService.addNewAccessLog(request, auditSctionString);
return "redirect:" + menuUrl;
}
}
位置:src/main/resources/static/css/landingImg.css
#intro {
background-image: url('../img/1120.jpg');
height: 90vh;
}
位置:src/main/resources/static/img/1120.jpg
任意圖案,不存在也無所謂
位置:src/main/resources/static/js/capLock.js
$(document).ready(function () {
$('#divCapsLock').css('display', 'none');
document.body.addEventListener('keyup', (event) => {
capLock(event);
})
});
function capLock(e) {
if (e.getModifierState("CapsLock")) {
$('#divCapsLock').css('display', 'block');
} else {
$('#divCapsLock').css('display', 'none');
}
}
位置:src/main/resources/templates/home/homePage.html
<!DOCTYPE html>
<html lang="UTF-8" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/Layout">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebAppSystem | Log in</title>
<!-- Google Font: Source Sans Pro -->
<link rel="stylesheet" th:href="@{/webjars/AdminLTE/3.2.0/plugins/fontawesome-free/css/all.min.css}">
<!-- Theme style -->
<link rel="stylesheet" th:href="@{/webjars/AdminLTE/3.2.0/dist/css/adminlte.min.css}">
</head>
<body class="hold-transition login-page">
<div class="login-box">
<!-- /.login-logo -->
<div class="card card-outline card-primary">
<div class="card-header text-center">
<span class="h1"><b>Web App</b></span>
</div>
<div class="card-body">
<div>
</div>
<div th:if="${param.retry}">
<div class="alert alert-danger">Invalid username and passwordx ${nonce}</div>
</div>
<div th:if="${param.logout}">
<div class="alert alert-danger">Logout Success.</div>
</div>
<div th:if="${param.expired}">
<div class="alert alert-danger">Session Expired.</div>
</div>
<form th:action="@{/loginSubmit}" method="post" AUTOCOMPLETE="off">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<div class="input-group mb-3">
<input id="useid" type="text" class="form-control" name="username" placeholder="UserName">
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-user"></span>
</div>
</div>
</div>
<div class="input-group mb-3">
<input id="password" type="password" class="form-control" name="ePasscode" placeholder="Password" autocomplete="false" >
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock"></span>
</div>
</div>
</div>
<div id="divCapsLock">Caps Lock is on.</div>
<button id="login" type="submit" class="btn btn-primary btn-block">Sign In</button>
<p th:each="url : ${urls}">
<a th:text="${url.key}" th:href="${url.value}">Client</a>
</p>
</form>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.login-box -->
<!-- jQuery -->
<script th:nonce="${nonce}" th:src="@{/webjars/AdminLTE/3.2.0/plugins/jquery/jquery.min.js}"></script>
<!-- Bootstrap 4 -->
<script th:nonce="${nonce}" th:src="@{/webjars/AdminLTE/3.2.0/plugins/bootstrap/js/bootstrap.bundle.min.js}"></script>
<!-- AdminLTE App -->
<script th:nonce="${nonce}" th:src="@{/webjars/AdminLTE/3.2.0/dist/js/adminlte.min.js}"></script>
<script th:nonce="${nonce}" th:src="@{/js/capLock.js}"></script>
</body>
</html>
位置:src/main/resources/templates/home/landingPage.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/Layout">
<head>
<link th:href="@{/css/landingImg.css}" rel="stylesheet" />
</head>
<body>
<!-- 指定 id=intro, 使用head內的style Background image -->
<div id="intro" class="content-wrapper" layout:fragment="content">
<!-- 指定在置中(h-100) ,若 (h-50) 則以上半部(50%高度)置中-->
<div class="d-flex align-items-center h-100">
<!-- 沒有 container 則 form 不會左右置中-->
<div class="container">
<div class="mx-auto text-center text-white">
<div class="row justify-content-center ">
<form class="bg-white rounded-2 shadow-5-strong p-5 ">
<h2>Welcome Section</h2>
<h4>歡迎 大駕光臨 </h4>
</form>
</div>
</div>
</div> <!-- row -->
</div> <!-- container -->
</div> <!--d-flex -->
</div> <!-- intro content -->
</body>
</html>