国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

springboot2.7整合springSecurity

這篇具有很好參考價(jià)值的文章主要介紹了springboot2.7整合springSecurity。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

springboot2.7整合springSecurity

0.簡(jiǎn)介

本著前人栽樹(shù),后人乘涼的這種思想,自己花了一些時(shí)間,用心的整理了一套springboot整合springsecurity的教程。

該教程是基于springboot2.7.3版本開(kāi)發(fā)的,在2.7以上版本中,springSecurity已經(jīng)廢棄了WebSecurityConfigurerAdapter,而是使用 bean 注入的方式,詳情可參閱官方文檔:https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#ldap-authentication

該教程是基于前后端分離,會(huì)實(shí)現(xiàn)以下兩種登錄功能:

  1. 用戶名+密碼+圖片驗(yàn)證碼

  1. 手機(jī)號(hào)登錄

這兩種方式可以同時(shí)存在,并且互不干預(yù)。

本教程會(huì)通過(guò)閱讀其內(nèi)置的用戶名密碼登錄的源碼,以及結(jié)合官網(wǎng)文檔來(lái)實(shí)現(xiàn)一些自定義的登錄方式。

注:教程主要是開(kāi)發(fā)思路的講解,其中涉及到的代碼僅供參考,為了方便,很多地方忽略了一些細(xì)節(jié)

1.快速開(kāi)始

1.1創(chuàng)建工程

創(chuàng)建一個(gè)名為springsecurity的工作,springboot的版本選擇2.7.3

1.2引入springsecurity依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--security依賴-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!--測(cè)試相關(guān)-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

啟動(dòng)程序,在瀏覽器中輸入localhost:你的端口號(hào),就會(huì)出現(xiàn)如下的界面

springboot2.7整合springSecurity

輸入用戶名和密碼即可登錄,這里的用戶名默認(rèn)是user,密碼則會(huì)打印在控制臺(tái)上

2.原理初探

2.1springSecurity的作用

Spring Security是一個(gè)提供了認(rèn)證、授權(quán)和防止常見(jiàn)攻擊的框架

認(rèn)證:驗(yàn)證當(dāng)前訪問(wèn)系統(tǒng)的是不是本系統(tǒng)的用戶,簡(jiǎn)單的講就是登錄

授權(quán):經(jīng)過(guò)認(rèn)證的用戶是否有權(quán)限進(jìn)行某個(gè)操作

2.2springSecurity的本質(zhì)

springSecurity提供了許多基礎(chǔ)的過(guò)濾器,當(dāng)客戶端向應(yīng)用發(fā)送請(qǐng)求,會(huì)經(jīng)過(guò)容器中已經(jīng)創(chuàng)建一個(gè)過(guò)濾鏈(許多過(guò)濾器),如圖:

springboot2.7整合springSecurity

springSecurity內(nèi)置了哪些過(guò)濾器?

springboot2.7整合springSecurity

這是官方文檔截圖的一部分,詳細(xì)的可以看官方文檔:

https://docs.spring.io/spring-security/reference/5.7.3/servlet/architecture.html

另外我們?cè)趩?dòng)程序的時(shí)候,也可以看到springSecurity創(chuàng)建了哪些過(guò)濾器:

springboot2.7整合springSecurity

2.3表單登錄流程

一般前后端不分離的項(xiàng)目,登錄流程如圖

springboot2.7整合springSecurity

用戶提交用戶名和密碼,服務(wù)器會(huì)進(jìn)行用戶名密碼比對(duì),如果失敗,會(huì)跳轉(zhuǎn)到登錄頁(yè),重新登錄,如果成功,會(huì)把用戶信息保存到session中,并且跳轉(zhuǎn)到能訪問(wèn)的首頁(yè)

2.4走進(jìn)源碼

閱讀哪一塊的源碼?怎么入手?

springboot2.7整合springSecurity

從官方文檔的截圖,從圖可以知道,用戶名密碼驗(yàn)證的主要是從UsernamePasswordAuthenticationFilter這個(gè)過(guò)濾器開(kāi)始的。

2.5AbstractAuthenticationProcessingFilter

打開(kāi)源碼可以看到UsernamePasswordAuthenticationFilter是繼承AbstractAuthenticationProcessingFilter,因此我們就從它開(kāi)始閱讀。

AbstractAuthenticationProcessingFilter是基于基于瀏覽器的HTTP身份驗(yàn)證請(qǐng)求的抽象處理器,實(shí)現(xiàn)了整個(gè)身份的驗(yàn)證過(guò)程,下面會(huì)閱讀重點(diǎn)部分的代碼

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
	//(1)如果不是登錄驗(yàn)證請(qǐng)求,就直接放行,執(zhí)行下一個(gè)過(guò)濾器
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    try {
        //(2)登錄驗(yàn)證的核心方法,具體實(shí)現(xiàn)是在UsernamePasswordAuthenticationFilter
        Authentication authenticationResult = attemptAuthentication(request, response);
        if (authenticationResult == null) {
            return;
        }
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);

        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        //(3)驗(yàn)證成功后的處理邏輯
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        //(4)驗(yàn)證失敗后的處理邏輯
        unsuccessfulAuthentication(request, response, ex);
    }
}

這一段代碼是整個(gè)認(rèn)證的核心,

(1)判斷用戶的請(qǐng)求是不是登錄認(rèn)證的請(qǐng)求,例如我們下面的認(rèn)證接口"/admin/login"

(2)登錄驗(yàn)證的核心方法,里面實(shí)現(xiàn)了用戶名密碼認(rèn)證

(3)驗(yàn)證成功后的處理邏輯

(4)驗(yàn)證失敗后的處理邏輯

我們先來(lái)分析一下驗(yàn)證成功后的處理邏輯

successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
       Authentication authResult) throws IOException, ServletException {
    //(1)SecurityContextHolder類很重要,主要是創(chuàng)建一個(gè)線程池保存用戶信息,后面接口要使用到用戶信息時(shí)就可以通過(guò)這個(gè)類去獲取
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    //這里是記住登錄狀態(tài)的功能
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    //(2)登錄成功后的相關(guān)處理,例如跳轉(zhuǎn)到首頁(yè)
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

我們可以看看this.successHandler是什么,

private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

我們可以看到this.successHandler初始化值是SavedRequestAwareAuthenticationSuccessHandler,我們進(jìn)這個(gè)類看看onAuthenticationSuccess方法

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws ServletException, IOException {
    SavedRequest savedRequest = this.requestCache.getRequest(request, response);
    if (savedRequest == null) {
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    String targetUrlParameter = getTargetUrlParameter();
    if (isAlwaysUseDefaultTargetUrl()
        || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
        this.requestCache.removeRequest(request, response);
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    clearAuthenticationAttributes(request);
    // Use the DefaultSavedRequest URL
    String targetUrl = savedRequest.getRedirectUrl();
    getRedirectStrategy().sendRedirect(request, response, targetUrl);
}

這段邏輯主要是登錄成功后跳轉(zhuǎn)到哪一個(gè)頁(yè)面。

之所以會(huì)講這一段,主要是因?yàn)槲覀兒竺孀远x自己的登錄方式會(huì)涉及到。例如前后端登錄成功后,使用redis保存用戶信息以及生產(chǎn)一竄token返回給前端。

這時(shí)我們可以參考SavedRequestAwareAuthenticationSuccessHandler,創(chuàng)建自己的AuthenticationSuccessHandler,然后賦值給this.successHandler

驗(yàn)證失敗后的處理邏輯這塊就不講源碼了,它的原理跟成功是一樣的,后面我們自定義一個(gè)失敗的handler去替換就好。

2.6UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是繼承AbstractAuthenticationProcessingFilter這個(gè)抽象類的,里面有一個(gè)非常重要的方法attemptAuthentication,里面實(shí)現(xiàn)了身份驗(yàn)證處理的邏輯

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    //(1)判斷是否是post提交
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    //(2)根據(jù)用戶名和密碼生成一個(gè)身份認(rèn)證令牌,其實(shí)就是保存一些用戶登錄的信息
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                                                                                                          password);
    setDetails(request, authRequest);
    //(3)調(diào)用這個(gè)AuthenticationManager接口類中的authenticate驗(yàn)證接口
    return this.getAuthenticationManager().authenticate(authRequest);
}

根據(jù)用戶名和密碼生成一個(gè)身份認(rèn)證令牌,然后把令牌傳到 AuthenticationManager 進(jìn)行驗(yàn)證。我們先來(lái)看看

UsernamePasswordAuthenticationToken

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	//用戶信息
	private final Object principal;
	//用戶憑證
	private Object credentials;
	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
        //這里是權(quán)限信息
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true);
	}
	
    //(1)第一次驗(yàn)證時(shí)調(diào)用這個(gè)方法
	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}
	
    //(2)已經(jīng)驗(yàn)證過(guò)后可以調(diào)用此方法
	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}
}

這個(gè)類比較意思,principal和credentials數(shù)據(jù)類型是object,接著往下看,后面會(huì)有我們的答案。

現(xiàn)在重點(diǎn)看看this.getAuthenticationManager().authenticate(authRequest)

我們打開(kāi)AuthenticationManager

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

這只是一個(gè)接口,其實(shí)現(xiàn)是在ProviderManager

2.7ProviderManager

springsecurity會(huì)提供許多AuthenticationProvider,而ProviderManager這個(gè)類就處理這些AuthenticationProvider

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    @Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
        //在容器啟動(dòng)時(shí),springsecurity已經(jīng)初始化了一些provider,其中就有DaoAuthenticationProvider,其主要是用戶名和密碼驗(yàn)證的
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
                //如果provider不支持UsernamePasswordAuthenticationToken,就跳到下一個(gè)provider
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
                //(1)DaoAuthenticationProvider就是這個(gè)provider
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}
}

這個(gè)類的內(nèi)容不是重點(diǎn),重點(diǎn)是這個(gè)DaoAuthenticationProvider,而它又是繼承AbstractUserDetailsAuthenticationProvider這個(gè)抽象類,因此我們先來(lái)了解一下它

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
        //(1)根據(jù)用戶名從緩存中獲取用戶信息
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                //(2)調(diào)用實(shí)現(xiàn)類的DaoAuthenticationProvider的retrieveUser,這里會(huì)得到保存在內(nèi)存中的用戶信息,如密碼,賬號(hào)狀態(tài)等
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
           	//(3)判斷用戶狀態(tài),是否是啟用
			this.preAuthenticationChecks.check(user);
            //(4)密碼比較
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
        //(5)創(chuàng)建一個(gè)成功的Authentication對(duì)象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
}

這里面實(shí)現(xiàn)的邏輯是這樣的

(2)調(diào)用實(shí)現(xiàn)類的DaoAuthenticationProvider的retrieveUser,獲取到保存在內(nèi)存中的用戶信息,如密碼,賬號(hào)狀態(tài)等

(3)判斷用戶狀態(tài)是否有效

(4)獲取的密碼跟提交過(guò)來(lái)的密碼是否一致

打開(kāi)DaoAuthenticationProvider,再看看retrieveUser

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
            //最終實(shí)現(xiàn)查詢用戶信息的代碼在這里
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
}

雖然這里的代碼很多,但簡(jiǎn)單來(lái)講就是調(diào)用UserDetailsService里面的loadUserByUsername接口查詢用戶信息,然后進(jìn)行密碼校驗(yàn)。

因此后面我們?cè)谧鍪謾C(jī)登錄的時(shí)候,就可以自定義一個(gè)provider,然后調(diào)用一個(gè)根據(jù)手機(jī)號(hào)查詢用戶信息的接口即可。

接下來(lái)重點(diǎn)看看這一段

//最終實(shí)現(xiàn)查詢用戶信息的代碼在這里
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

2.8InMemoryUserDetailsManager

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername是接口UserDetailsService里面的一個(gè)方法,是通過(guò)用戶名獲取用戶信息,其實(shí)現(xiàn)是在InMemoryUserDetailsManager

public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(1)從緩存中獲取用戶信息
		UserDetails user = this.users.get(username.toLowerCase());
		if (user == null) {
			throw new UsernameNotFoundException(username);
		}
        //(2)返回一個(gè)UserDetails
		return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
				user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
	}
}

springSecurity獲取用戶信息,默認(rèn)從緩存中換取的,然后返回一個(gè)UserDetails,我們看看其定義的User類,下面只是列出用部分

public class User implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);
	
	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;


	public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		this(username, password, true, true, true, true, authorities);
	}

	public User(String username, String password, boolean enabled, boolean accountNonExpired,
			boolean credentialsNonExpired, boolean accountNonLocked,
			Collection<? extends GrantedAuthority> authorities) {
		Assert.isTrue(username != null && !"".equals(username) && password != null,
				"Cannot pass null or empty values to constructor");
		this.username = username;
		this.password = password;
		this.enabled = enabled;
		this.accountNonExpired = accountNonExpired;
		this.credentialsNonExpired = credentialsNonExpired;
		this.accountNonLocked = accountNonLocked;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}

	@Override
    //權(quán)限
	public Collection<GrantedAuthority> getAuthorities() {
		return this.authorities;
	}
}

首先這個(gè)類繼承UserDetails了,定義了一些基本信息,最重要的是用戶名、密碼、權(quán)限。

后面我們會(huì)重寫(xiě)這部分邏輯,我們會(huì)重寫(xiě)loadUserByUsername,使其查詢我們的數(shù)據(jù)庫(kù)。

2.9SecurityContextPersistenceFilter

當(dāng)?shù)卿洺晒?,再次發(fā)送請(qǐng)求時(shí),springsecurity時(shí)如何知道該用戶已經(jīng)通過(guò)驗(yàn)證的?

這時(shí)我們就要認(rèn)識(shí)一下SecurityContextPersistenceFilter

它的作用主要是: 創(chuàng)建SecurityContext安全上下文信息和請(qǐng)求結(jié)束時(shí)清空SecurityContextHolder

接下來(lái)看源碼

@Deprecated
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    @Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		//(1)同一個(gè)請(qǐng)求只處理一次
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		if (this.forceEagerSessionCreation) {
            //(2)創(chuàng)建一個(gè)HttpSession
			HttpSession session = request.getSession();
			if (this.logger.isDebugEnabled() && session.isNew()) {
				this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
			}
		}
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        //(3)獲取登錄用戶的相關(guān)信息,如果沒(méi)有登錄,用戶信息為空
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
            //(4)SecurityContextHolder綁定SecurityContext對(duì)象
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}
            //(5)下一個(gè)過(guò)濾器處理
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// (5)在任何其他操作之前刪除SecurityContextHolder內(nèi)容
			SecurityContextHolder.clearContext();
            //(6)重新保存用戶信息
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
			this.logger.debug("Cleared SecurityContextHolder to complete request");
		}
	}
}

到現(xiàn)在為止,登錄驗(yàn)證的主要源碼已經(jīng)介紹完畢,如果沒(méi)有看明白,不要緊,只要有個(gè)印象就行,接下來(lái)自定義自己的登錄接口時(shí),會(huì)仿照其源碼。

3.準(zhǔn)備工作

3.1創(chuàng)建數(shù)據(jù)表

/*用戶角色映射表*/
CREATE TABLE `sys_user_role`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` bigint(200) NOT NULL COMMENT '用戶id',
  `role_id` bigint(200) NOT NULL DEFAULT 0 COMMENT '角色id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

/*用戶表*/
CREATE TABLE `sys_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '用戶名',
  `nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '昵稱',
    `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '電話',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '密碼',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '賬號(hào)狀態(tài)(0正常 1停用)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用戶表' ROW_FORMAT = Dynamic;

/*角色菜單映射表*/
CREATE TABLE `sys_role_menu`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `role_id` bigint(200) NOT NULL COMMENT '角色I(xiàn)D',
  `menu_id` bigint(200) NOT NULL DEFAULT 0 COMMENT '菜單id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

/*角色表*/
CREATE TABLE `sys_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

/*菜單表*/
CREATE TABLE `sys_menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '菜單名',
  `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '權(quán)限標(biāo)識(shí)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜單表' ROW_FORMAT = Dynamic;

新建一條用戶數(shù)據(jù):賬號(hào):test 密碼:123456

INSERT INTO `springsecurity`.`sys_user`(`user_name`, `nick_name`, `password`, `phone`, `status`) VALUES ('test', '1', '$2a$12$pgFnH5Ot.XIvbaTM7X9nNe8AGwBV.3eggszusKShXXG2HJ1fFdNMO', '13662301000', '0');

3.2生成代碼

本教程使用mybatis-plus

引入數(shù)據(jù)庫(kù)相關(guān)依賴:

<!--操作mysql的相關(guān)依賴-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.32</version>
</dependency>

<!--代碼生成器相關(guān)依賴-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
</dependency>

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.28</version>
</dependency>

配置:

server.port=8002

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?Unicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

生成方法:

package com.example.springsecuritydemo1;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.TemplateType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.ibatis.annotations.Mapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collections;

@SpringBootTest
class AutoGeneratorTests {

    @Test
    void contextLoads() {
        FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/springsecurity","root","root")
                .globalConfig(builder -> {
                    builder.author("myh") // 設(shè)置作者
                            .outputDir("F:\\javaStudy\\springsecurity-demo1\\src\\main\\java"); // 指定輸出目錄
                })
                .packageConfig(builder -> {
                    builder.parent("com.example.springsecuritydemo1") // 設(shè)置父包名
                            .moduleName("user") // 設(shè)置父包模塊名
                            .entity("entity")
                            .service("service")
                            .serviceImpl("service.impl")
                            .mapper("mapper")
                            .xml("mapper.xml")
                            .controller("controller")
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "F:\\javaStudy\\springsecurity-demo1\\src\\main\\resources\\mapper")); // 設(shè)置mapperXml生成路徑
                })
                .templateConfig(builder -> {
                    builder.disable(TemplateType.ENTITY)
                            .entity("/templates/entity.java")
                            .service("/templates/service.java")
                            .serviceImpl("/templates/serviceImpl.java")
                            .mapper("/templates/mapper.java")
                            .controller("/templates/controller.java");
                })
                .strategyConfig(builder -> {
                    builder.controllerBuilder().enableRestStyle();
                    builder.entityBuilder().enableLombok();
                    builder.mapperBuilder().mapperAnnotation(Mapper.class);
                    builder.serviceBuilder().formatServiceFileName("Service");
                    builder.serviceBuilder().formatServiceImplFileName("ServiceImp");
                    builder.addInclude("sys_menu") // 設(shè)置需要生成的表名
                            .addInclude("sys_user_role").addInclude("sys_user").addInclude("sys_role").addInclude("sys_role_menu");
                    // .addTablePrefix("sys_"); // 設(shè)置過(guò)濾表前綴

                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默認(rèn)的是Velocity引擎模板
                .execute();
    }

}

3.3其他

主要使用hutool這個(gè)基礎(chǔ)工具類

引入redis及相關(guān)工具類

<!--hutool基礎(chǔ)工具類-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.20</version>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.33</version>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>

redis配置

#-------------------------------------------------------------------------------
# Redis客戶端配置樣例
# 每一個(gè)分組代表一個(gè)Redis實(shí)例
# 無(wú)分組的Pool配置為所有分組的共用配置,如果分組自己定義Pool配置,則覆蓋共用配置
# 池配置來(lái)自于:https://www.cnblogs.com/jklk/p/7095067.html
#-------------------------------------------------------------------------------

#----- 默認(rèn)(公有)配置
# 地址,默認(rèn)localhost
host = localhost
# 端口,默認(rèn)6379
port = 6379
# 超時(shí),默認(rèn)2000
timeout = 2000
# 連接超時(shí),默認(rèn)timeout
connectionTimeout = 2000
# 讀取超時(shí),默認(rèn)timeout
soTimeout = 2000
# 密碼,默認(rèn)無(wú)
password = 12345
# 數(shù)據(jù)庫(kù)序號(hào),默認(rèn)0
database = 0
# 客戶端名,默認(rèn)"Hutool"
clientName = Hutool
# SSL連接,默認(rèn)false
ssl = false;

統(tǒng)一返回值類:

package com.demo.springsecurity.common.util;

import lombok.Data;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 9:38
 */
@Data
public class ResultUtil<T> {
    private Integer code;
    private String message;
    private T data;

    public ResultUtil() {
        this.code = 200;
        this.message = "success";
    }

    public ResultUtil(Integer code, String msg) {
        this.code = code;
        this.message = msg;
    }

    public ResultUtil(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public ResultUtil(T data) {
        this.code = 200;
        this.message = "success";
        this.data = data;
    }

    public static <T> ResultUtil<T> ok(){
        return new ResultUtil<T>();
    }

    public static <T> ResultUtil<T> ok(T data){
        return new ResultUtil<T>(data);
    }

    public static <T> ResultUtil<T> ok(Integer code,String message){
        return new ResultUtil<T>(code,message);
    }

    public static <T> ResultUtil<T> fail(String message){
        return new ResultUtil<T>(500,message);
    }

    public static <T> ResultUtil<T> fail(Integer code,String message){
        return new ResultUtil<T>(code,message);
    }
}

全局異常及自定義異常封裝

package com.demo.springsecurity.common.exception;

import com.demo.springsecurity.common.util.ResultUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 18:13
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResultUtil<Void> error(Exception e){
        return ResultUtil.fail(e.getMessage());
    }

    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public ResultUtil<Void> serviceException(ServiceException e){
        return ResultUtil.fail(e.getCode(),e.getMsg());
    }
}
package com.demo.springsecurity.common.exception;

import lombok.Data;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 18:16
 */
@Data
public class ServiceException extends RuntimeException{
    private Integer code;
    private String msg;

    public ServiceException(Integer code,String msg){
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public ServiceException(String msg){
        super(msg);
        this.code = 500;
        this.msg = msg;
    }

}

4.用戶名密碼登錄驗(yàn)證

4.1登錄校驗(yàn)流程

springboot2.7整合springSecurity

這張圖網(wǎng)上抄的,我們前后端分離的實(shí)現(xiàn)過(guò)程就按照這個(gè)流程

4.2圖片驗(yàn)證碼

@GetMapping("admin/code")
public void getHtoolVerifyCode(HttpServletResponse response) throws IOException {
    RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
    LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
    lineCaptcha.setGenerator(randomGenerator);
    lineCaptcha.createCode();
    Jedis jedis = RedisDS.create().getJedis();
    jedis.set(RedisKey.ADMIN_VERIFY_CODE +lineCaptcha.getCode(),lineCaptcha.getCode());
    response.setContentType(MediaType.IMAGE_PNG_VALUE);
    lineCaptcha.write(response.getOutputStream());
}

使用hutool生成驗(yàn)證碼,并且存儲(chǔ)到緩存里面,注意key的規(guī)則 特殊字符+當(dāng)前驗(yàn)證碼作為key

開(kāi)頭的時(shí)候已經(jīng)說(shuō)了,引入security后所有的方法需要經(jīng)過(guò)登錄驗(yàn)證才能訪問(wèn),而我們的登錄接口和驗(yàn)證碼是不要登錄驗(yàn)證的,因此需要在配置文件中放行這兩個(gè)接口

新建一個(gè)SecurityConfig配置類,這個(gè)配置文件后面會(huì)詳細(xì)

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auhtor-> auhtor
                        //這里配置的接口不需要驗(yàn)證
                        .antMatchers("/admin/code","/admin/login").permitAll()
                        //其它接口都需要經(jīng)過(guò)驗(yàn)證
                        .anyRequest().authenticated()

                );
        return http.build();
    }
}

"/admin/code","/admin/login"這兩個(gè)請(qǐng)求被允許通過(guò),anyRequest()其它任何請(qǐng)求必須經(jīng)過(guò)驗(yàn)證

4.3自定義登錄過(guò)濾器

接下來(lái)就是重頭戲了,如何仿照源碼做開(kāi)發(fā)了,按照我們?cè)创a分析的流程,創(chuàng)建一個(gè)名為AdminUsernamePasswordAuthenticationFilter過(guò)濾器,找到UsernamePasswordAuthenticationFilter,直接復(fù)制源碼,然后進(jìn)行修改,以下是貼出核心代碼:

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//(1)新增驗(yàn)證碼
public static final String SPRING_SECURITY_FORM_CODE_KEY  = "code";
//(2)改成我們登錄地址
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/admin/login", "POST");

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY;

private boolean postOnly = true;

public AdminUsernamePasswordAuthenticationFilter() {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}

public AdminUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    //(3)驗(yàn)證碼判斷
    String code = obtainCode(request);
    Jedis jedis = RedisDS.create().getJedis();
    if (!jedis.exists(RedisKey.ADMIN_VERIFY_CODE+code)){
        System.out.print("Verification Code is error");
        throw new AuthenticationServiceException("Verification Code is error");
    }else{
        //刪除緩存
        jedis.del(RedisKey.ADMIN_VERIFY_CODE+code);
    }

    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                                                                                                          password);
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

這里只是加了一個(gè)圖片驗(yàn)證碼的校驗(yàn)。

當(dāng)然這里還有另外一種方式,因?yàn)閁sernamePasswordAuthenticationFilter本身就提供用戶名密碼驗(yàn)證的功能,我們可以使用它,至于驗(yàn)證碼就定義一個(gè)過(guò)濾器進(jìn)行校驗(yàn)。

另外還有一點(diǎn),這里并沒(méi)有對(duì)UsernamePasswordAuthenticationToken進(jìn)行重新定義,因?yàn)檫@個(gè)類本身就基于用戶名和密碼的,如果自定義,后面會(huì)遇到一些坑,你可以回顧一下ProviderManager源碼,里面就有提高到過(guò):

if (!provider.supports(toTest)) {
    //如果provider不支持UsernamePasswordAuthenticationToken,就跳到下一個(gè)provider
    continue;
}

到這里初步的登錄驗(yàn)證已經(jīng)完成,接下來(lái)就是如何將自己的過(guò)濾器注入到sprngsecurity的過(guò)濾鏈中

4.4添加到springsecurity過(guò)濾鏈

上面我們已經(jīng)定義了一個(gè)AdminUsernamePasswordAuthenticationFilter過(guò)濾器,必須把它添加到容器中,它才能生效

新建一個(gè)名為SpringSecurityAdminConfig的配置文件,用來(lái)管理后臺(tái)用戶名密碼登錄,其內(nèi)容如下

@Component
public class SpringSecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
        adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        //注入過(guò)濾器,addFilterAt替換UsernamePasswordAuthenticationFilter
        http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

注意我們是用adminUsernamePasswordAuthenticationFilter替換了原來(lái)的UsernamePasswordAuthenticationFilter

然后在SecurityConfig中加載SpringSecurityAdminConfig即可

@EnableWebSecurity
public class SecurityConfig {
    @Resource
    SpringSecurityAdminConfig springSecurityAdminConfig;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            	//禁用它,因?yàn)榍昂蠖朔蛛x不需要
            	.csrf(AbstractHttpConfigurer::disable)
            	//禁用session
            	.sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auhtor-> auhtor
                        .antMatchers("/admin/code","/admin/login").permitAll()
                        .anyRequest().authenticated()

                );

        //后臺(tái)登錄配置
        http.apply(springSecurityAdminConfig);
        return http.build();
    }
}

http.apply就說(shuō)引入其它配置,包括后面的手機(jī)號(hào)登錄也是這樣,這樣比較方便管理。

另外還禁用了csrf和session,因?yàn)榍昂蠖朔蛛x的項(xiàng)目不需要這兩個(gè)東西

//禁用它,因?yàn)榍昂蠖朔蛛x不需要
.csrf(AbstractHttpConfigurer::disable)
//禁用session
.sessionManagement(AbstractHttpConfigurer::disable)

如何知道它是否添加到容器中?

在"2.2springSecurity的本質(zhì)"有提到,每次啟動(dòng)時(shí),控制臺(tái)都會(huì)打印出存在容器中的過(guò)濾器,是否添加成功,只要看一下就知道了。

4.5從數(shù)據(jù)庫(kù)查詢用戶信息

雖然我們自定義了登錄過(guò)濾器,但并沒(méi)有改變用戶信息來(lái)源于內(nèi)存,而不是數(shù)據(jù)庫(kù)。在閱讀源碼的時(shí)候我們已經(jīng)說(shuō)過(guò),我們要重寫(xiě)UserDetailsService接口里面的loadUserByUsername方法。

新建BaseUserDetailsService類,繼承UserDetailsService,內(nèi)容如下:

package com.example.springsecuritydemo1.springsecurity.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * @author mayanhui
 */
public interface BaseUserDetailsService extends UserDetailsService {

    /**
     *手機(jī)號(hào)登錄
     */
    UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException;

    /**
     *微信
     */
    //UserDetails loadUserByAppId(String appId) throws UsernameNotFoundException;

}

這里我們擴(kuò)展了通過(guò)手機(jī)號(hào)查詢用戶信息的方法,這個(gè)我們后面會(huì)用到。

接下來(lái)就是新建UserDetailServiceImpl類,實(shí)現(xiàn)這兩個(gè)方法

@Service
public class UserDetailServiceImpl implements BaseUserDetailsService {
	@Autowired
    SysUserMapper sysUserMapper;
    
    @Override
    public SysUserDTO loadUserByUsername(String username) throws UsernameNotFoundException {
        //通過(guò)用戶名查詢用戶信息
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUserName, username);
        List<SysUser> sysUsers = sysUserMapper.selectList(wrapper);
        if (CollectionUtils.isEmpty(sysUsers)){
            throw new ServiceException("該用戶不存在");
        }

        //獲取權(quán)限信息
        List<SysMenu> userHasMenu = sysUserMapper.getUserHasMenu(sysUsers.get(0).getId());
        
        return new SysUserDTO(sysUsers.get(0),userHasMenu);
    }

    @Override
    public SysUserDTO loadUserByPhone(String phone) throws UsernameNotFoundException {
        return null;
    }
}

這些接口的返回值類型都是UserDetails,這里的SysUserDTO是繼承UserDetails,主要是保存用戶信息及權(quán)限信息,可以參考一下其內(nèi)置的User類,然后做出一下的修改

@Data
//(1)必須繼承UserDetails
public class SysUserDTO implements UserDetails {
    //(2)把用戶信息封裝成實(shí)體類,比較容易管理和操作,比如說(shuō)新增一些字段,只需在實(shí)體類里面加上即可
    private SysUser sysUser;
	
    //(3)權(quán)限信息,這里需要注意的是要禁止序列化,不然存儲(chǔ)到緩存中會(huì)有問(wèn)題
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

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

    @Override
    public String getPassword() {
        return this.getPassword();
    }

    @Override
    public String getUsername() {
        return this.getUsername();
    }
	
    @Override
    public boolean isAccountNonExpired() {
        //賬號(hào)是否過(guò)期,因?yàn)橛脩舯砝锩鏇](méi)有這個(gè)字段,因此默認(rèn)賬號(hào)不過(guò)期,下面幾個(gè)方法同理
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

到這里就已經(jīng)寫(xiě)好了從數(shù)據(jù)庫(kù)中查詢用戶信息的代碼,接下來(lái)就是怎樣使springsecurity使用我們寫(xiě)的loadUserByUsername這個(gè),而不是其默認(rèn)的。

springsecurity為此提供了使用配置的方式:

@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    SpringSecurityAdminConfig springSecurityAdminConfig;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auhtor-> auhtor
                        .antMatchers("/admin/code", "/admin/login").permitAll()
                        .anyRequest().authenticated())
                .cors();
        //后臺(tái)登錄配置
        http.apply(springSecurityAdminConfig);
        //注入新的AuthenticationManager
        http.authenticationManager(authenticationManager(http));

        return http.build();
    }

    /**
     *密碼加密規(guī)則
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    /**
     *構(gòu)造一個(gè)AuthenticationManager,使用自定義的userDetailsService和passwordEncoder
     */
    @Bean
    AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
        return authenticationManager;
    }

}

首先我們是使用AuthenticationManagerBuilder這個(gè)構(gòu)建器重新構(gòu)造了一個(gè)AuthenticationManager,然后綁定我們寫(xiě)的userDetailService。

還記得創(chuàng)建表的時(shí)候插入了這樣的一條用戶數(shù)據(jù)

INSERT INTO `springsecurity`.`sys_user`(`user_name`, `nick_name`, `password`, `phone`, `status`) VALUES ('test', '1', '$2a$12$pgFnH5Ot.XIvbaTM7X9nNe8AGwBV.3eggszusKShXXG2HJ1fFdNMO', '13662301000', '0');

其密碼是預(yù)先經(jīng)過(guò)BCryptPasswordEncoder加密的,因此在做密碼校驗(yàn)的時(shí)候也要使用它,而不是使用springSecurity默認(rèn)的。

最后就是替換原來(lái)的

//注入新的AuthenticationManager
http.authenticationManager(authenticationManager(http));

接下來(lái)就登錄測(cè)試一下:

springboot2.7整合springSecurity

not found "/" 說(shuō)明已經(jīng)登錄成功了,還記得開(kāi)頭那會(huì),登錄成功后會(huì)跳轉(zhuǎn)到首頁(yè)(/),但很顯然這并不滿足前后的分離項(xiàng)目,因此還需要進(jìn)行改造。

4.6登錄成功/失敗處理器

前后端分離的項(xiàng)目是通過(guò)header攜帶token進(jìn)行驗(yàn)證的,因此登錄成功后需要返回一竄token,我們?cè)陂喿xAbstractAuthenticationProcessingFilter源碼時(shí)已經(jīng)講過(guò)其默認(rèn)是跳轉(zhuǎn)到固定頁(yè)面,因此需要我們需要自定義一個(gè)successHandlerfailureHandler

新建AdminAuthenticationSuccessHandler,讓它繼承AuthenticationSuccessHandler

/**
 * @Description: 后臺(tái)登錄成功處理器
 * @Author: mayanhui
 * @Date: 2023/2/14 12:43
 */
@Component
public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //拿到登錄用戶信息
        SysUserDTO userDetails = (SysUserDTO)authentication.getPrincipal();

        //生成jwt(token)
        Map<String, Object> map = new HashMap<>();
        map.put("uid", userDetails.getSysUser().getId());
        map.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
        String jwtToken = JWTUtil.createToken(map, "1234".getBytes());

        //將用戶信息保存到redis
        Jedis jedis = RedisDS.create().getJedis();
        String key = RedisKey.ADMIN_USER_INFO + userDetails.getSysUser().getId().toString();
        jedis.set(key,JSON.toJSONString(userDetails));

        //當(dāng)前token也保存到redis//單點(diǎn)登錄
        jedis.set(RedisKey.ADMIN_USER_TOKEN + userDetails.getSysUser().getId().toString(),jwtToken);

        Map<String,Object> resultMap = new HashMap<>();
        resultMap.put("token", TokenHeader.ADMIN_TOKEN_PREFIX+jwtToken);

        //輸出結(jié)果
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        				      httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.ok(resultMap)));
    }
}

注意在返回token的時(shí)候加了一個(gè)前綴,TokenHeader.ADMIN_TOKEN_PREFIX,其作用就說(shuō)區(qū)分用戶名密碼登錄還是其它登錄方式生產(chǎn)的token。

接下來(lái)就是將它傳入到AdminUsernamePasswordAuthenticationFilter,因?yàn)檫@里是這樣的:

AdminUsernamePasswordAuthenticationFilter繼承AbstractAuthenticationProcessingFilter,同時(shí)繼承了相關(guān)屬性

打開(kāi)SpringSecurityAdminConfig,創(chuàng)建過(guò)濾器的時(shí)候,設(shè)置一下AuthenticationSuccessHandler即可

@Component
public class SpringSecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
        adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        
   adminUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);//傳入我們自定義的成功的處理器

        //注入過(guò)濾器,addFilterAt替換UsernamePasswordAuthenticationFilter
        http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

同理,登錄失敗的處理器也是這個(gè)邏輯

/**
 * @Description: 后臺(tái)登錄失敗處理器
 * @Author: mayanhui
 * @Date: 2023/2/14 12:43
 */
@Component
public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //修改編碼格式
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");

        if (e instanceof BadCredentialsException){
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,"用戶名或密碼錯(cuò)誤")));
        }else {
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,e.getMessage())));
        }

    }
}

啟動(dòng)測(cè)試一下:

springboot2.7整合springSecurity

4.7認(rèn)證過(guò)濾器

當(dāng)我們登錄成功后,是通過(guò)token去訪問(wèn)我們接口的,因此需要自定義一個(gè)過(guò)濾器,這個(gè)過(guò)濾器會(huì)去獲取請(qǐng)求頭中的token,對(duì)token進(jìn)行解析取出其中的用戶ID,然后根據(jù)用戶ID去獲取緩存中的用戶信息,存入到SecurityContextHolder中

@Component
@Slf4j
public class AdminAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        //獲取token
        String token = request.getHeader("token");
        if (!StringUtils.hasLength(token) || !token.startsWith(TokenHeader.ADMIN_TOKEN_PREFIX)){
            //如果不存在和前綴不是TokenHeader.ADMIN_TOKEN_PREFIX,就放行到下一個(gè)過(guò)濾器
            chain.doFilter(request, response);
            SecurityContextHolder.clearContext();
            return;
        }
        //獲取真實(shí)的token(去掉前綴)
        String authToken = token.substring(TokenHeader.ADMIN_TOKEN_PREFIX.length());

        //解析token
        JWT jwt;
        String code = null;
        try{
            jwt = JWTUtil.parseToken(authToken);
        }catch (Exception e){
            chain.doFilter(request, response);
            return;
        }
        if (!Objects.isNull(jwt.getPayload("uid"))) {
            code = jwt.getPayload("uid").toString();
        }
        if (!StringUtils.hasLength(code)){
            chain.doFilter(request, response);
            return;
        }

        Jedis jedis = RedisDS.create().getJedis();
        //單點(diǎn)登錄
        if (!authToken.equals(jedis.get(RedisKey.ADMIN_USER_TOKEN+code))){
            chain.doFilter(request, response);
            return;
        }

        //從緩存中獲取用戶信息
        String key = RedisKey.ADMIN_USER_INFO + code;
        if (!jedis.exists(key)){
            chain.doFilter(request, response);
            return;
        }

        Object userObj = jedis.get(key);
        if (Objects.isNull(userObj)){
            chain.doFilter(request, response);
            return;
        }

        //保存相關(guān)的驗(yàn)證信息
        SysUserDTO user = JSONObject.parseObject(userObj.toString(), SysUserDTO.class);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}

這里我們繼承OncePerRequestFilter,看名字大概猜出它的意思:“同一個(gè)請(qǐng)求只處理一次”。就類似于SecurityContextPersistenceFilter源碼中的

//(1)同一個(gè)請(qǐng)求只處理一次
if (request.getAttribute(FILTER_APPLIED) != null) {
    chain.doFilter(request, response);
    return;
}

另外在保存用戶信息時(shí) ,要?jiǎng)?chuàng)建一個(gè)token

UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

然后把過(guò)濾器注入到springsecurity的過(guò)濾鏈中,打開(kāi)SpringSecurityAdminConfig

//注入過(guò)濾器,addFilterAt替換UsernamePasswordAuthenticationFilter
http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
 .addFilterBefore(adminAuthenticationTokenFilter,AdminUsernamePasswordAuthenticationFilter.class);

把它加在AdminUsernamePasswordAuthenticationFilter這個(gè)過(guò)濾器前面。

4.8認(rèn)證異常拋出

如果未登錄,或者登錄后,隨便攜帶一個(gè)token,它是如何拋出異常的?

springsecurity中的HttpSecurity對(duì)象提供了了exceptionHandling方法,只需要在配置文件中綁定相關(guān)的處理接口即可

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auhtor-> auhtor
                               .antMatchers("/admin/code", "/admin/login").permitAll()
                               .anyRequest().authenticated())
        .cors();
    //后臺(tái)登錄配置
    http.apply(springSecurityAdminConfig);
    //注入新的AuthenticationManager
    http.authenticationManager(authenticationManager(http));
	
    //認(rèn)證異常處理器
    http.exceptionHandling(ex->ex.authenticationEntryPoint(authenticationEntryPointIHandler));

    return http.build();
}

authenticationEntryPointIHandler內(nèi)容如下:

@Component
public class AuthenticationEntryPointIHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.toJsonStr(ResultUtil.fail(HttpStatus.UNAUTHORIZED.value(), "認(rèn)證失敗,請(qǐng)重新登錄")));
        response.getWriter().flush();
    }
}

效果如下:

springboot2.7整合springSecurity

4.9退出登錄

springsecurity中的HttpSecurity對(duì)象提供了logout方法,但本教程使用自定義的方式,寫(xiě)一個(gè)退出登錄的接口

@GetMapping("/admin/logout")
public ResultUtil<Void> adminLogout(){
    String key = RedisKey.ADMIN_USER_INFO + httpRequestComponent.getAdminUserId().toString();
    //刪除緩存即可
    Jedis jedis = RedisDS.create().getJedis();
    jedis.del(key);
    SecurityContextHolder.clearContext();
    return ResultUtil.ok();
}

這段代碼還是比較簡(jiǎn)單的,主要是刪除緩存及清空SecurityContextHolder里面的用戶信息,httpRequestComponent是獲取登錄用戶信息的組件,代碼如下:

@Component
public class HttpRequestComponent {

    /**
     * 獲取token
     */
    public String getToken(){
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            throw new ServiceException("授權(quán)令牌為空");
        }
        return token;
    }

    /**
     * 獲取用戶信息
     */
    public SysUserDTO getAdminUserInfo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SysUserDTO userDetialsDTO = (SysUserDTO) authentication.getPrincipal();
        if (Objects.isNull(userDetialsDTO)){
            throw new ServiceException(10000,"登錄失效,請(qǐng)重新登錄");
        }

        return userDetialsDTO;
    }

    /**
     * 獲取用戶ID
     */
    public Long getAdminUserId(){
        if (Objects.isNull(this.getAdminUserInfo().getSysUser())){
            throw new ServiceException(10000,"登錄失效,請(qǐng)重新登錄");
        }
        return this.getAdminUserInfo().getSysUser().getId();
    }
}

以上就是用戶名密碼登錄驗(yàn)證的整個(gè)過(guò)程,接下來(lái)就是手機(jī)號(hào)登錄驗(yàn)證

5.手機(jī)號(hào)登錄

手機(jī)號(hào)登錄的流程跟用戶名密碼原理是一樣的,因此改造起來(lái)就比較簡(jiǎn)單了,還是跟用戶名密碼登錄一樣,咱們照貓畫(huà)虎。

5.1獲取手機(jī)驗(yàn)證碼

@GetMapping("/mobile/code")
public String getMobileCode(String mobile){
    //產(chǎn)生四位隨機(jī)數(shù)
    long rand = RandomUtil.randomLong(1000, 9999);
    //調(diào)用手機(jī)服務(wù)商接口
    
    Jedis jedis = RedisDS.create().getJedis();
    jedis.set(RedisKey.ADMIN_VERIFY_CODE +mobile,String.valueOf(rand));

    return String.valueOf(rand);
}

獲取手機(jī)驗(yàn)證碼,這里做了模擬,通過(guò)接口返回。

注:驗(yàn)證碼接口要放行

.antMatchers("/admin/code", "/admin/login","/mobile/code").permitAll()

5.2自定義登錄過(guò)濾器

新建一個(gè)MobileAuthenticationFilter過(guò)濾器

以下是核心代碼:

public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //手機(jī)號(hào)
    public static final String SPRING_SECURITY_MOBILE_KEY = "mobile";
    //驗(yàn)證碼
    public static final String SPRING_SECURITY_CODE_KEY  = "code";

    //改成手機(jī)號(hào)登錄地址
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/mobile/login", "POST");

    private String mobileParameter = SPRING_SECURITY_MOBILE_KEY;

    private String codeParameter = SPRING_SECURITY_CODE_KEY;

    private boolean postOnly = true;

    public MobileAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public MobileAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();
		
        if (!StringUtils.hasLength(code)){
            throw new AuthenticationServiceException("Verification Code is empty");
        }
        //判斷輸入的驗(yàn)證碼是否正確
        String code = obtainCode(request);
        Jedis jedis = RedisDS.create().getJedis();
        if (!code.equals(jedis.get(RedisKey.MOBILE_VERIFY_CODE+mobile))){
            throw new AuthenticationServiceException("Verification Code is error");
        }

        //生產(chǎn)一個(gè)手機(jī)號(hào)驗(yàn)證令牌
        MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile);
        setDetails(request, mobileAuthenticationToken);

        return this.getAuthenticationManager().authenticate(mobileAuthenticationToken);
    }

	…………………………………………
}

需要注意的是跟用戶名密碼驗(yàn)證不同的是身份認(rèn)證令牌MobileAuthenticationToken,需要自定義,其內(nèi)容如下:

public class MobileAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;

    public MobileAuthenticationToken(String mobile){
        super(null);
        this.principal = mobile;
        super.setAuthenticated(false);
    }

    public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }

}

以上內(nèi)容比較簡(jiǎn)單,抄過(guò)來(lái)改動(dòng)一下即可

5.3添加到springsecurity過(guò)濾鏈

新建一個(gè)名為SpringSecurityMobileConfig的配置文件,用來(lái)管理后手機(jī)號(hào)登錄登錄,其內(nèi)容如下

@Component
public class SpringSecurityMobileConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        //注入過(guò)濾器
        http.addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
    }


}

然后在SecurityConfig中調(diào)用該配置

//后臺(tái)登錄配置
http.apply(springSecurityAdminConfig);
//注入新的AuthenticationManager
http.authenticationManager(authenticationManager(http));

//手機(jī)登錄配置
http.apply(springSecurityMobileConfig);

最后啟動(dòng)一下容器,看看控制臺(tái)是否打印出MobileAuthenticationFilter

5.4從數(shù)據(jù)庫(kù)查詢用戶信息

這里跟用戶密碼登錄有點(diǎn)區(qū)別,回顧一下,在源碼部分我們講到了ProviderManager這個(gè)管理類,其中一個(gè)DaoAuthenticationProvider,它的作用就是對(duì)用戶名進(jìn)行驗(yàn)證。

而springsecurity中沒(méi)有提供對(duì)手機(jī)號(hào)進(jìn)行驗(yàn)證的,因此這里我們需要自定義一個(gè)Provider。

public class MobileAuthenticationProvider implements AuthenticationProvider {

    private UserDetailServiceImpl userDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
		//(1)通過(guò)手機(jī)號(hào)從數(shù)據(jù)庫(kù)中查詢用戶信息
        SysUserDTO sysUserDTO = this.userDetailService.loadUserByPhone(mobileAuthenticationToken.getPrincipal().toString());

        if (Objects.isNull(sysUserDTO)){
            throw new BadCredentialsException("手機(jī)登錄失敗");
        }

        MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(sysUserDTO,sysUserDTO.getAuthorities());
        authenticationToken.setDetails(authenticationToken.getCredentials());
		
        //返回一個(gè)驗(yàn)證token
        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public void setBaseUserDetailsService(UserDetailServiceImpl userDetailsService){
        this.userDetailService = userDetailsService;
    }
}

接下來(lái)實(shí)現(xiàn)我們前面定義好的loadUserByPhone

@Override
public SysUserDTO loadUserByPhone(String phone) throws UsernameNotFoundException {
    //用戶信息
    LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(SysUser::getPhone, phone);
    List<SysUser> sysUsers = sysUserMapper.selectList(wrapper);
    if (CollectionUtils.isEmpty(sysUsers)){
        throw new ServiceException("該用戶不存在");
    }
    //不做授權(quán),把a(bǔ)uthorities設(shè)置為空

    return new SysUserDTO(sysUsers.get(0),null);
}

這里面的邏輯也很簡(jiǎn)單,參考loadUserByUsername即可。

把自定義的MobileAuthenticationProvider加入容器中

@Override
public void configure(HttpSecurity http) throws Exception {
    MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
    mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

    //new一個(gè)mobileAuthenticationProvider
    MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
    mobileAuthenticationProvider.setBaseUserDetailsService(userDetailService);

    //注入過(guò)濾器
    http.authenticationProvider(mobileAuthenticationProvider)
        .addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
}

5.5登錄成功/失敗處理器

整個(gè)邏輯和用戶名密碼登錄是一樣的,只是改了一些KEY值,這里就不重復(fù)講

MobileAuthenticationSuccessHandler

@Component
public class MobileAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //拿到登錄用戶信息
        SysUserDTO userDetails = (SysUserDTO)authentication.getPrincipal();

        //生成jwt
        Map<String, Object> map = new HashMap<>();
        map.put("uid", userDetails.getSysUser().getId());
        map.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
        String jwtToken = JWTUtil.createToken(map, "1234".getBytes());

        //將用戶信息保存到redis
        Jedis jedis = RedisDS.create().getJedis();
        String key = RedisKey.MOBILE_USER_INFO + userDetails.getSysUser().getId().toString();
        jedis.set(key,JSON.toJSONString(userDetails));

        //當(dāng)前token也保存到redis//單點(diǎn)登錄
        jedis.set(RedisKey.MOBILE_USER_TOKEN + userDetails.getSysUser().getId().toString(),jwtToken);

        Map<String,Object> resultMap = new HashMap<>();
        resultMap.put("token", TokenHeader.MOBILE_TOKEN_PREFIX+jwtToken);

        //輸出結(jié)果
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.ok(resultMap)));
    }
}

MobileAuthenticationFailureHandler

@Component
public class MobileAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //修改編碼格式
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");

        if (e instanceof BadCredentialsException){
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,"手機(jī)號(hào)登錄異常")));
        }else {
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,e.getMessage())));
        }
    }
}

SpringSecurityMobileConfig

@Component
public class SpringSecurityMobileConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    MobileAuthenticationSuccessHandler mobileAuthenticationSuccessHandler;

    @Autowired
    MobileAuthenticationFailureHandler mobileAuthenticationFailureHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(mobileAuthenticationFailureHandler);

        //new一個(gè)mobileAuthenticationProvider
        MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
        mobileAuthenticationProvider.setBaseUserDetailsService(userDetailService);

        //注入過(guò)濾器
        http.authenticationProvider(mobileAuthenticationProvider)
                .addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
    }
    
}

5.6認(rèn)證過(guò)濾器

整個(gè)邏輯和用戶名密碼登錄是一樣的,只是改了一些KEY值,這里就不重復(fù)講

@Component
@Slf4j
public class MobileAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        //獲取token
        String token = request.getHeader("token");
        if (!StringUtils.hasLength(token) || !token.startsWith(TokenHeader.MOBILE_TOKEN_PREFIX)){
            //如果不存在和前綴不是TokenHeader.MOBILE_TOKEN_PREFIX,就放行到下一個(gè)過(guò)濾器
            chain.doFilter(request, response);
            SecurityContextHolder.clearContext();
            return;
        }
        //獲取真實(shí)的token(去掉前綴)
        String authToken = token.substring(TokenHeader.MOBILE_TOKEN_PREFIX.length());

        //解析token
        JWT jwt;
        String code = null;
        try{
            jwt = JWTUtil.parseToken(authToken);
        }catch (Exception e){
            chain.doFilter(request, response);
            return;
        }
        if (!Objects.isNull(jwt.getPayload("uid"))) {
            code = jwt.getPayload("uid").toString();
        }
        if (!StringUtils.hasLength(code)){
            chain.doFilter(request, response);
            return;
        }

        Jedis jedis = RedisDS.create().getJedis();
        //單點(diǎn)登錄
        if (!authToken.equals(jedis.get(RedisKey.MOBILE_USER_TOKEN+code))){
            chain.doFilter(request, response);
            return;
        }

        //從緩存中獲取用戶信息
        String key = RedisKey.MOBILE_USER_INFO + code;
        if (!jedis.exists(key)){
            chain.doFilter(request, response);
            return;
        }

        Object userObj = jedis.get(key);
        if (Objects.isNull(userObj)){
            chain.doFilter(request, response);
            return;
        }

        //保存手機(jī)號(hào)驗(yàn)證的相關(guān)信息
        SysUserDTO user = JSONObject.parseObject(userObj.toString(), SysUserDTO.class);
        MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(user, null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}

5.7退出登錄

@GetMapping("/mobile/logout")
public ResultUtil<Void> mobileLogout(){
    String key = RedisKey.MOBILE_USER_INFO + httpRequestComponent.getAdminUserId().toString();
    //刪除緩存即可
    Jedis jedis = RedisDS.create().getJedis();
    jedis.del(key);
    SecurityContextHolder.clearContext();
    return ResultUtil.ok();
}

6.授權(quán)

6.1權(quán)限驗(yàn)證的流程

springsecurity中,會(huì)使用 AuthorizationFilter進(jìn)行權(quán)限校驗(yàn),如下圖所示(官網(wǎng)文檔截圖)

springboot2.7整合springSecurity

① AuthorizationFilter 通過(guò) SecurityContextHolder上下文對(duì)象去獲取已經(jīng)登錄的用戶信息

②AuthorizationFilter創(chuàng)建一個(gè)FilterInvocation,用于保存一些對(duì)象

③將前面的一些對(duì)象傳入AuthorizationManager,進(jìn)行驗(yàn)證

④失敗………………

⑤成功……

因?yàn)槭跈?quán)部分不需要改造,就不看源碼了,如果有興趣大家可以根據(jù)這個(gè)圖去閱讀源碼。

6.2授權(quán)信息的封裝

我們?cè)趯?shí)現(xiàn)loadUserByUsername這個(gè)方法的時(shí)候有這樣的一段代碼:

//獲取權(quán)限信息
List<GrantedAuthority> authorities = new ArrayList<>();
List<SysMenu> userHasMenu = sysUserMapper.getUserHasMenu(sysUsers.get(0).getId());
if (!CollectionUtils.isEmpty(userHasMenu)){
    for (SysMenu menu : userHasMenu){
        if (!StringUtils.hasLength(menu.getPerms())){
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(menu.getPerms());
            authorities.add(simpleGrantedAuthority);
        }
    }
}
return new SysUserDTO(sysUsers.get(0),authorities);

在查詢用戶基本信息的同時(shí)也把權(quán)限信息給查出來(lái),并且保存到緩存中。

上面我們知道了授權(quán)驗(yàn)證這個(gè)流程是springsecurity已經(jīng)封裝好,并且不需要改造它。因此接下來(lái)的問(wèn)題就是,怎么給接下來(lái)的用戶請(qǐng)求授權(quán)。

springsecurity提供了多種方式

注解方式:

在此之前需要在配置文件中開(kāi)啟授權(quán)驗(yàn)證的配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("/list")
@PreAuthorize("hasAuthority('sysUser/list')")
public ResultUtil<String> list(){
    return ResultUtil.ok(200,"訪問(wèn)列表成功");
}

基于配置的方式:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auhtor-> auhtor
                               .antMatchers("/verify/code","/admin/login").permitAll()
                          .antMatchers("/user/sysUser/list").hasAuthority("sysUser/list")
                               .anyRequest().authenticated())
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
        .cors();

    return http.build();
}

到此為止整個(gè)登錄驗(yàn)證及授權(quán)過(guò)程已經(jīng)完成

6.3授權(quán)異常拋出

在認(rèn)證異常拋出有這樣的一段代碼:

//認(rèn)證異常處理器
http.exceptionHandling(ex->ex.authenticationEntryPoint(authenticationEntryPointIHandler));

它也提供了授權(quán)異常的方法:accessDeniedHandler,我們定義一個(gè)授權(quán)異常處理器,代碼如下:

@Component
public class AccessDeniedImplHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.toJsonStr(ResultUtil.fail(HttpStatus.FORBIDDEN.value(),"沒(méi)有權(quán)限訪問(wèn)")));
        response.getWriter().flush();
    }
}

但是會(huì)發(fā)現(xiàn)并不生效

主要是因?yàn)槲覀兌x了一個(gè)全局異常,基于注解的授權(quán)方式,拋出的異常會(huì)被全局異常捕獲,因此在全局異常類中加入如下代碼:

GlobalExceptionHandler

@ExceptionHandler(AccessDeniedException.class)
public ResultUtil<Void> accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
    return ResultUtil.fail(403, "沒(méi)有權(quán)限訪問(wèn)");
}

7.微信、qq登錄

微信、qq登錄等第三方登錄也可以參考手機(jī)登錄的模式,自定義自己的登錄方式。

springSecurity也提供了OAuth 2.0登錄功能,并且實(shí)現(xiàn)了用例"使用Google登錄”或“使用GitHub登錄"

這一塊更新中……

源碼:https://gitee.com/myha/springsecurity文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-419823.html

到了這里,關(guān)于springboot2.7整合springSecurity的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • java SpringBoot2.7整合Elasticsearch(ES)7 進(jìn)行文檔增刪查改

    java SpringBoot2.7整合Elasticsearch(ES)7 進(jìn)行文檔增刪查改

    首先 我們?cè)?ES中加一個(gè) books 索引 且?guī)в蠭K分詞器的索引 首先 pom.xml導(dǎo)入依賴 application配置文件中編寫(xiě)如下配置 spring.elasticsearch.hosts: 172.16.5.10:9200 我這里是用的yml格式的 告訴它指向 我們本地的 9200服務(wù) 然后 我們?cè)趩?dòng)類同目錄下 創(chuàng)建一個(gè)叫 domain的包 放屬性類 然后在這個(gè)包

    2024年02月19日
    瀏覽(19)
  • 【SpringBoot2】SpringBoot開(kāi)發(fā)實(shí)用篇

    【SpringBoot2】SpringBoot開(kāi)發(fā)實(shí)用篇

    ? 什么是熱部署?簡(jiǎn)單說(shuō)就是你程序改了,現(xiàn)在要重新啟動(dòng)服務(wù)器,嫌麻煩?不用重啟,服務(wù)器會(huì)自己悄悄的把更新后的程序給重新加載一遍,這就是熱部署。 ? 熱部署的功能是如何實(shí)現(xiàn)的呢?這就要分兩種情況來(lái)說(shuō)了,非springboot工程和springboot工程的熱部署實(shí)現(xiàn)方式完全

    2023年04月25日
    瀏覽(31)
  • 1、Springboot2簡(jiǎn)介

    在學(xué)習(xí) SpringBoot 之前,建議先具備 SpringMVC(控制層)、Spring(業(yè)務(wù)層)和 Mybatis(持久層)的相關(guān)知識(shí) Spring 框架雖然很出色,但是有一個(gè)明顯的缺點(diǎn):配置文件過(guò)于繁瑣和復(fù)雜; 在單體項(xiàng)目中,因?yàn)榕渲梦募恍枰帉?xiě)一遍即可,所以該缺點(diǎn)只是一個(gè)小問(wèn)題; 在微服務(wù)項(xiàng)目

    2024年02月05日
    瀏覽(22)
  • SpringBoot2-核心技術(shù)(一)

    SpringBoot2-核心技術(shù)(一)

    1. properties 同以前的用法 2. yaml YAML : “YAML Ain‘t Markup Language ”(yaml 不是一種遞歸標(biāo)記語(yǔ)言) 的遞歸縮寫(xiě), 在開(kāi)發(fā)這種語(yǔ)言時(shí),YAML 的意思是:Yet Another Markup Language (仍是一種標(biāo)記語(yǔ)言) 非常適合用來(lái)做以數(shù)據(jù)為中心的配置文件 1. 基本語(yǔ)法 key: value ; value與: 之間存在空格

    2024年02月07日
    瀏覽(16)
  • SpringBoot2.0(Lombok,SpringBoot統(tǒng)一返回封裝)

    SpringBoot2.0(Lombok,SpringBoot統(tǒng)一返回封裝)

    ? java工程中,我們要?jiǎng)?chuàng)建很多的java Bean。這些javaBean中都會(huì)寫(xiě)getter,setter,equals,hashCode和toString的模板代碼,這些代碼都沒(méi)啥技術(shù)含量。 ? 那么我們就是使用Lombok來(lái)自動(dòng)生成這些代碼,通過(guò)注解的方式。提高我們的工作效率。 ? Lombok的原理:JSR 269插件化注解處理。就是在

    2024年02月09日
    瀏覽(22)
  • 【SpringBoot】SpringBoot2.x知識(shí)點(diǎn)雜記

    本文僅供學(xué)習(xí)交流使用 為什么要使用 Spring Boot 因?yàn)镾pring, SpringMVC 需要使用的大量的配置文件 (xml文件) 還需要配置各種對(duì)象,把使用的對(duì)象放入到spring容器中才能使用對(duì)象 需要了解其他框架配置規(guī)則。 SpringBoot 就相當(dāng)于 不需要配置文件的Spring+SpringMVC。 常用的框架和第三

    2024年02月03日
    瀏覽(19)
  • 3、SpringBoot2之配置文件

    3、SpringBoot2之配置文件

    在 Spring Boot 工程中,實(shí)行統(tǒng)一的配置管理,即所有參數(shù)配置都會(huì)集中到一個(gè)固定位置和命名的文件中; 配置文件的固定位置是在 src/main/resources 目錄下,該目錄是 Spring Boot 工程默認(rèn)的類路徑(classpath); 配置文件的命名格式為:application+后綴+擴(kuò)展名,擴(kuò)展名可以是 propert

    2024年02月04日
    瀏覽(22)
  • SpringBoot2.0集成WebSocket

    適用于單客戶端,一個(gè)賬號(hào)登陸一個(gè)客戶端,登陸多個(gè)客戶端會(huì)報(bào)錯(cuò) The remote endpoint was in state [TEXT_FULL_WRITING]? 這是因?yàn)榇藭r(shí)的session是不同的,只能鎖住一個(gè)session,解決此問(wèn)題的方法把全局靜態(tài)對(duì)象鎖住,因?yàn)橘~號(hào)是唯一的 新建配置類 這個(gè)注解需要打上聲明是開(kāi)發(fā)環(huán)境,否

    2024年02月11日
    瀏覽(19)
  • 8、SpringBoot2之打包及運(yùn)行

    8、SpringBoot2之打包及運(yùn)行

    為了演示高級(jí)啟動(dòng)時(shí)動(dòng)態(tài)配置參數(shù)的使用,本文在SpringBoot2之配置文件的基礎(chǔ)上進(jìn)行 普通的 web 項(xiàng)目,會(huì)被打成一個(gè) war 包,然后再將 war 包放到 tomcat 的 webapps 目錄中; 當(dāng) tomcat 啟動(dòng)時(shí),在 webapps 目錄中的 war 包會(huì)自動(dòng)解壓,此時(shí)便可訪問(wèn)該 web 項(xiàng)目的資源或服務(wù); 因?yàn)?spri

    2024年02月03日
    瀏覽(24)
  • Springboot2.0快速入門(mén)(第一章)

    Springboot2.0快速入門(mén)(第一章)

    Spring是一個(gè)開(kāi)源框架,2003 年興起的一個(gè)輕量級(jí)的Java 開(kāi)發(fā)框架,作者:Rod Johnson 。 Spring是為了解決企業(yè)級(jí)應(yīng)用開(kāi)發(fā)的復(fù)雜性而創(chuàng)建的,簡(jiǎn)化開(kāi)發(fā)。 為了降低Java開(kāi)發(fā)的復(fù)雜性,Spring采用了以下4種關(guān)鍵策略: 1、基于POJO的輕量級(jí)和最小侵入性編程,所有東西都是bean; 2、通過(guò)

    2024年02月11日
    瀏覽(24)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包