【SpringSecurity】基础入门

在这里插入图片描述

目录

  • 权限管理
    • 什么是权限管理
    • 认证
    • 授权
    • 权限管理解决方案
      • 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思想,下面简单了解下这些过滤器链,

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
    根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理拦截器

  2. org.springframework.security.web.context.SecurityContextPersistenceFilter
    SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。

  3. org.springframework.security.web.header.HeaderWriterFilter
    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制

  4. org.springframework.security.web.csrf.CsrfFilter
    csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。

  5. org.springframework.security.web.authentication.logout.LogoutFilter
    匹配URL为/logout的请求,实现用户退出,清除认证信息。

  6. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。

  7. org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

  8. org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
    由此过滤器可以生产一个默认的退出登录页面

  9. org.springframework.security.web.authentication.www.BasicAuthenticationFilter
    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。

  10. org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest

  11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
    针对ServletRequest进行了一次包装,使得request具有更加丰富的API

  12. org.springframework.security.web.authentication.AnonymousAuthenticationFilter
    当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。

  13. org.springframework.security.web.session.SessionManagementFilter
    securityContextRepository限制同一用户开启多个会话的数量

  14. org.springframework.security.web.access.ExceptionTranslationFilter
    异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常

  15. 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部分数据。

  1. 系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问。

  2. 另一份数据就是用户权限信息:请求用户拥有权限
    系统用户发送一个请求:系统配置信息和用户权限信息作比对,如果比对成功则允许访问。

当一个系统授权规则比较简单,基本不变时候,系统的权限配置信息可以写在我们的代码里面去的。比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成,如果权限复杂, 例如办公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的演化进程
用户与权限直接关联

想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某个用户具有某些权限。如图:
在这里插入图片描述

  • 张三具有所有权限他可能是一个超级管理员.
  • 李四,王五 具有添加商品和审核商品的权限有可能是一个普通业务员

这种模型能够清晰的表达用户与权限之间的关系,足够简单。但同时也存在问题:

  1. 现在用户是张三、李四,王五以后随着人员增加,每一个用户都需要重新授权
  2. 操作人员的他的权限发生变更后,需要对每个一个用户重新授予新的权限
用户与角色关联

-这样只需要维护角色和权限之间的关系就可以了. 如果业务员的权限发生变更, 只需要变动业务员角色和权限之前的关系进行维护就可以了. 用户和权限就分离开来了. 如下图
在这里插入图片描述

基于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());
}
  • 项目
    • 项目
      • 项目

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/492585.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JRT菜单

上一章搭建了登录界面的雏形和抽取了登录接口。给多组使用登录和菜单功能提供预留&#xff0c;做到不强行入侵别人业务。任何产品只需要按自己表实现登录接口后配置到容器即可共用登录界面和菜单部分。最后自己的用户关联到JRT角色表即可。 登录效果 这次构建菜单体系 首先用…

错误记录

Packet for query is too large 错误原因 一般是没有修改Mysql允许传输的最大数据包大小&#xff0c;使用 SHOW VARIABLES LIKE %max_allowed_packet%;可以看到默认的大小&#xff0c;一般默认为1M。 处理方法 暂时修改&#xff1a;重启mysql后失效 --修改为10M set global…

使用 Web Components 实现输入法更换皮肤 (vue)

更换皮肤 (界面外观) 是拼音输入法的常见功能. 要实现更换皮肤, 有许多种不同的具体技术方案可以使用. 本文选择 Web Components 技术 (vue) 来实现这个功能. 目录 1 效果展示 1.1 发布新版本 2 Web Components 简介3 vue 使用 Web Components 3.1 使用 vue 实现 Web Compon…

比对word文档并提取差异片段(java版)

整体比较 有时候&#xff0c;我们想比对两个word文档&#xff0c;标记出两个文档之间的差异&#xff0c;这样一眼就能看出来修改了哪些地方&#xff0c;如下图,左边文档中的扩招2000人删除了&#xff0c;辞呈改成了说明&#xff0c;新增了并且加重处罚等文字&#xff0c;是否一…

DP背包模型

目录 采药&#xff08;01背包&#xff09;代码实现 装箱问题&#xff08;01背包&#xff09;代码实现 *宠物小精灵之收服&#xff08;二维费用01背包&#xff09;题目分析代码实现 数字组合&#xff08;01背包&#xff09;代码实现 买书&#xff08;完全背包&#xff09;代码实…

【学习】软件测试中误区汇总分析

大家有没有想过这个问题&#xff1a;软件测试中有哪些误区呢&#xff1f;想起这个题目&#xff0c;是因为最近遇到好几次关于这方面的讨论。发觉即便做过几年测试的老员工也或多或少有些这方面的困惑。当然一家之言&#xff0c;仅作抛砖引玉之谈。 误区一&#xff1a;测试就是…

git提交-分支开发合并-控制台操作

git提交-分支开发合并-控制台操作 git的基本概念工作区、暂存区和版本库工作区&#xff1a;就是你在电脑里能看到的目录&#xff08;隐藏目录 .git不算工作区&#xff09;。暂存区&#xff1a;英文叫 stage 或 index。一般存放在本地的.git目录下的index 文件&#xff08;.git/…

【机器学习之---数学】统计学基础概念

every blog every motto: You can do more than you think. https://blog.csdn.net/weixin_39190382?typeblog 0. 前言 统计学基础 1. 频率派 频率学派&#xff08;传统学派&#xff09;认为样本信息来自总体&#xff0c;通过对样本信息的研究可以合理地推断和估计总体信息…

为什么requests不是python标准库?

在知乎上看到有人问&#xff1a;为什么requests不是python标准库&#xff1f; 这确实是部分人困惑的问题&#xff0c;requests作为python最受欢迎的http请求库&#xff0c;已经成为爬虫必备利器&#xff0c;为什么不把requests直接装到python标准库里呢&#xff1f;可以省去第…

rostopic echo /tf 筛选特定数据

rostopic echo /tf 筛选特定数据 在使用rostopic echo命令时&#xff0c;您可以使用参数-n指定输出的消息数量&#xff0c;并且可以使用参数-p将输出以消息格式打印。然而&#xff0c;rostopic echo命令本身并不支持直接筛选指定的消息。 如果想要筛选特定的消息&#xff0c;…

【Java程序设计】【C00368】基于(JavaWeb)Springboot的箱包存储系统(有论文)

TOC 博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;博客中有上百套程序可供参考&#xff0c;欢迎共同交流学习。 项目简介 项目获取 &#x1f345;文末点击卡片…

Mybatis-核心配置文件 / Mybatis增删改查

1. 核心配置文件 1.1. 概述 核心配置文件是MyBatis框架中用于集中定义全局配置信息的XML文件&#xff0c;其内部包含了一系列预设标签&#xff0c;用于设置数据库连接、对象映射、类型处理等关键参数。这些标签遵循特定的排列顺序&#xff0c;尽管并非所有标签都是强制性的&a…

【LVGL-选项卡部件(lv_tabview_create)】

LVGL-选项卡部件&#xff08;lv_tabview_create&#xff09; ■ LVGL-选项卡部件&#xff08;lv_tabview_create&#xff09;■ 综合示例&#xff1a; ■ LVGL-选项卡部件&#xff08;lv_tabview_create&#xff09; ■ 综合示例&#xff1a; 装饰部分

06_Request

文章目录 前置知识点URL和URIHTTP请求报文和HTTP响应报文 Request请求行请求头请求体特殊信息获取客户机和服务器主机信息 请求参数直接封装引用类型 POST请求请求参数乱码文件上传案例&#xff08;与前面的getServletContext结合&#xff09; Request做请求的转发 前置知识点 …

pip安装pyqt5报错

已解决pip安装pyqt5报错 ERROR: Could not build wheels for PyQt5-sip, which is required to install pyproject.toml-based projects 安装C生成工具

查询 in条件下按顺序排序

查询语句 select * from user where id in (5,21,6);查询结果是不是按照参数顺序排列的&#xff0c;为了保证查询顺序可以使用 select * from sj_user where id in(5,21,6) order by FIELD(id,5,21,6); //或者 select * from sj_user where id in(5,21,6) order by FIND_IN_S…

MFC标签设计工具 图片控件上,移动鼠标显示图片控件内的鼠标xy的水平和垂直辅助线要在标签模板上加上文字、条型码、二维码 找准坐标和字体大小 源码

需求&#xff1a;要在标签模板上加上文字、条型码、二维码 找准坐标和字体大小 我生成标签时&#xff0c;需要对齐和 调文字字体大小。这工具微调 能快速知道位置 和字体大小。 标签设计(点击图片&#xff0c;上下左右箭头移动 或-调字体) 已经够用了&#xff0c;滚动条还没完…

使用docker-compose搭建wordpress博客

1、从远程仓库拉取worldpress镜像到本地 2、新建一个项目&#xff0c;然后在新建的项目目录里面新建一个docker-compose.yml模版文件。 3、编写docker-compose.yml文件 4、docker-compose up 运行项目。 5、在浏览器测试 使用docker-compose搭建wordpress博客实验成功。

过滤器 Filter

目录 1、Filter是什么 2、原理 3、怎样使用 步骤&#xff1a; Filter的执行流程&#xff1a; 拦截路径配置&#xff1a; 配置方式&#xff1a; 过滤器链&#xff1a; 1、Filter是什么 Filter是一个在计算机中用于筛选、过滤和修改数据的组件或模块。它在数据传输和处理…

python入门题:输入输出练习

以下是Python基础语法的练习&#xff0c;项目要求和代码如下&#xff1a; """ 例3&#xff1a;小精灵&#xff1a;你好&#xff0c;欢迎古灵阁&#xff0c;请问您需要帮助吗&#xff1f;需要or不需要&#xff1f; 你&#xff1a;需要 小精灵&#xff1a;请问你需…