springboot集成shiro自定義登陸過濾器方法
前言
在上一篇博客springboot集成shiro權(quán)限管理簡單實現(xiàn)中,用戶在登錄的過程中,有以下幾個問題:
- 用戶在沒有登陸的情況下,訪問需要權(quán)限的接口,服務(wù)器自動跳轉(zhuǎn)到登陸頁面,前端無法控制;
- 用戶在登錄成功后,服務(wù)器自動跳轉(zhuǎn)到成功頁,前端無法控制;
- 用戶在登錄失敗后,服務(wù)器自動刷新登錄頁面,前端無法控制;
很顯然,這樣的交互方式,用戶體驗上不是很好,并且在某些程度上也無法滿足業(yè)務(wù)上的要求。所以,我們要對默認(rèn)的FormAuthenticationFilter進(jìn)行覆蓋,實現(xiàn)我們自定義的Filter來解決用戶交互的問題。
自定義UsernamePasswordAuthenticationFilter
首先我們需要繼承原先的FormAuthenticationFilter
之所以繼承這個FormAuthenticationFilter,有以下幾點原因:
1.FormAuthenticationFilter是默認(rèn)攔截登錄功能的過濾器,我們本身就是要改造登錄功能,所以繼承它很正常;
2.我們自定義的Filter需要復(fù)用里面的邏輯;
public class UsernamePasswordAuthenticationFilter extends FormAuthenticationFilter{}其次,為了解決第一個問題,我們需要重寫saveRequestAndRedirectToLogin方法
/**
* 沒有登陸的情況下,訪問需要權(quán)限的接口,需要引導(dǎo)用戶登陸
*
* @param request
* @param response
* @throws IOException
*/
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
// 保存當(dāng)前請求,以便后續(xù)登陸成功后重新請求
this.saveRequest(request);
// 1. 服務(wù)端直接跳轉(zhuǎn)
// ? - 服務(wù)端重定向登陸頁面
if (autoRedirectToLogin) {
?this.redirectToLogin(request, response);
} else {
?// 2. json模式
?// ? - json數(shù)據(jù)格式告知前端需要跳轉(zhuǎn)到登陸頁面,前端根據(jù)指令跳轉(zhuǎn)登陸頁面
?HttpServletRequest req = (HttpServletRequest) request;
?HttpServletResponse res = (HttpServletResponse) response;
?Map<String, String> metaInfo = new HashMap<>();
?// 告知前端需要跳轉(zhuǎn)的登陸頁面
?metaInfo.put("loginUrl", getLoginUrl());
?// 告知前端當(dāng)前請求的url;這個信息也可以保存在前端
?metaInfo.put("currentRequest", req.getRequestURL().toString());
?ResultWrap.failure(802, "請登陸后再操作!", metaInfo)
.writeToResponse(res);
}
}在這個方法中,我們通過配置autoRedirectToLogin參數(shù)的方式,既保留了原來服務(wù)器自動跳轉(zhuǎn)的功能,又增強了服務(wù)器返回json給前端,讓前端根據(jù)返回結(jié)果跳轉(zhuǎn)到登陸頁面的功能。這樣就增強了應(yīng)用程序的可控性和靈活性了。
重寫登陸成功的處理方法onLoginSuccess:
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
// 查詢當(dāng)前用戶自定義的登陸成功需要跳轉(zhuǎn)的頁面,可以更加靈活控制用戶頁面跳轉(zhuǎn)
String successUrl = loginSuccessPageFetch.successUrl(token, subject);
// 如果沒有自定義的成功頁面,那么跳轉(zhuǎn)默認(rèn)成功頁
if (StringUtils.isEmpty(successUrl)) {
?successUrl = this.getSuccessUrl();
}
if (loginSuccessAutoRedirect) {
?// 服務(wù)端直接重定向到目標(biāo)頁面
?WebUtils.redirectToSavedRequest(request, response, successUrl);
} else {
?SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
?if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) {
? ?successUrl = savedRequest.getRequestUrl();
}
?// 返回json數(shù)據(jù)格式告知前端跳轉(zhuǎn)目標(biāo)頁面
?HttpServletResponse res = (HttpServletResponse) response;
?Map<String, String> data = new HashMap<>();
?// 登陸成功后跳轉(zhuǎn)的目標(biāo)頁面
?data.put("successUrl", successUrl);
?ResultWrap.success(data).writeToResponse(res);
}
return false;
}1.登陸成功后,我們內(nèi)置了一個個性化的成功頁,用于保證針對不同的用戶會有定制化的登陸成功頁。
2.通過自定義的loginSuccessAutoRedirect屬性來決定用戶登陸成功后是直接由服務(wù)端控制頁面跳轉(zhuǎn)還是返回json讓前端控制交互行為。
3.我們在用戶登陸成功后,會獲取前面保存的請求,以便用戶在登錄成功后能直接回到登錄前點擊的頁面。
重寫用戶登錄失敗的方法onLoginFailure:
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
if (log.isDebugEnabled()) {
log.debug("Authentication exception", e);
}
this.setFailureAttribute(request, e);
if (!loginFailureAutoRedirect) {
// 返回json數(shù)據(jù)格式告知前端跳轉(zhuǎn)目標(biāo)頁面
HttpServletResponse res = (HttpServletResponse) response;
ResultWrap.failure(803, "用戶名或密碼錯誤,請核對后無誤后重新提交!", null).writeToResponse(res);
}
return true;
}登陸失敗我們使用自定義屬性loginFailureAutoRedirect來控制失敗的動作是由服務(wù)端直接跳轉(zhuǎn)頁面還是返回json由前端控制用戶交互。
在這個方法的邏輯里面沒有看到跳轉(zhuǎn)的功能,是因為我們直接把父類的默認(rèn)實現(xiàn)拿過來了,在原有的邏輯上做了修改。既然默認(rèn)是服務(wù)端跳轉(zhuǎn)的功能,那么我們只需要補充返回json的功能即可。
覆蓋默認(rèn)的FormAuthenticationFilter
現(xiàn)在我們已經(jīng)寫好了自定義的用戶名密碼登陸過濾器,下面我們就把它加入到shiro的配置中去,這樣才能生效:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 設(shè)置不需要權(quán)限的url
String[] permitUrls = properties.getPermitUrls();
if (ArrayUtils.isNotEmpty(permitUrls)) {
for (String permitUrl : permitUrls) {
filterChainDefinitionMap.put(permitUrl, "anon");
}
}
// 設(shè)置退出的url
String logoutUrl = properties.getLogoutUrl();
filterChainDefinitionMap.put(logoutUrl, "logout");
? ?// 設(shè)置需要權(quán)限驗證的url
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 設(shè)置提交登陸的url
String loginUrl = properties.getLoginUrl();
shiroFilterFactoryBean.setLoginUrl(loginUrl);
// 設(shè)置登陸成功跳轉(zhuǎn)的url
String successUrl = properties.getSuccessUrl();
shiroFilterFactoryBean.setSuccessUrl(successUrl);
// 添加自定義Filter
shiroFilterFactoryBean.setFilters(customFilters());
return shiroFilterFactoryBean;
}
?
/**
* 自定義過濾器
*
* @return
*/
private Map<String, Filter> customFilters() {
Map<String, Filter> filters = new LinkedHashMap<>();
// 自定義FormAuthenticationFilter,用于管理用戶登陸的,包括登陸成功后的動作、登陸失敗的動作
// 可查看org.apache.shiro.web.filter.mgt.DefaultFilter,可覆蓋里面對應(yīng)的authc
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter();
// 不允許服務(wù)器自動控制頁面跳轉(zhuǎn)
usernamePasswordAuthenticationFilter.setAutoRedirectToLogin(false);
usernamePasswordAuthenticationFilter.setLoginSuccessAutoRedirect(false);
usernamePasswordAuthenticationFilter.setLoginFailureAutoRedirect(false);
filters.put("authc", usernamePasswordAuthenticationFilter);
return filters;
}上面的代碼重點看 【添加自定義Filte】 ,其實原理就是把默認(rèn)的authc過濾器給覆蓋掉,換成我們自定義的過濾器,這樣的話,我們的過濾器才能生效。
完整UsernamePasswordAuthenticationFilter代碼
import com.example.awesomespring.vo.ResultWrap;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
?
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
?
/**
* @author zouwei
* @className UsernamePasswordAuthenticationFilter
* @date: 2022/8/2 上午12:14
* @description:
*/
@Data
@Slf4j
public class UsernamePasswordAuthenticationFilter extends FormAuthenticationFilter {
// 如果用戶沒有登陸的情況下訪問需要權(quán)限的接口,服務(wù)端是否自動調(diào)整到登陸頁面
private boolean autoRedirectToLogin = true;
// 登陸成功后是否自動跳轉(zhuǎn)
private boolean loginSuccessAutoRedirect = true;
// 登陸失敗后是否跳轉(zhuǎn)
private boolean loginFailureAutoRedirect = true;
/**
* 個性化定制每個登陸成功的賬號跳轉(zhuǎn)的url
*/
private LoginSuccessPageFetch loginSuccessPageFetch = new LoginSuccessPageFetch(){};
?
public UsernamePasswordAuthenticationFilter() {
}
?
/**
* 沒有登陸的情況下,訪問需要權(quán)限的接口,需要引導(dǎo)用戶登陸
*
* @param request
* @param response
* @throws IOException
*/
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
// 保存當(dāng)前請求,以便后續(xù)登陸成功后重新請求
this.saveRequest(request);
// 1. 服務(wù)端直接跳轉(zhuǎn)
// ? - 服務(wù)端重定向登陸頁面
if (autoRedirectToLogin) {
this.redirectToLogin(request, response);
} else {
// 2. json模式
// ? - json數(shù)據(jù)格式告知前端需要跳轉(zhuǎn)到登陸頁面,前端根據(jù)指令跳轉(zhuǎn)登陸頁面
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
Map<String, String> metaInfo = new HashMap<>();
// 告知前端需要跳轉(zhuǎn)的登陸頁面
metaInfo.put("loginUrl", getLoginUrl());
// 告知前端當(dāng)前請求的url;這個信息也可以保存在前端
metaInfo.put("currentRequest", req.getRequestURL().toString());
ResultWrap.failure(802, "請登陸后再操作!", metaInfo)
.writeToResponse(res);
}
}
?
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
// 查詢當(dāng)前用戶自定義的登陸成功需要跳轉(zhuǎn)的頁面,可以更加靈活控制用戶頁面跳轉(zhuǎn)
String successUrl = loginSuccessPageFetch.successUrl(token, subject);
// 如果沒有自定義的成功頁面,那么跳轉(zhuǎn)默認(rèn)成功頁
if (StringUtils.isEmpty(successUrl)) {
successUrl = this.getSuccessUrl();
}
if (loginSuccessAutoRedirect) {
// 服務(wù)端直接重定向到目標(biāo)頁面
WebUtils.redirectToSavedRequest(request, response, successUrl);
} else {
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) {
successUrl = savedRequest.getRequestUrl();
}
// 返回json數(shù)據(jù)格式告知前端跳轉(zhuǎn)目標(biāo)頁面
HttpServletResponse res = (HttpServletResponse) response;
Map<String, String> data = new HashMap<>();
// 登陸成功后跳轉(zhuǎn)的目標(biāo)頁面
data.put("successUrl", successUrl);
ResultWrap.success(data).writeToResponse(res);
}
return false;
}
?
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
if (log.isDebugEnabled()) {
log.debug("Authentication exception", e);
}
this.setFailureAttribute(request, e);
if (!loginFailureAutoRedirect) {
// 返回json數(shù)據(jù)格式告知前端跳轉(zhuǎn)目標(biāo)頁面
HttpServletResponse res = (HttpServletResponse) response;
ResultWrap.failure(803, "用戶名或密碼錯誤,請核對后無誤后重新提交!", null).writeToResponse(res);
}
return true;
}
/**
* 針對不同的人員登陸成功后有不同的跳轉(zhuǎn)頁面而設(shè)計
*/
public interface LoginSuccessPageFetch {
?
default String successUrl(AuthenticationToken token, Subject subject) {
return StringUtils.EMPTY;
}
}
}ResultWrap.java
import com.example.awesomespring.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
?
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;
?
/**
* @author zouwei
* @className ResultWrap
* @date: 2022/8/2 下午2:02
* @description:
*/
@Data
@AllArgsConstructor
public class ResultWrap<T, M> {
// 方便前端判斷當(dāng)前請求處理結(jié)果是否正常
private int code;
// 業(yè)務(wù)處理結(jié)果
private T data;
// 產(chǎn)生錯誤的情況下,提示用戶信息
private String message;
// 產(chǎn)生錯誤情況下的異常堆棧,提示開發(fā)人員
private String error;
// 發(fā)生錯誤的時候,返回的附加信息
private M metaInfo;
?
/**
* 成功帶處理結(jié)果
*
* @param data
* @param <T>
* @return
*/
public static <T> ResultWrap success(T data) {
return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null);
}
?
/**
* 成功不帶處理結(jié)果
*
* @return
*/
public static ResultWrap success() {
return success(HttpStatus.OK.name());
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @return
*/
public static <M> ResultWrap failure(int code, String message, String error, M metaInfo) {
return new ResultWrap(code, null, message, error, metaInfo);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @param metaInfo
* @param <M>
* @return
*/
public static <M> ResultWrap failure(int code, String message, Exception error, M metaInfo) {
return failure(code, message, error.getStackTrace().toString(), metaInfo);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param error
* @return
*/
public static ResultWrap failure(int code, String message, Exception error) {
String errorMessage = StringUtils.EMPTY;
if (Objects.nonNull(error)) {
errorMessage = error.getStackTrace().toString();
}
return failure(code, message, errorMessage, null);
}
?
/**
* 失敗
*
* @param code
* @param message
* @param metaInfo
* @param <M>
* @return
*/
public static <M> ResultWrap failure(int code, String message, M metaInfo) {
return failure(code, message, StringUtils.EMPTY, metaInfo);
}
?
private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8";
?
/**
* 把結(jié)果寫入響應(yīng)中
*
* @param response
*/
public void writeToResponse(HttpServletResponse response) {
int code = this.getCode();
if (Objects.isNull(HttpStatus.resolve(code))) {
response.setStatus(HttpStatus.OK.value());
} else {
response.setStatus(code);
}
response.setContentType(APPLICATION_JSON_VALUE);
try (PrintWriter writer = response.getWriter()) {
writer.write(JsonUtil.obj2String(this));
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}JsonUtil.java
?import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
?
import java.util.Objects;
?
/**
* @author zouwei
* @className JsonUtil
* @date: 2022/8/2 下午3:08
* @description:
*/
@Slf4j
public final class JsonUtil {
?
/** 防止使用者直接new JsonUtil() */
private JsonUtil() {}
?
private static ObjectMapper objectMapper = new ObjectMapper();
?
static {
// 對象所有字段全部列入序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
/** 所有日期全部格式化成時間戳 因為即使指定了DateFormat,也不一定能滿足所有的格式化情況,所以統(tǒng)一為時間戳,讓使用者按需轉(zhuǎn)換 */
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
/** 忽略空Bean轉(zhuǎn)json的錯誤 假設(shè)只是new方式創(chuàng)建對象,并且沒有對里面的屬性賦值,也要保證序列化的時候不報錯 */
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
/** 忽略反序列化中json字符串中存在,但java對象中不存在的字段 */
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
?
/**
* 對象轉(zhuǎn)換成json字符串
*
* @param obj
* @param <T>
* @return
*/
public static <T> String obj2String(T obj) {
return obj2String(obj, null);
}
/**
* 對象轉(zhuǎn)換成json字符串
*
* @param obj
* @param <T>
* @return
*/
public static <T> String obj2String(T obj, String defaultValue) {
if (Objects.isNull(obj)) {
return defaultValue;
}
try {
return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse object to String error", e);
// 即使序列化出錯,也要保證程序走下去
return null;
}
}
?
/**
* 對象轉(zhuǎn)json字符串(帶美化效果)
*
* @param obj
* @param <T>
* @return
*/
public static <T> String obj2StringPretty(T obj) {
if (Objects.isNull(obj)) {
return null;
}
try {
return obj instanceof String
? (String) obj
: objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse object to String error", e);
// 即使序列化出錯,也要保證程序走下去
return null;
}
}
?
/**
* json字符串轉(zhuǎn)簡單對象
*
* @param <T>
* @param json
* @param clazz
* @return
*/
public static <T> T string2Obj(String json, Class<T> clazz) {
if (StringUtils.isEmpty(json) || Objects.isNull(clazz)) {
return null;
}
try {
return clazz.equals(String.class) ? (T) json : objectMapper.readValue(json, clazz);
} catch (Exception e) {
log.warn("Parse String to Object error", e);
// 即使序列化出錯,也要保證程序走下去
return null;
}
}
?
/**
* json字符串轉(zhuǎn)復(fù)雜對象
*
* @param json
* @param typeReference 例如:new TypeReference<List<User>>(){}
* @param <T> 例如:List<User>
* @return
*/
public static <T> T string2Obj(String json, TypeReference<T> typeReference) {
if (StringUtils.isEmpty(json) || Objects.isNull(typeReference)) {
return null;
}
try {
return (T)
(typeReference.getType().equals(String.class)
? (T) json
: objectMapper.readValue(json, typeReference));
} catch (Exception e) {
log.warn("Parse String to Object error", e);
// 即使序列化出錯,也要保證程序走下去
return null;
}
}
?
/**
* json字符串轉(zhuǎn)復(fù)雜對象
*
* @param json
* @param collectionClass 例如:List.class
* @param elementClasses 例如:User.class
* @param <T> 例如:List<User>
* @return
*/
public static <T> T string2Obj(
String json, Class<?> collectionClass, Class<?>... elementClasses) {
if (StringUtils.isEmpty(json)
|| Objects.isNull(collectionClass)
|| Objects.isNull(elementClasses)) {
return null;
}
JavaType javaType =
objectMapper
.getTypeFactory()
.constructParametricType(collectionClass, elementClasses);
try {
return objectMapper.readValue(json, javaType);
} catch (Exception e) {
log.warn("Parse String to Object error", e);
// 即使序列化出錯,也要保證程序走下去
return null;
}
}
}這樣在shiro中如何實現(xiàn)更靈活的登陸控制就編寫完畢了。后面會陸續(xù)講解我在使用shiro時遇到的其他問題,以及相應(yīng)的解決方案。
到此這篇關(guān)于springboot集成shiro自定義登陸過濾器方法的文章就介紹到這了,更多相關(guān)springboot集成shiro 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java中循環(huán)刪除list中元素的方法總結(jié)
下面小編就為大家?guī)硪黄猨ava中循環(huán)刪除list中元素的方法總結(jié)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-12-12
Java日常練習(xí)題,每天進(jìn)步一點點(55)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你2021-08-08
Java使用C3P0數(shù)據(jù)源鏈接數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了Java使用C3P0數(shù)據(jù)源鏈接數(shù)據(jù)庫,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08
20秒教你學(xué)會java?List函數(shù)排序操作示例
這篇文章主要為大家介紹了20秒教你學(xué)會List函數(shù)排序操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09

