1.简介
- 1.Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案
- 2.Web应用的安全性一般包括:认证(Authentication)和授权(Authorization)两个部分
- 认证:验证某个用户是否为系统的合法用户
- 授权:验证用户是否有权限进行某个操作
- 3.认证和授权是SpringSecurity的核心功能
2.特点
- 1.Spring Security和Spring无缝整合
- 2.其具有全面的权限控制
- 3.重量级框架
1.对比Shiro
- 1.Apache旗下的轻量级权限控制框架
- 2.不局限于Web环境
2.模块划分
- 1.Core - spring-security-remoting.jar
- 2.Remoting - spring-security-web.jar
- 3.Web - spring-security-web.jar
- 4.Config - spring-security-config,jar
- 5.LDAP - spring-security-ldap.jar
- 6.OAuth 2.0 Core - spring-security-oauth2-core.jar
- 7.OAuth 2.0 Client - spring-security-oauth2.client.jar
- 8.OAuth 2.0 JOSE - spring-security-oauth2-jose.jar
- 9.OAuth 2.0 Resource Server - spring-security-oauth2-resource-server.jar
- 10.ACL - spring-security-acl,jar
- 11.CAS - spring-security-cas.jar
- 12.OpenID - spring-security-openid.jar
- 13.Test -spring-security-test.jar
3.搭建步骤
1.搭建SpringBoot项目
- 参照Spring Boot文章中的
第一个SpringBoot程序
2.引入Spring Security依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.4.0</version> </dependency>
- 1
- 2
- 3
- 4
- 5
3.初始认证页面
- 1.引入依赖后重新启动项目即可生效
- 2.未导入依赖时可以直接访问接口,重启后再次访问会自动跳转到认证页面(Spring Security默认登陆页面)
- 3.初始默认用户名是user,密码输出在控制台
- 4.登录认证之后才能对接口进行访问
- 5.注意:如果是通过Postman等测试工具测试,需要通过Authorization输入账号密码认证
4.认证
1.认证流程
- 1.认证流程图
- 2.说明
- 1.前后端分离时认证的核心是token,它是加密之后的字符串
- 2.通过是否携带token判断是否是系统的用户(可以拿到token加密之前的数据去判断是哪个用户)
- 3.流程
- 1.前端:携带用户名密码访问登录接口
- 2.服务器:根据数据库中的用户名和密码进行效验;如果正确,使用用户名/用户id,生成一个jwt(加密后生成的字符串),把jwt作为tokend的值响应给前端
- 3.前端:将token进行存储
- 4.前端:登录后访问其他请求在请求头中携带保存的token
- 5.后端:获取请求头中的token进行解析,如果合法,即获取userID(或其他加密的信息)
- 6.后端:根据用户id获取用户相关信息,判断权限制允许访问哪些相关资源,如果能访问则将访问的信息响应给前端
1.认证流程详情
- 1.接口
- 1.Authentication接口:它的实现类表示当前访问系统的用户,封装了用户相关信息
- 2.AuthenticationManager接口:它的实现类定义了认证Authentication的方法
- 3.UserDetailsService接口:加载用户特定数据的核心接口,定义了一个根据用户名查询用户信息的方法
- 4.UserDetails接口:提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,最后将这些信息封装到Authentication对象中
- 2.说明
- 1.前端提交用户密码
- 2.UsernamePasswordAuthenticationFilter将前端提交的信息封装成Authentication对象
- 3.将Authentication对象传递给ProviderManager,调用ProviderManager的authenticate方法进行认证
- 4.将Authentication对象传递给DaoAuthenticationProvider,调用DaoAuthenticationProvider的authenticate方法进行认证
- 5.调用loadUserByUsername方法查询用户
- 5.1.根据用户名去查询对应的用户及对应的权限信息,
- 5.2把对象的用户信息包括权限信息封装成UserDetails对象
- 6.返回UserDetails对象
- 7.通过PasswordEncoder对比UserDetails中的密码和Authentication的密码是否正确
- 8.如果正确就把UserDetails中的权限信息设置到Authertication对象中
- 9.返回Authentication对象
- 10.如果上一步返回了Authentication对象就使用SecurityContextHolder.getContext().setAuthentication方法存储该对象
- 11.其他过滤器会通过SecurityContextHolder来获取当前用户信息
2.认证原理
1.过滤器链(三个核心过滤器)
- 1.Spring Security本质是一个过滤器链,内部包含了提供各种功能的过滤器
- 2.启动时可以在控制台中看到所有的过滤器
- 3.通过断点调试也可查看所有的过滤器:通过Evaluate计算run.getBean(DefaultSecurityFilterChain.class)
- 4.其中核心的三个过滤器
![]()
- 1.UsernamePasswordAuthenticationFilter:负责对/login的POST请求做拦截,效验表单中的用户名密码
- 2.ExceptionTranslationFilter:负责处理过滤器链中出现的异常
- 3.FilterSecurityInterceptor:负责处理授权,位于过滤器的最底部
2.过滤器加载过程
- 1.将初始化过程委托给DelegatingFilterProxy(委托代理过滤器)中的doFilter的initDelegate(委托初始化)
- 2.获取FilterChainProxy的targetBeanName,然后实例化
- 3.调用FilterChainProxy的doFilter方法
- 4.实际调用私有方法doFilterInternal
- 5.然后调用getFilters方法获取所有的过滤器
3.两个核心接口
- 1.UserDetailsService接口
- 1.默认配置时账号密码由Spring Security定义生成
- 2.实际开发中账号密码是通过数据库查询
- 3.实现UserDetailsService接口可以完整自定义生成账号密码
- 2.PasswordEncoder接口
- 1.实际开发中密码一般使用密文存储
- 2.PasswordEncoder接口是用来对User中的密码进行加密(数据加密接口)
@Test public void testPasswordEncoder(){ BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String username = bCryptPasswordEncoder.encode("123456"); System.out.println("加密:"+username); boolean result = bCryptPasswordEncoder.matches("123456", username); System.out.println("是否匹配:"+result); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
3.实现
1.自定义登陆设置
- 1.第一种方式:通过修改配置文件
spring: security: user: name: root password: 123456
- 1
- 2
- 3
- 4
- 5
- 2.第二种方式:通过添加配置类
package com.wd.config; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String password = passwordEncoder.encode("123456"); //简单设置账号,密码,角色并保存在内存中 auth.inMemoryAuthentication().withUser("tester").password(password).roles("admin"); } @Bean //需要注入该接口实现类 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" public PasswordEncoder getPasswordEncoder(){ 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
- 3.第三种方式:自定义编写实现类
第一步:创建配置类,设置使用哪个userDetailsService实现类 第二步:编写实现类,返回User对象,User对象包含用户名密码和操作权限
- 1
- 2
package com.wd.config; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder()); } @Bean public PasswordEncoder getPasswordEncoder(){ 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
package com.wd.service.impl; 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; import java.util.List; //简单示例,未操作数据库 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); //简单设置账号,密码,角色 return new User("auths", new BCryptPasswordEncoder().encode("123456"), auths); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
package com.wd.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.wd.mapper.UserMapper; 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; import javax.annotation.Resource; import java.util.List; //通过UserMapper查询数据库数据 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.调用userMapper方法,根据用户名查询数据库 QueryWrapper<com.wd.domains.User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name",username); com.wd.domains.User user = userMapper.selectOne(queryWrapper); //2.用户不存在则直接报错 if(user == null){ throw new UsernameNotFoundException("用户名不存在!"); } //3.用户存在则返回User对象 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("auths", new BCryptPasswordEncoder().encode(user.getPassword()), auths); } }
- 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
2.自定义登陆页面
- 1.配置类
package com.wd.config; 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; import javax.annotation.Resource; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder()); } @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义登陆页面 .loginPage("/login.html") //设置登陆页面 .loginProcessingUrl("/user/login") //设置登陆访问路径 .defaultSuccessUrl("/user/index").permitAll() //登陆成功后,默认跳转路径 .and() .authorizeRequests() //自定义请求认证 .antMatchers("/","/user/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证 .anyRequest().authenticated() //其余路径需要验证 .and() .csrf().disable(); //关闭csrf防护 } }
- 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
- 2.登陆页面
DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆title> head> <body> <form action="/springboot_test/user/login" method="post"> 用户名:<input type="text" name="username"> <br/> 密码:<input type="text" name="password"> <br/> <input type="submit" value="登陆"> form> body> html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
5.授权
- 基于角色或权限进行访问控制
1.授权流程
2.授权原理
3.实现
1.授权的四个方法
1.hasAuthority
- 1.如果当前的用户具有指定的权限,则返回true,否则返回false
- 2.表示哪种权限可以访问当前地址
- 3.需要设置UserDetailsService的实现类中返回User对象对应的权限
2.hasAnyAuthority
- 1.如果当前的用户具有指定的多个权限中的任意一个权限,则返回true,否则返回false
- 2.表示哪些权限可以访问当前地址
- 3.需要设置UserDetailsService的实现类中返回User对象对应的一种权限
3.hasRole
- 1.如果当前用户具有指定角色在,则返回true,否则返回false
- 2.表示哪种角色可以访问当前地址
- 3.需要设置UserDetailsService的实现类中返回User对象对应的角色
4.hasAnyRole
- 1.如多当前用户具有指定的多个角色中的任意一个角色,则返回true,否则返回false
- 2.表示哪些角色可以访问当前地址
- 3.需要设置UserDetailsService的实现类中返回User对象对应的一种角色
- 注意:使用hasRole和hasAnyRole时,UserDetailsService实现类的角色前需要加上ROLE_
package com.wd.config; 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; import javax.annotation.Resource; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder()); } @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //自定义登陆页面 .loginPage("/login.html") //设置登陆页面 .loginProcessingUrl("/user/login") //设置登陆访问路径 .defaultSuccessUrl("/user/index").permitAll() //登陆成功后,默认跳转路径 .and() .authorizeRequests() //自定义请求认证 .antMatchers("/","/user/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证 //表示当前登陆用户,只有具有admin权限才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) // .antMatchers("/user/index").hasAuthority("admin") //表示当前登陆用户,只要具有admin或user权限才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) // .antMatchers("/user/index").hasAnyAuthority("admin","user") //表示当前登陆用户,只有属于user角色才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) //注意:配置完成后,需要在UserDetailsService实现类的授权的前面加上ROLE_,因为源码中默认在该角色前加上了ROLE_ .antMatchers("/user/index").hasRole("user") // .antMatchers("/user/index").hasAnyRole("user","admin") .anyRequest().authenticated() //其余路径需要验证 .and() .csrf().disable(); //关闭csrf防护 } }
- 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
package com.wd.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.wd.mapper.UserMapper; 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; import javax.annotation.Resource; import java.util.List; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1.调用userMapper方法,根据用户名查询数据库 QueryWrapper<com.wd.domains.User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name",username); com.wd.domains.User user = userMapper.selectOne(queryWrapper); //2.用户不存在则直接报错 if(user == null){ throw new UsernameNotFoundException("用户名不存在!"); } //3.用户存在则返回User对象 // Listauths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin"); // Listauths = AuthorityUtils.commaSeparatedStringToAuthorityList("user"); List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_user"); // Listauths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin"); return new User("auths", new BCryptPasswordEncoder().encode(user.getPassword()), auths); } }
- 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
2. 自定义权限不足页面
- 1.跳转设置
package com.wd.config; 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; import javax.annotation.Resource; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder()); } @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { //权限不足跳转页面,即UserDetailsService中的权限不满足 http.exceptionHandling() .accessDeniedPage("/unAuth.html"); http.formLogin() //自定义登陆页面 .loginPage("/login.html") //设置登陆页面 .loginProcessingUrl("/user/login") //设置登陆访问路径 .defaultSuccessUrl("/user/index").permitAll() //登陆成功后,默认跳转路径 .and() .authorizeRequests() //自定义请求认证 .antMatchers("/","/user/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证 //表示当前登陆用户,只有具有admin权限才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) // .antMatchers("/user/index").hasAuthority("admin") //表示当前登陆用户,只要具有admin或user权限才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) // .antMatchers("/user/index").hasAnyAuthority("admin","user") //表示当前登陆用户,只有属于user角色才能访问这个路径,没有这个权限则会报403错误(没有访问权限 403) //注意:配置完成后,需要在UserDetailsService实现类的授权的前面加上ROLE_,因为源码中默认在该角色前加上了ROLE_ .antMatchers("/user/index").hasRole("user") // .antMatchers("/user/index").hasAnyRole("user","admin") .anyRequest().authenticated() //其余路径需要验证 .and() .csrf().disable(); //关闭csrf防护 } }
- 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
6.注解
1.@Secured
- 1.判断是否具有指定角色,如果具有则允许访问(该注解在方法执行前进行权限验证)
- 2.使用该注解需要先开启注解功能(启动类上或配置类加上@EnableGlobalMethodSecurity(securedEnalbed = true))
- 3.Controller类的具体接口上使用该注解,设置授权角色
- 4.UserDetailsService实现类中设置用户角色(如果该角色符合授权角色则允许访问,否则403权限不足)
- 5.注意:匹配的字符串需要添加前缀 ROLE_
2.@PreAuthorize
- 1.判断是否具有指定权限或角色,如果具有则允许访问(该注解在方法执行前进行权限验证)
- 2.使用该注解需要先开启注解功能(启动类上或配置类加上@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
- 3.Controller类的具体接口上使用该注解使用该注解,设置授权角色或权限
- 4.UserDetailsService实现类中设置用户角色或权限(如果符合则允许访问,否则403权限不足)
3.@PostAuthorize
- 1.方法执行完成后,判断是否有指定权限或角色,如果具有则允许访问(该注解在方法执行后进行权限验证)
- 2.使用该注解需要先开启注解功能(启动类上或配置类加上@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
- 3.Controller类的具体接口上使用该注解使用该注解,设置授权角色或权限
- 4.UserDetailsService实现类中设置用户角色或权限(如果符合则允许返回,否则403权限不足)
4.@PostFilter
- 1.权限验证并执行方法之后对数据进行过滤,留下满足条件的数据
- 2.表达式中filterObject引用的是方法返回值List中的某一个元素
5.@PreFilter
- 1.权限验证并执行方法之前对参数进行过滤,留下满足条件的参数
6.完整案例
package com.wd.controller; import com.wd.domains.User; import com.wd.enums.SexEnum; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/user") public class UserController { @GetMapping("hello") public String hello(){ return "Hello user"; } @GetMapping("index") public String index(){ return "Hello index"; } @GetMapping("update") @Secured({"ROLE_sale","ROLE_user1"}) public String update() { return "hello update"; } @GetMapping("insert") // @PreAuthorize("hasRole('ROLE_user1')") @PreAuthorize("hasAnyAuthority('menu:system')") public String insert() { return "hello insert"; } @GetMapping("getAllUsers") @PreAuthorize("hasAnyRole('ROLE_user1')") @PostFilter("filterObject.name == 'admin'") public List<User> getAllUsers(){ ArrayList<User> list = new ArrayList<>(); list.add(new User(1,"test1","123456", SexEnum.MALE,18,181.5,63.5)); list.add(new User(1,"test2","123456", SexEnum.MALE,18,181.5,63.5)); list.add(new User(1,"admin","123456", SexEnum.MALE,18,181.5,63.5)); return list; } @GetMapping("getAllParameter") @PreAuthorize("hasAnyAuthority('ROLE_user1')") @PreFilter(value = "filterObject.age%2==0") public List<User> getAllParameter(@RequestBody List<User> list){ list.add(new User(1,"test1","123456", SexEnum.MALE,18,181.5,63.5)); list.add(new User(1,"test2","123456", SexEnum.MALE,19,181.5,63.5)); list.add(new User(1,"admin","123456", SexEnum.MALE,18,181.5,63.5)); return list; } }
- 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
7.注销
1.添加注销链接
2.配置类中添加注销映射地址
2.密码加密存储
- 实际项目中我们不会把密码明文存储在数据库中
- 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password,它会根据id去判断密码的加密方式
- 但是我们一般不会采用这种方式,所以就需要替换PasswordEncoder
- 一般使用SpringSecurity为我们提供的BCryptPasswordEncoder
- 我们只需要把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码效验
- 我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter
3.登录接口
- 自定义登录接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不登录也能访问
- 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器
- 认证成功的话要生成一个jwt,放入响应中返回,并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key
4.退出登录
- 定义一个登录接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可
1.流程
- 1.登陆
- 1.自定义登陆接口
- 1.调用ProviderManager的方法进行认证,如果认证通过生成jwt
- 2.把用户信息存入redis中
- 2.自定义UserDetailsService
- 1.在这个实现类中去查询数据库
- 2.效验
- 1.自定义jwt认证过滤器
- 1.获取并解析token获取其中userid
- 2.从redis中获取用户信息存入SecurityContextHolder
2.依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> <version>2.6.3version> dependency> <dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.8version> dependency> <--jwt--> <dependency> <groupId>io.jsonwebtokengroupId> <artifactId>jjwtartifactId> <version>0.9.0version> dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
3.配置
- 参照Redis中的Spring Boot操作Redis的配置类
授权
1.权限系统的作用
- 不同的用户可以使用不同的功能,这就是授权系统要去实现的效果
- 我们不能只依赖前端是去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作
- 所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的操作
2.授权的基本流程
- 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限效验,在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,当前用户是否拥有访问当前资源所需的权限
- 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication
- 然后设置我们的资源所需的权限即可
授权实现
1.限制访问资源所需权限
- SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限
- 但是要使用它我们需要先开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解,@PreAuthorize
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}
2.封装权限信息
- 在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回
- 先直接把权限信息写死封装到UserDetails中进行测试
- 之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改
从数据库查询权限信息
RBAC权限模型
- RBAC(Role-Based Access Control)权限模型:即:基于角色的权限控制,这是目前最常被开发者使用也是相对易用,通用的权限模型
一个角色可以具有多个权限,一个权限可能对应多个角色,多对多关系
- 一个用户可以拥有多个角色,一个角色可以对应多个用户,多对多关系
- 注意,用户名id和角色名id组成起来需要唯一
准备工作
use security;
CREATE TABLE tb_menu(
id BIGINT(20) PRIMARY KEY AUTO_INCREMENT,
menu_name VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
path VARCHAR(200) DEFAULT NULL COMMENT '路由地址',
component VARCHAR(255) DEFAULT 'NULL' COMMENT '组件路径',
visible CHAR(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
status CHAR(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
perms VARCHAR(100) DEFAULT NULL COMMENT '权限标识',
icon VARCHAR(100) DEFAULT '#' COMMENT '菜单图标',
create_by BIGINT(20) DEFAULT NULL,
create_time DATETIME DEFAULT NULL,
update_by BIGINT(20) DEFAULT NULL,
update_time DATETIME DEFAULT NULL,
del_flag int(11) DEFAULT 0 COMMENT '是否删除(0未删除 1已删除)',
remark VARCHAR(500) DEFAULT NULL COMMENT '备注'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
CREATE TABLE tb_role_menu(
role_id BIGINT(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
menu_id BIGINT(200) NOT NULL DEFAULT '0' COMMENT '菜单ID',
PRIMARY KEY(role_id,menu_id)
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE tb_user_role(
user_id BIGINT(200) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
role_id BIGINT(200) NOT NULL DEFAULT '0' COMMENT '角色ID',
PRIMARY KEY(user_id,role_id)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
CREATE TABLE tb_role(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
name VARCHAR(128) DEFAULT NULL,
role_key VARCHAR(100) DEFAULT NULL COMMENT '用户角色权限字符串',
status CHAR(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
del_flag INT(1) DEFAULT 0 COMMENT 'del_falg',
create_by BIGINT(100) DEFAULT NULL,
create_time DATETIME DEFAULT NULL,
update_by BIGINT(100) DEFAULT NULL,
update_time DATETIME DEFAULT NULL,
remake VARCHAR(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY(id)
)ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
-- 根据userid查询perms
-- 对应的role和menu都必须是正常状态
SELECT
DISTINCT m.perms
FROM
tb_user_role ur
LEFT JOIN tb_role r ON ur.role_id = r.id
LEFT JOIN tb_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN tb_menu m ON m.id = rm.menu_id
WHERE
user_id = 2
AND r.`status` = 0
AND m.`status` = 0
代码实现
- 只需要根据用户id去查询到其锁对应的权限信息即可
- 所以我们可以先定义mapper,其中提供一个方法可以根据userid查询权限信息
1.自定义失败处理
- 希望在认证失败或者授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制
- 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到,在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常
- 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理
- 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法区进行异常处理
所以如果我们需要自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
自定义实现类
- 定义认证失败处理器
package com.wd.www.handler;
import com.alibaba.fastjson.JSON;
import com.wd.www.domain.ResponseResult;
import com.wd.www.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
//处理异常
ResponseResult<Object> result = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请重新登录");
String json = JSON.toJSONString(request);
WebUtils.renderString(response,json);
}
}
- 定义授权失败处理器
package com.wd.www.handler;
import com.alibaba.fastjson.JSON;
import com.wd.www.domain.ResponseResult;
import com.wd.www.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//处理异常
ResponseResult<Object> result = new ResponseResult<>(HttpStatus.FORBIDDEN.value(),"您的权限不足");
String json = JSON.toJSONString(request);
WebUtils.renderString(response,json);
}
}
跨域
- 浏览器处于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议,域名,端口号都完全一直
- 前后端分离项目,前端和后端项目一般都是不同源的,所以肯定会存在跨域请求问题
- 所以需要处理一下,让前端能进行跨域请求
- 先对SpringBoot配置,运行跨域请求
- 再对SpringSecurity进行配置,开启SpringSecurity的跨域访问
- 由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问
其他权限效验方法
- 前面使用的@PreAuthorized注解,然后在其中使用的是hasAuthority方法进行效验。SpringSecurity还为我们提供了其他方法。例:hasAnyAuthority,hasRole,hasAnyRole等
- 并且可以选择定义效验方法,实现我们自己的效验逻辑
- hasAuthority方法实际是执行了SecurityExpressionRoot的hasAuthority,通过断点调试即可知道内部效验原理
- 内部其实是调用authentication的getAuthorities方法获取用户的权限列表,然后判断我们存入的方法参数数据在权限列表中
- hasAnyAuthority方法可以传入多个权限,只要用户有其中任意一个权限都可以访问对应资源
- hasRole要求对应的角色才可以方法,但是它内部会把我们传入的参数拼接上ROLE_后再去比较,所以这种情况下要用户对应的权限也要有ROLE_这个前缀才可以
- hasAnyRole有任意的角色就可以访问,它内部也会把我们传入的参数拼接上ROLE_后再去比较,所以这种情况下要用户的权限也要有ROLE_这个前缀才可以
自定义权限效验方法
- 我们也可以定义自己的权限效验方法,在@PreAuthorize注解中使用我们的方法
- 在SPEL表达式中使用@ex相当于获取容器中Bean的名字为ex的对象,然后再调用这个对象的hasAuthority方法
基于配置的权限控制
- 我们也可以在配置类中使用配置的方式对资源进行权限控制
CSRF
- CSRF是指跨站请求伪造(Cross-site requet forgery),是web常见的攻击之一
- SpringSecurity去放置CSRF工具的方式就是通过csrf_token,后端会生成一个csrf_token,前端发起请求的时候需要携带crsf_token,后端会有过滤器进行效验,如果没有携带或者时伪造的就不允许访问
- 我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们认证信息其实是token,而token并不是存储在cookie中,并且需要前端代码去吧token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
登录成功处理器
- 实际上在UsernamePasswordAuthenticationFilter进行登录验证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器
- 我们也可以自己去定义成功处理进行成功后的相应处理
认证失败处理器
- 实际上UsernamePasswordAuthenticationFilter进行登录认证的时候,如果认证失败了了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的,
- 我们也可以自己去定义失败处理进行失败后的相应处理
其他认证方案