七、Spring Boot整合Spring Security之前後分離認證最佳實現

那你为何对我三笑留情發表於2024-11-06

前言

本文介紹前後分離認證最佳實現,配合以下內容觀看效果更佳!!!

  • 什麼是前後分離認證流程最佳方案,為什麼這麼設計?請檢視六、Spring Boot整合Spring Security之前後分離認證流程最佳方案
  • 哇偶,明白了前後分離認證流程最佳方案的原理,那怎麼實現這套方案呢?請檢視七、Spring Boot整合Spring Security之前後分離認證最佳實現
  • Nice,知道了怎麼程式碼實現前後分離認證流程最佳方案,那我怎麼測試呢?請檢視八、Spring Boot整合Spring Security之前後分離認證最佳實現對接測試
  • 博主,幫人幫到底,送佛送到西,提不提供原始碼呀?請點選下載

一、自定義使用者名稱密碼認證過濾器RestfulUsernamePasswordAuthenticationFilter

1、註冊過濾器方式

  1. 使用httpSecurity.addFilter/addFilterBefore/addFilterAfter向過濾器鏈中新增過濾器,其中addFilter只能新增內建的過濾器,順序已在過濾器順序註冊器(FilterOrderRegistration)中設定;addFilterBefore/addFilterAfter可以新增自定義過濾器,新增在指定的過濾器之前/之後。該方式優點是使用簡單,缺點是無法使用spring security內建的元件,與RestfulUsernamePasswordAuthenticationFilter需要使用AuthenticationManager元件衝突,故不使用該方式。
  2. 使用SecurityConfigurer透過配置類的方式向過濾器鏈中新增過濾器,官方使用的方式。該方式優點是可以使用spring security內建的元件,缺點是實現較為笨重,而且只能註冊過濾器順序註冊器(FilterOrderRegistration)中設定的過濾器。該方式可以使用spring security內建的元件,所以採用本方式,需要修改過濾器順序註冊器新增自定義的過濾器。

2、修改並覆蓋過濾器順序註冊器

  1. FilterOrderRegistration類為final類且未提供開放的註冊自定義過濾器的方式,所以只能重寫該類,並新增自定義過濾器的順序
package org.springframework.security.config.annotation.web.builders;

import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.filter.CorsFilter;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

final class FilterOrderRegistration {


    private static final int INITIAL_ORDER = 100;

    private static final int ORDER_STEP = 100;

    private final Map<String, Integer> filterToOrder = new HashMap<>();

    FilterOrderRegistration() {
        Step order = new Step(INITIAL_ORDER, ORDER_STEP);
        put(DisableEncodeUrlFilter.class, order.next());
        put(ForceEagerSessionCreationFilter.class, order.next());
        put(ChannelProcessingFilter.class, order.next());
        order.next(); // gh-8105
        put(WebAsyncManagerIntegrationFilter.class, order.next());
        put(SecurityContextHolderFilter.class, order.next());
        put(SecurityContextPersistenceFilter.class, order.next());
        put(HeaderWriterFilter.class, order.next());
        put(CorsFilter.class, order.next());
        put(CsrfFilter.class, order.next());
        put(LogoutFilter.class, order.next());
        this.filterToOrder.put(
                "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
                order.next());
        this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
                order.next());
        put(X509AuthenticationFilter.class, order.next());
        put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
                order.next());
        this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
                order.next());
        //新增自定義過濾器
        put(RestfulUsernamePasswordAuthenticationFilter.class, order.next());
        put(UsernamePasswordAuthenticationFilter.class, order.next());
        order.next(); // gh-8105
        this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
        put(DefaultLoginPageGeneratingFilter.class, order.next());
        put(DefaultLogoutPageGeneratingFilter.class, order.next());
        put(ConcurrentSessionFilter.class, order.next());
        put(DigestAuthenticationFilter.class, order.next());
        this.filterToOrder.put(
                "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
                order.next());
        put(BasicAuthenticationFilter.class, order.next());
        put(RequestCacheAwareFilter.class, order.next());
        put(SecurityContextHolderAwareRequestFilter.class, order.next());
        put(JaasApiIntegrationFilter.class, order.next());
        put(RememberMeAuthenticationFilter.class, order.next());
        put(AnonymousAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
                order.next());
        put(SessionManagementFilter.class, order.next());
        put(ExceptionTranslationFilter.class, order.next());
        put(FilterSecurityInterceptor.class, order.next());
        put(AuthorizationFilter.class, order.next());
        put(SwitchUserFilter.class, order.next());
    }

    /**
     * Register a {@link Filter} with its specific position. If the {@link Filter} was
     * already registered before, the position previously defined is not going to be
     * overriden
     *
     * @param filter   the {@link Filter} to register
     * @param position the position to associate with the {@link Filter}
     */
    void put(Class<? extends Filter> filter, int position) {
        String className = filter.getName();
        if (this.filterToOrder.containsKey(className)) {
            return;
        }
        this.filterToOrder.put(className, position);
    }

    /**
     * Gets the order of a particular {@link Filter} class taking into consideration
     * superclasses.
     *
     * @param clazz the {@link Filter} class to determine the sort order
     * @return the sort order or null if not defined
     */
    Integer getOrder(Class<?> clazz) {
        while (clazz != null) {
            Integer result = this.filterToOrder.get(clazz.getName());
            if (result != null) {
                return result;
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    private static class Step {

        private final int stepSize;
        private int value;

        Step(int initialValue, int stepSize) {
            this.value = initialValue;
            this.stepSize = stepSize;
        }

        int next() {
            int value = this.value;
            this.value += this.stepSize;
            return value;
        }

    }

}

3、建立RestfulUsernamePasswordAuthenticationFilter

  1. 參考UsernamePasswordAuthenticationFilter
  2. 將引數獲取方式從request.getParameter改為從body體中
  3. 建立UsernamePasswordAuthenticationToken
  4. 設定細節
  5. 呼叫getAuthenticationManager()的authenticate方法獲取認證資訊
package com.yu.demo.spring.filter;

import com.yu.demo.util.SpringUtil;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 自定義前後端分離/restful方式的使用者名稱密碼認證過濾器
 * 參考UsernamePasswordAuthenticationFilter
 */
public class RestfulUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //是否只支援post方法
    private final boolean postOnly;
    private final String username;
    private final String password;

    public RestfulUsernamePasswordAuthenticationFilter(String username, String password, String loginUrl, String httpMethod) {
        super(new AntPathRequestMatcher(loginUrl, httpMethod));
        postOnly = HttpMethod.POST.name().equals(httpMethod);
        this.username = username;
        this.password = password;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
        if (this.postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            Map<String, String> body = SpringUtil.rawBodyToMap(request);
            String name = body.get(username);
            String pswd = body.get(password);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(name, pswd);
            setDetails(request, authRequest);
            return getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

}

4、建立自定義使用者名稱密碼認證過濾器配置類RestfulLoginConfigurer

  1. 參考FormLoginConfigurer
  2. 註冊自定義使用者名稱密碼認證過濾器RestfulUsernamePasswordAuthenticationFilter
  3. 設定登入地址和請求方式
package com.yu.demo.spring.filter;

import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
 * 自定義前後端分離/restful方式的使用者名稱密碼驗證過濾器配置器,用於註冊認證過濾器
 * 參考FormLoginConfigurer
 */
public class RestfulLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, RestfulLoginConfigurer<H>, RestfulUsernamePasswordAuthenticationFilter> {
    private final String loginMethod;

    public RestfulLoginConfigurer(RestfulUsernamePasswordAuthenticationFilter authenticationFilter, String defaultLoginProcessingUrl, String loginMethod) {
        super(authenticationFilter, defaultLoginProcessingUrl);
        this.loginMethod = loginMethod;
    }

    @Override
    public RestfulLoginConfigurer<H> loginPage(String loginPage) {
        return super.loginPage(loginPage);
    }

    @Override
    public void init(H http) throws Exception {
        super.init(http);
    }

    @Override
    protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
        return new AntPathRequestMatcher(loginProcessingUrl, loginMethod);
    }
}

二、自定義安全上下文倉庫SecurityContextRepositoryImpl

  1. 基於分散式快取實現安全上下文倉庫
  2. 獲取上下文時從請求頭中獲取token,透過token從快取中獲取上下文,不存在時返回空值安全上下文
  3. 儲存上下文時從請求頭或者登入使用者資訊中獲取token,將token和上下文儲存到快取中

1、分散式快取介面和實現

package com.yu.demo.manager;

import org.springframework.security.core.context.SecurityContext;

public interface CacheManager {

    /**
     * 透過token獲取認證資訊
     *
     * @param token token
     * @return 認證資訊
     */
    SecurityContext getSecurityContext(String token);

    /**
     * 是否包含token
     *
     * @param token token
     * @return 是否包含token
     */
    boolean contains(String token);

    /**
     * 透過token新增認證資訊
     *
     * @param token           token
     * @param securityContext 認證資訊
     */
    void addSecurityContext(String token, SecurityContext securityContext);

    /**
     * 透過token刪除認證資訊
     *
     * @param token token
     */
    void deleteSecurityContext(String token);

}

為演示方便,這裡採用過期Map,實際使用將map改為redis或者其他分散式快取即可

package com.yu.demo.manager.impl;

import com.yu.demo.manager.CacheManager;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class CacheManagerImpl implements CacheManager {

    private static ExpiringMap<String, SecurityContext> SECURITY_CONTEXT_CACHE;

    @PostConstruct
    public void init() {
        SECURITY_CONTEXT_CACHE = ExpiringMap.builder().maxSize(200).expiration(30, TimeUnit.MINUTES).expirationPolicy(ExpirationPolicy.ACCESSED).variableExpiration().build();
    }

    @Override
    public SecurityContext getSecurityContext(String token) {
        return SECURITY_CONTEXT_CACHE.get(token);
    }

    @Override
    public boolean contains(String token) {
        return SECURITY_CONTEXT_CACHE.containsKey(token);
    }

    @Override
    public void addSecurityContext(String token, SecurityContext securityContext) {
        SECURITY_CONTEXT_CACHE.put(token, securityContext);
    }

    @Override
    public void deleteSecurityContext(String token) {
        SECURITY_CONTEXT_CACHE.remove(token);
    }
}

2、建立SecurityContextRepositoryImpl

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.manager.CacheManager;
import com.yu.demo.util.SecurityUtil;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SecurityContextRepositoryImpl implements SecurityContextRepository {

    private static final String AUTHORIZATION = "Authorization";
    @Autowired
    private CacheManager cacheManager;

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        //獲取請求頭中的token,未登入訪問系統時Token為空
        String token = requestResponseHolder.getRequest().getHeader(AUTHORIZATION);
        if (StringUtil.isNotBlank(token)) {
            SecurityContext securityContext = cacheManager.getSecurityContext(token);
            //securityContext已過期時為空
            if (SecurityUtil.isNotAuthenticated(securityContext)) {
                return SecurityContextHolder.createEmptyContext();
            }
            UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            if (token.equals(userDetails.getToken())) {
                //測試過程中偽造的Token(不修改header和body,只修改signature部分字元)有機率出現可以解析成功的情況,可能是secret太短的原因,未深究,所以這裡在驗證下輸入的Token和快取中的token
                return securityContext;
            }
        }
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(SecurityContext securityContext, HttpServletRequest request, HttpServletResponse response) {
        //獲取請求頭中的token(登出時有,登入時沒有)
        String token = request.getHeader(AUTHORIZATION);
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
        if (StringUtil.isBlank(token) && SecurityUtil.isNotAuthenticated(securityContext)) {
            //未登入、驗證碼、使用者名稱密碼校驗失敗
            return;
        }
        //第一次登入時Token為空
        if (StringUtil.isBlank(token)) {
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            //登入成功
            cacheManager.addSecurityContext(userDetails.getToken(), securityContext);
            return;
        }
        //退出或token過期(快取中設定token過期時間)
        if (SecurityUtil.isNotAuthenticated(securityContext)) {
            cacheManager.deleteSecurityContext(token);
            return;
        }
        //更新Token
        cacheManager.addSecurityContext(token, securityContext);
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        //本版本的Spring Security只有SessionManagementFilter中呼叫該方法
        //已禁用SessionManagementFilter,該方法不會被呼叫
        String token = request.getHeader(AUTHORIZATION);
        if (StringUtil.isBlank(token)) {
            return false;
        }
        return cacheManager.contains(token);
    }

}

三、自定義使用者詳情UserDetailsImpl

package com.yu.demo.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@Setter
@Getter
@ToString
public class UserDetailsImpl implements UserDetails {
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
    /**
     * token
     */
    private String token;

    public UserDetailsImpl(String username, String password, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, boolean enabled, Set<GrantedAuthority> grantedAuthorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 賬號是否未過期
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    /**
     * 賬號是否未鎖定
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    /**
     * 密碼是否未過期
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    /**
     * 賬號是否啟用
     *
     * @return true:是,false:否
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

四、自定義使用者詳情資料庫查詢UserDetailsServiceImpl

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    //@Autowired
    //private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 透過username從資料庫中獲取使用者,將使用者轉UserDetails
        //User user = userService.getByUsername(username);
        //return new User(username, user.getPassword(), user.getEnable(), user.getAccountNonExpired(), user.getCredentialsNonExpired(), user.getAccountNonLocked(), user.getAuthorities());
        //{noop}不使用密碼加密器,密碼123的都可以驗證成功
        UserDetailsImpl userDetails = new UserDetailsImpl(username, "{noop}123", true, true, true, true, null);
        //userDetails中設定token,該token只是實現認證流程,未使用jwt
        userDetails.setToken(UUID.randomUUID().toString());
        return userDetails;
    }

}

五、自定義登入登出結果處理器

package com.yu.demo.spring.impl;


import com.yu.demo.entity.ApiResp;
import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.util.SpringUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class LoginResultHandler implements AuthenticationSuccessHandler, LogoutSuccessHandler, AuthenticationEntryPoint, AuthenticationFailureHandler {

    /**
     * 登入成功
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
        UserDetailsImpl userDetailsImpl = (UserDetailsImpl) usernamePasswordAuthenticationToken.getPrincipal();
        //登陸成功後,擦除密碼
        userDetailsImpl.setPassword(null);
        //token返回到前端
        SpringUtil.respJson(response, ApiResp.success(userDetailsImpl.getToken()));
    }

    /**
     * 登入失敗
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.loginFailure());
    }

    /**
     * 登出成功
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.success());
    }

    /**
     * 未登入訪問需要登入的頁面時
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        SpringUtil.respJson(response, ApiResp.notLogin());
    }

}

六、過濾器鏈個性化配置

package com.yu.demo.config;

import com.yu.demo.spring.filter.RestfulLoginConfigurer;
import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import com.yu.demo.spring.impl.LoginResultHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    //登入引數使用者名稱
    private static final String LOGIN_ARG_USERNAME = "username";
    //登入引數密碼
    private static final String LOGIN_ARG_PASSWORD = "password";
    //登入請求型別
    private static final String LOGIN_HTTP_METHOD = HttpMethod.POST.name();
    //登入請求地址
    private static final String LOGIN_URL = "/login";
    //登出請求地址
    private static final String LOGOUT_URL = "/logout";

    @Autowired
    private LoginResultHandler loginResultHandler;
    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                //禁用UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter
                .formLogin(FormLoginConfigurer::disable)
                //禁用BasicAuthenticationFilter
                .httpBasic(HttpBasicConfigurer::disable)
                //禁用CsrfFilter
                .csrf(CsrfConfigurer::disable)
                //禁用SessionManagementFilter
                .sessionManagement(SessionManagementConfigurer::disable)
                //異常處理配置
                .exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer.authenticationEntryPoint(loginResultHandler))
                //http請求認證
                .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer
                        //任何請求
                        .anyRequest()
                        //需要認證
                        .authenticated())
                //安全上下文配置
                .securityContext(securityContextCustomizer -> securityContextCustomizer
                        //設定自定義securityContext倉庫
                        .securityContextRepository(securityContextRepository)
                        //顯示儲存SecurityContext,官方推薦
                        .requireExplicitSave(true))
                //登出配置
                .logout(logoutCustomizer -> logoutCustomizer
                        //登出地址
                        .logoutUrl(LOGOUT_URL)
                        //登出成功處理器
                        .logoutSuccessHandler(loginResultHandler)
                )
                //註冊自定義登入過濾器的配置器:自動註冊自定義登入過濾器;
                //需要重寫FilterOrderRegistration的構造方法FilterOrderRegistration(){},在構造方法中新增自定義過濾器的序號,否則註冊不成功
                .apply(new RestfulLoginConfigurer<>(new RestfulUsernamePasswordAuthenticationFilter(LOGIN_ARG_USERNAME, LOGIN_ARG_PASSWORD, LOGIN_URL, LOGIN_HTTP_METHOD), LOGIN_URL, LOGIN_HTTP_METHOD))
                //設定登入地址:未設定時系統預設生成登入頁面,登入地址/login
                .loginPage(LOGIN_URL)
                //設定登入成功之後的處理器
                .successHandler(loginResultHandler)
                .failureHandler(loginResultHandler);

        //建立過濾器鏈物件
        return httpSecurity.build();
    }

}

七、其他類

package com.yu.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * 介面響應物件
 *
 * @author admin
 */
@Setter
@Getter
@ToString
@AllArgsConstructor
public class ApiResp {

    private static final String CODE_SUCCESS = "00000";
    private static final String CODE_LOGIN_FAILURE = "10000";
    private static final String MESSAGE_LOGIN_FAILURE = "登入失敗";
    private static final String CODE_NOT_LOGIN = "10010";
    private static final String MESSAGE_NOT_LOGIN = "未登入";

    /**
     * 響應碼
     */
    private String code;
    /**
     * 描述
     */
    private String message;
    /**
     * 資料
     */
    private Object data;

    /**
     * 成功
     */
    public ApiResp(String code) {
        this.code = code;
    }

    /**
     * 失敗+失敗描述
     */
    public ApiResp(String code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 成功+返回值
     */
    public ApiResp(String code, Object data) {
        this.code = code;
        this.data = data;
    }

    /**
     * 成功無返回資料
     *
     * @return 介面響應物件
     */
    public static ApiResp success() {
        return new ApiResp(CODE_SUCCESS);
    }

    /**
     * 成功有返回資料
     *
     * @param data 資料
     * @return 介面響應物件
     */
    public static ApiResp success(Object data) {
        return new ApiResp(CODE_SUCCESS, data);
    }

    /**
     * 登入失敗
     *
     * @return 介面響應物件
     */
    public static ApiResp loginFailure() {
        return new ApiResp(CODE_LOGIN_FAILURE, MESSAGE_LOGIN_FAILURE);
    }

    /**
     * 未登入
     *
     * @return 介面響應物件
     */
    public static ApiResp notLogin() {
        return new ApiResp(CODE_NOT_LOGIN, MESSAGE_NOT_LOGIN);
    }

}

package com.yu.demo.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;

import java.lang.reflect.Type;
import java.util.Map;

/**
 * JSON工具類
 *
 * @author admin
 */
public class JsonUtil {
    private JsonUtil() {
        throw new AssertionError();
    }

    /**
     * 物件轉json
     *
     * @param javaObject 物件或集合或者陣列
     * @return json
     */
    public static String object2Json(Object javaObject) {
        return JSONObject.toJSONString(javaObject);
    }

    public static <K, V> Map<K, V> json2Map(String jsonString, Type type) {
        return JSON.parseObject(jsonString, type);
    }
}

package com.yu.demo.util;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;

/**
 * Spring框架工具類
 */
public class SecurityUtil {

    private SecurityUtil() {
        throw new AssertionError();
    }

    public static boolean isAuthenticated(SecurityContext securityContext) {
        if (securityContext == null) {
            return false;
        }
        Authentication authentication = securityContext.getAuthentication();
        if (authentication == null) {
            return false;
        }
        return authentication.isAuthenticated();
    }

    public static boolean isNotAuthenticated(SecurityContext securityContext) {
        return !isAuthenticated(securityContext);
    }

}

package com.yu.demo.util;

import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * Spring框架工具類
 */
public class SpringUtil {

    private SpringUtil() {
        throw new AssertionError();
    }

    /**
     * 請求body引數轉為map
     *
     * @param request 請求
     * @return 引數map
     * @throws IOException IO流異常
     */
    public static Map<String, String> rawBodyToMap(HttpServletRequest request) throws IOException {
        BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
        StringBuilder responseStrBuilder = new StringBuilder();
        String inputStr;
        while ((inputStr = streamReader.readLine()) != null) {
            responseStrBuilder.append(inputStr);
        }
        return JsonUtil.json2Map(responseStrBuilder.toString(), Map.class);
    }


    public static void respJson(HttpServletResponse response, Map<String, Object> apiResp) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().print(JsonUtil.object2Json(apiResp));
    }

}

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.yu</groupId>
    <artifactId>spring-boot-security2-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-security2-demo</name>
    <description>Spring Boot整合Spring Security樣例</description>

    <properties>
        <java.version>8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.46</version>
        </dependency>
        <!--過期map-->
        <dependency>
            <groupId>net.jodah</groupId>
            <artifactId>expiringmap</artifactId>
            <version>0.5.11</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.0</version>
        </dependency>
    </dependencies>

</project>

八、案例原始碼獲取

  • 下載地址
  • 私聊、評論區、+V均可

相關文章