目录

Security - 信息验证

笔记

本内容从配置文件、内存、数据库一一获取用户信息,并进行权限验证。

2021-12-25 @Young Kbt

# 环境准备

创建一个 Spring Boot 项目,添加如下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 基于配置文件的用户信息验证

目录结构如下:

包名 cn.kbt
├── controller 
│   ├── HelloController
│
└── SecurityApplication
1
2
3
4
5

resources 目录结构:

resources
│
└── application.yml
1
2
3

在 application.yml 配置如下信息:

spring:
  security:
    user:
      name: kele  # 自定义用户名
      password: 123456   # 自定义密码
1
2
3
4
5

创建 Controller 层,新建 HelloController 类,添加如下内容:

@RestController
public class HelloController {

    @RequestMapping("/")
    public String sayHello1() {
        return "Hello SpringSecurity 安全管理框架!";
    }
}
1
2
3
4
5
6
7
8

创建启动类 SecurityYmlApplication,添加如下内容:

@SpringBootApplication
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityYmlApplication.class, args);
    }
}
1
2
3
4
5
6
7

然后启动项目,访问 http://localhost:8080/login,页面如图:

image-20211225142929671

输入在配置文件配置的用户名和密码后,就会如下效果:

image-20211225143459154

注意:页面提交方式默认为 POST 请求,用户名,密码必须为 username、password,但是我们可以修改,具体看 完整的配置模板

# 基于内存的用户信息验证

# 方式一

一看到内存,就知道,这种方式有好有坏,好是处理迅速,坏是一旦重启项目,那么内存的用户信息都会被清空,一般用户量少才推荐使用。

目录结构如下:

包名 cn.kbt
├── config 
│   ├── MyWebSecurityConfig
├── controller 
│   ├── HelloController
│
└── SecurityApplication
1
2
3
4
5
6
7

创建 config 层,新建 MyWebSecurityConfig 类,添加如下内容:

@Configuration
@EnableWebSecurity  // 开启 Spring Security
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pr = passwordEncoder();
        // inMemoryAuthentication 代表内存
        auth.inMemoryAuthentication()
                .withUser("kele")
                .password(pr.encode("123456")) // 密码加密到内存
                .roles();
        auth.inMemoryAuthentication()
                .withUser("bing")
                .password(pr.encode("123456"))
                .roles();
        auth.inMemoryAuthentication()
                .withUser("xue")
                .password(pr.encode("123456"))
                .roles();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

密码加密到了内存,但是用户登录还是会使用自己注册的密码,只不过后台查看的密码是加密的,防止窃取后被使用。

创建 Controller 层,新建 HelloController 类,添加如下内容:

@RestController
public class HelloController {

    @RequestMapping("/")
    public String sayHello1() {
        return "Hello SpringSecurity 安全管理框架!";
    }
}
1
2
3
4
5
6
7
8

创建启动类 SecurityYmlApplication,添加如下内容:

@SpringBootApplication
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityYmlApplication.class, args);
    }
}
1
2
3
4
5
6
7

其实大家会发现,内存的验证只是把配置文件的配置移植到内存里,其他不变,那么访问效果也是和 配置文件的信息验证 一样。

# 方式二

如果你不喜欢直接在 configure 添加多个内存的用户信息,那么可以在另一个方法里添加内存的用户信息,然后 return 给 configure 方法。

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {  

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        PasswordEncoder encoder = passwordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("kele")
                           .password(encoder.encode("123456"))
                           .roles("admin","user").build());

        manager.createUser(User.withUsername("bing")
                           .password(encoder.encode("123456"))
                           .roles("user").build());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.userDetailsService(userDetailsService);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

可以看到,我们需要使用 InMemoryUserDetailsManager 类,代表方式一的 auth.inMemoryAuthentication()

# 基于角色Role的身份认证

有些资源要求用户必须具有某个角色的权限才能访问,那么我们需要给用户添加对应的 Role 角色才能访问资源,这里演示给内存的用户信息添加 Role 角色,其实数据库只需要新建一个 Role 表, 然后查询获取即可。

内存怎么添加 Role 角色呢,在 基于内存的用户信息验证 的 MyWebSecurityConfig 基础上,加入角色,如下:

/**
 * @EnableGlobalMethodSecurity: 启用方法级别认证
 *      prePostEnabled:布尔类型 默认为false
 *          true:开启@PreAuthorize 注解 和 @PostAuthorize 注解
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pr = passwordEncoder();
        auth.inMemoryAuthentication()
                .withUser("kele")
                .password(pr.encode("123456"))
                .roles("normal");    		// 该用户拥有一个角色
        auth.inMemoryAuthentication()
                .withUser("bing")
                .password(pr.encode("123456"))
                .roles("admin");			// 该用户拥有一个角色
        auth.inMemoryAuthentication()
                .withUser("xue")
                .password(pr.encode("123456"))
                .roles("admin","normal");  // 该用户拥有两个角色
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

给用户设置好了角色,那么如何在访问资源的时候,判断满足角色条件?当然是在 Controller 里判断。

而要想判断角色时,我们需要在上方代码的第 8 行开启允许判断的注解 @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)

在 Controller 的方法上添加角色判断,如下:

@RestController
public class HelloController {

    @RequestMapping("hello")
    @ResponseBody
    @PreAuthorize(value = "hasAnyRole('normal','admin')") // 普通用户和管理员都可以访问
    @Secured({"ROLE_normal","ROLE_admin"})  // 判断是否具有角色,角色要加上 ROLE_
    public String sayHello() {
        return "Hello SpringSecurity 安全管理框架!";
    }

    @RequestMapping("/")
    @ResponseBody
    @PreAuthorize(value = "hasRole('admin')")  // 只有管理员都可以访问
    public String sayHelloAdmin() {
        return "Hello SpringSecurity 安全管理框架!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这样,虽然用户登录成功了,但是如果没有对象的角色,那么无法触发对应的方法。

值得注意的是第 7 行代码,判断角色的时候,必须加上 ROLE_ ,因为源码就是在添加的角色基础上,加上这个字符串作为前缀。

为什么 hasAnyRole 和 hasRole 不用加上 ROLE_ 因为方法内部已经自动加上,所以我们不需要重复加上。

如果想探究 @PreAuthorize@Secured 的含义,请点击 Controller 注解

# 基于权限的认证

大家要明白一点,一个用户可以是多个角色 Role,一个角色可以拥有多个权限,那么权限和角色的区别是什么呢?

想想一个场景,新人入职,需要给他分配一些权限,那么这些权限不会是一个,而是很多个,但是每个新人入职都要给这些一样的权限,那么是不是很麻烦。我们直接把这些权限给一个角色:普通员工。那么之后新人直接分配「普通员工」即可拥有这些权限,肯定比一个一个权限分配来的效率高。

其实 Spring Security 并没有把权限和角色分得很细,他们很大的区别就是角色的值会在前面加上 ROLE_ 作为前缀,而权限不需要加上。

权限的赋予和角色的赋予一样,如下:

/**
 * @EnableGlobalMethodSecurity: 启用方法级别认证
 *      prePostEnabled:布尔类型 默认为false
 *          true:开启@PreAuthorize 注解 和 @PostAuthorize 注解
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder pr = passwordEncoder();
        auth.inMemoryAuthentication()
                .withUser("kele")
                .password(pr.encode("123456"))
                .authorities("normal");    		// 该用户拥有一个权限
        auth.inMemoryAuthentication()
                .withUser("bing")
                .password(pr.encode("123456"))
                .authorities("admin");			// 该用户拥有一个权限
        auth.inMemoryAuthentication()
                .withUser("xue")
                .password(pr.encode("123456"))
                .authorities("admin","normal");  // 该用户拥有两个权限
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

那么 Controller 改为如下:

@RestController
public class HelloController {

    @RequestMapping("hello")
    @ResponseBody
    @PreAuthorize(value = "hasAnyAuthority('normal','admin')") // 普通用户和管理员都可以访问
    @Secured({"normal","admin"})  // normal 和 admin 可以访问
    public String sayHello() {
        return "Hello SpringSecurity 安全管理框架!";
    }

    @RequestMapping("/")
    @ResponseBody
    @PreAuthorize(value = "hasAuthority('admin')")  // 只有管理员都可以访问
    public String sayHelloAdmin() {
        return "Hello SpringSecurity 安全管理框架!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

看出区别了吗?无非就是改个方法名,以及去掉 ROLE_ 前缀。

# 基于jdbc的用户认证

这种认证方式是最常用的,也是最推荐使用的。我们需要写 sql 来从数据库获取用户的信息和角色。而在哪个类写 sql 呢?还记得前面的常用接口里有说过吗,就是实现 UserDetailsService 接口,Spring Security 会自动调用该接口的方法。

流程

  1. 重写 UserDetails loadUserByUsername(String username) 方法

  2. 通过 username 获取数据库的用户信息,必须有三个字段(用户名,密码,角色名称)

  3. 创建 GrantedAuthority 的 List 集合,用于存储 GrantedAuthority,其实就是存储多个角色名称

  4. 创建单个 GrantedAuthority,用户存储用户信息,其实就是存储一个角色名称,必须固定以 ROLE_ 开头,后面加上自己的角色名称

  5. 创建User实例,该User是security提供的User,构造器需要传入三个参数,用户名,密码,角色集合,即 User 类需要2的用户信息,其中角色名称放在步骤 3 和 4 里

  6. return 该 User 对象

  7. 在配置类的重写 configure(AuthenticationManagerBuilder auth) 中,使用前面写好的继承 UserDetailsService 类

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserInfoDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = null;
        if(username != null){
            //找到数据库的用户信息,必须有三个字段(用户名,密码,角色名称)
            UserInfo userInfo = userDao.findUserByUsername(username);
            if(userInfo != null){
                //创建GrantedAuthority的List集合,用于存储GrantedAuthority
                List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
                //创建单个GrantedAuthority,用户存储用户信息
                //固定以ROLE_开头
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + userInfo.getRole());
                //存储进去
                grantedAuthorities.add(grantedAuthority);
                // 创建User实例,该User是security的User,需要传入三个参数,用户名,密码,角色集合
                user = new User(userInfo.getUsername(), userInfo.getPassowrd(), grantedAuthorities);
                // 如果不查询数据库,可以直接指定用户的权限或者角色
                // user = new User(userInfo.getUsername(), userInfo.getPassowrd(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_admin"));
            }
        }
        return user;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

第 23 行代码,是直接在代码里赋予用户 admin 权限和 admin 角色,而前面我们已经说过了,角色和权限的区别就在于是否加了 ROLE_ 作为前缀。

目前写的其实是白写,因为我们并没有把这个类交给 Spring Security 管理,所以在配置类引入自己重写的 UserDetailsService 类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Qualifier("MyUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加自己写得类,并对密码加密解密
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

其他的类我就不写了,就是 service 层、mapper 层,至于数据库的三个张表(用户表、角色表、用户角色关联表),效果如图:

image-20211224192105286

提供 findUserByUsername 方法的 sql:

select r.* from sys_user u,sys_role r,user_role ur where u.id = ur.user_id and r.id = ur.role_id and username = #{username}
1

# 完整的配置模板

学习完前面几个认证方式后,相比还是有些疑惑,比如:

  • 如何使用自己写的登录页面?

  • 如何批量的对一些请求加入角色或者权限验证?

  • 如何对一些请求不需要验证

  • 如何限制一个网站的用户登录量

  • ......

# 模板内容

这是精通的部分,也就是配置类的内容,下面提供模板:

注意:页面提交方式默认为 POST 请求,用户名,密码必须为 username、password,但是我们可以修改,具体如下 40 - 41 行代码:

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义的类
     */
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private MySuccessHandler successHandler;
    @Autowired
    private MyFailureHandler failureHandler;
    @Autowired
    private DataSource dataSource;
    // 更多自定义类引用 ......
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl token = new JdbcTokenRepositoryImpl();
        token.setDataSource(dataSource);  // 配置数据源
        // token.setCreateTableOnStartup(true); // 如果没有建表,主动建表,表名是实体类名
        return token;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加自己写得类,并对密码加密解密
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // 配置没有权限访问跳转自己的自定义页面
       http.cors().and().csrf().disable()  // 关闭跨域安全问题
            .authorizeRequests().antMatchers("/component/**","/css/**", "/fonts/**",
            "/images/**","/main/main.js", "/project/**", "/utils/**", "/"
            ).permitAll()  // 上面的请求都放行
            .antMatchers("/login/user/**").hasAnyRole("USER")  // 拥有的权限验证
            .antMatchers("/login/read/**").hasAnyRole("READ")
            .antMatchers("/login/admin/**").hasAnyRole("ADMIN")
            .antMatchers("/login/admins/**").hasAnyAuthority("admins")  // 拥有的权限验证
            .anyRequest().authenticated() // 任何请求都需要进行判断是否授权
           
            // 登录处理
            .and().formLogin()
           		  .loginPage("/userLogin")		// 配置哪个 url 为登录页面
           		  .loginProcessingUrl("/login") 	// 设置哪个是登录的 url。
				  .successForwardUrl("/success")  // 登录成功之后跳转到哪个 url
				  .failureForwardUrl("/fail")		// 登录失败之后跳转到哪个 url
                  .permitAll()	// 允许所有用户
                  .successHandler(authenticationSuccessHandler)	// 登录成功处理逻辑
                  .failureHandler(authenticationFailureHandler)	// 登录失败处理逻辑
           		  .usernameParameter("name")		// 客户端传来的用户名参数 key
           		  .passwordParameter("pwd")		// 客户端传来的密码参数 key
           
            // 登出处理
            .and().logout()
           		  .logoutUrl("/logout")			// 登出请求的 url
           		  .logoutSuccessUrl("/index")	// 登出成功后,跳转的 url
                  .logoutSuccessHandler(logoutSuccessHandler)	// 登出成功处理逻辑
                  .deleteCookies("JSESSIONID")	// 登出后删除 cookie
                  .invalidateHttpSession(true)	// 登出成功后使 session 失效
           		  .permitAll()	// 允许所有用户
           
           // 记住我功能,客户端 传来的 name 默认是 remember-me
           and().rememberMe()
           		.tokenRepository(persistentTokenRepository)  // 配置数据库源
           		.userDetailsService(userDetailsService)		// 配置能访问数据库的类
           		.tokenValiditySeconds(60 * 60)  // 记住我的超时时间,单位是秒
           		.rememberMeParameter("remember")	// 客户端传来的参数 key
           		.alwaysRemember(flase)		// 是否总是记住我。true,则超时时间无效
            
           // 异常处理(权限拒绝、登录失效等)
            .and().exceptionHandling()
           		.accessDeniedPage("/unauth")	// 403 页面
                .accessDeniedHandler(accessDeniedHandler)	// 权限拒绝处理逻辑
                .authenticationEntryPoint(authenticationEntryPoint)	// 匿名用户访问无权限资源时的异常处理
           
            // 会话管理
            .and().sessionManagement()
                  .invalidSessionUrl("/userLogin")
                  .maximumSessions(1)	//同一账号同时登录最大用户数
                  .expiredSessionStrategy(sessionInformationExpiredStrategy);	// 会话失效(账号被挤下线)处理逻辑
        
    // 指定拦截器顺序,在 FilterSecurityInterceptor 之前执行 securityInterceptor
    http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
}
    public void configure(WebSecurity web) throws Exception {
       // 将项目中的静态资源路径开发,这里配置是不需要经过 Filter 过滤器的
    	web.ignoring().antMatchers("/css/**","/fonts/**","/img/**","/js/**");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

# 常用参数配置详解

HttpSecurity 类的一些参数解释如下:

常用

方法 含义
authorizeRequests() 请求权限验证
antMatchers("xxx") xxx 为网页的 url,需要在 controller 配置该 url 对应的网页
permitAll() 白名单,不需要进行验证,直接放行
hasRole() 角色验证,仅限一个,方法内部自动加 ROLE_ 前缀
hasAnyRole() 多个角色验证,逗号隔开,方法内部自动加 ROLE_ 前缀
hasAuthority() 权限验证,仅限一个
hasAnyAuthority 多个权限验证,逗号隔开
anyRequest() 任意请求
authenticated() 拦截
and() 下一步,下一步必须是另外一个功能配置
logout() 允许所有用户退出
csrf().disable() 关于跨问的内容禁用,关闭跨域

登录处理

方法 含义
formLogin() 表单验证方式
successHandler(new AuthenticationSuccessHandler) 登录成功后调用的接口,参数是一个类,需要自定义一个类继承它,然后传入即可
failureHandler(new AuthenticationFailureHandler) 登录失败后调用的接口,参数是一个类,需要自定义一个类继承它,然后传入即可
usernameParameter("xxx") 自定义接收用户名的字符串,与 input 标签的 name 对象,默认 username
passwordParameter("xxx") 自定义接收用户名的字符串,与input框的name对象,默认 password
loginPage() 指定登录页面,代替官方自动的页面
loginProcessingUrl() form 表单的请求地址,默认是 /login,如果自己的表单改了 action,则需要对应上
successForwardUrl() 登录成功后跳转的 url
failureUrl() 登录失败后跳转的 url

登出处理

方法 含义
logout() 声明登出后续处理
logoutUrl("/logout") 登出请求的 url
logoutSuccessUrl("/index") 登出成功后,跳转的 url
logoutSuccessHandler(logoutSuccessHandler) 登出成功处理逻辑
deleteCookies("JSESSIONID") 登出后删除 cookie
invalidateHttpSession(true) 登出成功后使 session 失效

记住我功能

方法 含义
rememberMe() 开启记住我功能
tokenRepository() 配置数据库源
userDetailsService() 配置能访问数据库的 DetailsService 类
tokenValiditySeconds(number) 记住我的超时时间,单位是秒
rememberMeParameter() 客户端传来的参数 key
alwaysRemember() 是否总是记住我。true,则超时时间无效

异常处理(权限拒绝、登录失效等)

方法 含义
accessDeniedPage() 配置 403 页面的 url
exceptionHandle() 异常处理类
accessDeniedPage() 没有权限时,跳转的地址

会话管理

方法 含义
sessionManagement() 开启会话管理
invalidSessionUrl() 登录的请求
maximumSessions(number) 同一账号同时登录最大用户数
expiredSessionStrategy() 会话失效(账号被挤下线)处理逻辑

# 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 方法进行防护。

# 实现原理

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

# 解决CSRF

有两种方法:

  • 在登录页面添加一个隐藏域。

    这里使用的是 thymeleaf 模板,如果是原生 html,则利用 js 将 csrf 的值加到该隐藏的 input 标签。

    <input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
    
    1
  • 关闭安全配置的类中的 csrf

    @EnableWebSecurity
    public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
           http.csrf().disable()  // 关闭跨域安全问题
    }
    
    1
    2
    3
    4
    5
    6
    7

# Controller注解

如果你看过上面的 基于角色Role的身份认证,那么就知道本内容讲解的注解是什么,上面的例子仅仅是初步使用2个注解,而类似的注解有很多个,本内容一一讲解。

首先要想在 Controller 使用 Spring Security 的注解,则需要开启,因为默认是不开启的。在配置类开启。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
	// ......
}
1
2
3
4
5
6

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



 




@RequestMapping("testSecured")
@ResponseBody
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() {
    return "hello,user";
}
1
2
3
4
5
6

@PreAuthorize:进入方法前的权限验证。

下面的验证虽然都是 admin,但是一个是角色,一个是权限,前者内部自动加上 ROLE_,后者则不需要。



 
 




@RequestMapping("/preAuthorize")
@ResponseBody
@PreAuthorize("hasRole('admin')")
@PreAuthorize("hasAuthority('admin')")
public String preAuthorize(){
    return "preAuthorize";
}
1
2
3
4
5
6
7

@PostAuthorize:在方法执行后再进行权限验证,适合验证带有返回值的权限,但是该注解使用不多。



 




@RequestMapping("/testPostAuthorize")
@ResponseBody
@PostAuthorize("hasAnyAuthority('admin')")
public String preAuthorize(){
    return "PostAuthorize";
}
1
2
3
4
5
6

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

表达式中的 filterObject 是客户端传来的集合,拦截集合里 key 为 id 、value 为偶数的值



 








@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_admin')")
@PreFilter(value = "filterObject.id % 2 == 0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list){
    list.forEach(t -> {
        System.out.println(t.getId() + "\t" + t.getUsername());
    });
    return list;
}
1
2
3
4
5
6
7
8
9
10

@PostFilter:权限验证之后对数据进行过滤。

如下代码:拦截用户名是 admin1 的数据,表达式中的 filterObject 引用的是方法返回值 List 中循环的每一个元素



 








@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_admin')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
    ArrayList<UserInfo> list = new ArrayList<>();
    list.add(new UserInfo(1l,"admin1","6666"));
    list.add(new UserInfo(2l,"admin2","888"));
    return list;
}
1
2
3
4
5
6
7
8
9
10

# 排除验证登录

如果你的项目安装了 Spring Security 依赖,但是不希望使用 Spring Security,则在启动类的注解加入 (exclude = {SecurityAutoConfiguration.class}) 即可,如下:

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}
1
2
3
4
5
6
7
更新时间: 2024/01/17, 05:48:13
最近更新
01
JVM调优
12-10
02
jenkins
12-10
03
Arthas
12-10
更多文章>