Appearance
Spring Security
权限管理系统RBAC和ABAC
目前广泛采用的两种权限模型:基于角色的访问控制(role-based access control RBAC)和基于属性的访问控制(attribute-based access control ABAC)。
ACL(Access Control List),访问控制列表
RBAC(Role-Based Access Control),基于角色的访问控制。
ABAC(Attribute-Based Access Control),基于属性的访问控制
又称为:
PBAC(Policy-Based Access Control),基于策略的访问控制
CBAC(Claims-Based Access Control),基于声明的访问控制
RBAC
比如当用户登录某财务管理系统的时候,允许哪些用户访问编辑哪些菜单,允许访问编辑哪些商品资源等,决定这些权限都取决于用户是哪个角色。
在RBAC,角色通常是指具有某些共同特征的一组人,例如:部门、地点、资历、级别、工作职责等。
在系统初始时Admin根据业务需要创建多个拥有不同权限组合的不同角色,如角色A拥有全部菜单的访问跟编辑权限,角色B只有菜单A的访问编辑权限无其他菜单的编辑权限。当需要赋予某个用户权限的时候,把用户归到相应角色里即可赋予符合需要的权限。
ABAC
ABAC访问控制利用了一组称为 “属性 “的特征。这包括用户属性、环境属性和资源属性。
用户属性:包括如用户的姓名、角色、组织、ID 和安全许可等内容。
环境属性:包括如访问时间、数据的位置和当前组织的威胁等级。
资源属性:包括如创建日期、资源所有者、文件名和数据敏感性。
可以根据业务需求制定不同的访问控制策略。如可以指定财务部门的人员可以在工作日上班时间并且在办公网络访问系统,若属于财务部门的人员使用办公网络但不在办公时间访问则增加进一步的鉴权验证,若属于财务部门的人员不在办公时间访问且非使用办公网络则拒绝访问。
RBAC和ABAC的区别
RBAC与ABAC之间的主要区别在于方法授予访问权限的方式。 RBAC按照角色授予访问权限,ABAC可以根据用户特征,对象特征,操作类型等属性确定访问权限。
RBAC优缺点
优点
RBAC 模型构建起来更加简单,对于中小型组织,维护角色和授权关系的工作量不大,反而定制各种策略相对麻烦,更容易接受RBAC授权模型。
缺点
对于大型组织,基于RBCA的控制模型需要维护大量的角色和授权关系,且无法做到对资源细粒度地授权。
ABAC优缺点
优点
对于大型组织,基于RBCA的控制模型需要维护大量的角色和授权关系,相比而言,ABAC更加灵活。
新增资源时,ABAC仅需要维护较少的资源,而RBAC需要维护所有相关的角色,ABAC可扩展性更强、更方便。
ABAC 有更加细粒度控制和根据上下文动态执行,RBAC只能基于静态的参数进行判断。
缺点
模型构建相对比较复杂。
Spring Security概述
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.
Spring Security是什么
Spring Security 是一个专注于身份认证和权限控制的安全框架。
Spring Security能干什么
- 认证(你是谁,用户/设备/系统)
- 验证(你能干什么,也叫权限控制/授权,允许执行的操作)
- 攻击防护(防止伪造身份)
Spring Security核心技术
- Filter
- Servlet
- Spring DI
- Spring AOP
Spring Security入门
Spring Security核心概念
主体principal
使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。
认证authentication
权限管理系统确认一个主体的身份,允许主体进入系统,也就是说用户能否访问该系统。简单说就是"主体"证明自己是谁。
授权authorization
将操作系统的"权力""授予""主体",这样主体就具备了操作系统中特定功能的能力。也就是给用户分配权限。
Spring Security快速入门
SSM项目
pom.xml
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.5.0</version>
</dependency>
web.xml
xml
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
SpringBoot项目
pom.xml
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SecurityConfig.java
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
WebSecurityConfigurerAdapter
我们可能希望指定Web安全的细节,这要通过重载WebSecurityConfigurerAdapter中的一个或多个方法来实现。我们可以通过重载WebSecurityConfigurerAdapter的三个configure()方法来配置Web安全性,这个过程中会使用传递进来的参数设置行为
configure(WebSecurity)
通过重载,配置Spring Security的Filter链
configure(HttpSecurity)
通过重载,配置如何通过拦截器保护请求
configure(AuthenticationManagerBuilder)
通过重载,配置user-detail服务
密码编码器
BCryptPasswordEncoder
Applies bcrypt strong hashing encryption
java
@Test
public void testBCryptPasswordEncoder() {
// 每次生成的密文都不一样
// 但是用matches进行对比,第一个参数为明文,第二个参数为密码,能够判断出是否通过明文加密成密文
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
boolean matches1 = passwordEncoder.matches("1234567", encode1);
System.out.println(matches1);
boolean matches2 = passwordEncoder.matches("1234567", encode2);
System.out.println(matches2);
System.out.println(encode1);
System.out.println(encode2);
}
NoOpPasswordEncoder
Applies no encoding
java
@Test
public void testNoOpPasswordEncoder() {
PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
boolean matches1 = passwordEncoder.matches("1234567", encode1);
System.out.println(matches1);
boolean matches2 = passwordEncoder.matches("1234567", encode2);
System.out.println(matches2);
System.out.println(encode1);
System.out.println(encode2);
}
Pbkdf2PasswordEncoder
Applies PBKDF2 encryption
java
@Test
public void testPbkdf2PasswordEncoder() {
PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
boolean matches1 = passwordEncoder.matches("1234567", encode1);
System.out.println(matches1);
boolean matches2 = passwordEncoder.matches("1234567", encode2);
System.out.println(matches2);
System.out.println(encode1);
System.out.println(encode2);
}
SCryptPasswordEncoder
Applies scrypt hashing encryption
java
@Test
public void testSCryptPasswordEncoder() {
PasswordEncoder passwordEncoder = new SCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
boolean matches1 = passwordEncoder.matches("1234567", encode1);
System.out.println(matches1);
boolean matches2 = passwordEncoder.matches("1234567", encode2);
System.out.println(matches2);
System.out.println(encode1);
System.out.println(encode2);
}
StandardPasswordEncoder
Applies SHA-256 hashing encryption
java
@Test
public void testStandardPasswordEncoder() {
PasswordEncoder passwordEncoder = new StandardPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
boolean matches1 = passwordEncoder.matches("1234567", encode1);
System.out.println(matches1);
boolean matches2 = passwordEncoder.matches("1234567", encode2);
System.out.println(matches2);
System.out.println(encode1);
System.out.println(encode2);
}
认证配置
- 基于内存
- 基于 JDBC
- 自定义用户服务
基于内存
- SecurityConfig.java
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("user")
.authorities("USER")
.and()
.withUser("admin")
.password("admin")
.authorities("ADMIN");
}
}
基于JDBC
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from tb_user where username=?")
.authoritiesByUsernameQuery(
"select username, authority from tb_role where username=?");
}
}
自定义用户服务
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder encoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService);
}
}
授权配置
访问控制
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/**/*.html", "/**/*.css", "/**/*.js") //表示配置请求路径
.permitAll() // 指定 URL 无需保护。
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 设置表单登录,创建UsernamePasswordAuthenticationFilter拦截器
http.formLogin();
// ...
}
}
登录配置
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.formLogin()
.loginPage("/login.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/user/login") // 设置哪个是登录的 url。
.successForwardUrl("/loginsuccess.html") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/loginfailure.html");// 登录失败之后跳转到哪个 url
http.csrf().disable();
// ...
}
}
注销配置
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.logout()
.logoutUrl("/user/logout") // 哪个地址是退出地址
.logoutSuccessUrl("/logoutsuccess.html"); // 退出后进入到哪个页面
// ...
}
403页面配置
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.exceptionHandling()
.accessDeniedPage("/403.html");
// ...
}
基本授权配置
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.httpBasic();
// ...
}
Spring Security深入
权限表达式
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#el-access
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/admin/*.jsp")
.denyAll();
// ...
}
hasRole(String role)
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.hasRole("USER");
// ...
}
hasAnyRole(String... roles)
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.hasAnyRole("USER", "ADMIN");
// ...
}
hasAuthority(String authority)
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.hasAuthority("ROLE_USER")
// ...
}
hasAnyAuthority(String... authorities)
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
// ...
}
permitAll()
Specify that URLs are allowed by anyone.
允许所有用户访问
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.permitAll();
// ...
}
denyAll()
Specify that URLs are not allowed by anyone.
拒绝任何用户访问
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.denyAll();
// ...
}
anonymous()
Specify that URLs are allowed by anonymous users.
允许所有匿名用户访问
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeRequests()
.antMatchers("/user/**")
.anonymous();
// ...
}
authenticated()
Specify that URLs are allowed by any authenticated user.
允许所有授权用户访问
java
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...
http.authorizeHttpRequests()
.anyRequest()
.authenticated();
// ...
}
3.1.9 access()
access()
Allows specifying a custom AuthorizationManager.
小结
permitAll = authenticated + anonymous
基于注解的权限控制
@EnableGlobalMethodSecurity
@Secured
指定角色
@PreAuthorize
访问方法之前进行权限认证
- 基于
SecurityExpressionOperations
接口的表达式 - 基于
UserDetails
的表达式 - 基于对入参的
SpEL
表达式处理
- 基于
@PostAuthorize
访问方法之后进行权限认证
@PreFilter
过滤控制器方法的参数
只有在控制器方法的参数是集合类型的时候才能使用@PreFilter注解
@PostFilter
过滤控制器方法的返回值
只有在控制器方法的返回值是集合类型的时候才能使用@PostFilter注解
@PermitAll
同意所有的访问
@DenyAll
拒绝所有的访问
@RolesAllowed
用法和
@Secured
一样@AuthenticationPrincipal
SecurityConfig.java
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.antMatcher("/**/*"); // 哪些资源,通过此配置
http.formLogin()
.loginPage("/login.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/user/login") // 设置哪个是登录的 url。
.successForwardUrl("/loginsuccess.html") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/loginfailure.html");// 登录失败之后跳转到哪个 url
http.logout()
.logoutUrl("/user/logout") // 哪个地址是退出地址
.logoutSuccessUrl("/logoutsuccess.html"); // 退出后进入到哪个页面
http.csrf().disable();
http.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("user")
.authorities("ROLE_USER")
.and()
.withUser("admin")
.password("admin")
.authorities("ROLE_ADMIN");
}
}
SecuredController.java
java
@Controller
@RequestMapping("/secured")
public class SecuredController {
@RequestMapping("/m1")
@ResponseBody
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public String m1() {
return "/secured/m1";
}
}
PreAuthroizeController.java
pre-post形式的注解,可执行SpEL表达式
@symbol
引用Spring Bean
#variableName
引用变量
${property-name}
引用配置文件
java
// 限制只能改自己的
@PreAuthorize("isAuthenticated() and authentication.principal.id.equals(#uid) or hasAuthority('ROLE_ADMIN')")
public void find(String uid) {
}
@PreAuthorize("isAuthenticated() and principal.id.equals(#uid) or hasAuthority('ROLE_ADMIN')")
public void find(String uid) {
}
java
// 限制只能查询Id小于10的用户
@PreAuthorize("#id<10")
public void find(int id) {
}
java
@PreAuthorize("permitAll()")
@PermitAll
public void method() {
}
java
@Service("checkService")
public class CheckService {
public boolean check(){
return true;
}
}
@Controller
public class UserController {
@PreAuthorize("@checkService.check()")
public void register(@RequestBody @Valid RegisterReq registerReq,
HttpServletRequest request) {
}
}
记住我
配置类
java
@Bean
public PersistentTokenRepository persistentTokenRepository(@Autowired DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
创建表
sql
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;
SecurityConfig.java
java
http.rememberMe()
.tokenValidSeconds(1000)
.tokenRepository(tokenRepository)
.userDetailsService(usersService);
前端
html
<input type="checkbox"name="remember-me"title="记住密码"/><br/>
前后端分离解决方案
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// 认证成功处理器
.successHandler(successHandler)
// 认证失败处理器
.failureHandler(authenticationFailureHandler)
.and()
.logout()
// 退出成功处理器
.logoutSuccessHandler(logoutSuccessHandler)
.and()
.exceptionHandling()
// 拒绝访问处理器
.accessDeniedHandler(accessDeniedHandler);
}
}
插入一个自定义过滤器
java
@Component
public class CaptchaFilter extends OncePerRequestFilter {
//定义如何过滤
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
String inputVCode = request.getParameter("inputCaptcha");
HttpSession session = request.getSession();
String realVcode = (String) session.getAttribute("realCaptcha");
if (inputVCode.equalsIgnoreCase(realVcode)){
filterChain.doFilter(request,response);
} else {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> map = new HashMap<>();
map.put("success","2");
map.put("message","验证码输入错误");
String json = objectMapper.writeValueAsString(map);
writer.write(json);
writer.flush();
}
writer.close();
session.removeAttribute("realVCode");
}
//判断是否应该过滤
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if (request.getRequestURI().contains("user_login")){
return false;
} else {
return true;
}
}
}
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CaptchaFilter captchaFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
// ...
;
}
}
获取认证信息
java
SecurityContextHolder.getContext().getAuthentication();
参考资料
https://book-spring-security-reference.vnzmi.com/2_whats_new_in_spring_security_4.html
Idea模板
pom.xml
- 右侧视图,选择
Other
选项卡- 右侧窗口中的左侧导航栏,选择
Maven
->Maven Project.xml
- 下拉到底部,将以下文本,CV到
</project>
上侧
xml
<!-- <parent>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-parent</artifactId>-->
<!-- <version>2.4.0</version>-->
<!-- </parent>-->
<dependencies>
<!-- spring-boot-starter -->
<!-- spring-boot-starter-web -->
<!-- spring-boot-starter-security -->
<!-- spring-boot-devtools -->
<!-- spring-boot-starter-test -->
<!-- tomcat-embed-jasper -->
<!-- mybatis-spring-boot-starter -->
<!-- druid-spring-boot-starter -->
<!-- pagehelper-spring-boot-starter -->
<!-- javax.servlet-api -->
<!-- spring-context -->
<!-- spring-aspects -->
<!-- spring-web -->
<!-- spring-webmvc -->
<!-- spring-jdbc -->
<!-- spring-security-web -->
<!-- spring-security-config -->
<!-- jackson-databind -->
<!-- hibernate-validator -->
<!-- taglibs-standard-impl -->
<!-- taglibs-standard-spec -->
<!-- mysql-connector-j -->
<!-- druid -->
<!-- mybatis -->
<!-- mybatis-spring -->
<!-- pagehelper -->
<!-- log4j -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>*.xml</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*</include>
<include>**/*</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
课程目标总结
- 了解认证和授权的概念
- 了解权限模块数据模型
- 掌握 Security 入门案例开发过程
- 掌握 Security 实现认证的过程
- 掌握 Security 实现授权的过程