针对项目的安全管理,Spring家族提供了安全框架Spring Security,它是一个基于Spring生态圈的,用于提供安全访问控制解决问题的框架。
常见的安全管理功能如下:
1.MVC Security是Spring Boot整合Spring MVC搭建Web应用的安全管理框架
2.WebFlux Security是Spring Boot整合Spring WebFlux搭建Web应用的安全管理,继承了其他安全功能优点,有可能越来越流行
3.OAuth2是大型项目的安全管理框架,可实现第三方认证、单点登录等功能,目前SpringBoot版文还不支持该框架
4.Actuator Security用于对项目的一些运行环境提供安全监控。
Spring Security使用体验
- Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。
- 认证即确认用户是否登录,并对用户登陆进行管控
- 授权即确定用户所拥有的的功能权限,并对用户权限进行管控
基础环境搭建
- 整合MCV Security安全管理功能,实现Authentication(认证)和Authorization(授权)功能
- 文件置于templates文件夹下:有需要静态资源的朋友可以评论我发你(太多了就不放在这儿了)
- 编写Web控制类
@Controller
public class FileController {
// 影片详情映射
@GetMapping("/detail/{type}/{path}")
public String toDetail(@PathVariable("type")String type,
@PathVariable("path")String path){
return "detail/" + type+ "/" + path;
}
}
至此准备工作结束,我们实现了一个传统的Web项目,有跳转等。
开启安全管理效果测试
- 添加依赖启动器
# 一旦项目引入该依赖启动器,MVC Securityhe和WebFlux Security负责的安全功能会立即生效(后者生效的前提是项目为WebFlux Web项目)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 项目启动测试
-
项目启动时会在控制台自动生成一个安全密码(security password,该密码每次启动项目都是随机生成的)
-
启动后,Spring Security会自带一个默认的登录页面,帐号为user,密码为生成的passord
-
尽管有登陆认证,但是这种默认安全管理方式存在诸多问题。(唯一默认登录用户user、密码随机生成过于暴露、登录页面不精美等)
-
MVC Security安全配置介绍
- MVC Security安全管理功能的默认安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。
- SecurityAutoConfiguration会导入并自动化配置SpringBootWebSecurityConfiguration用于启动Web安全管理
- UserDetailsServiceAutoConfiguration用于配置用户身份信息
- WebSecurityConfigurerAdapter的主要方法:
- configure(AuthenticationManagerBuilder auth): 定制用户认证管理员来实现用户认证
- configure(HttpSecurity http): 定制基于HTTP请求的用户访问控制
自定义用户认证
通过自定义WebSecurityConfigurerAdapter类型的Bean组件,并重写configure(AuthenticationManagerBuilder auth)方法,可以自定义用户认证
三个常用的自定义用户认证:
1.In-Memory Authentication(内存身份认证)
2.JDBC Authentication(JDBC身份认证)
3.UserDetailsService(身份认证)
内存身份认证
- 主要用于Security安全认证体验和测试
- 自定义WebSecurityConfigurerAdapter配置类
@EnableWebSecurity //开启Security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
// @EnableWebSecurity 为一组合注解 = @Configuration、@Import、@EnableGlobalAuthentication
- 使用内存进行身份认证
- 自定义的SecurityConfig类中重写configure(AuthenticationManagerBuilder auth)方法
@EnableWebSecurity //开启Security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//1.使用内存用户信息,作为测试使用
auth.inMemoryAuthentication().passwordEncoder(encoder)
.withUser("tangou").password(encoder.encode("123456")).roles("common")
.and()
.withUser("张三").password(encoder.encode("123456")).roles("vip");
}
}
- 效果测试-通过
- 在实际开发中,用户都是在页面注册和登录时进行认证管理的,而非在程序内部使用内存管理的方式手动控制注册用户,故该种方式无法用于实际生产。
JDBC身份认证
- JDBC Authentication(JDBC身份认证)是通过JDBC连接数据库对已有用户身份进行认证
- 数据准备
drop table if exists ‘t_customer’;
//创建表t_customer插入数据
create table t_customer(
//Security在进行用户查询时先通过username定位是否存在唯一用户的
id int(20) not null auto_increment,
username varchar(200) default null,
password varchar(200) default null,
//必须额外定义一个tinyint类型的字段,对应boolean类型,用于校验用户身份是否合法
valid tinyint(1) not null default '1',
primary key(id)
)ENGINE=InnoDB auto_increment=4 default charset=utf8;
//$2a$10$YUthO3FOocQFxBgMMke2ROV2MKqAJwHEb8VeVhgcy4oZkx5S/zTHm 对应123456通过BCryptPasswordEncoder 加密后的形式
insert into t_customer values('1','tangou','$2a$10$YUthO3FOocQFxBgMMke2ROV2MKqAJwHEb8VeVhgcy4oZkx5S/zTHm','1');
insert into t_customer values('2','张三','$2a$10$YUthO3FOocQFxBgMMke2ROV2MKqAJwHEb8VeVhgcy4oZkx5S/zTHm','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;
//权限表:权限值必须带有“ROLE_”的前缀,默认的用户角色值是对应权限制去掉“ROLE_”前缀
insert into t_authority values('1','ROLE_common');
insert into t_authority values('2','ROLE_vip');
//创建表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');
- 添加JDBC连接数据库的依赖启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 进行数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=admin
- 使用JDBC进行身份验证
@EnableWebSecurity //开启Security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//1.使用内存用户信息,作为测试使用
// auth.inMemoryAuthentication().passwordEncoder(encoder)
// .withUser("tangou").password(encoder.encode("123456")).roles("common")
// .and()
// .withUser("张三").password(encoder.encode("123456")).roles("vip");
//2.使用JDBC进行身份认证
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 username=?";
auth.jdbcAuthentication().passwordEncoder(encoder)
.dataSource(dataSource)
.usersByUsernameQuery(userSQL)
.authoritiesByUsernameQuery(authoritySQL);
}
}
- 效果测试-与注释的内存身份认证效果相同
UserDetailService身份认证
对于流量较大的项目来说,频繁地使用JDBC进行数据库查询认证不仅麻烦,而且会降低网站响应速度。
- 核心代码如下
@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;
}
}
@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("当前用户不存在!");
}
}
}
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 3、使用UserDetailsService进行身份认证
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
- 测试效果如上面两种
- (测试前记得开启redis服务,有需要整个代码的可以评论)