SpringSecurity是一个基于Spring开发的非常强大的权限验证框架,其核心功能包括:
学习SpringSecurity的笔记
- 认证 (用户登录)
- 授权 (此用户能够做哪些事情)
- 攻击防护 (防止伪造身份攻击)
简单环境配置
首先按照mvc的模板,写初始化
import org.example.config.WebConfiguration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class MainInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{WebConfiguration.class}; //基本的Spring配置类,一般用于业务层配置
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[0]; //配置DispatcherServlet的配置类、主要用于Controller等配置,这里为了教学简单,就不分这么详细了,只使用上面的基本配置类
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"}; //匹配路径,与上面一致
}
}
写配置,thymeleaf,数据库
@EnableWebMvc
@ComponentScans({
@ComponentScan("org.example")
})
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
//我们需要使用ThymeleafViewResolver作为视图解析器,并解析我们的HTML页面
@Bean
public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine springTemplateEngine){
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setOrder(1); //可以存在多个视图解析器,并且可以为他们设定解析顺序
resolver.setCharacterEncoding("UTF-8"); //编码格式是重中之重
resolver.setTemplateEngine(springTemplateEngine); //和之前JavaWeb阶段一样,需要使用模板引擎进行解析,所以这里也需要设定一下模板引擎
return resolver;
}
//配置模板解析器
@Bean
public SpringResourceTemplateResolver templateResolver(){
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setSuffix(".html"); //需要解析的后缀名称
resolver.setPrefix("classpath:"); //需要解析的HTML页面文件存放的位置,默认是webapp目录下,如果是类路径下需要添加classpath:前缀
resolver.setCharacterEncoding("UTF-8"); // 关键设置!
return resolver;
}
//配置模板引擎Bean
@Bean
public SpringTemplateEngine springTemplateEngine(ITemplateResolver resolver){
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver); //模板解析器,默认即可
return engine;
}
@Bean //单独创建一个Bean,方便之后更换
public DataSource dataSource(){
return new PooledDataSource("com.mysql.cj.jdbc.Driver",
"jdbc:mysql://localhost:3306/", "root", "1234");
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){ //直接参数得到Bean对象
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean;
}
//让 Spring MVC 用 FastJson 处理 JSON 格式的数据
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters){
converters.add(new FastJsonHttpMessageConverter());
}
}
写Controller和前端
@Controller
public class HelloController {
//处理登录操作并跳转
@PostMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session,
Model model){
if("test".equals(username) && "123456".equals(password)) {
session.setAttribute("login", true);
return "redirect:/";
} else {
model.addAttribute("status", true);
return "login";
}
}
//处理首页或是登录界面跳转
@GetMapping("/")
public String index(HttpSession session){
if(session.getAttribute("login") != null) {
return "index";
}else {
return "login";
}
}
@ResponseBody
@PostMapping("/pay")
public JSONObject pay(@RequestBody Map<String, String> requestMap,
HttpSession session){
String account = requestMap.get("account");
JSONObject object = new JSONObject();
//登录之后才能转账
if(session.getAttribute("login") != null) {
System.out.println("转账给"+account+"成功,交易已完成!");
object.put("success", true);
} else {
System.out.println("转账给"+account+"失败,用户未登录!");
object.put("success", false);
}
return object;
}
}
这里要显式指定参数名(Java 编译默认不保留参数名,@RequestParam
需要知道前端传递的参数名(如 username
、password
)才能正确映射到方法参数,不然会出现500错误)
-
@RequestBody → 前端发JSON(不要手动加headers)
-
@RequestParam → 前端发表单(用URLSearchParams),这里处理不好会出现415错误
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>白马银行 - 首页</title>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录白马银行</title>
</head>
<body>
<form action="login" method="post">
<label>
用户名:
<input name="username" type="text">
</label>
<label>
密码:
<input name="password" type="password">
</label>
<button type="submit">登录</button>
</form>
<div th:if="${status}">登录失败,用户名或密码错误!</div>
</body>
</html>
<script>
function pay() {
const account = document.getElementById("account").value;
axios.post('/mvc/pay', { account: account },{
headers: {
'Content-Type': 'application/json'
}})
.then(({data}) => {
if(data.success)
alert("转账成功")
else
alert("转账失败")
})
}
</script>
再添加security的初始化器和配置类
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
//不用重写任何内容
//这里实际上会自动注册一个Filter,SpringSecurity底层就是依靠N个过滤器实现的,我们之后再探讨
}
@Configuration
@EnableWebSecurity //开启WebSecurity相关功能
public class SecurityConfiguration {
}
在根容器中加入security的配置类
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{MainConfiguration.class, SecurityConfiguration.class};
}
Web攻击方式:
1. CSRF(跨站请求伪造)
简单理解:
攻击者诱导你在已登录的情况下,去访问另一个网站的接口,从而冒充你的身份进行操作。
举例:
你登录了银行网站A,没退出。此时访问了攻击者的网站B,B偷偷让你的浏览器请求A的转账接口,银行以为是你本人操作,钱就被转走了。
2. SFA(会话固定攻击,Session Fixation Attack)
简单理解:
攻击者提前拿到一个合法的会话ID,然后诱导你用这个ID登录,这样攻击者就能用同一个ID冒充你。
举例:
攻击者给你发一个带有特定sessionid的链接,你用这个链接登录后,攻击者用同样的sessionid访问网站,就能冒充你的身份。
3. XSS(跨站脚本攻击)
简单理解:
攻击者把恶意脚本代码注入到网站页面中,当其他用户访问时,这些代码会在他们的浏览器里执行,窃取信息或冒充操作。
举例:
攻击者在评论区发了一段恶意JS代码,其他人看到评论时,这段代码会自动运行,比如窃取他们的cookie。
内存验证
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean //UserDetailsService就是获取用户信息的服务
public UserDetailsService userDetailsService() {
//每一个UserDetails就代表一个用户信息,其中包含用户的用户名和密码以及角色
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER") //角色目前我们不需要关心,随便写就行,后面会专门讲解
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
//创建一个基于内存的用户信息管理器作为UserDetailsService
}
}
这里会有登入界面,如果不登入,就会一直重定向到login界面,
现在去掉之前自己写的登入
@Controller
public class HelloController {
//现在所有接口不需要任何验证了,因为Security已经帮我们做了,没登录是根本进不来的
@GetMapping("/")
public String index(){
return "index";
}
@ResponseBody
@PostMapping("/pay")
public JSONObject pay(@RequestParam String account){
JSONObject object = new JSONObject();
System.out.println("转账给"+account+"成功,交易已完成!");
object.put("success", true);
return object;
}
}
然后出现了403错误
这是因为SpringSecurity自带了csrf防护,需求我们在POST请求中携带页面中的csrfToken才可以,否则一律进行拦截操作,这里我们可以将其嵌入到页面中,随便找一个地方添加以下内容:
<input type="text" th:id="${_csrf.getParameterName()}" th:value="${_csrf.token}" hidden>
接着在axios发起请求时,携带这个input的value值:
<script>
function pay() {
const account = document.getElementById("account").value;
const _csrf = document.getElementById("_csrf").value;
axios.post('/mvc/pay', { account: account,_csrf:_csrf },{
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf // 将 CSRF token 传递到请求头中
}})
.then(({data}) => {
if(data.success)
alert("转账成功")
else
alert("转账失败")
})
}
</script>
如果csrf没有配置好会出现403错误
由于密码是明文写在上面的,非常的不安全,所以我们需要加密
首先在security配置类中写入(可以使用官方提供的BCrypt加密工具)
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
//这里将BCryptPasswordEncoder直接注册为Bean,Security会自动进行选择
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User
.withUsername("user")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("USER")
.build();
System.out.println(encoder.encode("password")); //一会观察一下加密出来之后的密码长啥样
UserDetails admin = User
.withUsername("admin")
.password(encoder.encode("password")) //这里将密码进行加密后存储
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}