Spring05:配置Spring Security、保护web请求
1 JavaWeb阶段的方式
Spring03:展现模型数据、处理及校验表单
https://www.cnblogs.com/fancy2022/p/16747070.html
在上几篇的Spring内容中,我们的web页面没有添加任何保护,任何人都可以访问主页面进行业务操作,或是发起恶意攻击,我们需要对web页面设置保护。
回顾一下在Javaweb阶段的项目中是如何做的
1.1 用户登录
login.html
<form th:action="@{/user.do}" method="post">
<input type="hidden" name="operate" value="login"/>
<label>用户名称:</label>
<input class="itxt" type="text" placeholder="请输入用户名" autocomplete="off" tabindex="1"
name="uname" id="username" value="lina" />
<br />
<br />
<label>用户密码:</label>
<input class="itxt" type="password" placeholder="请输入密码" autocomplete="off" tabindex="1"
name="pwd" id="password" value="ok"/>
<br />
<br />
<input type="submit" value="登录" id="sub_btn" />
</form>
UserController
import com.atguigu.book.pojo.Book;
import com.atguigu.book.pojo.Cart;
import com.atguigu.book.pojo.User;
import com.atguigu.book.service.BookService;
import com.atguigu.book.service.CartItemService;
import com.atguigu.book.service.UserService;
import javax.servlet.http.HttpSession;
import java.util.List;
public class UserController {
private UserService userService ;
private CartItemService cartItemService ;
public String login(String uname , String pwd , HttpSession session){
User user = userService.login(uname, pwd);
if(user!=null){
Cart cart = cartItemService.getCart(user);
user.setCart(cart);
session.setAttribute("currUser",user);
return "redirect:book.do";
}
return "user/login";
}
}
Service和DAO
import com.atguigu.book.pojo.User;
public interface UserService {
User login(String uname , String pwd );
}
public class UserServiceImpl implements UserService {
private UserDAO userDAO ;
@Override
public User login(String uname, String pwd) {
return userDAO.getUser(uname,pwd);
}
}
import com.atguigu.book.pojo.User;
public interface UserDAO {
User getUser(String uname , String pwd );
}
import com.atguigu.book.dao.UserDAO;
import com.atguigu.book.pojo.User;
import com.atguigu.myssm.basedao.BaseDAO;
public class UserDAOImpl extends BaseDAO<User> implements UserDAO {
@Override
public User getUser(String uname, String pwd) {
return load("select * from t_user where uname like ? and pwd like ? " , uname , pwd );
}
}
User类
public class User {
private Integer id ;
private String uname ;
private String pwd ;
private String email;
private Integer role ;
}
1.2 合法用户验证
通过1中的设置,我们可以实现用户登陆功能,并能从数据库中获取用户的相关信息,加载到业务页面。
目前存在的问题是,可以绕过登陆页面,直接访问登陆之后跳转的页面也是可以的。
解决方法是:
-
新建SessionFilter , 用来判断session中是否保存了currUser
-
如果没有currUser,表明当前不是一个登录合法的用户,应该跳转到登录页面让其登录
复习一下过滤器Filter:
过滤器Filter可在Servlet之前,对request和response进行拦截,做相应处理后再放行,通过FilterChain的传递,可以让我们更加灵活地控制请求的流转。
实现合法用户验证的自定义过滤器代码如下:
package com.atguigu.z_book.filters;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@WebFilter(urlPatterns = {"*.do","*.html"},
initParams = {
@WebInitParam(name = "bai",
value = "/pro25/page.do?operate=page&page=user/login,/pro25/user.do?null")
})
public class SessionFilter implements Filter {
List<String> baiList = null ;
@Override
public void init(FilterConfig config) throws ServletException {
String bai = config.getInitParameter("bai");
String[] baiArr = bai.split(",");
baiList = Arrays.asList(baiArr);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest ;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//http://localhost:8080/pro25/page.do?operate=page&page=user/login
System.out.println("request.getRequestURI() = " + request.getRequestURI());
System.out.println("request.getQueryString() = " + request.getQueryString());
String uri = request.getRequestURI() ;
String queryString = request.getQueryString() ;
String str = uri + "?" + queryString ;
if(baiList.contains(str)){
filterChain.doFilter(request,response);
}else{
HttpSession session = request.getSession() ;
Object currUserObj = session.getAttribute("currUser");
if(currUserObj==null){
response.sendRedirect("page.do?operate=page&page=user/login");
}else{
filterChain.doFilter(request,response);
}
}
}
@Override
public void destroy() {
}
}
通过@WebInitParam注解,配置白名单的方式,对登陆页面:/pro25/page.do?operate=page&page=user/login和验证用户的/pro25/user.do?null的请求放行,在过滤器SessionFilter初始化时,获取注解中的
value = "/pro25/page.do?operate=page&page=user/login,/pro25/user.do?null"
通过分隔符“,”,转为数组后放入baiList集合中
在执行doFilter方法中,进行判断:if(baiList.contains(str)){ filterChain.doFilter(request,response); }
由此来放行白名单的url。
最终实现了:
-
登陆页面的URL放行
-
查询用户的请求放行
-
若session中没有user对象,拦截并跳转登陆页面
2 使用Spring Security
2.1 什么是Spring Security
-
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
-
Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
-
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。springsecurity底层实现为一条过滤器链,这和我们Javaweb阶段的实现是类似的,使用的是filterChain类。
2.2 配置Spring Security
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这时访问主页会弹出一个认证
输入错误的密码会被拦截并弹出错误
需要输入我们日志文件中生成的随机密码才能进行访问
Using generated security password: 0d2a0780-ce31-43f8-bc30-e1be32c287cc
2.3 自定义用户登录认证
以上可见使用Spring Security,我们快速为我们的web应用配置了一层保护。
但是这不完全是我们想要的,我们希望和Javaweb阶段一样:
-
用户信息储存关系数据库中;
-
有一个登录页面提供用户认证;
-
提供多个用户信息和注册功能;
-
对不同的请求路径执行不同的安全规则。
基础配置类-securityConfig
该配置类为我们展现一个和之前一样的登录页面的用户名、密码表单
我们通过重写其中的configure方法来自定义用户存储的方式,还可以实现密码的转码存储。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//tag::configureAuthentication_jdbc_passwordEncoder[]
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from Users " +
"where username=?")
.authoritiesByUsernameQuery(
"select username, authority from UserAuthorities " +
"where username=?")
.passwordEncoder(new StandardPasswordEncoder("53cr3t");
}
//end::configureAuthentication_jdbc_passwordEncoder[]
}
将明文密码转码存储
在SecurityConfig中添加密码转码器encoder
@Bean
public PasswordEncoder encoder() {
return new StandardPasswordEncoder("53cr3t");
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
2.4 注册用户
登陆页面
<p>New here? Click
<a th:href="@{/register}">here</a> to register.</p>
注册页面
<form method="POST" th:action="@{/register}" id="registerForm">
<label for="username">Username: </label>
<input type="text" name="username"/><br/>
<label for="password">Password: </label>
<input type="password" name="password"/><br/>
...更多用户信息
<input type="submit" value="Register"/>
</form>
controller
@Controller
@RequestMapping("/register")
public class RegistrationController {
private UserRepository userRepo;
private PasswordEncoder passwordEncoder;
public RegistrationController(
UserRepository userRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public String registerForm() {
return "registration";
}
@PostMapping
public String processRegistration(RegistrationForm form) {
userRepo.save(form.toUser(passwordEncoder));
return "redirect:/login";
}
}
处理路径为"/register"的GET请求,调用registerForm()方法返回逻辑视图名"registration",由thymeleaf模板渲染:
registration表单视图
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
</head>
<body>
<h1>Register</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<form method="POST" th:action="@{/register}" id="registerForm">
<label for="username">Username: </label>
<input type="text" name="username"/><br/>
<label for="password">Password: </label>
<input type="password" name="password"/><br/>
<label for="confirm">Confirm password: </label>
<input type="password" name="confirm"/><br/>
<label for="fullname">Full name: </label>
<input type="text" name="fullname"/><br/>
<label for="street">Street: </label>
<input type="text" name="street"/><br/>
<label for="city">City: </label>
<input type="text" name="city"/><br/>
<label for="state">State: </label>
<input type="text" name="state"/><br/>
<label for="zip">Zip: </label>
<input type="text" name="zip"/><br/>
<label for="phone">Phone: </label>
<input type="text" name="phone"/><br/>
<input type="submit" value="Register"/>
</form>
</body>
</html>
用户填写完表单后,该controller再调用public String processRegistration(RegistrationForm form)方法对POST请求进行处理,其中使用了三个类:RegistrationForm、UserRepository和PasswordEncoder,在用户提交注册表单后他们分别起到了哪些作用?
RegistrationForm
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.Data;
import tacos.User;
@Data
public class RegistrationForm {
private String username;
private String password;
private String fullname;
private String street;
private String city;
private String state;
private String zip;
private String phone;
public User toUser(PasswordEncoder passwordEncoder) {
return new User(
username, passwordEncoder.encode(password),
fullname, street, city, state, zip, phone);
}
}
调用toUser方法,使用PasswordEncoder类对用户提交的密码进行加密,并生成一个与提交表单对应的User对象。
UserRepository
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.User;
public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
是一个继承于CrudRepository的接口,用于对User对象进行各种自定义的增删改查操作(自定义的User类实现了UserDetails),使用的是Spring Data JPA来生成他的实现类。
其中的save方法就是我们在RegistrationController通过UserRepository对象调用的CrudRepository里的save方法,他将生成的user对象信息存入数据库中。
PasswordEncoder
我们在之前的基础配置类SecurityConfig里注入了一个Bean:PasswordEncoder
@Bean
public PasswordEncoder encoder() {
return new StandardPasswordEncoder("53cr3t");
}
他用于用户输入密码的加密操作
3 保护web请求
3.1 合法用户验证
以上我们完成了自定义的用户登陆和注册功能之后,此时的Web请求默认都需要认证,所以我们需要想Javaweb阶段配置“白名单"那样,使用Filter过滤器对web请求进行拦截或放行,自定义登录页和注册页的请求保护。
现在我们使用Spring Security来实现
我们使用到的是WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法,只需要在SecurityConfig里配置即可完成:
-
拦截请求确保用户具备适当权限
-
配置自定义的登录页
-
支持用户退出应用
-
预防跨站请求伪造
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/**").access("permitAll")
.and()
.formLogin()
.loginPage("/login")
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.csrf()
.ignoringAntMatchers("/h2-console/**")
// Allow pages to be loaded in frames from the same origin; needed for H2-Console
.and()
.headers()
.frameOptions()
.sameOrigin()
;
}
.access("hasRole('ROLE_USER')"):具备ROLE_USER权限的用户才能访问"/design", "/orders"(业务界面) .antMatchers("/**").access("permitAll"):其他的请求允许所有用户访问 (注册页面)
对比Javaweb阶段的实现方式,现在通过这些配置方法我们可以更高效的配置我们任何想要的安全限制,
可以看到,Spring Security底层和Javaweb阶段时一样,都使用了Filter过滤器类。
3.2 防止跨站请求伪造
3.2.1 什么是跨站请求伪造?
跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见的安全攻击,让用户在一个恶意的Web页面上填写信息,之后将表单提交到另一个应用上。
3.2.2 如何防御跨站请求伪造?
我们只需要了解Spring Security会自动帮我们防范CSRF攻击即可。
参考资料:
《Spring实战第5版》