Spring Security
Spring Security 核心部门
- 认证:登录、用户认证
- 授权:对资源的访问控制、基于角色实现
- 安全:内置了实现主流的加密算法
- 会话管理:多个请求响应之间的业务流程切换【基于cookie、session】
认证与授权的联系和区别
认证是识别用户身份是否合法,授权是给已经通过认证的用户访问权限,授权是发生在认证之后的。
认证
认证的4种方式
- 默认设置
- 用户名:默认 user
- 密码:控制台输出的一串安全密码
- 基于配置文件中的用户认证设置
- 在配置文件中设置一个用户名和密码
- 基于内存的认证
- 用户名和密码以编码方式写在程序中,加载配置文件,属于硬编码,写死了
- 基于 JDBC 的认证
- 定义至少两张表
接下来体验一下 4 种认证设置方式
首先 Spring 新建工程,勾选 Spring Boot DevTools 、Spring Security、Spring Web
2、新建一个 SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter,重写两个其中两个方法,一个用于用户认证管理、一个用于用户授权管理,为了了解用户认证设置,这里先重点关注第一个方法
① 默认设置
直接运行项目,会自动生成一串安全密码,用户名默认是user,随后用于访问浏览器时登录
public class SecurityConfig extends
WebSecurityConfigurerAdapter{
// 用户认证管理
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
super.configure(auth);
}
// 用户授权管理
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
查看控制台输出,自动生成的安全密码:
②基于配置文件中的用户认证
在配置文件中写入下面两句配置信息
# 自定义
spring.security.user.name=test
spring.security.user.password=123456
随后注释掉用户认证方法里的这一句话,就会启用配置文件中自定义的
// super.configure(auth);
③基于内存的认证
用户名和密码写在程序中,运行时加载,注意密码需要加密处理,否则会出错,加密方式是 SHA2+盐
//基于 Java 定义的配置文件
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter{
// 用户认证管理
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("cc").password(passwd("1234")).roles("USER")
.and()
.withUser("admin").password(passwd("1111")).roles("USER", "ADMIN");
}
/**
* 密码编码
*
* @param string 明文
* @return 密文
*/
private String passwd(String string) {
// SHA2 + 盐【即:随机数据】
return new BCryptPasswordEncoder().encode(string);
}
// 用户授权管理
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
④基于JDBC
配置文件中写入下面这几句话,连接数据库需要用到的:
新建一个配置类 DataSourceConfig,读取配置信息
@Component
@Configuration
public class DataSourceConfig {
//Spring EL 读取了配置文件中的参数信息
@Value("${spring.datasource.url}")
String url;
@Value("${spring.datasource.username}")
String username;
@Value("${spring.datasource.password}")
String password;
@Value("${spring.datasource.driver-class-name}")
String driver;
@Value("${newer.path}")
String path;
// Spring IoC 容器,工厂
// 数据源的工厂
// 需要数据源时调用
@Bean
public DataSource getDataSource() {
System.out.println("读取了自定义的配置信息"+path);
// Builder 模式
return DataSourceBuilder.create()
.driverClassName(driver)
.url(url)
.username(username)
.password(password)
.build();
}
}
查看一下源码可以知道结构,至少需要建立两张表:users、authorities,从数据库中读取用户登录信息以及用户的角色
//基于 Java 定义的配置文件
@Configuration
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter{
// 依赖注入
@Autowired
DataSource dataSource;
// 用户认证管理
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,enabled from users where username = ?")
.authoritiesByUsernameQuery("select username,authority from authorities where username = ?");
}
// 用户授权管理
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
表结构如下
user表,注意的是存入的密码是经过SHA2+盐加密处理的
authorities表,注意角色要大写且前缀是ROLE_
授权
了解了4中用户认证设置方式,下面看看重写的另一个重要的方法用户授权管理,认证方式是基于JDBC
1、简单写一个注册的页面,把注册的新用户存入数据库中,数据库中建4张表,实现新增用户时,3张表同时存入相应的数据,一张存用户基本信息、一张存用户登录信息、一张存用户角色【一个用户可以有多个角色】、还有一张是角色表【存放所有的角色】
index.html
<!doctype html>
<html lang="en">
<head>
<title>Security</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-3">Spring Security</h1>
<p class="lead">用户认证、授权</p>
<p>
<a href="signup">用户注册</a>
</p>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
signup.html
<!doctype html>
<html lang="en">
<head>
<title>用户注册</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<div class="container">
<div class="card mt-5">
<div class="card-body">
<h4 class="card-title">用户注册</h4>
<!--基于表单提交-->
<form action="/api/signup" method="post">
<div class="form-group">
<label for="">用户名</label>
<input type="text" class="form-control" name="name" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="">密码</label>
<input type="password" class="form-control" name="password" placeholder="请输入密码" maxlength="6" required>
</div>
<div class="form-group">
<label for="">角色</label>
<select class="form-control" name="title">
<option value="ROLE_MEMBER">会员</option>
<option value="ROLE_VIP">贵宾</option>
<option value="ROLE_VVIP">超级贵宾</option>
<option value="ROLE_ADMIN">管理员</option>
<option value="ROLE_NORMAL">普通游客</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-block">注册</button>
</form>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
mapper :
@Mapper
public interface UserMapper {
// 新增
@Insert("insert into `user`(`name`) values(#{name})")
void create(String name);
}
@Mapper
public interface AccountMapper {
// 新增
@Insert("insert into `account`(`name`,`password`) values(#{name},#{password})")
void create(String name,String password);
}
用户角色表,角色这一列存的是角色的id,对应角色表中的 id
@Mapper
public interface UserRoleMapper {
// 新增
@Insert("insert into `user_role`(`name`,`role_id`) values(#{name},#{role_id})")
void create(String name,int role_id);
}
@Mapper
public interface RoleMapper {
// 根据角色名查询角色编号
@Select("select id from role where title=#{title}")
int findTitleByTitle(String title);
}
为了实现新增一个用户同时插入3个表中,新建一个Service 类
@Service
public class AccountService {
@Autowired
UserMapper userMapper;
@Autowired
AccountMapper accountMapper;
@Autowired
RoleMapper roleMapper;
@Autowired
UserRoleMapper userRoleMapper;
public void insert(String name,String password,String title) {
// 插入用户基本信息表
userMapper.create(name);
// 插入用户登录表
accountMapper.create(name, new BCryptPasswordEncoder().encode(password));
// 插入用户角色表
userRoleMapper.create(name, roleMapper.findTitleByTitle(title));
}
}
写几个控制器,简单测试一下
controller:
@Controller
public class HomeController {
// 返回 主页面
@GetMapping
public String home() {
return "index.html";
}
// 返回 注册视图
@GetMapping("/signup")
public String signup() {
return "signup.html";
}
}
注册新用户:
@Controller
@RequestMapping("api")
public class AccontController {
@Autowired
AccountService accountService;
// 注册成功,显示注册信息
@PostMapping("/signup")
@ResponseBody
String signup(String name,String password,String title) {
accountService.insert(name, password, title);
return "欢迎您, "+name+ " 已注册成功,角色是 "+title;
}
}
其他角色的控制器类似,这里就放admin的例子
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping
String admin() {
return "这里是管理员控制台";
}
@GetMapping("/{name}")
String admin(@PathVariable String name) {
return "ADMIN:"+name+" 管理员控制台";
}
}
重点是安全配置类,重写其中两个重要的方法,用户授权管理的方法中可以设置用户角色可以访问的资源
@Component
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
DataSource dataSource;
/*
* 用户认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
auth.jdbcAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.dataSource(dataSource)
.usersByUsernameQuery("select `name`,`password`,`enabled` from `account` where `name` = ?")
.authoritiesByUsernameQuery("select `name`,`title` from `user_role`,`role` where `role_id`=`id` and `name` = ? ");
}
/*
* 用户授权
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 启动 POST 提交
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/","/api/signup","/signup").permitAll()
.antMatchers("/member","/member/*").hasAnyRole("VIP","VVIP","MEMBER")
.antMatchers("/vip","/vip/*").hasAnyRole("VIP","VVIP")
.antMatchers("/vvip","/vvip/*").hasRole("VVIP")
.antMatchers("/admin","/admin/*").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().and().httpBasic();
}
}
会话管理
-
HTTP 请求是基于 请求-响应 的通信模型,浏览器发出请求,服务器做出响应,一个会话就结束了。
-
HTTP 协议,是一种无状态协议【不保存状态】,本身不对请求和 响应之间的通信状态进行保存,对于发送过的请求和响应都不做持久化处理。
-
缺陷:Spring Security 基于session,为Web应用设计的,如果前后端分离则不可用,需要 token【支持前后端分离】。
会话管理技术
为了保存状态,浏览器 和 服务端 技术提供了状态的管理
- cookie 【浏览器提供的】
- session【服务器提供的,基于cookie实现】
- url encoding【服务器提供,解决cookie被禁用的情况】
- token 【令牌 支持移动端及前后端分离 】请查看我的另一边文章 JWT
- OAuth 开发认证
cookie
- 服务端生成的文本格式的键值对,随响应头发送给用户浏览器
- 1)写入用户磁盘【存在有效期】
- 2)驻留在用户浏览器的内存空间【临时的】
- 浏览器再次请求该域名时,浏览器会在HTTP请求头自动携带该域名的cookie发送给服务器
- 浏览器中可以禁用cookie,下次发送请求时不携带cookie,但其实浏览器发生了cookie,只是用户没接收,使用url encoding 可以解决
session
- 基于 jsessionid 的 cookie 实现,用户的状态存储在服务端
- 浏览器在第一次访问时,服务器会创建一个名为jsessionid且值唯一的cookie,服务器会在内存中创建一个hashmap,每一个jsessionid都有一个hashmap,可以存储用户的状态信息,session是存放在服务端的,默认有效期 TTL 为30分钟
- 浏览器再次访问服务器时,会在请求它携带jsessionid,到达服务器后可以根据jsessionid 读取hashmap中的数据,并进行了TTL续约,又重新获得30分钟的有效期
- 会话超时,服务器会消耗session
- 会话状态 持久化:可以考虑写入数据库,否则有效期只有30min
session 与 cookie 的关系与区别
- 关系:session 是基于 cookie 实现的
- 区别:
- cookie 是由浏览器支持,文本格式数据,存储在客户的磁盘中,不占用服务器资源,存在 有效期
- session 是由服务器提供的,hashmap 格式,存储在服务端的内存中,占用服务器资源,超时 不可用【TTL = 30 min】
- cookie 可以被用户禁用,大小4K,不存储敏感数据
- session 不能禁用,因为是服务器提供的
- 用户关闭浏览器,cookie 可以继续使用,而 session 不可用