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)以下兩種登錄功能:
用戶名+密碼+圖片驗(yàn)證碼
手機(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)如下的界面

輸入用戶名和密碼即可登錄,這里的用戶名默認(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ò)濾器),如圖:

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

這是官方文檔截圖的一部分,詳細(xì)的可以看官方文檔:
https://docs.spring.io/spring-security/reference/5.7.3/servlet/architecture.html
另外我們?cè)趩?dòng)程序的時(shí)候,也可以看到springSecurity創(chuàng)建了哪些過(guò)濾器:

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

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

從官方文檔的截圖,從圖可以知道,用戶名密碼驗(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)流程

這張圖網(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è)試一下:

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è)successHandler和failureHandler
新建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è)試一下:

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();
}
}
效果如下:

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)文檔截圖)

① 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登錄"
這一塊更新中……文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-419823.html
源碼: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)!