目录
- 权限管理
- 什么是权限管理
- 认证
- 授权
- 权限管理解决方案
- Shiro
- 开发者自定义
- Spring Security
- Spring Security
- 特性
- Spring、Spring Boot 和 Spring Security 三者的关系
- 整体架构
- 1.认证
- AuthenticationManager
- Authentication
- SecurityContextHolder
- 2.授权
- AccessDecisionManager
- AccessDecisionVoter
- ConfigAttribute
- 入门案例
- 1.引入依赖
- 2.创建控制器(Controller)
- 3.启动项目
- SpringSecurity认证
- 1.数据库准备
- 2.原理解析
- 3.认证方式
- 3.1 HttpBasic认证
- 3.2 formLogin登录认证模式
- 4.表单认证
- 4.1自定义表单登录页面
- 问题一: localhost将您重定向次数过多
- 问题二: 访问login.html 报404错误
- 问题三: 访问login.html 后发现页面没有相关样式
- 4.2.表单登录
- 页面源码(DEMO)
- Controller源码
- 4.3基于数据库实现认证功能
- 4.3.1编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
- 4.3.2在SecurityConfiguration配置类中指定自定义用户认证
- 4.3.3密码加密认证
- BCrypt算法介绍
- 在项目中使用BCrypt
- 4.3.4获取当前登录用户
- 传统实现
- Spring Security 还提供了2种方式可以获取.
- remember me 记住我
- 前端代码
- 后台代码
- 再次完成登录功能.
- 4.3.5Cookie窃取伪造演示
- 4.3.6安全验证
- 4.3.7自定义登录成功处理和失败处理
- 代码实现登录成功或失败的自定义处理
- 异步用户登录实现
- 4.3.8退出登录
- SpringSecurity授权
- 1.内置权限表达式
- 2.url安全表达式
- 2.1设置url访问权限
- 2.MyAccessDeniedHandler自定义权限不足类
- 2.3设置用户对应的角色权限
- 3.在Web 安全表达式中引用自定义Bean授权
- 3.1定义自定义授权类
- 3.2配置类
- 3.3携带路径变量
- 4.Method安全表达式
- 4.1开启方法级别的注解配置
- 4.2在方法上使用注解
- @ProAuthorize : 注解适合进入方法前的权限验证
- @PostAuthorize:
- @PreFilter: 可以用来对集合类型的参数进行过滤, 将不符合条件的元素剔除集合
- @PostFilter: 可以用来对集合类型的返回值进行过滤, 将不符合条件的元素剔除集合
- 基于数据库的RBAC数据模型的权限控制
- 1.RBAC权限模型简介
- 2.RBAC的演化进程
- 用户与权限直接关联
- 用户与角色关联
- 基于RBAC设计权限表结构
- 3.基于Spring Security 实现RBAC权限管理
- 3.1动态查询数据库中用户对应的权限
- 3.2给登录用户授权
- 3.3设置访问权限
权限管理
什么是权限管理
-
基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
-
权限管理包括用户身份认证鉴权(授权)两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
认证
- 身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
- 对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
授权
- 授权,即访问控制,控制谁能访问哪些资源。
- 主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
权限管理解决方案
Shiro
Shiro 本身是一个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、可以在JavaSE环境中使用等。不过,在微服务时代,Shiro 就显得力不从心了,在微服务面前和扩展方面,无法充分展示自己的优势。
开发者自定义
也有很多公司选择自定义权限,即自己开发权限管理。但是一个系统的安全,不仅仅是登录和权限控制这么简单,我们还要考虑种各样可能存在的网络政击以及防彻策略,从这个角度来说,开发者白己实现安全管理也并非是一件容易的事情,只有大公司才有足够的人力物力去支持这件事情。
Spring Security
Spring Security,作为spring 家族的一员,在和 Spring 家族的其他成员如 Spring Boot Spring Clond等进行整合时,具有其他框架无可比拟的优势,同时对 OAuth2 有着良好的支持,再加上Spring Cloud对 Spring Security的不断加持(如推出 Spring Cloud Security ),让 Spring Securiy 不知不觉中成为微服务项目的首选安全管理方案。
Spring Security
- 官网:https://spring.io/projects/spring-security
- Spring Security是一个功能强大、可高度定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
- Spring Security是一个面向Java应用程序提供身份验证和安全性的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以轻松地扩展以满足定制需求。
- Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
特性
- 对身份验证和授权的全面且可扩展的支持
- 防御会话固定、点击劫持,跨站请求伪造等攻击
- 支持 Servlet API 集成
- 支持与 Spring Web MVC 集成
Spring、Spring Boot 和 Spring Security 三者的关系
整体架构
在的架构设计中,认证和授权 是分开的,无论使用什么样的认证方式。都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是可以非常方便地整合一些外部的解决方案。
1.认证
AuthenticationManager
在Spring Security中认证是由AuthenticationManager接口来负责的,接口定义为:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
//返回 Authentication 表示认证成功
//返回 AuthenticationException 异常,表示认证失败。
AuthenticationManager 主要实现类为 ProviderManager,在ProviderManager 中管理了众多AuthenticationProvider 实例。在一次完整的认证流程中,Spring Security 允许存在多个AuthenticationProvider ,用来实现多种认证方式,这些 AuthenticationProvider 都是由ProviderManager 进行统一管理的。
Authentication
认证以及认证成功的信息主要是由 Authentication 的实现类进行保存的,其接口定义为:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
//getAuthorities 获取用户权限信息
//getCredentials 获取用户凭证信息,一般指密码
//getDetails 获取用户详细信息
//getPrincipal 获取用户身份信息,用户名、用户对象等
//isAuthenticated 用户是否认证成功
SecurityContextHolder
- SecurityContextHolder 用来获取登录之后用户信息。
- Spring Security 会将登录用户数据保存在 Session 中。
- 但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。
- 当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。
- SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。
- 当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。
- 以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。
- 这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。
2.授权
当完成认证后,接下来就是授权了。在 Spring Security 的授权体系中,有两个关键接口
AccessDecisionManager
访问决策管理器,用来决定此次访问是否被允许。
AccessDecisionVoter
访问决定投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
ConfigAttribute
用来保存授权时的角色信息
在 Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具各的角色和请求某个
资源所需的 ConfigAtuibute 之间的关系。
入门案例
1.引入依赖
当我们引入Spring Security依赖时,所有接口都会被默认保护
<!--引入spring security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.创建控制器(Controller)
@RestController
public class HelloController {
@GetMapping("hello")
public String hello(){
return "Hello Spring security";
}
}
3.启动项目
- 访问 http://localhost:9090/hello发现直接跳转到登录页面。其实这时候我们的请求已经被保护起来了,要想访问,需要先登录。
- Spring Security 默认提供了一个用户名为 user 的用户,其密码在控制台可以找到
SpringSecurity认证
1.数据库准备
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限名称',
`permission_tag` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标签',
`permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限地址',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_permission
-- ----------------------------
INSERT INTO `t_permission` VALUES (1, '查询所有用户', 'user:findAll', '/user/findAll');
INSERT INTO `t_permission` VALUES (2, '用户添加或修改', 'user:saveOrUpdate', '/user/saveOrUpadate');
INSERT INTO `t_permission` VALUES (3, '用户删除', 'user:delete', '/delete/{id}');
INSERT INTO `t_permission` VALUES (4, '根据ID查询用户', 'user:getById', '/user/{id}');
INSERT INTO `t_permission` VALUES (5, '查询所有商品', 'product:findAll', '/product/findAll');
INSERT INTO `t_permission` VALUES (6, '商品添加或修改', 'product:saveOrUpdate', '/product/saveOrUpadate');
INSERT INTO `t_permission` VALUES (7, '商品删除', 'product:delete', '/product//delete/{id}');
INSERT INTO `t_permission` VALUES (8, '商品是否显示', 'product:show', '/product/show/{id}/{isShow}');
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'ADMIN', '超级管理员');
INSERT INTO `t_role` VALUES (2, 'USER', '用户管理');
INSERT INTO `t_role` VALUES (3, 'PRODUCT', '商品管理员');
INSERT INTO `t_role` VALUES (4, 'PRODUCT_INPUT', '商品录入员');
INSERT INTO `t_role` VALUES (5, 'PRODUCT_SHOW', '商品审核员');
-- ----------------------------
-- Table structure for t_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`, `PID`) USING BTREE,
INDEX `FK_Reference_12`(`PID`) USING BTREE,
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `t_permission` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_role_permission
-- ----------------------------
INSERT INTO `t_role_permission` VALUES (1, 1);
INSERT INTO `t_role_permission` VALUES (2, 1);
INSERT INTO `t_role_permission` VALUES (1, 2);
INSERT INTO `t_role_permission` VALUES (2, 2);
INSERT INTO `t_role_permission` VALUES (1, 3);
INSERT INTO `t_role_permission` VALUES (2, 3);
INSERT INTO `t_role_permission` VALUES (1, 4);
INSERT INTO `t_role_permission` VALUES (2, 4);
INSERT INTO `t_role_permission` VALUES (1, 5);
INSERT INTO `t_role_permission` VALUES (3, 5);
INSERT INTO `t_role_permission` VALUES (4, 5);
INSERT INTO `t_role_permission` VALUES (5, 5);
INSERT INTO `t_role_permission` VALUES (1, 6);
INSERT INTO `t_role_permission` VALUES (3, 6);
INSERT INTO `t_role_permission` VALUES (4, 6);
INSERT INTO `t_role_permission` VALUES (1, 7);
INSERT INTO `t_role_permission` VALUES (3, 7);
INSERT INTO `t_role_permission` VALUES (4, 7);
INSERT INTO `t_role_permission` VALUES (1, 8);
INSERT INTO `t_role_permission` VALUES (3, 8);
INSERT INTO `t_role_permission` VALUES (5, 8);
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,
`status` int(1) NULL DEFAULT NULL COMMENT '用户状态1-启用 0-关闭',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'admin', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1);
INSERT INTO `t_user` VALUES (2, 'zhaoyang', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1);
INSERT INTO `t_user` VALUES (3, 'user1', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1);
INSERT INTO `t_user` VALUES (4, 'user2', '$2a$10$m8WqgTzr0TO.XG.aR91.jegJJmDnGSvWs69aMWPR.WNvCzemHpLum', 1);
INSERT INTO `t_user` VALUES (5, 'user3', '$2a$10$Wk1jWJPoMQ5s7UIp0S/tu.WTcUZUspUUQH6K3BQpa8uHXWRUQc3/a', 1);
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`, `RID`) USING BTREE,
INDEX `FK_Reference_10`(`RID`) USING BTREE,
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `t_role` (`ID`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `t_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 1);
INSERT INTO `t_user_role` VALUES (2, 2);
INSERT INTO `t_user_role` VALUES (3, 4);
INSERT INTO `t_user_role` VALUES (4, 5);
SET FOREIGN_KEY_CHECKS = 1;
2.原理解析
在使用SpringSecurity框架,该框架会默认自动地替我们将系统中的资源进行保护,每次访问资源的时候都必须经过一层身份的校验,如果通过了则重定向到我们输入的url中,否则访问是要被拒绝的。那么SpringSecurity框架是如何实现的呢? Spring Security功能的实现主要是由一系列过滤器相互配合完成。也称之为过滤器链
过滤器是一种典型的AOP思想,下面简单了解下这些过滤器链,
-
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理拦截器 -
org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 -
org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制 -
org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。 -
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。 -
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。 -
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。 -
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面 -
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。 -
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest -
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API -
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。 -
org.springframework.security.web.session.SessionManagementFilter
securityContextRepository限制同一用户开启多个会话的数量 -
org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常 -
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
Spring Security默认加载15个过滤器, 但是随着配置可以增加或者删除一些过滤器.
3.认证方式
3.1 HttpBasic认证
-
HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证。
-
在使用的Spring Boot早期版本为1.X版本,依赖的Security 4.X版本,那么就无需任何配置,启动项目访问则会弹出默认的httpbasic认证。现在使用的是spring boot2.0以上版本(依赖Security 5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式。
-
HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 “admin” ,密码是“ admin”,则将字符串"admin:admin" 使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=。HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,想要破解并不难.
3.2 formLogin登录认证模式
Spring Security的HttpBasic模式,该模式比较简单,只是进行了通过携带Http的Header进行简单的登录验证,而且没有定制的登录页面,所以使用场景比较窄。对于一个完整的应用系统,与登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面, spring boot2.0以上版本(依赖Security 5.X版本)默认会生成一个登录页面.
4.表单认证
4.1自定义表单登录页面
在config包下编写SecurityConfiguration配置类
package com.kgc.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc:
*/
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic()//开启httpbasic认证
.and().authorizeRequests().
anyRequest().authenticated();//所有请求都需要登录认证才能访问*/
http.formLogin()//开启表单认证
.and().authorizeRequests()
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
}
}
问题一: localhost将您重定向次数过多
因为设置登录页面为login.html 后面配置的是所有请求都登录认证,陷入了死循环. 所以需要将login.html放行不需要登录认证
http.formLogin().loginPage("/login.html")//开启表单认证
.and().authorizeRequests().
antMatchers("/login.html").permitAll()//放行登录页面
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
问题二: 访问login.html 报404错误
spring boot整合thymeleaf 之后 所有的静态页面以放在resources/templates下面,所以得通过请求访问到模板页面, 将/login.html修改为/toLoginPage
http.formLogin().loginPage("/toLoginPage")//开启表单认证
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()//放行登录页面
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
问题三: 访问login.html 后发现页面没有相关样式
因为访问login.html需要一些js , css , image等静态资源信息, 所以需要将静态资源放行, 不需要认证
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/favicon.ico");
}
Spring Security中,安全构建器HttpSecurity和WebSecurity的区别是 :
-
WebSecurity不仅通过HttpSecurity定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
-
HttpSecurity仅用于定义需要安全控制的请求(当然HttpSecurity也可以指定某些请求不需要安全控制);
-
可以认为HttpSecurity是WebSecurity的一部分,WebSecurity是包含HttpSecurity的更大的一个概念;
-
构建目标不同
- WebSecurity构建目标是整个Spring Security安全过滤器FilterChainProxy`,
- HttpSecurity的构建目标仅仅是FilterChainProxy中的一个SecurityFilterChain。
4.2.表单登录
通过讲解过滤器链中我们知道有个过滤器UsernamePasswordAuthenticationFilter是处理表单登录的. 那么下面我们来通过源码观察下这个过滤器.
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
...
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
在源码中可以观察到, 表单中的input的name值是username和password, 并且表单提交的路径为/login, 表单提交方式method为post, 这些可以修改为自定义的值.
/**
* http请求处理方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic()//开启httpbasic认证
.and().authorizeRequests().
anyRequest().authenticated();//所有请求都需要登录认证才能访问*/
http.formLogin()//开启表单认证
.loginPage("/toLoginPage")//自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
.usernameParameter("username").passwordParameter("password")//修改自定义表单name值.
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll()//放行登录页面
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
}
页面源码(DEMO)
//login.tml
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Security登录</title>
</head>
<body>
<h1>Security登录</h1>
<form method="post" th:action="@{/login}">
<p>用户名:<input name="username" type="text"/></p>
<p>密码:<input name="password" type="password"/></p>
<p><input type="submit" value="登录"/></p>
</form>
</body>
</html>
//index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Spring security</title>
</head>
<body>
<h1>Hello Spring security,Login Success!!!</h1>
</body>
</html>
Controller源码
package com.kgc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc:
*/
@Controller
public class LoginController {
@RequestMapping("/toLoginPage")
public String login() {
return "login";
}
@PostMapping("/")
public String doLogin(){
return "index";
}
}
备注:如果用到了iframe,还需要配置允许iframe加载
http.formLogin()//开启表单认证
...
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
4.3基于数据库实现认证功能
之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面loadUserByUsername即可
4.3.1编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
package com.kgc.service.impl;
import com.kgc.mapper.UserMapper;
import com.kgc.pojo.User;
import com.kgc.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc: 基于数据库中完成认证
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserMapper userMapper;
/**
* 根据username查询用户实体
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);// 用户名没有找到
}
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
// 需要返回一个SpringSecurity的UserDetails对象
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(user.getUsername(),
"{noop}" + user.getPassword(),// {noop}表示不加密认证。
true, // 用户是否启用 true 代表启用
true,// 用户是否过期 true 代表未过期
true,// 用户凭据是否过期 true 代表未过期
true,// 用户是否锁定 true 代表未锁定
authorities);
return userDetails;
}
}
4.3.2在SecurityConfiguration配置类中指定自定义用户认证
/**
* 身份验证管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);// 使用自定义用户认证
}
4.3.3密码加密认证
- 在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加{noop}前缀。
- Spring Security 中PasswordEncoder就是我们对密码进行编码的工具接口。该接口只有两个功能: 一个是 匹配验证。另一个是 密码编码。
BCrypt算法介绍
- 任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。
- bcrypt算法相对来说是运算比较慢的算法,在密码学界有句常话:越慢的算法越安全。黑客破解成本越高.通过salt和const这两个值来减缓加密过程,它的加密时间(百ms级)远远超过md5(大概1ms左右)。对于计算机来说,Bcrypt 的计算速度很慢,但是对于用户来说,这个过程不算慢。bcrypt是单向的,而且经过salt和cost的处理,使其受攻击破解的概率大大降低,同时破解的难度也提升不少,相对于MD5等加密方式更加安全,而且使用也比较简单
- bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
。(其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。)
在项目中使用BCrypt
首先看下PasswordEncoderFactories 密码器工厂
之前我们在项目中密码使用的是明文的是noop , 代表不加密使用明文密码, 现在用BCrypt只需要将noop换成bcrypt即可
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);// 用户名没有找到
}
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
// 需要返回一个SpringSecurity的UserDetails对象
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(user.getUsername(),
"{bcrypt}" + user.getPassword(),// {noop}表示不加密认证。{bcrypt} 加密认证
true, // 用户是否启用 true 代表启用
true,// 用户是否过期 true 代表未过期
true,// 用户凭据是否过期 true 代表未过期
true,// 用户是否锁定 true 代表未锁定
authorities);
return userDetails;
}
同时需要将数据库中的明文密码修改为加密密码
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode1 = bCryptPasswordEncoder.encode("123456");
String encode2 = bCryptPasswordEncoder.encode("123456");
System.out.println(encode1);
System.out.println(encode2);
}
//$2a$10$0i8cYHtb91REhnef7G2hTu8IGgbtcwKSI.iCS/W2R8JrOF61Mnwqa
//$2a$10$qgRZb.Iv5L9q6Bc5CvwWf.WW3EjDz.ZBEnJeIjIMvzMG0vrOkrdZ6
//每次加密数据都不一样,选择一个放入数据库即可,反解出来的明文会一样
4.3.4获取当前登录用户
在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户, 那么Spring Security中我们如何获取当前已经登录的用户呢?
- SecurityContextHolder
保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息。 - SecurityContext
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
传统实现
/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser1")
@ResponseBody
public UserDetails getCurrentUser() {
UserDetails userDetails = (UserDetails)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails;
}
Spring Security 还提供了2种方式可以获取.
/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails;
}
/**
* 获取当前登录用户
*
* @return
*/
@RequestMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
remember me 记住我
在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现, 下面我们来看下他的原理图
- 简单的Token生成方法
Token=MD5(username+分隔符+expiryTime+分隔符+password)
注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie被盗取很容易破解. - 持久化的Token生成方法
存入数据库Token包含:
- token: 随机生成策略,每次访问都会重新生成
- series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用remember-me功能,该值保持不变
- expiryTime: token过期时间。
CookieValue=encode(series+token)
前端代码
前端页面需要增加remember-me的复选框
<div class="form-group">
<div >
<!--记住我 name为remember-me value值可选true yes 1 on 都行-->
<input type="checkbox" name="remember-me" value="true"/>记住我
</div>
</div>
后台代码
后台代码开启remember-me功能
/**
* http请求处理方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic()//开启httpbasic认证
.and().authorizeRequests().
anyRequest().authenticated();//所有请求都需要登录认证才能访问*/
http.formLogin()//开启表单认证
.loginPage("/toLoginPage")//自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
//.usernameParameter().passwordParameter(). 修改自定义表单name值.
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()//放行登录页面与静态资源
.anyRequest().authenticated()//所有请求都需要登录认证才能访问;
.and().rememberMe()//开启记住我功能
.tokenValiditySeconds(1209600)// token失效时间默认2周
.rememberMeParameter("remember-me")// 自定义表单name值
.tokenRepository(getPersistentTokenRepository());// 设置tokenRepository
// 关闭csrf防护
http.csrf().disable();
}
@Resource
private DataSource dataSource;
/**
* 持久化token,负责token与数据库之间的相关操作
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);//设置数据源
// 启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则会报错
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
项目启动成功后,观察数据库,会帮助我们创建persistent_logins表
再次完成登录功能.
观察数据库,会插入一条记录.说明持久化token方式已经生效
4.3.5Cookie窃取伪造演示
- 使用网页登录系统,记录remember-me的值
- 使用ApiPost伪造cookie
4.3.6安全验证
/**
* 根据用户ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
//获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 判断认证信息是否来源于RememberMe
if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
throw new RememberMeAuthenticationException("认证信息来源于RememberMe,请重新登录");
}
User user = userService.getById(id);
return user;
}
4.3.7自定义登录成功处理和失败处理
在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集, 或者在现在目前前后端分离的情况下用户登录成功和失败后需要给前台页面返回对应的错误信息, 有前台主导登录成功或者失败的页面跳转. 这个时候需要要到用到AuthenticationSuccessHandler与AnthenticationFailureHandler.
-
自定义成功处理:
实现AuthenticationSuccessHandler接口,并重写onAnthenticationSuccesss()方法. -
自定义失败处理:
实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure()方法;
代码实现登录成功或失败的自定义处理
MyAuthenticationService类
package com.kgc.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc:自定义登录成功或失败处理类
*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
}
@Resource
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后续处理....");
//redirectStrategy.sendRedirect(request, response, "/");
Map result = new HashMap();
result.put("code", HttpStatus.OK.value());// 设置响应码
result.put("message", "登录成功");// 设置响应信息
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后续处理....");
//redirectStrategy.sendRedirect(request, response, "/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value());// 设置响应码
result.put("message", exception.getMessage());// 设置错误信息
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
异步用户登录实现
前端页面改造
<input type="button" onclick="login()" value="登录">
<script>
function login() {
$.ajax({
type: "POST",//方法类型
dataType: "json",//服务器预期返回类型
url: "/login", // 登录url
data: $("#formLogin").serialize(),
success: function (data) {
console.log(data)
if (data.code == 200) {
window.location.href = "/";
} else {
alert(data.message);
}
}
});
}
</script>
4.3.8退出登录
- org.springframework.security.web.authentication.logout.LogoutFilter:匹配URL为/logout的请求,实现用户退出,清除认证信息。
- 只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除
- 前端
<a href="/logout"><span></span>退出登录</a>
- 后端
public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler{
...
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出成功后续处理....");
redirectStrategy.sendRedirect(request, response, "/toLoginPage");
}
}
.and().logout().logoutUrl("/logout")//设置退出url
.logoutSuccessHandler(myAuthenticationService)//自定义退出处理
SpringSecurity授权
原理图
在我们应用系统里面,如果想要控制用户权限,需要有2部分数据。
-
系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问。
-
另一份数据就是用户权限信息:请求用户拥有权限
系统用户发送一个请求:系统配置信息和用户权限信息作比对,如果比对成功则允许访问。
当一个系统授权规则比较简单,基本不变时候,系统的权限配置信息可以写在我们的代码里面去的。比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成,如果权限复杂, 例如办公OA, 电商后台管理系统等就不能使用写在代码里面了. 需要RBAC权限模型设计
1.内置权限表达式
Spring Security 使用Spring EL来支持,主要用于Web访问和方法安全上, 可以通过表达式来判断是否具有访问权限. 下面是Spring Security常用的内置表达式. ExpressionUrlAuthorizationConfigurer定义了所有的表达式
表达式 | 说明 |
---|---|
permitAll | 指定任何人都允许访问。 |
denyAll | 指定任何人都不允许访问 |
anonymous | 指定匿名用户允许访问。 |
rememberMe | 指定已记住的用户允许访问。 |
authenticated | 指定任何经过身份验证的用户都允许访问,不包含anonymous |
fullyAuthenticated | 指定由经过身份验证的用户允许访问,不包含anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用户允许访问, 会自动在角色前面插入’ROLE_’ |
hasAnyRole([role1,role2]) | 指定需要任意一个角色的用户允许访问, 会自动在角色前面插入’ROLE_’ |
hasAuthority(authority) | 指定需要特定的权限的用户允许访问 |
hasAnyAuthority([authority,authority]) | 指定需要任意一个权限的用户允许访问 |
hasIpAddress(ip) | 指定需要特定的IP地址可以访问 |
2.url安全表达式
基于web访问使用表达式保护url请求路径.
2.1设置url访问权限
// 设置/user/** 访问需要ADMIN角色
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
// 设置/user/** 访问需要PRODUCT角色和IP地址为127.0.0.1 .hasAnyRole("PRODUCT,ADMIN")
http.authorizeRequests().antMatchers("/product/**")
.access("hasAnyRole('ADMIN,PRODUCT') and hasIpAddress('127.0.0.1')");
// 设置自定义权限不足信息.
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
2.MyAccessDeniedHandler自定义权限不足类
package com.kgc.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc:自定义权限不足信息
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("权限不足,请联系管理员!");
}
}
2.3设置用户对应的角色权限
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<GrantedAuthority> authorities = new ArrayList<>();
if ("admin".equalsIgnoreCase(user.getUsername())) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
} else {
authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
}
3.在Web 安全表达式中引用自定义Bean授权
3.1定义自定义授权类
package com.kgc.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/**
* @author: zjl
* @datetime: 2024/3/27
* @desc:自定义授权类
*/
@Component
public class MyAuthorizationService {
/**
* 检查用户是否有对应的访问权限
*
* @param authentication 登录用户
* @param request 请求对象
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request) {
User user = (User) authentication.getPrincipal();
// 获取用户所有权限
Collection<GrantedAuthority> authorities = user.getAuthorities();
// 获取用户名
String username = user.getUsername();
// 如果用户名为admin,则不需要认证
if (username.equalsIgnoreCase("admin")) {
return true;
} else {
// 循环用户的权限, 判断是否有ROLE_ADMIN权限, 有返回true
for (GrantedAuthority authority : authorities) {
String role = authority.getAuthority();
if ("ROLE_ADMIN".equals(role)) {
return true;
}
}
}
return false;
}
}
3.2配置类
//使用自定义Bean授权
http.authorizeRequests().antMatchers("/user/**").
access("@myAuthorizationService.check(authentication,request)");
3.3携带路径变量
/**
* 检查用户是否有对应的访问权限
*
* @param authentication 登录用户
* @param request 请求对象
* @param id 参数ID
* @return
*/
public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {
if (id > 10) {
return false;
}
return true;
}
//使用自定义Bean授权,并携带路径参数
http.authorizeRequests().antMatchers("/user/delete/{id}").
access("@myAuthorizationService.check(authentication,request,#id)");
4.Method安全表达式
针对方法级别的访问控制比较复杂,spring security提供了4种注解分别是@PreAuthorize,@PostAuthorize,@PreFilter,@PostFilter.
4.1开启方法级别的注解配置
在security配置类中添加注解
/**
* Security配置类
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解支持
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter
4.2在方法上使用注解
@ProAuthorize : 注解适合进入方法前的权限验证
/**
* 查询所有用户
*
* @return
*/
@RequestMapping("/findAll")
@PreAuthorize("hasRole('ADMIN')")//需要ADMIN权限
public String findAll(Model model) {
List<User> userList = userService.list();
model.addAttribute("userList", userList);
return "user_list";
}
/**
* 用户修改页面跳转
*
* @return
*/
@RequestMapping("/update/{id}")
@PreAuthorize("#id<10")//针对参数权限限定 id<10可以访问
public String update(@PathVariable Integer id, Model model) {
User user = userService.getById(id);
model.addAttribute("user", user);
return "user_update";
}
@PostAuthorize:
@PostAuthorize在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL提供返回对象能够在表达式语言中获取到返回对象的 returnObject
/**
* 根据ID查询用户
*
* @return
*/
@GetMapping("/{id}")
@ResponseBody
@PostAuthorize("returnObject.username== authentication.principal.username")//判断查询用户信息是否是当前登录用户信息.否则没有权限
public User getById(@PathVariable Integer id) {
User user = userService.getById(id);
return user;
}
returnObject : 代表return返回的值
@PreFilter: 可以用来对集合类型的参数进行过滤, 将不符合条件的元素剔除集合
/**
* 商品删除-多选删除
*
* @return
*/
@GetMapping("/delByIds")
@PreFilter(filterTarget = "ids", value = "filterObject%2==0")//剔除参数为基数的值
public String delByIds(@RequestParam(value = "id") List<Integer> ids) {
for (Integer id : ids) {
System.out.println(id);
}
return "redirect:/user/findAll";
}
@PostFilter: 可以用来对集合类型的返回值进行过滤, 将不符合条件的元素剔除集合
/**
* 查询所有用户-返回json数据
*
* @return
*/
@RequestMapping("/findAllTOJson")
@ResponseBody
@PostFilter("filterObject.id%2==0")//剔除返回值ID为偶数的值
public List<User> findAllTOJson() {
List<User> userList = userService.list();
return userList;
}
基于数据库的RBAC数据模型的权限控制
我们开发一个系统,必然面临权限控制的问题,不同的用户具有不同的访问、操作、数据权限。形成理论的权限控制模型有:自主访问控制(DAC: Discretionary Access Control)、强制访问控制(MAC: Mandatory Access Control)、基于属性的权限验证(ABAC: Attribute-Based Access Control)等。最常被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control)
1.RBAC权限模型简介
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:
- 用户:系统接口及访问的操作者
- 权限:能够访问某接口或者做某操作的授权资格
- 角色:具有一类相同操作权限的总称
RBAC权限模型核心授权逻辑如下:
- 某用户是什么角色?
- 某角色具有什么权限?
- 通过角色对应的权限推导出用户的权限
2.RBAC的演化进程
用户与权限直接关联
想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某个用户具有某些权限。如图:
- 张三具有所有权限他可能是一个超级管理员.
- 李四,王五 具有添加商品和审核商品的权限有可能是一个普通业务员
这种模型能够清晰的表达用户与权限之间的关系,足够简单。但同时也存在问题:
- 现在用户是张三、李四,王五以后随着人员增加,每一个用户都需要重新授权
- 操作人员的他的权限发生变更后,需要对每个一个用户重新授予新的权限
用户与角色关联
这样只需要维护角色和权限之间的关系就可以了. 如果业务员的权限发生变更, 只需要变动业务员角色和权限之前的关系进行维护就可以了. 用户和权限就分离开来了. 如下图
基于RBAC设计权限表结构
- 一个用户有一个或多个角色
- 一个角色包含多个用户
- 一个角色有多种权限
- 一个权限属于多个角色
3.基于Spring Security 实现RBAC权限管理
3.1动态查询数据库中用户对应的权限
package com.kgc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kgc.pojo.Permission;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 根据用户ID查询权限
*
* @param id
* @return
*/
@Select("SELECT p.* FROM t_permission p,t_role_permission rp,t_role r,t_user_role ur,t_user u " +
"WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id =#{id}")
List<Permission> findByUserId(Integer id);
}
3.2给登录用户授权
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 查询用户对应所有权限
List<Permission> permissions = permissionService.findByUserId(user.getId());
for (Permission permission : permissions) {
// 授权
authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
3.3设置访问权限
// 查询数据库所有权限列表
List<Permission> permissions = permissionService.list();
for (Permission permission : permissions) {
//添加请求权限
http.authorizeRequests().
antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag());
}
- 项目
- 项目
- 项目
- 项目