一、Spring Security基础
1.Spring Security介绍
a.Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架。
b.Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。
2.Spring Boot整合Spring Security实现的安全管理功能
a.MVC Security是Spring Boot整合Spring MVC框架搭建的Web应用的安全管理。
b.WebFlux Security是Spring Boot整合Spring WebFlux框架搭建的Web应用的安全管理。
c.OAuth2是大型项目的安全管理框架,可以实现第三方认证、单点登录等功能。
d.Actuator Security用于对项目的一些运行环境提供安全监控,例如Health健康信息、Info运行信息等,它主要作为系统指标供运维人员查看管理系统的运行情况。
二、Spring Security入门
1.基础环境搭建
a.创建Spring Boot项目
引入Web和Thymeleaf的依赖启动器
b.引入页面Html资源文件
在项目的resources下templates目录中,引入案例所需的资源文件,项目结构如下
c.编写Web控制层
@Controller
public class FilmController {
// 影片详情页
@GetMapping("/detail/{type}/{path}")
public String toDetail(@PathVariable("type")String type, @PathVariable("path")String path) {
return "detail/"+type+"/"+path;
}
}
至此,使用Spring Boot整合Spring MVC框架实现了一个传统且简单的Web项目,
目前项目没有引入任何的安全管理依赖,也没有进行任何安全配置,
项目启动成功后,通过http://localhost:8080访问首页,单击影片进入详情详情页。
2.开启安全管理效果测试
a.添加spring-boot-starter-security启动器
一旦项目引入spring-boot-starter-security启动器,MVC Security和WebFlux Security负责的安全功能都会立即生效
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
b.项目启动测试
项目启动时会在控制台Console中自动生成一个安全密码
如果是热部署重启项目,可能不会有安全密码,那就关闭项目,再启动
浏览器访问http://localhost:8080/查看项目首页,会跳转到一个默认登录页面。
因为添加了Security依赖后,会进行spring security的自动化配置,需要先登录,才能访问首页,Spring Security会自带一个默认的登录页面。
随意输入一个错误的用户名和密码,会出现错误提示
Security会默认提供一个可登录的用户信息,其中用户名为user,密码随机生成,
这个密码会随着项目的每次启动随机生成并打印在控制台上,在登录页面输入用户名和密码。
这种默认安全管理方式存在诸多问题,例如:
只有唯一的默认登录用户user、密码随机生成且过于暴露、登录页面及错误提示页面不是我们想要的等。
三、MVC Security安全配置
1.MVC Security安全配置简介
项目引入spring-boot-starter-security依赖启动器,MVC Security安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。
SecurityAutoConfiguration导入并自动化配置SpringBootWebSecurityConfiguration用于启动Web安全管理.
UserDetailsServiceAutoConfiguration用于配置用户身份信息.
这两个类的位置:
先看spring-boot-autoconfigure-2.0.7.RELEASE.jar下的/META-INF/spring.factories文件,发现org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
发现在org.springframework.boot.autoconfigure.security.servlet这个包下,
2.关闭Sercurity提供的Web应用默认安全配置
1.要完全关闭Security提供的Web应用默认安全配置,可以自定义WebSecurityConfigurerAdapter类型的Bean组件以及自定义UserDetailsService、AuthenticationProvider或AuthenticationManager类型的Bean组件。
2.另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件来覆盖默认访问规则。
3.WebSecurityConfigurerAdapter类的主要方法及说明
方法 | 描述 |
configure(AuthenticationManagerBuilder auth) | 定制用户认证管理器来实现用户认证 |
configure(HttpSecurity http) | 定制基于HTTP请求的用户访问控制 |
四、自定义用户认证
1.内存身份认证
a.自定义WebSecurityConfigurerAdapter配置类
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
注:
@EnableWebSecurity注解是一个组合注解,主要包括@Configuration注解、@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class})注解和@EnableGlobalAuthentication注解
b.使用内存进行身份认证
SecurityConfig类中重写configure(AuthenticationManagerBuilder auth)方法,并在该方法中使用内存身份认证的方式自定义了认证用户信息。定义用户认证信息时,设置了两个用户名和密码以及对应的角色信息。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
auth.inMemoryAuthentication().passwordEncoder(encoder)
.withUser("shitou").password(encoder.encode("123456")).roles("common")
.and()
.withUser("李四").password(encoder.encode("123456")).roles("vip");
}
}
c.效果测试
重启项目进行效果测试,项目启动成功后,仔细查看控制台打印信息,发现没有默认安全管理时随机生成的密码了。通过浏览器访问http://localhost:8080/
2.JDBC身份认证
a.数据准备
# 选择使用数据库
USE springbootdata;
# 创建表t_customer并插入相关数据
DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`username` varchar(200) DEFAULT NULL,
`password` varchar(200) DEFAULT NULL,
`valid` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer` VALUES ('1', 'shitou', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
INSERT INTO `t_customer` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
# 创建表t_authority并插入相关数据
DROP TABLE IF EXISTS `t_authority`;
CREATE TABLE `t_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`authority` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `t_authority` VALUES ('1', 'ROLE_common');
INSERT INTO `t_authority` VALUES ('2', 'ROLE_vip');
# 创建表t_customer_authority并插入相关数据
DROP TABLE IF EXISTS `t_customer_authority`;
CREATE TABLE `t_customer_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`customer_id` int(20) DEFAULT NULL,
`authority_id` int(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer_authority` VALUES ('1', '1', '1');
INSERT INTO `t_customer_authority` VALUES ('2', '2', '2');
b.添加JDBC连接数据库的依赖启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
c.进行数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
d.使用JDBC进行身份认证
在SecurityConfig 类中的configure(AuthenticationManagerBuilder auth)方法中使用JDBC身份认证的方式进行自定义用户认证,使用JDBC身份认证时,首先需要对密码进行编码设置(必须与数据库中用户密码加密方式一致);然后需要加载JDBC进行认证连接的数据源DataSource;最后,执行SQL语句,实现通过用户名username查询用户信息和用户权限。
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String userSQL ="select username,password,valid from t_customer "+ "where username = ?";
String authoritySQL="select c.username,a.authority from t_customer c, "+"t_authority a,t_customer_authority ca where "+"ca.customer_id=c.id and ca.authority_id=a.id and c.username =?";
auth.jdbcAuthentication().passwordEncoder(encoder).dataSource(dataSource)
.usersByUsernameQuery(userSQL).authoritiesByUsernameQuery(authoritySQL);
}
e.效果测试
先停止运行再启动或者直接relaunch下。在浏览器中http://localhost:8080/
如果热部署,可能出现下面错误
比如a bean of type 'javax.sql.DataSource' that could not be found.
3.UserDetailsService 身份认证
a.要用到jpa和redis,所以加入两个依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
b.处理类要用到其他类,项目结构如下
c.上述类的主要代码如下:
@Entity(name = "t_authority ")
public class Authority implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String authority ;
}
@Entity(name = "t_customer")
public class Customer implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
}
public interface AuthorityRepository extends JpaRepository<Authority,Integer> {
@Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)
public List<Authority> findAuthoritiesByUsername(String username);
}
public interface CustomerRepository extends JpaRepository<Customer,Integer> {
Customer findByUsername(String username);
}
//CustomerService业务处理类,用来通过用户名获取用户及权限信息
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private AuthorityRepository authorityRepository;
@Autowired
private RedisTemplate redisTemplate;
// 业务控制:使用唯一用户名查询用户信息
public Customer getCustomer(String username){
Customer customer=null;
Object o = redisTemplate.opsForValue().get("customer_"+username);
if(o!=null){
customer=(Customer)o;
}else {
customer = customerRepository.findByUsername(username);
if(customer!=null){
redisTemplate.opsForValue().set("customer_"+username,customer);
}
}
return customer;
}
// 业务控制:使用唯一用户名查询用户权限
public List<Authority> getCustomerAuthority(String username){
List<Authority> authorities=null;
Object o = redisTemplate.opsForValue().get("authorities_"+username);
if(o!=null){
authorities=(List<Authority>)o;
}else {
authorities=authorityRepository.findAuthoritiesByUsername(username);
if(authorities.size()>0){
redisTemplate.opsForValue().set("authorities_"+username,authorities);
}
}
return authorities;
}
}
d.定义UserDetailsService用于封装认证用户信息
UserDetailsService是Security提供的进行认证用户信息封装的接口,该接口提供的loadUserByUsername(String s)方法用于通过用户名加载用户信息。使用UserDetailsService进行身份认证的时,自定义一个UserDetailsService接口的实现类,通过loadUserByUsername(String s)方法调用用户业务处理类中已有的方法进行用户详情封装,返回一个UserDetails封装类,来供Security认证使用。
自定义一个接口实现类UserDetailsServiceImpl进行用户认证信息UserDetails封装,重写了UserDetailsService接口的loadUserByUsername(String s)方法,在该方法中,使用CustomerService业务处理类获取用户的用户信息和权限信息,并通过UserDetails进行认证用户信息封装。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private CustomerService customerService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 通过业务方法获取用户及权限信息
Customer customer = customerService.getCustomer(s);
List<Authority> authorities = customerService.getCustomerAuthority(s);
// 对用户权限进行封装
List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
// 返回封装的UserDetails用户详情类
if(customer!=null){
UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);
return userDetails;
} else {
// 如果查询的用户不存在(用户名不存在),必须抛出此异常
throw new UsernameNotFoundException("当前用户不存在!");
}
}
}
e.在SecurityConfig中使用UserDetailsService进行身份认证
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
如果是springboot3.0:
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public AuthenticationManager authenticationManager() throws Exception {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
authenticationProvider.setUserDetailsService(userDetailsService);
ProviderManager providerManager = new ProviderManager(Arrays.asList(authenticationProvider));
return providerManager;
}
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
f.效果测试
重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/
五、自定义用户授权管理
1.自定义用户访问控制
a.在自定义配置类SecurityConfig中重写configure(HttpSecurity http)方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated()
.and()
.formLogin();
}
路径是“/”,直接放行。
路径是"/detail/common/**",只有用户角色是common才允许访问。
路径是"/detail/vip/**",只有用户角色是vip才允许访问。
其他请求要先登录认证后才放行。
b.效果测试
重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/
项目首页单击普通电影或者VIP专享电影名称查询电影详情
在此登录界面输入普通用户的用户名和密码,访问普通电影
在项目首页中单击VIP专享电影名称查看影片详情,
在查看VIP电影详情时,页面会出现403 Forbidden的错误信息
2.自定义用户登录
a.在项目的resources/ templates目录下创建login/login.html,核心代码如下
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户登录界面</title>
<link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/userLogin}" th:method="post" >
<img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
<h1 class="h3 mb-3 font-weight-normal">请登录</h1>
<!-- 用户登录错误信息提示框 -->
<div th:if="${param.error}" style="color: red;height: 40px;text-align: left;font-size: 1.1em">
<img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
</div>
<input type="text" name="name" class="form-control" placeholder="用户名" required="" autofocus="">
<input type="password" name="pwd" class="form-control" placeholder="密码" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
<p class="mt-5 mb-3 text-muted">Copyright© 2019-2020</p>
</form>
</body>
</html>
通过<form>标签定义了一个用户登录功能,且登录数据以POST方式向“/userLogin”路径进行提交。
其中,登录表单中的用户名参数和密码参数可以自行定义;登录数据提交方式必须为post,提交的路径也可以自行定义。
b.拷贝静态资源文件到项目resources下的static目录中
c.自定义用户登录跳转
在之前创建的FilmController类中添加一个跳转到登录页面login.html的方法
@GetMapping("/userLogin")
public String toLoginPage() {
return "login/login";
}
注:Spring Security默认使用Get方式的“/login”请求用于向登录页面跳转,默认使用Post方式的“/login”请求用于对登录后的数据进行处理。因此,自定义用户登录控制时,需要提供向用户登录页面跳转的方法,且自定义的登录页跳转路径必须与数据处理提交路径一致。
d.自定义用户登录控制
打开SecurityConfig类,重写configure(HttpSecurity http)方法实现用户登录控制
http.authorizeRequests().antMatchers("/").permitAll()
// 需要对static文件夹下静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/userLogin").permitAll()
.usernameParameter("name").passwordParameter("pwd")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
上面.usernameParameter("name").passwordParameter("pwd")中的name和pwd与login.html中的文本框name属性值一致, 如下:
e.效果测试
重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/
会直接进入到项目首页,在项目首页,单击普通电影或者VIP专享电影名称查询电影详情.
输入错误的账号信息
3.自定义用户退出
a.添加自定义用户退出链接
在index.html添加自定义用户退出链接
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
</form>
注:Spring Boot项目中引入Spring Security框架后会自动开启CSRF防护功能(跨站请求伪造防护,此处作为了解即可,后续小节将详细说明),用户退出时必须使用POST请求;如果关闭了CSRF防护功能,那么可以使用任意方式的HTTP请求进行用户注销。
b.自定义用户退出控制
在SecurityConfig类,在configure(HttpSecurity http)方法中添加如下代码进行用户退出控制
http.logout()
.logoutUrl("/mylogout")
.logoutSuccessUrl("/");
c.效果测试
重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/
先访问影片详情自动跳转到自定义的用户登录页面login.html,在登录界面输入正确的用户名和密码后,如下:
单击影片详情中的“返回”链接回到项目首页(此时用户仍处于登录状态),单击首页中的“注销”链接进行用户注销
4.登录用户信息获取
a.使用HttpSession获取用户信息及测试
在FilmeController类中新增一个用于获取当前会话用户信息的getUser()方法,在该方法中通过获取当前HttpSession的相关方法遍历并获取了会话中的用户信息。
在获取认证用户信息时,使用了Authentication的getPrincipal()方法,默认返回的也是一个Object对象,其本质是封装用户信息的UserDetails封装类,其中包括有用户名、密码、权限、是否过期等。
/**
* 通过传统的HttpSession获取Security控制的登录用户信息
* @param session
*/
@GetMapping("/getuserBySession")
@ResponseBody
public void getUser(HttpSession session) {
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
String element = names.nextElement();
if (session.getAttribute(element) instanceof SecurityContextImpl) {
SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
System.out.println("element: " + element);
System.out.println("attribute: " + attribute);
Authentication authentication = attribute.getAuthentication();
UserDetails principal = (UserDetails) authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: " + principal.getUsername());
}
}
}
以Debug模式重启项目,浏览器访问http://localhost:8080/随意查看一个影片详情进行用户登录。
登录成功后,在保证当前浏览器未关闭的情况下,使用同一浏览器执行http://localhost:8080/getuserBySession来获取用户详情。
b.使用SecurityContextHolder获取用户信息
在FilmController控制类中新增一个获取当前会话用户信息的getUser2()方法
/**
* 通过Security提供的SecurityContextHolder获取登录用户信息
*/
@GetMapping("/getuserByContext")
@ResponseBody
public void getUser2() {
// 获取应用上下文
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("userDetails: "+context);
// 获取用户相关信息
Authentication authentication = context.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: "+principal.getUsername());
}
c.使用@AuthenticationPrincipal获取用户信息
@GetMapping("/getuserByAnnotation")
@ResponseBody
public void getUser(@AuthenticationPrincipal User user) {
System.out.println(user);
}
d.使用Principal获取用户信息
@GetMapping("/getuserByPrincipal")
@ResponseBody
public void getUser(Principal user) {
System.out.println(user);
}
结果:
UsernamePasswordAuthenticationToken [Principal=
org.springframework.security.core.userdetails.User [Username=shitou, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_common]],
Credentials=[PROTECTED], Authenticated=true,
Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=EDE0B14447B32A83D32746831BCC6DC1], Granted Authorities=[ROLE_common]]
e.使用Authentication获取用户信息
@GetMapping("/getuserByAuthentication")
@ResponseBody
public void getUser(Authentication user) {
System.out.println(user);
}
结果:
UsernamePasswordAuthenticationToken [Principal=
org.springframework.security.core.userdetails.User [Username=shitou, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_common]],
Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=BF2BF5770E08BF8DCD63BC2AF3CED2B8], Granted Authorities=[ROLE_common]]
5.记住我功能(记住时长:登录之后,注销之前或token失效之前)
a.基于简单加密Token的方式
在之前创建的项目用户登录页login.html中新增一个记住我功能勾选框
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200);
基于简单加密Token的方式效果测试
重启项目进行效果测试,通过浏览器访问http://localhost:8080/userLogin
在登录界面输入正确的用户名和密码信息,同时勾选记住我功能,
就会默认跳转到项目首页index.html,关闭再打开浏览器访问项目首页,直接查看影片详情
b.基于持久化Token方式
需要在数据库中创建一个存储cookie信息的持续登录用户表
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);
打开SecurityConfig类,重写configure(HttpSecurity http)方法的记住我功能
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200)
// 对cookie信息进行持久化管理
.tokenRepository(tokenRepository());
注入dataSource,增加一个方法
@Autowired
private DataSource dataSource;
/**
* 持久化Token存储
* @return
*/
@Bean
public JdbcTokenRepositoryImpl tokenRepository(){
JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
jr.setDataSource(dataSource);
return jr;
}
基于持久化Token方式效果测试
重启项目进行效果测试,通过浏览器访问项目首页,输入正确的账户信息,
勾选记住我后,跳转到项目首页index.html,
查看数据库中创建的存储cookie信息的持续登录用户表。
重新打开刚才使用的浏览器,访问项目首页并直接查看影片详情,会发现无需重新登录就可以直接访问。此时,再次查看数据库中表数据信息。Token更新过了
返回到浏览器首页,单击首页上方的用户“注销”连接,在Token有效期内进行用户手动注销。此时,再次查看数据库中表数据信息。
6.CSRF防护功能
a.简介
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
CSRF攻击攻击原理及过程如下:
其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户。
1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
参考:CSRF攻击与防御(写得非常好)_挺住我先走的博客_csrf漏洞
b.CSRF防护功能演示
创建数据修改页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>用户修改</title></head>
<body>
<div align="center">
<form method="post" action="/updateUser">
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
</div></body></html>
编写后台控制层方法,编写的toUpdate()方法用于向用户修改页面跳转,updateUser()方法用于对用户修改提交数据处理。其中,在updateUser()方法中只是演示了获取的请求参数,没有具体的业务实现。
@Controller
public class CSRFController {
// 向用户修改页跳转
@GetMapping("/toUpdate")
public String toUpdate() {
return "csrf/csrfTest";
}
// 用户修改提交处理
@ResponseBody
@PostMapping(value = "/updateUser")
public String updateUser(@RequestParam String username, @RequestParam String password,
HttpServletRequest request) {
System.out.println(username);
System.out.println(password);
String csrf_token = request.getParameter("_csrf");
System.out.println(csrf_token);
return "ok";
}
}
重启chapter07项目,通过浏览器访问http://localhost:8080/toUpdate,
由于前面配置了请求拦截,会先被拦截跳转到用户登录页面。
在用户登录页面输入正确的用户信息后,就会自动跳转到用户修改页面。
数据修改请求中没有携带CSRF Token(CSRF令牌)相关的参数信息,所以被认为是不安全的请求。
整合Spring Security安全框架后,项目默认启用了CSRF安全防护功能,项目中所有涉及到数据修改方式的请求都会被拦截。
c.直接关闭CSRF功能
配置类SecurityConfig,在重写的configure(HttpSecurity http)方法中进行关闭配置即可
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
...
}
d.配置Security需要的CSRF Token
先开启CSRF再测试
Spring Security提供的CSRF Token配置,主要有:
针对Form表单数据修改的CSRF Token配置
<form method="post" action="/updateUser">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
用户名: <input type="text" name="username" /> <br />
密 码: <input type="password" name="password" /> <br />
<button type="submit">修改</button>
</form>
针对Ajax数据修改请求的CSRF Token配置(不做演示)
在页面<head>标签中添加<meta>子标签,并配置CSRF Token信息
<html>
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
在具体的Ajax请求中获取<meta>子标签中设置的CSRF Token信息并绑定在HTTP请求头中进行请求验证
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
<!-- 在 Ajax 请求发送之前将CSRF Token信息绑定在HTTP请求头-->
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
演示:添加jquery.min.js,如下图
修改csrfTest.html,重启项目后再次访问/toUpdate
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<script th:src="@{/js/jquery.min.js}"></script>
<script type="text/javascript">
function loadXMLDoc(){
$.ajax({
url:"/updateUser",
data:{username:"haha",password:"oooo"},
type:"post"
});
}
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
</script>
<title>用户修改</title>
</head>
<body>
<button type="button" onclick="loadXMLDoc()">请求数据</button>
<div align="center">
<form method="post" action="/updateUser">
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
</div>
</body>
</html>
六、Security管理前端页面
之前我们只是对前端页面做了权限控制,并没有做任何处理,用户体验差。所以我们使用security与thymeleaf整合实现前端页面的管理。
1.添加thymeleaf-extras-springsecurity5依赖启动器
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
2.修改前端页面,使用Security相关标签进行页面控制
在index.html页面中引入Security安全标签
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
页面顶部通过“xmlns:sec”引入了Security安全标签,页面中根据需要编写了4个<div>模块.
<div sec:authorize="isAnonymous()">
<h2 align="center">
游客您好,如果想查看电影<a th:href="@{/userLogin}">请登录</a>
</h2>
</div>
<div sec:authorize="isAuthenticated()">
<h2 align="center">
<span sec:authentication="name" style="color: #007bff"></span>
您好,您的用户权限为
<span sec:authentication="principal.authorities"
style="color:darkkhaki"></span>,您有权观看以下电影
</h2>
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
</form>
</div>
<div sec:authorize="hasRole('common')">
<h3>普通电影</h3>
<ul>
<li><a th:href="@{/detail/common/1}">我不是药神</a></li>
<li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
</ul>
</div>
<div sec:authorize="hasAuthority('ROLE_vip')">
<h3>VIP专享</h3>
<ul>
<li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
<li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
</ul>
</div>
3.效果测试
重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/