参考:
- https://www.cnblogs.com/minxiang-luo/p/12492905.html
- https://www.javadevjournal.com/spring-boot/spring-boot-application-intellij/
- 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!
- https://s31k31.github.io/2020/05/04/JavaSpringBootCodeAudit-5-IDOR/
打开IDEA => File => New => Project,选择选择 Spring Initializr
如果IDEA自动下载的依赖中断了,或者网络问题,可以手动:
mvn -U idea:idea
新建Spring Boot项目之后,依赖Spring Security。
在application.properties文件中输入账号密码:
spring.security.user.name=cqq
spring.security.user.password=pass
并创建一个Controller,
package com.cqq.csrf1.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@PostMapping("/transfer")
public void transferMoney(String name, Integer money) {
System.out.println("[*] name = " + name);
System.out.println("[*] 转账 ");
System.out.println("[*] money = " + money);
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
启动这个Application,打开应用根目录,会重定向到/login,
这就是Spring Security自带的登录界面:
登录的时候发现会自动带上一个_csrf
的字段,看起来是个csrf token:
当去掉这个字段的时候,即便账号密码是正确的,也不会登录成功,
通过查看前端代码,知道这个_csrf字段是预埋在网页里的。
先不登录,试一下我们写的这个/transfer接口:
在没有_csrf值的情况下:
发现直接重定向到登录页面。
那就老老实实登录吧,
使用正确的用户名密码登录成功之后,会返回有效的JSESSIONID,用作登录凭证:
于是带着这个Cookie再请求/transfer接口,
结果响应403。相比这就是Spring Security的CSRF防御了。
回头看看Spring Boot启动的信息:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.1)
2021-01-13 15:45:37.465 INFO 47736 --- [ main] com.cqq.csrf1.Csrf1Application : Starting Csrf1Application using Java 1.8.0_172 on cqq with PID 47736 (C:\Users\Administrator\Downloads\csrf-1\target\classes started by Administrator in C:\Users\Administrator\Downloads\csrf-1)
2021-01-13 15:45:37.467 INFO 47736 --- [ main] com.cqq.csrf1.Csrf1Application : No active profile set, falling back to default profiles: default
2021-01-13 15:45:38.016 INFO 47736 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-01-13 15:45:38.022 INFO 47736 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-01-13 15:45:38.022 INFO 47736 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41]
2021-01-13 15:45:38.077 INFO 47736 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-01-13 15:45:38.077 INFO 47736 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 578 ms
2021-01-13 15:45:38.184 INFO 47736 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-01-13 15:45:38.351 INFO 47736 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@66ba7e45, org.springframework.security.web.context.SecurityContextPersistenceFilter@2b214b94, org.springframework.security.web.header.HeaderWriterFilter@13047d7d, org.springframework.security.web.csrf.CsrfFilter@a64e035, org.springframework.security.web.authentication.logout.LogoutFilter@985696, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ae263bf, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@215a34b4, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@70e02081, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6be25526, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@49601f82, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@250b236d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9d200de, org.springframework.security.web.session.SessionManagementFilter@65bb9029, org.springframework.security.web.access.ExceptionTranslationFilter@782168b7, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@65ae095c]
2021-01-13 15:45:38.396 INFO 47736 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-13 15:45:38.403 INFO 47736 --- [ main] com.cqq.csrf1.Csrf1Application : Started Csrf1Application in 1.223 seconds (JVM running for 3.875)
2021-01-13 15:45:48.429 INFO 47736 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-01-13 15:45:48.430 INFO 47736 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-01-13 15:45:48.431 INFO 47736 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
看到这样一行:
Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@66ba7e45,
org.springframework.security.web.context.SecurityContextPersistenceFilter@2b214b94,
org.springframework.security.web.header.HeaderWriterFilter@13047d7d,
org.springframework.security.web.csrf.CsrfFilter@a64e035,
org.springframework.security.web.authentication.logout.LogoutFilter@985696,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ae263bf,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@215a34b4,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@70e02081,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6be25526,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@49601f82,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@250b236d,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9d200de,
org.springframework.security.web.session.SessionManagementFilter@65bb9029,
org.springframework.security.web.access.ExceptionTranslationFilter@782168b7,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@65ae095c]
看到了CsrfFilter
,想必这个就是默认用于防御CSRF攻击的Filter了吧。
这个类在spring-security-web包下面:
发现访问这两个接口都是默认需要登录凭据的:
在/hello接口下断点。不带Cookie情况下断不下来,直接响应302。为了弄清楚Spring Security是如何判断然后响应302的,我先带着有效的Cookie访问,成功想/hello接口断下来。
GET /hello HTTP/1.1
Host: cqq.com:8080
Cookie: JSESSIONID=64460ED9A74B51260CB77738D73B8AC7
Accept: text/html
Connection: close
然后寻找不带Cookie时可能经过的调用栈。
经测试,不带Cookie时发现org.springframework.security.web.authentication.AnonymousAuthenticationFilter
的doFilter方法可以断下来。
在这里给我们设定了Anonymous用户,
然后最后到达这个FilterChain的最后一个Filter,
准确地说,它是一个Intercepter:
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
不过它也实现了Filter接口,算是一种特殊的Filter,难怪还是给它放到了FilterChain里面:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter
这里把请求、响应、Chain封装成一个FilterInvocation
对象
在
在beforeInvocation方法中,
this.attemptAuthorization(object, attributes, authenticated);
看方法名应该是对这个Anonymous的用户对象进行Authorization(鉴权),看它是否有权限访问要访问的这个url。
然后通过调用:
this.accessDecisionManager.decide(authenticated, object, attributes);
继续判断,然后这里判断没有这个访问权限,然后抛出了异常,这里被捕获之后,进行可能的日志等操作之后,继续抛出。
然后被上一个Filter:org.springframework.security.web.access.ExceptionTranslationFilter#doFilter
的catch捕获,
然后继续处理,发现这里异常是AccessDeniedException
继续跟进handleAccessDeniedException
方法:
继续跟进sendStartAuthentication
方法:
在commece(开始)方法里,找到应该进行认证的url是/login
:
构造这个重定向的url:
构造出完整了重定向url之后,就进行重定向:
至于javax.servlet.http.HttpServletResponse如何调用sendRedirect方法进行重定向就不继续跟进了。
整理一下:
判断没有有效的JSESSIONID之后,就通过AnonymousAuthenticationFilter
将这个用户强行设置为匿名用户(AnonymousUser),并且已经认证(Authenticated)。封装成一个FilterInvocation对象,交给accessDecisionManager去decide,然后对比/hello接口需要的用户权限(认证用户的权限)和这个请求所带的用户权限(AnonymousUser权限),判断为权限不足,于是交给ExceptionTranslationFilter捕获,然后对这个异常进行处理,即重定向。
注意,
-
继承了
org.springframework.web.filter.OncePerRequestFilter
的Filter,比如CsrfFilter,调用的是doFilterInternal
方法。 -
继承了
org.springframework.web.filter.GenericFilterBean
的Filter,比如LogoutFilter,调用的是doFilter
。
注意到不管是doFilterInternal
还是doFilter
其方法接收的参数都是这三个:
(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
request很好理解,因为Filter就是对请求进行"filter"(过滤),拿到请求的body,header之类的;
response,因为有时候也会写入到response对象中,等待整个FilterChain结束之后统一写入到响应中;
filterChain有一个变量表示当前在进行哪个Filter,有数字来进行定位。
CsrfFilter
因为我们用的是GET方法,在这个Filter设置的白名单中,不用进行CSRF攻击检查。
LogoutFilter
在LogoutFilter中,我以为会对请求的url与系统的退出url(/logou
)进行匹配,然而只是比较了一下方法就返回了fasle(//TODO)
UsernamePasswordAuthenticationFilter
这个Filter也跟LogoutFilter一样,还没到匹配url(/login
)的地方,直接判断方法就返回false了。
继续下一个Filter。
附录
不同请求头导致的不同响应:
带有Accept头时,会302重定向:
不带时会401,
带上X-Requested-With: XMLHttpRequest
时,
默认Spring Security进行了CSRF防御,想要禁用这个防御机制,需要新建一个类SecurityConfig,继承自WebSecurityConfigurerAdapter
,
package com.cqq.csrf1.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable();
}
}
然后再调用/transfer接口,即可成功调用:
前后端分离的项目
不是将 _csrf 放在 Model 中返回前端了,而是放在 Cookie 中返回前端
由于直接拿到Cookie才能拿到CSRF token,攻击者不能拿到受害者的Cookie,所以能防止CSRF攻击。
需要再写一个类继承自WebSecurityConfigurerAdapter
:
package com.cqq.csrf1.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
public class SecurityConfig2 extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.successHandler((req,resp,authentication)->{
resp.getWriter().write("success");
})
.permitAll()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
前端html页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/jquery.min.js"></script>
<script src="js/jquery.cookie.js"></script>
</head>
<body>
<div>
<input type="text" id="username">
<input type="password" id="password">
<input type="button" value="登录" id="loginBtn">
</div>
<script>
$("#loginBtn").click(function () {
let _csrf = $.cookie('XSRF-TOKEN');
$.post('/login.html',{username:$("#username").val(),password:$("#password").val(),_csrf:_csrf},function (data) {
alert(data);
})
})
</script>
</body>
</html>
这里设置withHttpOnlyFalse
,即设置HttpOnly属性为False,即允许前端js操作Cookie。
然后把之前的类SecurityConfig注释掉,重新启动项目。
抓包发现:
提交的_csrf
值确实与Cookie中的XSRF-TOKEN
一致。
当修改这个值时,就不能请求成功。
不使用Spring Security框架
新建一各Spring Boot项目,只依赖Spring Web,不选择Spring Security,
没有Spring Security的各种Filter,直接就进入了Controller的逻辑,
使用Shiro进行鉴权和认证
安装Shiro官方教程中与Spring Boot集成的教程:
https://shiro.apache.org/spring-boot.html
依然只依赖Spring Web启动IDEA,然后在pom.xml里加上Shiro跟Spring Boot适配的依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>
从它的pom.xml文件里可以看出,引入他的同时也引入了
- spring-boot-starter
- spring-boot-starter-web
- shiro-spring-boot-starter
等。
在配置文件application.properties
中配置:
shiro.loginUrl = /login.html
# Let Shiro Manage the sessions
shiro.userNativeSessionManager = true
# disable URL session rewriting
shiro.sessionManager.sessionIdUrlRewritingEnabled = false
然后从shiro官方给的spring-boot-web示例:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web
中引入静态代码html等到templates目录。
输出这样的错误。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.1)
2021-01-14 15:08:27.420 INFO 64796 --- [ main] com.cqq.csrf3.Csrf3Application : Starting Csrf3Application using Java 1.8.0_172 on cqq with PID 64796 (C:\Users\Administrator\Downloads\csrf-3\target\classes started by Administrator in C:\Users\Administrator\Downloads\csrf-3)
2021-01-14 15:08:27.421 INFO 64796 --- [ main] com.cqq.csrf3.Csrf3Application : No active profile set, falling back to default profiles: default
2021-01-14 15:08:27.901 INFO 64796 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration$$EnhancerBySpringCGLIB$$e650c5b8] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.910 INFO 64796 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration$$EnhancerBySpringCGLIB$$e8e4045d] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.918 INFO 64796 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'eventBus' of type [org.apache.shiro.event.support.DefaultEventBus] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.949 INFO 64796 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration' of type [org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration$$EnhancerBySpringCGLIB$$aed2622] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.951 INFO 64796 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration$$EnhancerBySpringCGLIB$$ac506788] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.956 WARN 64796 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shiroEventBusAwareBeanPostProcessor' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroBeanAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.shiro.spring.ShiroEventBusBeanPostProcessor]: Factory method 'shiroEventBusAwareBeanPostProcessor' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'eventBus' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroBeanAutoConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'authorizationAttributeSourceAdvisor' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.class]: Unsatisfied dependency expressed through method 'authorizationAttributeSourceAdvisor' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityManager' defined in class path resource [org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.class]: Unsatisfied dependency expressed through method 'securityManager' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'missingRealm' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.shiro.realm.Realm]: Factory method 'missingRealm' threw exception; nested exception is org.apache.shiro.spring.boot.autoconfigure.exception.NoRealmBeanConfiguredException
2021-01-14 15:08:27.963 INFO 64796 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-01-14 15:08:27.977 ERROR 64796 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
No bean of type 'org.apache.shiro.realm.Realm' found.
Action:
Please create bean of type 'Realm' or add a shiro.ini in the root classpath (src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).
简单来说应该就是没有配置org.apache.shiro.realm.Realm
这个bean.
解决方案是可以在以下文件中配置:
- src/main/resources/shiro.ini
- src/main/resources/META-INF/shiro.ini
通过查看shiro官方文档,发现不仅可以通过ini配置文件,也可以通过Shiro的API来配置:
Realm realm = //instantiate or acquire a Realm instance. We'll discuss Realms later.
SecurityManager securityManager = new DefaultSecurityManager(realm);
//Make the SecurityManager instance available to the entire application via static memory:
SecurityUtils.setSecurityManager(securityManager);
参考Shiro的官方示例:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/java/org/apache/shiro/samples/WebApp.java
在@SpringBootApplication
所在的类加上这段代码也可以完成Realm的设置:
@Bean
public Realm realm() {
TextConfigurationRealm realm = new TextConfigurationRealm();
realm.setUserDefinitions("joe.coder=password,user\n" +
"jill.coder=password,admin");
realm.setRoleDefinitions("admin=read,write\n" +
"user=read");
realm.setCachingEnabled(true);
return realm;
}
然后发现如果不在application.properties中设置:
shiro.loginUrl = /login.html
则访问/会被无限重定向到login.jsp,而我并没有配这个文件。导致失败。
加上这个配置之后就能正确地重定向到登录页面/login.html了:
然后发现配置之后,默认Shiro就启动了rememberMe的功能:
然后测试一下这个功能:
# If enabled Shiro will manage the HTTP sessions instead of the container. ref: https://shiro.apache.org/spring-boot.html
shiro.userNativeSessionManager = true
安装官方的说明,这个设置为true之后,就会让Shiro来管理session,而不是容器(这里是Tomcat)
发现Shiro管理Session的话,它的JSESSION的形式是:
JSESSIONID=51c54adb-1eb3-4ad3-970c-0494d4e412a2
与容器(Tomcat)的SESSION管理的格式不一样:
JSESSIONID=79F667E5FAACC540ED6FB69D6EDC5825
而且用Tomcat管理session的时候,重定向的时候url后面会带着jsessionid。
自定义RememberMeManager:
在shiro.ini中配置:
[main]
...
rememberMeManager = com.my.impl.RememberMeManager
securityManager.rememberMeManager = $rememberMeManager
参考:https://shiro.apache.org/web.html#Web-Custom%7B%7BRememberMeManager%7D%7D
Cookie中SESSIONID的名字、rememberMe的名字都是可以自定义的:
# Cookie中SESSION的名字可以自定义
shiro.sessionManager.cookie.name = shiro_SESSIONID
# Cookie中rememberMe的名字可以自定义
shiro.rememberMeManager.cookie.name = remember-shiro-me
修改完之后重新启动,就变成这样了。
然后依然会无限重定向:
即便我们已经在templates里已经有了login.html。
后来思考可能这个templates目录里是给Spring控制的,而可能static目录下是不需要Spring来路由的,于是将login.html放到static目录下,果然可以访问了:
但是static目录貌似不是这样用的,而是存放一些js和css之类的文件的。
于是删除static目录下的login.html,再参考官方示例,给加一个LoginController,对/login.html
这个url进行mapping:
package com.cqq.csrf3.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login.html")
public String loginTemplate() {
return "login";
}
}
这里的方法名loginTemplate
无所谓,方便说明即可。反正有注解对url进行mapping。
Spring Boot配置path之后报404的某个原因
参考:
https://www.cnblogs.com/FondWang/p/12951064.html
在Class上注解了@RestController之后,必须在这个类上面加上@RequestMapping给它加一个path映射,否则出现404.
javax.servlet.ServletException: Circular view path [login]: would dispatch back to the current handler URL [/login] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
详情:
at org.springframework.web.servlet.view.InternalResourceView.prepareForRendering(InternalResourceView.java:210) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:148) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1394) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1139) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1078) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.2.jar:5.3.2]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.2.jar:5.3.2]
参考:
- https://www.cnblogs.com/guochunyang2004/p/8629112.html
- https://www.baeldung.com/spring-circular-view-path-error
Consider defining a bean named ‘authenticator’ in your configuration.
@Bean public SecurityManager securityManager() {……}
改为默认的即可:
@Bean public DefaultWebSecurityManager securityManager() {……}
参考:
- https://github.com/ityouknow/spring-boot-examples/issues/56
配置权限
写一个类,进行@Configuration注解:
@Configuration
public class ShiroConfig
拿到logger对象,进行日志处理:
private final Logger logger = LoggerFactory.getLogger(this.getClass());
剩下的默认:
@Bean
MyRealm myRealm() {
return new MyRealm();
}
// 参考: https://github.com/ityouknow/spring-boot-examples/issues/56
@Bean
DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
然后就是进行权限配置:
@Bean("shiroFilterFactoryBean")
ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")SecurityManager securityManager) {
logger.info("启动shiroFilter--时间是:" + new Date());
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login"); // 登录的
bean.setSuccessUrl("/hello"); // 登录成功之后跳转的
bean.setUnauthorizedUrl("/unauth");
Map<String, String> map = new LinkedHashMap<>();
map.put("/static/**", "anon");
map.put("/css/**", "anon");
map.put("/js/**", "anon");
map.put("/login", "anon");
map.put("/doLogin", "anon");
// 以上接口都是未授权可以访问,剩下的默认就是需要授权访问了
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
这里设置用于登录的url和登录成功之后跳转的url。
然后设置一些不需要登录就可以访问的url,比如静态资源,和登录/注册url等。
其他的url就是默认需要认证才能访问的。
下面就是在没有将/css/
加入到anon
的结果,css资源也要认证才能访问。
Spring Boot无需配置 web.xml,但在其他Java项目中,web.xml是一个非常重要的文件,用来配置Servlet、Filter、Listener等。
参考:
- https://s31k31.github.io/2020/04/26/JavaSpringBootCodeAudit-2-SpringBoot/
配置MyBatis的Java接口和Mapper.xml文件。
步骤:
1、在application.proterties文件中指定xml文件的位置:
mybatis.mapper-locations=classpath:mapper/*Mapper.xml
一般都是resources目录下的mapper目录。
2、在dao或者mapper目录下新建一个Mapper,比如UserMapper.java文件
import org.apache.ibatis.annotations.Mapper;
// 注意这里要写这个注解,否则
@Mapper
public interface UserMapper {
User selectByLoginNameAndPasswd(@Param("username") String username, @Param("password") String password);
}
定义方法名,接收参数名、参数类型、以及返回对象。
这里的User类在entity目录下定义。
3、在UserMapper.xml中实现SQL语句,
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cqq.csrf3.dao.UserMapper">
<resultMap id="BaseResultMap" type="com.cqq.csrf3.entity.User">
<id column="user_id" jdbcType="BIGINT" property="userId"/>
<result column="login_name" jdbcType="VARCHAR" property="loginName"/>
<result column="password_md5" jdbcType="VARCHAR" property="passwordMd5"/>
</resultMap>
<select id="selectByLoginNameAndPasswd" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from user
where login_name = #{loginName} and password_md5 = #{passwordMD5}
</select>
</mapper>
在mapper标签里写语句,定义返回类型resultMap
,指定namespace为
这个xml匹配的Mapper接口类,这里是com.cqq.csrf3.dao.UserMapper
。
如果namespace出错,会报这个错Invalid bound statement (not found)
参考:https://blog.csdn.net/qq_35981283/article/details/78590090
4、在Controller中doLogin方法调用service的login方法,在service的Impl中实现具体的login方法,在Impl中调用Mapper接口的查询方法selectByLoginNameAndPasswd
// LoginController.java
@Controller
public class LoginController {
@Resource
private UserService userService;
// 只允许POST,但是并不抛出错误
@RequestMapping(value = "/doLogin", method = RequestMethod.POST)
@ResponseBody
public Result doLogin(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession httpSession) {
String loginResult = userService.login(username, MD5Util.MD5Encode(password, "UTF-8"), httpSession);
}
}
// UserService.java
import org.springframework.stereotype.Service;
@Service
public interface UserService {
String login(String loginName, String passwordMD5, HttpSession httpSession);
}
// UserServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String login(String loginName, String passwordMD5, HttpSession httpSession) {
User user = userMapper.selectByLoginNameAndPasswd(loginName, passwordMD5);
...
}
最终在
UserServiceImpl 这里调用UserMapper 的实现(xml中实现)执行sql语句。
注意xml中引用的变量名要与Mapper接口中的变量名一致:
<select id="selectByLoginNameAndPasswd" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from mall_user
where login_name = #{loginName} and password_md5 = #{passwordMD5}
</select>
UserMapper.java
import org.apache.ibatis.annotations.Param;
User selectByLoginNameAndPasswd(@Param("loginName") String username, @Param("passwordMD5") String password);
或者如果这里不写@Param,直接写
User selectByLoginNameAndPasswd(String loginName, String passwordMD5);
会默认使用这个局部变量名与xml中引用的变量名保持一致。
在/doLogin这个映射的方法中,返回类型是Result类型,实现了序列化接口,
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int resultCode;
private String message;
private T data;
}
进行@ResponseBody
注解,表示返回的是一个序列化的对象
若不进行这个注解,则会报错:
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [doLogin], template might not exist or might not be accessible by any of the configured Template Resolvers
另外注意到这里返回的中文是乱码。原因是Response的默认content-type是application/json;charset=ISO-8859-1
解决方法是在@Mapping注解加上produces = "application/json;charset=utf-8"
@RequestMapping(value = "/doLogin", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
@ResponseBody
但是这种方法只是在某个方法上设置content-type。其他的全局方法参考以下
参考:
- https://www.cnblogs.com/uqing/p/10163094.html
- https://blog.csdn.net/zknxx/article/details/52423608
Shiro的Filter
Shiro中默认的Filter有以下,org\apache\shiro\web\filter\mgt\DefaultFilter
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.filter.authz.PortFilter;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.filter.authz.SslFilter;
import org.apache.shiro.web.filter.session.NoSessionCreationFilter;
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class),
invalidRequest(InvalidRequestFilter.class);
...
主要在org.apache.shiro.web.filter包下面。
在FilterChain中会被调用preHandle
方法。
具体的流程是:
org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter
在这个方法中执行:
this.doFilterInternal(request, response, filterChain);
而由于这个类OncePerRequestFilter
是一个抽象类,其具体的实现方法由其子类完成:
org.apache.shiro.web.servlet.AdviceFilter
就继承了它,并实现了这个方法,在其doFilterInternal
方法中,
boolean continueChain = this.preHandle(request, response);
if (continueChain) {
this.executeChain(request, response, chain);
}
this.postHandle(request, response);
通过调用preHandle
方法,通过其返回值进行判断是否继续调用FilterChain中的Filter。
再继续PathMatchingFilter
继承了AdviceFilter
,继续调用preHandle
方法.
然后下面调用的是AccessControlFilter和InvalidRequestFilter
,这次不是调用preHandle方法了。