Spring Security 学习笔记
本篇博客是基于B站尚硅谷 Spring Security 框架教程学习并整理的个人学习笔记,作者是初学者,笔记中若有错误的地方欢迎大家留言评论、批评指正。
一、基本原理
Spring Scurity 本质是一个过滤器链。
1.1 过滤器加载过程
-
使用 SpringSecurity 配置过滤器 DelegatingFilterProxy;
-
在 DelegatingFilterProxy 的 doFilter() 方法中调用了 initDelegate() 方法初始化成员变量 delegate;
delegateToUse = this.initDelegate(wac); this.delegate = delegateToUse;
-
在 initDelegate() 方法中通过 getTargetBeanName() 从 Spring 容器中获取到 Filter 的实现类 FilterChainProxy;
-
在 FilterChainProxy 中的 doFilter() 方法中,最终调用了doFilterInternal()方法;
-
在 doFilterInternal() 方法中以列表的形式获取到了过滤器链;
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
1.2 两个重要接口
1.2.1 UserDetailsService
使用方法
- 创建类继承 UsernamePasswordAuthenticationFilter 类,重写 attemptAuthentication()、successfulAuthentication() 和 unsuccessfulAuthentication() 方法;
- 创建类实现 UserDetailsService 接口,编写查询数据库中用户名和密码的过程,返回 User 对象,这个 User 对象是安全框架 (Spring Security) 提供的对象;
1.2.2 PasswordEncoder
数据加密接口,用于加密 User 对象里面的密码。
二、Web权限基础实例
2.1 引入相关依赖
创建 Spring Boot 项目,并在 pom.xml 文件中导入相关依赖
<dependencies>
<!--Spring Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Spring Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Mybatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--Mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
</dependencies>
2.2 创建数据库和数据库表
在数据库中建立一个数据库,然后执行下列 sql 语句创建表
drop table if exists `tb_user_info`;
create table `tb_user_info` (
`id` int(11) auto_increment,
`username` varchar(20) not null,
`password` varchar(20) not null,
primary key(`id`)
)engine=INNODB default charset=utf8;
2.3 创建实体类并配置数据库连接
在项目文件夹下新建包 pojo,然后创建 UserInfo.java
@Data
@AllArgsConstructor
@NoArgsConstructor
//以上三个是 lombok 插件的注解,自动生成构造方法和各个属性的 get/set 方法
@TableName("tb_user_info")
public class UserInfo {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String username;
private String password;
}
在 resource/application.yml 配置如下信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=Asia/Shanghai
username: 访问数据库的用户名
password: 访问数据库的密码
driver-class-name: com.mysql.cj.jdbc.Driver
2.4 整合Mybatis-Plus创建 DAO 层
在项目文件夹下新建包 dao/mapper,然后创建 UserInfoMapper.java
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
继承 Mybatis-Plus 中提供的 BaseMapper 类会自动完成简单 CRUD 的接口创建。
2.5 在 Service 层中调用 DAO 层接口
在项目文件夹下新建包 service,然后创建自定义的 MyUserDetailsService.java
@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用 userInfoMapper 方法,根据用户名查询数据库
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
UserInfo userInfo = userInfoMapper.selectOne(wrapper);
// 判断
if(userInfo == null) {
throw new UsernameNotFoundException("用户不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,superAdmin,ROLE_admin,ROLE_superAdmin");
return new User(userInfo.getUsername(),
new BCryptPasswordEncoder().encode(userInfo.getPassword()), auths);
}
}
自定义的 MyUserDetailsService 实现 UserDetailsService 接口可以调用 DAO 层的方法查询数据库中的用户信息,然后交由 Spring Security 进行验证。
2.6 编写 Spring Security 配置类
在项目文件夹下新建包 config,然后创建 WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService myUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/403.html");
http.formLogin() //自定义登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/test/index").permitAll() //登录成功之后,跳转路径
.and().authorizeRequests()
.antMatchers("/","user/login").permitAll() //直接放行的页面/资源
//.antMatchers("/test/hello").hasAnyAuthority("admin,superAdmin")
//.antMatchers("/test/hello").hasAnyRole("admin,superAdmin")
.antMatchers("/test/hello").hasRole("visitor")
.anyRequest().authenticated() //任何请求都要授权
.and().csrf().disable(); //关闭跨域请求防护
}
}
代码中是通过配置的方式是实现权限和角色的控制
.antMatchers("URL").hasAuthority("权限名")
.antMatchers("URL").hasAnyAuthority("权限1,权限2...")
.antMatchers("URL").hasRole("角色名")
.antMatchers("URL").hasAnyRole("角色1,角色2...")
在自定义的 MyUserDetailsService 类中的 loadUserByUsername() 方法中编写权限和角色的集合,以 “ROLE_” 为前缀的是角色名。
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,superAdmin,ROLE_admin,ROLE_superAdmin");
2.7 自定义登录页和403页面
在 resource/static文件夹下创建 login.html 和 403.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input name="username" type="text">
</br>
密 码:<input name="password" type="password">
</br>
<input type="submit" name="login_btn" value="登录"/>
</form>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>无权限</title>
</head>
<body>
<h1>您没有相关权限,无法访问!</h1>
</body>
</html>
2.8 编写控制类进行测试
在项目文件夹下新建包 controller,然后创建 TestController.java
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/hello")
public String hello() {
return "Hello Spring Security!";
}
@RequestMapping("/index")
public String index() {
return "我是起始页!";
}
}
2.9 通过注解的方式实现权限和角色的控制
以上代码中是通过在 WebSecurityConfig 配置类中通过配置的方式进行权限和角色的控制,下面则使用注解进行权限和角色的控制。(个人认为注解可以简化代码,更方便理清代码的逻辑)
2.9.1 启动注解
在启动类/配置类中开启注解
//分别开启 @Secured 和 @PreAuthorize、PostAuthorize 注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@SpringBootApplication
public class SpringSecurityStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityStudyApplication.class, args);
}
}
2.9.2 配置注解
在 Controller 层的方法上使用注解,设置角色/权限
@RestController
@RequestMapping("/test")
public class TestController {
//@Secured 只作用于用户角色
@RequestMapping("/add")
@Secured({"ROLE_admin","ROLE_superAdmin"})
public String add() {
return "添加用户!";
}
//@PreAuthorize 作用于角色和权限,是在执行方法前验证权限/角色
@RequestMapping("/del")
//@PreAuthorize("hasAnyAuthority('admin','visitor')")
@PreAuthorize("hasAnyRole('ROLE_admin', 'visitor')") //角色名不加前缀也可以
public String del() {
return "删除用户!";
}
//@PostAuthorize 作用于角色和权限,是在执行方法后才验证权限/角色
@RequestMapping("/upd")
@PostAuthorize("hasAnyAuthority('admin','visitor')")
//@PostAuthorize("hasAnyRole('ROLE_admin', 'visitor')") //角色名不加前缀也可以
public String upd() {
//若用户没有相关权限/角色,也会执行下面的语句,但是在网页中会跳转到 403.html
System.out.println("执行了更新用户的方法!");
return "更新用户!";
}
}
2.9.3 设置角色和权限
在自定义的 MyUserDetailsService 中设置用户角色和权限
@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserInfoMapper userInfoMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用 userInfoMapper 方法,根据用户名查询数据库
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
UserInfo userInfo = userInfoMapper.selectOne(wrapper);
// 判断
if(userInfo == null) {
throw new UsernameNotFoundException("用户不存在!");
}
//!!!在此设置用户角色和权限!!!
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,superAdmin,visitor,ROLE_admin,ROLE_superAdmin,ROLE_visitor");
return new User(userInfo.getUsername(),
new BCryptPasswordEncoder().encode(userInfo.getPassword()), auths);
}
}
2.10 用户注销
2.10.1 在配置类中添加退出的配置
在 WebSecurityConfig 中的 configure 方法中添加
//配置退出
http.logout().logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
2.10.2 测试
-
创建一个登录成功页,添加退出的超链接
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>成功页</title> </head> <body> <h1>登录成功!</h1> <a href="/logout">退出</a> </body> </html>
-
修改配置类中登录成功跳转到的页面URL
//登录成功之后,跳转路径 .defaultSuccessUrl("/loginSuccess.html").permitAll()
-
登录成功后在成功页面点击退出,再去访问其它URL
2.11 实现记住我功能
2.11.1 原理
2.11.2 功能实现
-
在数据库中创建表
drop table if exists `persistent_logins`; 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;
-
在 WebSecurityConfig 配置类中注入数据源,配置数据库操作对象
@Resource private DataSource dataSource; //注入数据源 //配置数据库操作对象 @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); return jdbcTokenRepository; }
-
在 WebSecurityConfig 配置类的 configure() 方法中配置 remember me
.and().rememberMe() .tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) //有效时长, 单位是秒 .userDetailsService(myUserDetailsService)
-
在登录页面中添加记住我复选框
<input name="remember-me" type="checkbox"/> 记住我 </br>
三、微服务权限基础实例
学完微服务后再学习记录…