1.初步搭建集成了Spring Security的工程
1.1 新建Maven项目
<!--简单的集成了spring security的web工程只需要如下两个依赖-->
<dependencies>
<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>
</dependencies>
1.2 新建Controller层接口
// 新建一个简单的接口,便于观察spring security的登录拦截
@RestController
@RequestMapping("/hello")
public class Hello {
@GetMapping("/world")
public String world() {
return "world";
}
}
1.3 浏览器访问该接口
浏览器输入:http://localhost:8080/hello/world
此时spring security
监测到用户尚未登录,会出现一个登录界面,该界面的用户为user
,密码为项目控制台输出的如:Using generated security password: fe4ce183-45db-4d7b-8e74-af9a0109ce85
,这是一个临时的密码,每次运行都会变化,因此读者要以自己的控制台输出的为主。输入用户名和密码后,就可以看到网页返回字符串world
了。
通过上述步骤,可以初步了解spring security
框架。
2. 进一步学习Spring Security的基本能力
2.1 关闭安全管理框架
如果需要关闭安全管理框架,可以在项目启动类上增加如下代码,表示:排除Security的配置,让其不生效
// 排除Security的配置,让其不生效
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
排除Security
的配置后,就可以直接访问hello/world
接口了。
2.2 配置文件的方式自定义登录用户名和密码
通过在application.properties
配置文件中增加以下内容,创建一个用户,并且可以使用该用户登录。**注意:**一旦创建了用户,控制台就不会再打印user
用户的密码了。
# 配置文件的方式自定义security的登录用户和密码
spring.security.user.name=kevin
spring.security.user.password=123456
按照1.3节的步骤,使用kevin
和123456
进行登录,可以正常登录。
2.3 内存中配置登录用户
使用内存中配置的用户,需要创建一个配置文件,并且继承:WebSecurityConfigurerAdapter
类,实现其:configure(AuthenticationManagerBuilder auth)
方法。
@Configuration //表示该类为配置类,等同于一个xml文件
@EnableWebSecurity //表示使用Spring Security的安全功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中构建一个用户名:wang,密码:123的用户
PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication().withUser("wang").password(pe.encode("123")).roles();
}
// 注入一个密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
按照1.3节的步骤,使用wang
和123
进行登录,可以正常登录。
说明: 本文使用的Spring Security
的版本是5.1.1
,该版本中,不允许使用明文密码,因此需要注入一个密码加密器,并对密码进行加密。
2.4 基于角色的身份认证
想要做到基于角色的身份认证,首先需要:启用方法级别的认证。在步骤2.3
中创建的SecurityConfig
类上增加注解:@EnableGlobalMethodSecurity(prePostEnabled = true)
,同时创建三个用户,代码如下:
// 启用方法级别的认证,prePostEnabled默认为false,为true时,可以使用@PreAuthorize(指定在方法访问之前进行角色的认证)和@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication().withUser("wang").password(pe.encode("123")).roles();
auth.inMemoryAuthentication().withUser("admin").password(pe.encode("admin")).roles("admin");
auth.inMemoryAuthentication().withUser("zhangsan").password(pe.encode("123")).roles("normal");
auth.inMemoryAuthentication().withUser("kevin").password(pe.encode("123")).roles("normal", "admin");
}
// 注入一个密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在Controller
中,创建需要不同角色才能访问的接口,内容如下:
// 只有admin权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('admin')")
@GetMapping("/admin")
public String admin() {
return "admin role";
}
// 只有normal权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('normal')")
@GetMapping("/normal")
public String normal() {
return "normal role";
}
// admin和normal权限的角色都能访问的接口
@PreAuthorize(value = "hasAnyRole('admin','normal')")
@GetMapping("/adminAndNormal")
public String adminAndNormal() {
return "admin and normal role";
}
完成上述的代码后,在浏览器分别输入不同的地址,访问不同的接口,然后,根据访问接口的角色,输入不同的账号和密码,输入正确的情况下,可以正常访问对应的接口,输入错误的话,会有相应的提示,如果是权限不足,则会出现403界面。
至此,完成了Spring Security
基本的学习,接下来会进入更加实际的使用中,会另开一个项目。
3. 结合数据库进行用户的操作
从数据库中获取用户的身份信息(用户名称,密码,角色)。在Spring Security
框架中对于用户信息的表示类是UserDetails
。UserDetails
是一个接口,是高度抽象的用户信息类(相当于项目中的User
类)。
org.springframework.security.core.userdetails.User
类是UserDetails
接口的实现类,构造方法有三个参数:User(String username, String password, Collection<? extends GrantedAuthority> authorities)
。需要向Spring Security
提供User
对象,这个对象的数据来自数据库的查询。
实现UserDetailsService
接口,重写方法:UserDetails loadUserByUsername(String username)
,根据用户名执行数据库的查询,得到数据库中的用户信息。
3.1 新建Maven工程
新建一个Maven工程,对其进行配置,使其能够访问数据库。本章节使用的是Mybatis Plus
框架来操作数据库的。pom.xml
依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<!-- mybatis plus 代码生成器依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<!-- 代码生成器模板 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<!-- mysql配置 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
application.properties
配置文件信息如下,具体数据库信息,需要自行修改。
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/kevin?useUnicode=true&useSSL=false&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=99.99
# 连接池配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 驱动配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
数据表信息:
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
有了数据表,需要对其进行数据的填充,数据库中的密码一栏内容需要是加密的,这里的加密方式需要与下面3.3节Spring Security配置类
的加密方式一致。具体的加密方式以及密码,都需要根据实际的需求来确定。以下仅为一种方式:
// 生成密码
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("123456"));
Mybatis plus
代码生成类内容如下,具体的数据库信息,需要按照实际修改:
public class CodeGenerator {
/**
* 作者
*/
private static final String AUTHOR = "kevin";
/**
* 表名 多个表名之间用逗号分隔
*/
private static final String TABLE_NAME = "user";
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor(AUTHOR);
gc.setOpen(false);
gc.setServiceName("%sService"); // service 命名方式
gc.setServiceImplName("%sServiceImpl"); // service impl 命名方式
// 自定义文件命名,注意 %s 会自动填充表实体属性!
gc.setMapperName("%sMapper");
gc.setXmlName("%sMapper");
gc.setFileOverride(true);
gc.setActiveRecord(true);
gc.setEnableCache(false); // XML 二级缓存
gc.setBaseResultMap(true); // XML ResultMap
gc.setBaseColumnList(false); // XML columnList
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(
"jdbc:mysql://127.0.0.1:3306/kevin?useUnicode=true&useSSL=false&characterEncoding=utf-8&serverTimezone=GMT");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("99.99");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.example.security_learn1");
pc.setEntity("entity");
pc.setService("service");
pc.setServiceImpl("service.impl");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
String moduleName = pc.getModuleName() == null ? "" : pc.getModuleName();
return projectPath + "/src/main/resources/mapper/" + moduleName + "/" + tableInfo.getEntityName()
+ "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(TABLE_NAME);
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
修改完数据库信息后,运行CodeGenerator
类,生成各个层的相关信息。
3.2 新建UserDetailsService接口的实现类
实现类的内容如下:
@Component("UserDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
@Qualifier("MyUserMapper")
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
org.springframework.security.core.userdetails.User returnUser = null;
if (username != null) {
User one = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (one != null) {
// Security中的角色必须以"ROLE_"开头,源代码中可以看到该规则
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + one.getRole());
returnUser = new org.springframework.security.core.userdetails.User(username, one.getPassword(), Arrays.asList(authority));
} else {
throw new UsernameNotFoundException("can not find user by username:" + username);
}
} else {
throw new UsernameNotFoundException("username can not null.");
}
return returnUser;
}
}
3.3 新建Spring Security配置类
配置类的内容如下:
// 启用方法级别的认证,prePostEnabled默认为false,为true时,可以使用@PreAuthorize(指定在方法访问之前进行角色的认证)和@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("UserDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
3.4 编写Controller中的接口,然后测试
在`Controller`中,创建需要不同角色才能访问的接口,内容如下:
```java
// 只有admin权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('admin')")
@GetMapping("/admin")
public String admin() {
return "admin role";
}
// 只有normal权限的角色才能访问的接口
@PreAuthorize(value = "hasAnyRole('normal')")
@GetMapping("/normal")
public String normal() {
return "normal role";
}
// admin和normal权限的角色都能访问的接口
@PreAuthorize(value = "hasAnyRole('admin','normal')")
@GetMapping("/adminAndNormal")
public String adminAndNormal() {
return "admin and normal role";
}
```
完成上述的代码后,在浏览器分别输入不同的地址,访问不同的接口,然后,根据访问接口的角色,输入不同的账号和密码,输入正确的情况下,可以正常访问对应的接口,输入错误的话,会有相应的提示,如果是权限不足,则会出现403界面。
4. 基于角色的权限
4.1 认证和授权
- 认证:
authentication
,访问者是谁。 - 授权:
authorization
,访问者能够做什么。
举例:张三想要查看公司的流水信息。首先,要对张三的身份进行认证
,即:判断张三是不是本公司的人员。确定张三是公司的人员后,查看张三的授权
信息,即:判断张三有没有权限查看公司的流水信息。
4.2 RBAC是什么?
RBAC
是基于角色的访问控制(Role-Based Access Control)
在RBAC
中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来也很方便。
其基本思想是,对操作系统的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组响应的权限,一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配相应的角色即可。而且角色的权限变更相比较用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。
权限:能对资源的操作,比如增加,修改,删除,查看等等。
角色:自定义的,表示权限的集合。一个角色可以有多个权限。
RBAC设计中的表:
-
用户表:用户认证(登录使用的表)
用户名,密码,是否启用等信息
-
角色表:定义角色信息
角色名称,角色的描述等
-
用户与角色的关系表
一个用户可以有多个角色,一个角色可以对应多个用户。两者是多对多的关系。
-
权限表
权限名,权限描述等
-
角色与权限的关系表
一个角色可以有多个权限,一个权限可以对应多个角色。两者是多对多的关系。
4.3 Spring Security中认证的接口和类
4.3.1 UserDetails接口
表示用户信息的接口,主要的方法如下:
/**
只有全部返回true的时候,认证才会通过,在实现类User的7个参数的构造方法中可以看到,对于这些boolean类型的返回值,都是传的true
*/
boolean isAccountNonExpired(); // 账号是否过期
boolean isAccountNonLocked(); // 账号是否锁定
boolean isCredentialsNonExpired(); // 账号证书是否过期
boolean isEnabled(); // 账号是否启用
Collection<? extends GrantedAuthority> getAuthorities(); // 权限集合
UserDetails接口的实现类:
org.springframework.security.core.userdetails.User
User类是UserDetails接口的一个重要实现类,可以根据系统需要,自定义类实现UserDetails接口,这个类可以交给Spring Security进行认证使用。
4.3.2 UserDetailsService接口
获取用户的信息,得到的是UserDetails
接口对象,一般项目中都需要自定义实现类实现这个接口,从数据库中获取数据。只有一个待实现的方法:
UserDetails loadUserByUsername(String username) // 根据用户的名称,获取用户的信息(用户名称,密码,角色集合,是否可用,是否锁定等信息)
UserDetailsService接口的实现类:
-
InMemoryUserDetailsManager:在内存中维护用户信息
优点:使用方便。缺点:数据不是持久的。重启后恢复原样。
-
JdbcUserDetailsManager:用户信息存放在数据库中,底层使用
jdbcTemplate
操作数据库。可以用其提供的方法完成用户的管理。数据库文件(users.ddl
)在:org.springframework.security.core.userdetails.jdbc
下
代码地址:代码下载
对于代码的说明均在不同module的HELP.md中。
4.3.3 Spring Security默认的登录页
默认的登录页是form
表单,其格式如下:
-
访问地址
/login
-
请求方式
post
-
请求参数
在configure(HttpSecurity http)里修改
- 用户名:
username
,可以自定义,在配置文件中使用.usernameParameter("")
修改 - 密码:
password
,可以自定义,在配置文件中使用.passwordParameter("")
修改
- 用户名:
UsernamePasswordAuthenticationFilter
过滤器,是对账号和密码的过滤器,默认的用户名和密码的命名就是在这里体现的。
4.3.4 前后端分离的方式,自定义登录页
默认的登录页是表单,仿照其重写后的默认的登录页依旧是表单,对于现在的前后端分离的方式不适合。如果要使用前后端分离,一般使用json
作为数据的交换格式,需要使用另一种方式才可以。
ajax
方式,用户端发起请求,Srping Security
接收请求验证用户的用户名和密码,把验证的结果(json
数据)返回给请求方
使用ajax
的方式进行登录,有两个处理器需要重写,如下:
AuthenticationSuccessHandler
当框架验证用户信息成功后执行的接口,执行的是如下方法
onAuthenticationSuccess()
AuthenticationFailureHandler
当框架验证用户信息失败后执行的接口,执行的是如下方法
onAuthenticationFailure()
4.3.5 登录页增加验证码
验证的验证使用的是过滤器,整个Spring Security
框架都是基于过滤器实现的。
请求的步骤:用户发起的请求 —> 过滤器1 —>过滤器2 —> 要访问的资源
在账号和密码的验证之前,先对验证验证进行验证,这样更加符合实际的设计。
在UsernamePasswordAuthenticationFilter
过滤器之前,新建一个过滤器,用于验证码的验证
新建过滤器的方式共有两种:
- 自定义过滤器
- 继承`OncePerRequestFilter
授权表达式举例说明
表达式 | 说明 |
---|---|
permitAll | 永远返回 true |
denyAll | 永远返回 false |
anonyous | 当前用户若是匿名用户返回 true |
rememberMe | 当前用户若是 rememberMe 用户返回 true |
authenticated | 当前用户若不是匿名(已认证)用户返回 true |
fullAuthenticated | 当前用户若既不是匿名用户又不是 rememberMe 用户时返回 true |
hasRole(role) | 当前用户权限集合中若拥有指定的 role 角色权限(匹配时会在你所指定的权限前加’ROLE_’,即判断是否有“ROLE_role”权限)时返回 true |
hasAnyRole(role1, role2, ...) | 当前用户权限集合中若拥有任意一个角色权限时返回 true |
hasAuthority(authority) | 当前用户权限集合中若具有 authority 权限(匹配是否有“authority”权限)时返回 true |
hasAnyAuthority(authority) | 当前用户权限集合中若拥有任意一个权限时返回 true |
hasIpAddress("192.168.1.0/24") | 发送请求的IP匹配时返回 true |