3.1 设置登录系统的账号、密码

04-web权限方案-认证-设置用户名和密码.png

  • 方式一:在 application.properties


spring.security.user.name=atguigu spring.security.user.password=atguigu

  • 第二种方式:配置类


  1. package com.antherd.securitydemo.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  5. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  6. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  7. import org.springframework.security.crypto.password.PasswordEncoder;
  8. @Configuration
  9. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  10. @Override
  11. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  12. BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  13. String password = passwordEncoder.encode("123");
  14. auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
  15. }
  16. // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  17. @Bean
  18. PasswordEncoder password() {
  19. return new BCryptPasswordEncoder();
  20. }
  21. }
  • 第三种方式:自定义编写实现类


  1. 创建配置类,设置使用哪个userDetailService实现类
  1. package com.antherd.securitydemo.config;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  6. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  7. import org.springframework.security.core.userdetails.UserDetailsService;
  8. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  9. import org.springframework.security.crypto.password.PasswordEncoder;
  10. @Configuration
  11. public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
  12. @Autowired
  13. private UserDetailsService userDetailsService;
  14. @Override
  15. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  16. auth.userDetailsService(userDetailsService).passwordEncoder(password());
  17. }
  18. // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  19. @Bean
  20. PasswordEncoder password() {
  21. return new BCryptPasswordEncoder();
  22. }
  23. }
  1. 编写实现类,返回User对象,User对象有用户名密码和操作权限
  1. package com.antherd.securitydemo.service;
  2. import com.antherd.securitydemo.entity.Users;
  3. import com.antherd.securitydemo.mapper.UsersMapper;
  4. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  5. import java.util.List;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.security.core.GrantedAuthority;
  8. import org.springframework.security.core.authority.AuthorityUtils;
  9. import org.springframework.security.core.userdetails.User;
  10. import org.springframework.security.core.userdetails.UserDetails;
  11. import org.springframework.security.core.userdetails.UserDetailsService;
  12. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  13. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  14. import org.springframework.stereotype.Service;
  15. @Service("userDetailsService")
  16. public class MyUserDetailsService implements UserDetailsService {
  17. @Override
  18. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  19. List<GrantedAuthority> auths =
  20. AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
  21. return new User("lucy", new BCryptPasswordEncoder().encode(123), auths);
  22. }
  23. }

启动项目,访问:http://localhost:8111/test/hello,自动跳转到登陆页面,输入配置的用户名密码后,跳转到访问页面。

3.2 实现数据库认证来完成用户登录

05-web权限方案-认证-查询数据库认证.png

整合MyBatisPlus完成数据库操作

  1. 引入相关依赖
<!-- mybatis-plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.0.5</version>
</dependency>

<!-- mysql -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- lombok 用来简化实体类 -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
  1. 创建数据库demo和数据库表
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加数据

INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (1, 'lucy', '123');
INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (2, 'mary', '456');
  1. 创建users表对应实体类
package com.antherd.securitydemo.entity;

import lombok.Data;

@Data
public class Users {

  private Integer id;

  private String username;

  private String password;
}
  1. 整合MybatisPlus,创建接口,继承map的接口
package com.antherd.securitydemo.mapper;

import com.antherd.securitydemo.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;

@Repository
public interface UsersMapper extends BaseMapper<Users> {

}
  1. 在UserDetailsService调用mapper里面的方法查询数据库进行用户认证
package com.antherd.securitydemo.service;

import com.antherd.securitydemo.entity.Users;
import com.antherd.securitydemo.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

  @Autowired
  private UsersMapper usersMapper;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 调用usersMapper方法,根据用户名查询数据库
    QueryWrapper<Users> wrapper = new QueryWrapper<>();
    // where username = ?
    wrapper.eq("username", username);
    Users users = usersMapper.selectOne(wrapper);
    // 判断
    if (users == null) { // 数据库没有用户名, 认证失败
      throw new UsernameNotFoundException("用户名不存在!");
    }
    List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
    // 从查询数据库返回users对象,得到用户名和密码,返回
    return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
  }
}
  1. 在启动类添加注解MapperScan

@MapperScan(“com.antherd.securitydemo.mapper”)

  1. 配置文件中配置数据库信息
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
  1. 启动测试

image.png

3.3 用户认证(自定义用户登录页面)

06-web权限方案-认证-自定义登录页面.png

  1. 在配置类实现相关的配置
package com.antherd.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(password());
  }

  // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  @Bean
  PasswordEncoder password() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // 自定义自己编写的登录页面
        .loginPage("/login.html") // 登录页面设置
        .loginProcessingUrl("/user/login") //登录访问路径
        .defaultSuccessUrl("/test/index").permitAll() // 登录成功后,跳转路径
        .successForwardUrl("/success") // 登录成功之后跳转到哪个 url
        .failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url
        .and().authorizeRequests() //
          .antMatchers("/", "/test/hello", "/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
        .anyRequest().authenticated() // 其他请求,需要认证
        .and().csrf().disable(); // 关闭csrf防护

  }
}
  1. 创建相关页面,controller在resource文件夹下创建static文件夹,添加文件login.html,用户名、密码参数必须为:username、password
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
  <form action="/user/login" method="post">
    用户名:<input type="text" name="username">
    <br/>
    密码:<input type="text" name="password">
    <br/>
    <input type="submit" value="login" />
  </form>
</body>
</html>

在TestController中添加接口

@GetMapping("/index")
public String index() {
  return "hello index";
}
  1. 启动测试

访问:http://localhost:8111/test/hello 不需要认证
访问:http://localhost:8111/test/index 跳转到自定义认证页面

注意:页面提交方式必须为 post 请求,所以上面的页面不能使用,用户名,密码必须为 username,password
原因: 在执行登录的时候会走一个过滤器 UsernamePasswordAuthenticationFilter

image.png

如果修改配置可以调用 usernameParameter()和 passwordParameter()方法。

<form action=”/login”method=”post”> 用户名:<input type=”text”name=”loginAcct”/><br/> 密码:<input type=”password”name=”userPswd”/><br/> <input type=”submit”value=”提交”/> </form>

image.png

3.4 基于角色或权限进行访问控制

07-web权限方案-基于角色或权限的访问控制.png

3.4.1 hasAuthority 方法

如果当前的主体具有指定的权限,则返回 true,否则返回 false

  • 修改配置类

image.png

  • 添加一个控制器

image.png

  • 给用户登录主体赋予权限

image.png

  • 测试:

http://localhost:8090/findAll 访问 findAll 进入登录页面

image.png

认证完成之后返回登录成功

image.png

3.4.2 hasAnyAuthority 方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.

访问 http://localhost:8090/find

image.png

3.4.3 hasRole 方法

如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。

底层源码

image.png

给用户添加角色:

image.png

修改配置文件:

注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。

image.png

3.4.4 hasAnyRole

表示用户具备任何一个条件都可以访问。

给用户添加角色:

image.png

修改配置文件:

image.png

3.5 基于数据库实现权限认证

3.5.1 准备 sql

create table users(
       id bigint primary key auto_increment,
    username varchar(20) unique not null,
    password varchar(100)
);

-- 密码 atguigu
insert into users values(1,'张san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
-- 密码 atguigu
insert into users values(2,'李si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');

create table role(
  id bigint primary key auto_increment,
  name varchar(20)
);

insert into role values(1,'管理员');
insert into role values(2,'普通用户');

create table role_user(
  uid bigint,
  rid bigint
);

insert into role_user values(1,1);
insert into role_user values(2,2);

create table menu(
  id bigint primary key auto_increment,
  name varchar(20),
  url varchar(100),
  parentid bigint,
  permission varchar(20)
);

insert into menu values(1,'系统管理','',0,'menu:system');
insert into menu values(2,'用户管理','',0,'menu:user');

create table role_menu(
  mid bigint,
  rid bigint
);

insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);

3.5.2 添加实体类

@Data
public class Menu {
    private Long id;
    private String name;
    private String url;
    private Long parentId;
    private String permission; 
}

@Data
public class Role {
    private Long id;
    private String name; 
}

3.5.3 编写接口与实现类

UserInfoMapper

/**
* 根据用户 Id 查询用户角色
* @param userId
* @return
*/
List<Role> selectRoleByUserId(Long userId);
/**
* 根据用户 Id 查询菜单
* @param userId
* @return
*/
List<Menu> selectMenuByUserId(Long userId);
上述接口需要进行多表管理查询:
需要在 resource/mapper 目录下自定义 UserInfoMapper.xml 
<?xml version="1.0"encoding="utf-8"?>
<!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 
3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 
<mapper namespace="com.atguigu.mapper.UserInfoMapper">
    <!--根据用户 Id 查询角色信息--> 
    <select id="selectRoleByUserId"resultType="com.atguigu.bean.Role">
         SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON 
        ru.rid=r.id where ru.uid=#{0}
    </select>

    <!--根据用户 Id 查询权限信息--> 
    <select id="selectMenuByUserId"resultType="com.atguigu.bean.Menu">
         SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m
         INNER JOIN role_menu rm ON m.id=rm.mid
        INNER JOIN role r ON r.id=rm.rid
        INNER JOIN role_user ru ON r.id=ru.rid
        WHERE ru.uid=#{0}
    </select>
</mapper>

UsersServiceImpl

image.png

3.5.3 在配置文件中添加映射

在配置文件中 application.yml 添加

mybatis: mapper-locations: classpath:mapper/*.xml

3.5.4 修改访问配置类

image.png

3.5.5 使用管理员与非管理员进行测试

如果非管理员测试会提示 403 没有权限

image.png

3.6 自定义 403 页面

08-web权限方案-配置403访问页面.png

3.6.1 修改访问配置类

// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");

3.6.2 添加对应控制器

@GetMapping("/unauth")
public String accessDenyPage(){
    return "unauth"; 
}
  • unauth.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
  <h1>没有访问权限!</h1>
</body>
</html>

3.7 用户授权(注解使用)

09-web权限方案-注解使用.png

3.7.1 @Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。

使用注解先要开启注解功能!

@EnableGlobalMethodSecurity(securedEnabled=true)


@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled=true)
public class DemosecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemosecurityApplication.class, args);
     } 
}

在控制器方法上添加注解

image.png

登录之后直接访问:http://localhost:8090/testSecured 控制器

将上述的角色改为 @Secured({“ROLE_normal”,“ROLE_管理员”})即可访问

image.png

3.7.2 @PreAuthorize

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的 roles/permissions 参数传到方法中。

image.png

3.7.3 @PostAuthorize

先开启注解功能:

@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.

image.png

3.7.4 @PostFilter

@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据
表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

image.png

3.7.5 @PreFilter

@PreFilter: 进入控制器之前对数据进行过滤

image.png

先登录,然后使用 postman 进行测试
image.png
image.png
测试的 Json 数据:

[ { “id”: “1”, “username”: “admin”, “password”: “666” }, { “id”: “2”, “username”: “admins”, “password”: “888” }, { “id”: “3”, “username”: “admins11”, “password”: “11888” }, { “id”: “4”, “username”: “admins22”, “password”: “22888”}]

3.7.6 权限表达式

https://docs.spring.io/springsecurity/site/docs/5.3.4.RELEASE/reference/html5/#el-access

3.8 基于数据库的记住我

10-web权限方案-用户注销.png
记住用户的流程

11-web权限方案-记住用户流程.png
实现原理

image.png

12-web权限方案-记住用户实现.png

3.8.1 创建表

CREATE TABLE `persistent_logins` (
   `username` varchar(64) NOT NULL,
   `series` varchar(64) NOT NULL,
   `token` varchar(64) NOT NULL,
   `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
  CURRENT_TIMESTAMP,
   PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.8.2 添加数据库的配置文件

spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.128:3306/test username: root password: root

3.8.3 编写配置类

创建配置类

// 注入数据源
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
  JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  jdbcTokenRepository.setDataSource(dataSource);
  // 自动创建表,第一次执行会创建,以后要执行就要删除掉!
  // jdbcTokenRepository.setCreateTableOnStartup(true); // 自动创建表
  return jdbcTokenRepository;
}

3.8.4 修改安全配置类

@Autowired
private UsersServiceImpl usersService;
@Autowired
private PersistentTokenRepository tokenRepository;
// 开启记住我功能
http.rememberMe()
 .tokenRepository(tokenRepository)
 .userDetailsService(usersService);

3.8.5 页面添加记住我复选框

记住我:<input type=”checkbox”name=”remember-me”title=”记住密码”/><br/>

此处:name 属性值必须位 remember-me.不能改为其他值

3.8.6 使用张三进行登录测试

登录成功之后,关闭浏览器再次访问 http://localhost:8090/find,发现依然可以使用!

image.png

3.8.7 设置有效期

默认 2 周时间。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。

在配置文件中设置

image.png

浏览器中存入了cookies,并且数据库中记录了相关信息

image.png

image.png

3.9 用户注销

  1. 在配置类中添加退出的配置
// 退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
  1. 测试:
    ① 修改配置类,登录成功之后跳转到成功页面
.defaultSuccessUrl("/success.html").permitAll() // 登录成功后,跳转路径

② 在成功页面添加超链接,写设置的退出路径

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  登录成功!
  <a href="/logout">退出</a>
</body>
</html>

③ 登录成功后,在点击页面点击退出

退出之后,是无法访问需要登录时才能访问的控制器!

3.10 CSRF


3.10.1 CSRF 理解

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS
利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

image.png

在pom文件中引入如下依赖

<!-- 对Thymeleaf添加Spring Security标签支持 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在templates目录下新建三个页面

csrf/csrf_token.html

<!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>
    <span th:text="${_csrf.token}"></span>
  </div>
</body>
</html>

csrf/csrfTest.html

<!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="update_token">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
      用户名:<input type="text" name="username" /> <br />
      密&nbsp;&nbsp;码:<input type="password" name="password" /> <br />
      <button type="submit">修改</button>
    </form>
  </div>
</body>
</html>

login/login.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
<form action="/user/login" method="post">
  用户名:<input type="text" name="username">
  <br/>
  密码:<input type="text" name="password">
  <br/>
  <input type="checkbox" name="remember-me" />自动登录
  <br />
  <input type="submit" value="login" />
</form>
</body>
</html>

新建 CSRFUserDetailsService

package com.antherd.securitydemo.service;

import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class CSRFUserDetailsService implements UserDetailsService {

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    List<SimpleGrantedAuthority> list = new ArrayList<>();
    list.add(new SimpleGrantedAuthority("role"));
    UserDetails userDetails = new User("lucy", new BCryptPasswordEncoder().encode("123"),
        list);
    return userDetails;
  }
}

SecurityConfigCsrf

package com.antherd.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfigCsrf extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  // 实现用户身份认证
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 配置url的访问权限
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/**update**").permitAll()
        .antMatchers("/login/**").permitAll()
        .anyRequest().authenticated();

    // 关闭csrf保护功能
//    http.csrf().disable();

    // 使用自定义的登录窗口
    http.formLogin()
        .loginPage("/userLogin").permitAll()
        .usernameParameter("username").passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/userLogin?error");
  }
}

CSRFController

package com.antherd.securitydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CSRFController {

  @GetMapping("/toupdate")
  public String test(Model model) {
      return "csrf/csrfTest";
  }

  @PostMapping("/update_token")
  public String getToken() {
    return "csrf/csrf_token";
  }
}

LoginController

package com.antherd.securitydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

  @GetMapping("/userLogin")
  public String login() {
    return "login/login";
  }
}

现将 csrfTest页面中如下内容注释掉

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

启动测试,访问 http://localhost:8111/toupdate , 发现登录触发CSRF,跳转到 /userLogin?error 页面
取消上面注释后,在启动测试,可以正常登录

3.10.2 Spring Security 实现 CSRF 的原理:

  1. 生成 csrfToken 保存到 HttpSession 或者 Cookie 中。

image.png

image.png

SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository

image.png

image.png

当前接口实现类:HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository

3. SpringSecurity Web 权限方案 - 图45

  1. 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当 前请求是否合法。主要通过 CsrfFilter 过滤器来完成。

3. SpringSecurity Web 权限方案 - 图46