iT邦幫忙

0

VScode 開發應用系統專案(8-3) - Spring Boot Security 客製化多元登入認證

  • 分享至 

  • xImage
  •  

Spring Boot 安全認證 — 客製化多元登入認證

概述

接續應用系統專案(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 (登入成功頁面)

1. DataBaseAuthentication(資料庫認證)

位置: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;
    }
}

2. ActiveDirectoryAuthentication(AD/LDAP認證)

位置: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 "";
    }
}

3. MemoryAuthentication(記憶體認證,開發測試用)

位置: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;
    }
}

4. CustomAuthenticationProvider (客製化多來源認證器)

位置: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;
    }

}

4. CSPNonceFilter(Content Security Policy 過濾器)

位置: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);
            }
        }
    }
}


WebSecurityConfig (Spring Security 主設定)

  • 這裡是 Spring Security的主要設定,以下兩個設定必須配合前端 Login Form Html的輸入變數名稱。
  • login.usernameParameter("username"); Spring Boot 會為Authentication username變數內容。
  • login.passwordParameter("ePasscode"); Spring Boot 會為Authentication password變數內容。
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;
        }
}


前端URL相關控制處理

1. HomePageController(登入相關 Endpoint URL)

位置: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;
    }

}

前端URL相關 Html相關文件(主要使用thymeleaf template)

1. landingImg.css (landing 登入成功頁面的css Style,顯示底圖)

位置:src/main/resources/static/css/landingImg.css

#intro {
    background-image: url('../img/1120.jpg');
    height: 90vh;
}

2. 1120.jpg (landing 登入成功頁面底圖)

位置:src/main/resources/static/img/1120.jpg

任意圖案,不存在也無所謂

3. capLock.js (顯示CapLock狀態的 Java Script)

位置: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');
    }
}

4. homePage.html (login 輸入畫面)

位置: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>

5. landingPage.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>


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言