<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在上一篇部落格springboot整合shiro許可權管理簡單實現中,使用者在登入的過程中,有以下幾個問題:
很顯然,這樣的互動方式,使用者體驗上不是很好,並且在某些程度上也無法滿足業務上的要求。所以,我們要對預設的FormAuthenticationFilter進行覆蓋,實現我們自定義的Filter來解決使用者互動的問題。
首先我們需要繼承原先的FormAuthenticationFilter
之所以繼承這個FormAuthenticationFilter,有以下幾點原因:
1.FormAuthenticationFilter是預設攔截登入功能的過濾器,我們本身就是要改造登入功能,所以繼承它很正常;
2.我們自定義的Filter需要複用裡面的邏輯;
public class UsernamePasswordAuthenticationFilter extends FormAuthenticationFilter{}
其次,為了解決第一個問題,我們需要重寫saveRequestAndRedirectToLogin方法
/** * 沒有登陸的情況下,存取需要許可權的介面,需要引導使用者登陸 * * @param request * @param response * @throws IOException */ @Override protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { // 儲存當前請求,以便後續登陸成功後重新請求 this.saveRequest(request); // 1. 伺服器端直接跳轉 // - 伺服器端重定向登陸頁面 if (autoRedirectToLogin) { this.redirectToLogin(request, response); } else { // 2. json模式 // - json資料格式告知前端需要跳轉到登陸頁面,前端根據指令跳轉登陸頁面 HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; Map<String, String> metaInfo = new HashMap<>(); // 告知前端需要跳轉的登陸頁面 metaInfo.put("loginUrl", getLoginUrl()); // 告知前端當前請求的url;這個資訊也可以儲存在前端 metaInfo.put("currentRequest", req.getRequestURL().toString()); ResultWrap.failure(802, "請登陸後再操作!", metaInfo) .writeToResponse(res); } }
在這個方法中,我們通過設定autoRedirectToLogin引數的方式,既保留了原來伺服器自動跳轉的功能,又增強了伺服器返回json給前端,讓前端根據返回結果跳轉到登陸頁面的功能。這樣就增強了應用程式的可控性和靈活性了。
重寫登陸成功的處理方法onLoginSuccess:
@Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { // 查詢當前使用者自定義的登陸成功需要跳轉的頁面,可以更加靈活控制使用者頁面跳轉 String successUrl = loginSuccessPageFetch.successUrl(token, subject); // 如果沒有自定義的成功頁面,那麼跳轉預設成功頁 if (StringUtils.isEmpty(successUrl)) { successUrl = this.getSuccessUrl(); } if (loginSuccessAutoRedirect) { // 伺服器端直接重定向到目標頁面 WebUtils.redirectToSavedRequest(request, response, successUrl); } else { SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) { successUrl = savedRequest.getRequestUrl(); } // 返回json資料格式告知前端跳轉目標頁面 HttpServletResponse res = (HttpServletResponse) response; Map<String, String> data = new HashMap<>(); // 登陸成功後跳轉的目標頁面 data.put("successUrl", successUrl); ResultWrap.success(data).writeToResponse(res); } return false; }
1.登陸成功後,我們內建了一個個性化的成功頁,用於保證針對不同的使用者會有客製化化的登陸成功頁。
2.通過自定義的loginSuccessAutoRedirect屬性來決定使用者登陸成功後是直接由伺服器端控制頁面跳轉還是返回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資料格式告知前端跳轉目標頁面 HttpServletResponse res = (HttpServletResponse) response; ResultWrap.failure(803, "使用者名稱或密碼錯誤,請核對後無誤後重新提交!", null).writeToResponse(res); } return true; }
登陸失敗我們使用自定義屬性loginFailureAutoRedirect來控制失敗的動作是由伺服器端直接跳轉頁面還是返回json由前端控制使用者互動。
在這個方法的邏輯裡面沒有看到跳轉的功能,是因為我們直接把父類別的預設實現拿過來了,在原有的邏輯上做了修改。既然預設是伺服器端跳轉的功能,那麼我們只需要補充返回json的功能即可。
現在我們已經寫好了自定義的使用者名稱密碼登陸過濾器,下面我們就把它加入到shiro的設定中去,這樣才能生效:
@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager()); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 設定不需要許可權的url String[] permitUrls = properties.getPermitUrls(); if (ArrayUtils.isNotEmpty(permitUrls)) { for (String permitUrl : permitUrls) { filterChainDefinitionMap.put(permitUrl, "anon"); } } // 設定退出的url String logoutUrl = properties.getLogoutUrl(); filterChainDefinitionMap.put(logoutUrl, "logout"); // 設定需要許可權驗證的url filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 設定提交登陸的url String loginUrl = properties.getLoginUrl(); shiroFilterFactoryBean.setLoginUrl(loginUrl); // 設定登陸成功跳轉的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,可覆蓋裡面對應的authc UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter(); // 不允許伺服器自動控制頁面跳轉 usernamePasswordAuthenticationFilter.setAutoRedirectToLogin(false); usernamePasswordAuthenticationFilter.setLoginSuccessAutoRedirect(false); usernamePasswordAuthenticationFilter.setLoginFailureAutoRedirect(false); filters.put("authc", usernamePasswordAuthenticationFilter); return filters; }
上面的程式碼重點看 【新增自定義Filte】 ,其實原理就是把預設的authc過濾器給覆蓋掉,換成我們自定義的過濾器,這樣的話,我們的過濾器才能生效。
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 { // 如果使用者沒有登陸的情況下存取需要許可權的介面,伺服器端是否自動調整到登陸頁面 private boolean autoRedirectToLogin = true; // 登陸成功後是否自動跳轉 private boolean loginSuccessAutoRedirect = true; // 登陸失敗後是否跳轉 private boolean loginFailureAutoRedirect = true; /** * 個性化客製化每個登陸成功的賬號跳轉的url */ private LoginSuccessPageFetch loginSuccessPageFetch = new LoginSuccessPageFetch(){}; public UsernamePasswordAuthenticationFilter() { } /** * 沒有登陸的情況下,存取需要許可權的介面,需要引導使用者登陸 * * @param request * @param response * @throws IOException */ @Override protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { // 儲存當前請求,以便後續登陸成功後重新請求 this.saveRequest(request); // 1. 伺服器端直接跳轉 // - 伺服器端重定向登陸頁面 if (autoRedirectToLogin) { this.redirectToLogin(request, response); } else { // 2. json模式 // - json資料格式告知前端需要跳轉到登陸頁面,前端根據指令跳轉登陸頁面 HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; Map<String, String> metaInfo = new HashMap<>(); // 告知前端需要跳轉的登陸頁面 metaInfo.put("loginUrl", getLoginUrl()); // 告知前端當前請求的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 { // 查詢當前使用者自定義的登陸成功需要跳轉的頁面,可以更加靈活控制使用者頁面跳轉 String successUrl = loginSuccessPageFetch.successUrl(token, subject); // 如果沒有自定義的成功頁面,那麼跳轉預設成功頁 if (StringUtils.isEmpty(successUrl)) { successUrl = this.getSuccessUrl(); } if (loginSuccessAutoRedirect) { // 伺服器端直接重定向到目標頁面 WebUtils.redirectToSavedRequest(request, response, successUrl); } else { SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) { successUrl = savedRequest.getRequestUrl(); } // 返回json資料格式告知前端跳轉目標頁面 HttpServletResponse res = (HttpServletResponse) response; Map<String, String> data = new HashMap<>(); // 登陸成功後跳轉的目標頁面 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資料格式告知前端跳轉目標頁面 HttpServletResponse res = (HttpServletResponse) response; ResultWrap.failure(803, "使用者名稱或密碼錯誤,請核對後無誤後重新提交!", null).writeToResponse(res); } return true; } /** * 針對不同的人員登陸成功後有不同的跳轉頁面而設計 */ 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> { // 方便前端判斷當前請求處理結果是否正常 private int code; // 業務處理結果 private T data; // 產生錯誤的情況下,提示使用者資訊 private String message; // 產生錯誤情況下的異常堆疊,提示開發人員 private String error; // 發生錯誤的時候,返回的附加資訊 private M metaInfo; /** * 成功帶處理結果 * * @param data * @param <T> * @return */ public static <T> ResultWrap success(T data) { return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null); } /** * 成功不帶處理結果 * * @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"; /** * 把結果寫入響應中 * * @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,也不一定能滿足所有的格式化情況,所以統一為時間戳,讓使用者按需轉換 */ objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true); /** 忽略空Bean轉json的錯誤 假設只是new方式建立物件,並且沒有對裡面的屬性賦值,也要保證序列化的時候不報錯 */ objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); /** 忽略反序列化中json字串中存在,但java物件中不存在的欄位 */ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } /** * 物件轉換成json字串 * * @param obj * @param <T> * @return */ public static <T> String obj2String(T obj) { return obj2String(obj, null); } /** * 物件轉換成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; } } /** * 物件轉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字串轉簡單物件 * * @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字串轉複雜物件 * * @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字串轉複雜物件 * * @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中如何實現更靈活的登陸控制就編寫完畢了。後面會陸續講解我在使用shiro時遇到的其他問題,以及相應的解決方案。
到此這篇關於springboot整合shiro自定義登陸過濾器方法的文章就介紹到這了,更多相關springboot整合shiro 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45