前言
本文介紹前後分離認證最佳實現,配合以下內容觀看效果更佳!!!
- 什麼是前後分離認證流程最佳方案,為什麼這麼設計?請檢視六、Spring Boot整合Spring Security之前後分離認證流程最佳方案
- 哇偶,明白了前後分離認證流程最佳方案的原理,那怎麼實現這套方案呢?請檢視七、Spring Boot整合Spring Security之前後分離認證最佳實現
- Nice,知道了怎麼程式碼實現前後分離認證流程最佳方案,那我怎麼測試呢?請檢視八、Spring Boot整合Spring Security之前後分離認證最佳實現對接測試
- 博主,幫人幫到底,送佛送到西,提不提供原始碼呀?請點選下載
一、自定義使用者名稱密碼認證過濾器RestfulUsernamePasswordAuthenticationFilter
1、註冊過濾器方式
- 使用httpSecurity.addFilter/addFilterBefore/addFilterAfter向過濾器鏈中新增過濾器,其中addFilter只能新增內建的過濾器,順序已在過濾器順序註冊器(FilterOrderRegistration)中設定;addFilterBefore/addFilterAfter可以新增自定義過濾器,新增在指定的過濾器之前/之後。該方式優點是使用簡單,缺點是無法使用spring security內建的元件,與RestfulUsernamePasswordAuthenticationFilter需要使用AuthenticationManager元件衝突,故不使用該方式。
- 使用SecurityConfigurer透過配置類的方式向過濾器鏈中新增過濾器,官方使用的方式。該方式優點是可以使用spring security內建的元件,缺點是實現較為笨重,而且只能註冊過濾器順序註冊器(FilterOrderRegistration)中設定的過濾器。該方式可以使用spring security內建的元件,所以採用本方式,需要修改過濾器順序註冊器新增自定義的過濾器。
2、修改並覆蓋過濾器順序註冊器
- FilterOrderRegistration類為final類且未提供開放的註冊自定義過濾器的方式,所以只能重寫該類,並新增自定義過濾器的順序
package org.springframework.security.config.annotation.web.builders;
import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.filter.CorsFilter;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
final class FilterOrderRegistration {
private static final int INITIAL_ORDER = 100;
private static final int ORDER_STEP = 100;
private final Map<String, Integer> filterToOrder = new HashMap<>();
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
//新增自定義過濾器
put(RestfulUsernamePasswordAuthenticationFilter.class, order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
/**
* Register a {@link Filter} with its specific position. If the {@link Filter} was
* already registered before, the position previously defined is not going to be
* overriden
*
* @param filter the {@link Filter} to register
* @param position the position to associate with the {@link Filter}
*/
void put(Class<? extends Filter> filter, int position) {
String className = filter.getName();
if (this.filterToOrder.containsKey(className)) {
return;
}
this.filterToOrder.put(className, position);
}
/**
* Gets the order of a particular {@link Filter} class taking into consideration
* superclasses.
*
* @param clazz the {@link Filter} class to determine the sort order
* @return the sort order or null if not defined
*/
Integer getOrder(Class<?> clazz) {
while (clazz != null) {
Integer result = this.filterToOrder.get(clazz.getName());
if (result != null) {
return result;
}
clazz = clazz.getSuperclass();
}
return null;
}
private static class Step {
private final int stepSize;
private int value;
Step(int initialValue, int stepSize) {
this.value = initialValue;
this.stepSize = stepSize;
}
int next() {
int value = this.value;
this.value += this.stepSize;
return value;
}
}
}
3、建立RestfulUsernamePasswordAuthenticationFilter
- 參考UsernamePasswordAuthenticationFilter
- 將引數獲取方式從request.getParameter改為從body體中
- 建立UsernamePasswordAuthenticationToken
- 設定細節
- 呼叫getAuthenticationManager()的authenticate方法獲取認證資訊
package com.yu.demo.spring.filter;
import com.yu.demo.util.SpringUtil;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* 自定義前後端分離/restful方式的使用者名稱密碼認證過濾器
* 參考UsernamePasswordAuthenticationFilter
*/
public class RestfulUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//是否只支援post方法
private final boolean postOnly;
private final String username;
private final String password;
public RestfulUsernamePasswordAuthenticationFilter(String username, String password, String loginUrl, String httpMethod) {
super(new AntPathRequestMatcher(loginUrl, httpMethod));
postOnly = HttpMethod.POST.name().equals(httpMethod);
this.username = username;
this.password = password;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
if (this.postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
Map<String, String> body = SpringUtil.rawBodyToMap(request);
String name = body.get(username);
String pswd = body.get(password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(name, pswd);
setDetails(request, authRequest);
return getAuthenticationManager().authenticate(authRequest);
}
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
4、建立自定義使用者名稱密碼認證過濾器配置類RestfulLoginConfigurer
- 參考FormLoginConfigurer
- 註冊自定義使用者名稱密碼認證過濾器RestfulUsernamePasswordAuthenticationFilter
- 設定登入地址和請求方式
package com.yu.demo.spring.filter;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* 自定義前後端分離/restful方式的使用者名稱密碼驗證過濾器配置器,用於註冊認證過濾器
* 參考FormLoginConfigurer
*/
public class RestfulLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, RestfulLoginConfigurer<H>, RestfulUsernamePasswordAuthenticationFilter> {
private final String loginMethod;
public RestfulLoginConfigurer(RestfulUsernamePasswordAuthenticationFilter authenticationFilter, String defaultLoginProcessingUrl, String loginMethod) {
super(authenticationFilter, defaultLoginProcessingUrl);
this.loginMethod = loginMethod;
}
@Override
public RestfulLoginConfigurer<H> loginPage(String loginPage) {
return super.loginPage(loginPage);
}
@Override
public void init(H http) throws Exception {
super.init(http);
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, loginMethod);
}
}
二、自定義安全上下文倉庫SecurityContextRepositoryImpl
- 基於分散式快取實現安全上下文倉庫
- 獲取上下文時從請求頭中獲取token,透過token從快取中獲取上下文,不存在時返回空值安全上下文
- 儲存上下文時從請求頭或者登入使用者資訊中獲取token,將token和上下文儲存到快取中
1、分散式快取介面和實現
package com.yu.demo.manager;
import org.springframework.security.core.context.SecurityContext;
public interface CacheManager {
/**
* 透過token獲取認證資訊
*
* @param token token
* @return 認證資訊
*/
SecurityContext getSecurityContext(String token);
/**
* 是否包含token
*
* @param token token
* @return 是否包含token
*/
boolean contains(String token);
/**
* 透過token新增認證資訊
*
* @param token token
* @param securityContext 認證資訊
*/
void addSecurityContext(String token, SecurityContext securityContext);
/**
* 透過token刪除認證資訊
*
* @param token token
*/
void deleteSecurityContext(String token);
}
為演示方便,這裡採用過期Map,實際使用將map改為redis或者其他分散式快取即可
package com.yu.demo.manager.impl;
import com.yu.demo.manager.CacheManager;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Component
public class CacheManagerImpl implements CacheManager {
private static ExpiringMap<String, SecurityContext> SECURITY_CONTEXT_CACHE;
@PostConstruct
public void init() {
SECURITY_CONTEXT_CACHE = ExpiringMap.builder().maxSize(200).expiration(30, TimeUnit.MINUTES).expirationPolicy(ExpirationPolicy.ACCESSED).variableExpiration().build();
}
@Override
public SecurityContext getSecurityContext(String token) {
return SECURITY_CONTEXT_CACHE.get(token);
}
@Override
public boolean contains(String token) {
return SECURITY_CONTEXT_CACHE.containsKey(token);
}
@Override
public void addSecurityContext(String token, SecurityContext securityContext) {
SECURITY_CONTEXT_CACHE.put(token, securityContext);
}
@Override
public void deleteSecurityContext(String token) {
SECURITY_CONTEXT_CACHE.remove(token);
}
}
2、建立SecurityContextRepositoryImpl
package com.yu.demo.spring.impl;
import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.manager.CacheManager;
import com.yu.demo.util.SecurityUtil;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SecurityContextRepositoryImpl implements SecurityContextRepository {
private static final String AUTHORIZATION = "Authorization";
@Autowired
private CacheManager cacheManager;
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
//獲取請求頭中的token,未登入訪問系統時Token為空
String token = requestResponseHolder.getRequest().getHeader(AUTHORIZATION);
if (StringUtil.isNotBlank(token)) {
SecurityContext securityContext = cacheManager.getSecurityContext(token);
//securityContext已過期時為空
if (SecurityUtil.isNotAuthenticated(securityContext)) {
return SecurityContextHolder.createEmptyContext();
}
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
if (token.equals(userDetails.getToken())) {
//測試過程中偽造的Token(不修改header和body,只修改signature部分字元)有機率出現可以解析成功的情況,可能是secret太短的原因,未深究,所以這裡在驗證下輸入的Token和快取中的token
return securityContext;
}
}
return SecurityContextHolder.createEmptyContext();
}
@Override
public void saveContext(SecurityContext securityContext, HttpServletRequest request, HttpServletResponse response) {
//獲取請求頭中的token(登出時有,登入時沒有)
String token = request.getHeader(AUTHORIZATION);
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
if (StringUtil.isBlank(token) && SecurityUtil.isNotAuthenticated(securityContext)) {
//未登入、驗證碼、使用者名稱密碼校驗失敗
return;
}
//第一次登入時Token為空
if (StringUtil.isBlank(token)) {
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
//登入成功
cacheManager.addSecurityContext(userDetails.getToken(), securityContext);
return;
}
//退出或token過期(快取中設定token過期時間)
if (SecurityUtil.isNotAuthenticated(securityContext)) {
cacheManager.deleteSecurityContext(token);
return;
}
//更新Token
cacheManager.addSecurityContext(token, securityContext);
}
@Override
public boolean containsContext(HttpServletRequest request) {
//本版本的Spring Security只有SessionManagementFilter中呼叫該方法
//已禁用SessionManagementFilter,該方法不會被呼叫
String token = request.getHeader(AUTHORIZATION);
if (StringUtil.isBlank(token)) {
return false;
}
return cacheManager.contains(token);
}
}
三、自定義使用者詳情UserDetailsImpl
package com.yu.demo.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
@Setter
@Getter
@ToString
public class UserDetailsImpl implements UserDetails {
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
/**
* token
*/
private String token;
public UserDetailsImpl(String username, String password, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, boolean enabled, Set<GrantedAuthority> grantedAuthorities) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = grantedAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 賬號是否未過期
*
* @return true:是,false:否
*/
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
/**
* 賬號是否未鎖定
*
* @return true:是,false:否
*/
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
/**
* 密碼是否未過期
*
* @return true:是,false:否
*/
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
/**
* 賬號是否啟用
*
* @return true:是,false:否
*/
@Override
public boolean isEnabled() {
return enabled;
}
}
四、自定義使用者詳情資料庫查詢UserDetailsServiceImpl
package com.yu.demo.spring.impl;
import com.yu.demo.entity.UserDetailsImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
//@Autowired
//private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//TODO 透過username從資料庫中獲取使用者,將使用者轉UserDetails
//User user = userService.getByUsername(username);
//return new User(username, user.getPassword(), user.getEnable(), user.getAccountNonExpired(), user.getCredentialsNonExpired(), user.getAccountNonLocked(), user.getAuthorities());
//{noop}不使用密碼加密器,密碼123的都可以驗證成功
UserDetailsImpl userDetails = new UserDetailsImpl(username, "{noop}123", true, true, true, true, null);
//userDetails中設定token,該token只是實現認證流程,未使用jwt
userDetails.setToken(UUID.randomUUID().toString());
return userDetails;
}
}
五、自定義登入登出結果處理器
package com.yu.demo.spring.impl;
import com.yu.demo.entity.ApiResp;
import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.util.SpringUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class LoginResultHandler implements AuthenticationSuccessHandler, LogoutSuccessHandler, AuthenticationEntryPoint, AuthenticationFailureHandler {
/**
* 登入成功
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;
UserDetailsImpl userDetailsImpl = (UserDetailsImpl) usernamePasswordAuthenticationToken.getPrincipal();
//登陸成功後,擦除密碼
userDetailsImpl.setPassword(null);
//token返回到前端
SpringUtil.respJson(response, ApiResp.success(userDetailsImpl.getToken()));
}
/**
* 登入失敗
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
SpringUtil.respJson(response, ApiResp.loginFailure());
}
/**
* 登出成功
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SpringUtil.respJson(response, ApiResp.success());
}
/**
* 未登入訪問需要登入的頁面時
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
SpringUtil.respJson(response, ApiResp.notLogin());
}
}
六、過濾器鏈個性化配置
package com.yu.demo.config;
import com.yu.demo.spring.filter.RestfulLoginConfigurer;
import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import com.yu.demo.spring.impl.LoginResultHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//登入引數使用者名稱
private static final String LOGIN_ARG_USERNAME = "username";
//登入引數密碼
private static final String LOGIN_ARG_PASSWORD = "password";
//登入請求型別
private static final String LOGIN_HTTP_METHOD = HttpMethod.POST.name();
//登入請求地址
private static final String LOGIN_URL = "/login";
//登出請求地址
private static final String LOGOUT_URL = "/logout";
@Autowired
private LoginResultHandler loginResultHandler;
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//禁用UsernamePasswordAuthenticationFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter
.formLogin(FormLoginConfigurer::disable)
//禁用BasicAuthenticationFilter
.httpBasic(HttpBasicConfigurer::disable)
//禁用CsrfFilter
.csrf(CsrfConfigurer::disable)
//禁用SessionManagementFilter
.sessionManagement(SessionManagementConfigurer::disable)
//異常處理配置
.exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer.authenticationEntryPoint(loginResultHandler))
//http請求認證
.authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer
//任何請求
.anyRequest()
//需要認證
.authenticated())
//安全上下文配置
.securityContext(securityContextCustomizer -> securityContextCustomizer
//設定自定義securityContext倉庫
.securityContextRepository(securityContextRepository)
//顯示儲存SecurityContext,官方推薦
.requireExplicitSave(true))
//登出配置
.logout(logoutCustomizer -> logoutCustomizer
//登出地址
.logoutUrl(LOGOUT_URL)
//登出成功處理器
.logoutSuccessHandler(loginResultHandler)
)
//註冊自定義登入過濾器的配置器:自動註冊自定義登入過濾器;
//需要重寫FilterOrderRegistration的構造方法FilterOrderRegistration(){},在構造方法中新增自定義過濾器的序號,否則註冊不成功
.apply(new RestfulLoginConfigurer<>(new RestfulUsernamePasswordAuthenticationFilter(LOGIN_ARG_USERNAME, LOGIN_ARG_PASSWORD, LOGIN_URL, LOGIN_HTTP_METHOD), LOGIN_URL, LOGIN_HTTP_METHOD))
//設定登入地址:未設定時系統預設生成登入頁面,登入地址/login
.loginPage(LOGIN_URL)
//設定登入成功之後的處理器
.successHandler(loginResultHandler)
.failureHandler(loginResultHandler);
//建立過濾器鏈物件
return httpSecurity.build();
}
}
七、其他類
package com.yu.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 介面響應物件
*
* @author admin
*/
@Setter
@Getter
@ToString
@AllArgsConstructor
public class ApiResp {
private static final String CODE_SUCCESS = "00000";
private static final String CODE_LOGIN_FAILURE = "10000";
private static final String MESSAGE_LOGIN_FAILURE = "登入失敗";
private static final String CODE_NOT_LOGIN = "10010";
private static final String MESSAGE_NOT_LOGIN = "未登入";
/**
* 響應碼
*/
private String code;
/**
* 描述
*/
private String message;
/**
* 資料
*/
private Object data;
/**
* 成功
*/
public ApiResp(String code) {
this.code = code;
}
/**
* 失敗+失敗描述
*/
public ApiResp(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 成功+返回值
*/
public ApiResp(String code, Object data) {
this.code = code;
this.data = data;
}
/**
* 成功無返回資料
*
* @return 介面響應物件
*/
public static ApiResp success() {
return new ApiResp(CODE_SUCCESS);
}
/**
* 成功有返回資料
*
* @param data 資料
* @return 介面響應物件
*/
public static ApiResp success(Object data) {
return new ApiResp(CODE_SUCCESS, data);
}
/**
* 登入失敗
*
* @return 介面響應物件
*/
public static ApiResp loginFailure() {
return new ApiResp(CODE_LOGIN_FAILURE, MESSAGE_LOGIN_FAILURE);
}
/**
* 未登入
*
* @return 介面響應物件
*/
public static ApiResp notLogin() {
return new ApiResp(CODE_NOT_LOGIN, MESSAGE_NOT_LOGIN);
}
}
package com.yu.demo.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import java.lang.reflect.Type;
import java.util.Map;
/**
* JSON工具類
*
* @author admin
*/
public class JsonUtil {
private JsonUtil() {
throw new AssertionError();
}
/**
* 物件轉json
*
* @param javaObject 物件或集合或者陣列
* @return json
*/
public static String object2Json(Object javaObject) {
return JSONObject.toJSONString(javaObject);
}
public static <K, V> Map<K, V> json2Map(String jsonString, Type type) {
return JSON.parseObject(jsonString, type);
}
}
package com.yu.demo.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
/**
* Spring框架工具類
*/
public class SecurityUtil {
private SecurityUtil() {
throw new AssertionError();
}
public static boolean isAuthenticated(SecurityContext securityContext) {
if (securityContext == null) {
return false;
}
Authentication authentication = securityContext.getAuthentication();
if (authentication == null) {
return false;
}
return authentication.isAuthenticated();
}
public static boolean isNotAuthenticated(SecurityContext securityContext) {
return !isAuthenticated(securityContext);
}
}
package com.yu.demo.util;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* Spring框架工具類
*/
public class SpringUtil {
private SpringUtil() {
throw new AssertionError();
}
/**
* 請求body引數轉為map
*
* @param request 請求
* @return 引數map
* @throws IOException IO流異常
*/
public static Map<String, String> rawBodyToMap(HttpServletRequest request) throws IOException {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
return JsonUtil.json2Map(responseStrBuilder.toString(), Map.class);
}
public static void respJson(HttpServletResponse response, Map<String, Object> apiResp) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().print(JsonUtil.object2Json(apiResp));
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.yu</groupId>
<artifactId>spring-boot-security2-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-security2-demo</name>
<description>Spring Boot整合Spring Security樣例</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
<!--過期map-->
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
</dependencies>
</project>
八、案例原始碼獲取
- 下載地址
- 私聊、評論區、+V均可