SpringSecurity
一、概述
- 官网:https://spring.io/projects/spring-securitySpringSecurity官网
- SpringSecurity是Spring公司开发一个基于认证和授权的安全框架
- 认证:登录的过程
- 授权:在登录的过程中,授予相应的操作权限
- 一般权限管理都是RBAC的
- RBAC:基于角色的访问控制(Role-Based Access Control)
二、快速入门
1. 实现步骤
- 创建SpringBoot项目
- 导入依赖
- 控制器
- 启动项目
- 访问控制器
2. 具体实现
2.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 接口
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello,Success!";
}
}
2.3 测试
-
访问接口
若被拦截,则跳回登录页
若登录成功,则跳转的之前访问的资源
用户名:user
密码:控制台随机生成
三、配置用户名、密码
1. SpringBoot配置文件
spring:
#指定用户名与密码
security:
user:
name: java2107
password: java2107
2. 配置类
2.1 基于内存【了解】
- 注释配置文件
- 引导类增加加密Bean
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- 增加配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String newPwd = passwordEncoder.encode("java2107");
//基于内存配置用户名与密码
auth.inMemoryAuthentication()
.withUser("java2107")
//这个密码一定需要加密,需要指定加密器
.password(newPwd)
//指定角色,具体角色可以不指定
.roles("");
}
}
- 测试
2.2 自定义认证类
- 基于内存的配置要注释掉
- 实现接口UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(null != username && "java2107".equals(username)) {
//比对密码,security框架会自动去比对密码
//参数一:数据库的用户名
//参数二:数据库的密码 $2a$10$mFcvAbMb7iMtuS0vuv/sXuSiKSeb/EVAM8xQekJ0PEWmo9M23UMfS --> java2107
//参数三:角色权限列表
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("");
return new User(username,
"$2a$10$mFcvAbMb7iMtuS0vuv/sXuSiKSeb/EVAM8xQekJ0PEWmo9M23UMfS",
authorities);
}
//认证未通过
return null;
}
}
- 基于认证类的配置类
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于认证类
//要通过哪个认证类去进行认证。比对密码时,密文密码要明文密码比对【需要指定加密器】
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder);
}
四、连接数据库
1. 数据库表
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(30) DEFAULT NULL,
`password` varchar(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `tb_user`(`id`,`username`,`password`) values (1,'zhangsan','123456');
#密码需要加密,自己加密
2. 导入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
3. 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_user")
public class TbUser implements Serializable {
@TableId
private Integer id;
private String username;
private String password;
}
4. Mapper接口
public interface TbUserMapper extends BaseMapper<TbUser> {
}
5. 引导类注解
@MapperScan(basePackages = {"com.qf.java2107.springsecurity.demo01.mapper"})
引导类
6. 改造认证类
通过用户名查询用户数据
package com.qf.java2107.springsecurity.demo01.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.qf.java2107.springsecurity.demo01.mapper.TbUserMapper;
import com.qf.java2107.springsecurity.demo01.pojo.TbUser;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collection;
/**
* 自定义认证类
* @author ghy
* @version 1.0
* @date 2022-01-13
**/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
TbUserMapper tbUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<TbUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
//通过用户名查询用户表
TbUser tbUser = tbUserMapper.selectOne(wrapper);
if(null != tbUser) {
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("");
return new User(tbUser.getUsername(), tbUser.getPassword(), authorities);
}
//认证未通过
return null;
}
}
7. 测试
输入数据库的用户名与密码
8. 获取用户名
- Security框架会为经过该框架过滤后的资源分配一个
匿名
账户,该账户没有认证过,但是Security为未认证用户分配一个临时账户anonymousUser
,供其访问部分资源
@GetMapping("/show/name")
public String name(){
//认证对象:当该资源经过Security框架后,就不会为空了。Security框架会为未认证的用户分配一个 匿名 账户,供其访问部分资源
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(null == principal) {
return "匿名";
}
//经过认证
if(principal instanceof User) {
User user = (User) principal;
return user.getUsername();
}
return principal.toString();
}
五、Security的配置
1. 自定义登录表单
- 默认情况下,Security会为未认证的用户自动生成一个登录表单
- 如果是我们自己的项目,那么这个登录表单跟我们的项目的静态资源很可能不匹配
1.1 实现
自定义登录表单:
- 用户名:username
- 密码:password
- 登录请求URL:/login
- 前端页面
<!-- 使用security之后, 登录请求交给它处理,不用自己写Controller -->
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br>
密 码:<input type="text" name="password"/><br>
<input type="submit" value="登录"/><br>
</form>
- 配置类【关闭跨站伪造请求】
/**
* 认证资源的配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
//登录表单中的用户名跟密码的name值
//.usernameParameter("username").passwordParameter("password")
//如果直接访问登录页,未指定目标资源,则跳到/success
.defaultSuccessUrl("/success")
//放行
.permitAll()
//开启另一个配置,就使用and分开
.and()
//指定拦截资源的规则
//所有请求,只要认证通过,即可访问
.authorizeRequests().anyRequest().authenticated()
//关闭跨站伪造请求
.and().csrf().disable();
}
2. 配置拦截规则
2.1 配置"经过Security框架,但不需要认证"的资源
- 比如:欢迎页、注册页…
- 新增测试接口
@RestController
public class HelloController {
//新增测试接口
@RequestMapping({"/","/index"})
public String index(){
return "Hello,index!!";
}
}
- Security配置类
//配置拦截规则,要放行的资源[经过security]
.and().authorizeRequests().antMatchers("/","/index","/user/show/name").permitAll()
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
//登录表单中的用户名跟密码的name值
//.usernameParameter("username").passwordParameter("password")
//如果直接访问登录页,未指定目标资源,则跳到/success
.defaultSuccessUrl("/success")
//放行
.permitAll()
//配置拦截规则,要放行的资源[经过security]
.and().authorizeRequests().antMatchers("/","/index","/user/show/name").permitAll()
//开启另一个配置,就使用and分开
.and()
//指定拦截资源的规则
//所有请求,只要认证通过,即可访问
.authorizeRequests().anyRequest().authenticated()
//关闭跨站伪造请求
.and().csrf().disable();
}
3. 基于角色权限拦截
Security是如何判断当前给予是角色还是权限呢
- 角色:以
ROLE_
开头- 权限:不是角色就是权限,普通字符串就是权限
- 新增测试接口
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/list")
public String list(){
return "order list";
}
}
- Security配置类
// hasRole : 单个角色。
// hasAnyRole : 只要满足其中一个角色即可
// hasAuthority() : 单个权限
// hasAnyAuthority() : 只要满足其中一个权限即可
// hasRole和hasAnyRole 在这里不要加ROLE_前缀
//拦截资源时,先写精确匹配,再写模糊匹配
//访问product/list资源需要product_list权限
.and().authorizeRequests().antMatchers("/product/list").hasAnyAuthority("product_list")
//访问product控制器中的资源需要user角色
.and().authorizeRequests().antMatchers("/product/**").hasRole("user")
//访问/order/**资源需要order角色
.and().authorizeRequests().antMatchers("/order/**").hasAnyRole("order")
- 测试(在认证过程中给角色或权限,以供测试)
- 如果没有相应的角色或者权限,则会出403错误【权限不足】
- 自定义权限不足页面
- Security配置类增加配置
4. 基于方法级别拦截
4.1 注释全局拦截配置
4.2 实现注解(常用)
4.2.1 @Secured
基于角色控制访问权限:是否有权限访问目标方法
-
处理器注解
-
开启注解生效
@EnableGlobalMethodSecurity(securedEnabled = true)
引导类
4.2.2 @PreAuthorize
基于API指定角色或权限控制是否有权限访问目标方法【方法之前控制】
-
处理器打注解
-
开启注解生效
引导类
4.2.3 @PostAuthorize
可以访问目标处理器,但是要具备相应的角色或者权限才能得到返回结果
5. 表达式写法
https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-access
6. 登出
默认的登出URL:/logout
- Security配置类
.and().logout().logoutUrl("/user/logout").logoutSuccessUrl("/login.html")
- 制作一个登录成功页面,让其退出
六、工作原理
客户端向服务器请求资源,其实是经过一个SpringSecurity封装的过滤器链才最终到达目录资源。得到目录资源之后,又执行过滤器链,再响应给客户端
- 过滤器链:由 N 多个过滤器组成一个过滤器链,而SpringSecurity就是通过这些过滤器完成的认证和授权。
- 执行什么样的操作,就会找对应的过滤器来干活