在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。
一、基础使用
1.依赖
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<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>
</dependencies>
2.application.yml
spring:
# Spring Security 配置项,对应 SecurityProperties 配置类
security:
# 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
user:
name: user # 账号
password: user # 密码
roles: ADMIN # 拥有角色
3.Controller
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/demo")
public String demo() {
return "success!";
}
}
项目启动成功后,浏览器访问 http://127.0.0.1:8080/admin/demo 接口。因为未登录,所以被 Spring Security 拦截到登录界面。
因为我们没有自定义登录界面,所以默认会使用 DefaultLoginPageGeneratingFilter 类,生成上述界面。
登录完成后,因为 Spring Security 会记录被拦截的访问地址,所以浏览器自动动跳转返回界面;
二、自定义规则(自定义 Spring Security 的配置,实现权限控制。)
1.SecurityConfig
创建 SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter 抽象类,实现 Spring Security 在 Web 场景下的自定义配置。代码如下:
// SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。
// SecurityConfig.java
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
//使用内存中的 InMemoryUserDetailsManager
inMemoryAuthentication()
//不使用 PasswordEncoder 密码编码器
.passwordEncoder(NoOpPasswordEncoder.getInstance())
// 配置 admin 用户
.withUser("admin").password("admin").roles("ADMIN")
// 配置 normal 用户
.and().withUser("normal").password("normal").roles("NORMAL");
}
重写 #configure(HttpSecurity http)
方法,主要配置 URL 的权限控制。代码如下:
// SecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// <X> 配置请求地址的权限
.authorizeRequests()
.antMatchers("/test/echo").permitAll() // 所有用户可访问
.antMatchers("/test/admin").hasRole("ADMIN") // 需要 ADMIN 角色
.antMatchers("/test/normal").access("hasRole('ROLE_NORMAL')") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
.and()
// <Y> 设置 Form 表单登录
.formLogin()
// .loginPage("/login") // 登录 URL 地址
.permitAll() // 所有用户可访问
.and()
// 配置退出相关
.logout()
// .logoutUrl("/logout") // 退出 URL 地址
.permitAll(); // 所有用户可访问
}
调用 HttpSecurity#authorizeRequests()
方法,开始配置 URL 的权限控制。注意看艿艿配置的四个权限控制的配置。下面,是配置权限控制会使用到的方法:
#(String... antPatterns)
方法,配置匹配的 URL 地址,可传入多个。- 【常用】
#permitAll()
方法,所有用户可访问。 - 【常用】
#denyAll()
方法,所有用户不可访问。 - 【常用】
#authenticated()
方法,登录用户可访问。 #anonymous()
方法,无需登录,即匿名用户可访问。#rememberMe()
方法,通过remember me 登录的用户可访问。#fullyAuthenticated()
方法,非remember me 登录的用户可访问。#hasIpAddress(String ipaddressExpression)
方法,来自指定 IP 表达式的用户可访问。- 【常用】
#hasRole(String role)
方法, 拥有指定角色的用户可访问。 - 【常用】
#hasAnyRole(String... roles)
方法,拥有指定任一角色的用户可访问。 - 【常用】
#hasAuthority(String authority)
方法,拥有指定权限(authority
)的用户可访问。 - 【常用】
#hasAuthority(String... authorities)
方法,拥有指定任一权限(authority
)的用户可访问。 - 【最牛】
#access(String attribute)
方法,执行结果为true
时,可以访问。
2.Controller
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
- 对于
/test/echo
接口,直接访问,无需登录。 - 对于
/test/home
接口,无法直接访问,需要进行登录。 - 对于
/test/admin
接口,需要登录「admin/admin」用户,因为需要 ADMIN 角色。 - 对于
/test/normal
接口,需要登录「normal/normal」用户,因为需要 NORMAL 角色。
3.Spring Security 的注解,实现权限控制
1.修改权限配置类
修该 security config 配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。代码如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
然后即可添加方法级别的权限验证
@PreAuthorize("hasRole('ROLE_NORMAL')")
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
三、Spring Session
Session 的一致性,简单来理解,就是相同 sessionid 在多个 Web 容器下,Session 的数据要一致。
我们先以用户使用浏览器,Web 服务器为单台 TomcatA 举例子。
- 浏览器在第一次访问 Web 服务器 TomcatA 时,TomcatA 会发现请求的 Cookie 中不存在 sessionid ,所以创建一个 sessionid 为 X 的 Session ,同时将该 sessionid 写回给浏览器的 Cookie 中。
- 浏览器在下一次访问 Web 服务器 TomcatA 时,TomcatA 会发现请求的 Cookie 中已存在 sessionid 为 X ,则直接获得 X 对应的 Session 。
但是如果情况如下:
- 浏览器访问 TomcatA ,获得 sessionid 为 X 。同时,在多台 Tomcat 的情况下,我们需要采用 Nginx 做负载均衡。
- 浏览器又发起一次请求访问 Web 服务器,Nginx 负载均衡转发请求到 TomcatB 上。TomcatB 会发现请求的 Cookie 中已存在 sessionid 为 X ,则直接获得 X 对应的 Session 。结果呢,找不到 X 对应的 Session ,只好创建一个 sessionid 为 X 的 Session 。
- 此时,虽然说浏览器的 sessionid 是 X ,但是对应到两个 Tomcat 中两个 Session 。那么,如果在 TomcatA 上做的 Session 修改,TomcatB 的 Session 还是原样,这样就会出现 Session 不一致的问题。
三种解决方案:
第一种,Session 黏连。
使用 Nginx 实现会话黏连,将相同 sessionid 的浏览器所发起的请求,转发到同一台服务器。这样,就不会存在多个 Web 服务器创建多个 Session 的情况,也就不会发生 Session 不一致的问题。
不过,这种方式目前基本不被采用。因为,如果一台服务器重启,那么会导致转发到这个服务器上的 Session 全部丢失。
第二种,Session 复制。
Web 服务器之间,进行 Session 复制同步。仅仅适用于实现 Session 复制的 Web 容器,例如说 Tomcat 、Weblogic 等等。
不过,这种方式目前基本也不被采用。试想一下,如果我们有 5 台 Web 服务器,所有的 Session 都要同步到每一个节点上,一个是效率低,一个是浪费内存。
第三种,Session 外部化存储。
不同于上述的两种方案,Session 外部化存储,考虑不再采用 Web 容器的内存中存储 Session ,而是将 Session 存储外部化,持久化到 MySQL、Redis、MongoDB 等等数据库中。这样,Tomcat 就可以无状态化,专注提供 Web 服务或者 API 接口,未来拓展扩容也变得更加容易。
而实现 Session 外部化存储也有两种方式:
① 基于 Tomcat、Jetty 等 Web 容器自带的拓展,使用读取外部存储器的 Session 管理器。例如说:tomcat 使用 Redis 存储 Session 、实现 Jetty 使用 MySQL、MongoDB 存储 Session 。
② 基于应用层封装 HttpServletRequest 请求对象,包装成自己的 RequestWrapper 对象,从而让实现调用 HttpServletRequest#getSession() 方法时,获得读写外部存储器的 SessionWrapper 对象。
依赖:
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Spring Session 使用 Redis 作为数据源的自动化配置 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- 实现对 Spring Data Redis 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 实现对 Spring Security 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
配置文件:
spring:
# 对应 RedisProperties 类
redis:
host: 127.0.0.1
port: 6379
password: # Redis 服务器密码,默认为空。生产中,一定要设置 Redis 密码!
database: 0 # Redis 数据库号,默认为 0 。
timeout: 0 # Redis 连接超时时间,单位:毫秒。
# 对应 RedisProperties.Jedis 内部类
jedis:
pool:
max-active: 8 # 连接池最大连接数,默认为 8 。使用负数表示没有限制。
max-idle: 8 # 默认连接数最大空闲的连接数,默认为 8 。使用负数表示没有限制。
min-idle: 0 # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
max-wait: -1 # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。
# 对应 SecurityProperties 类
security:
user: # 配置内存中,可登录的用户名和密码
name: nihao
password: nihao
SessionConfiguration:
@Configuration
@EnableRedisHttpSession // 自动化配置 Spring Session 使用 Redis 作为数据源
public class SessionConfiguration {
}