SpringSecurity认证原理与实战

项目前期准备

首先我们需要初始化我们的项目。

添加maven依赖

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.guslegend</groupId>
    <artifactId>security-management</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-management</name>
    <description>security-management</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <!--添加thymeleaf依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--添加web依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--添加热部署依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!--添加lombok 依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--添加mp 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!--添加mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.21</version>
        </dependency>
        <!--添加redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

添加数据库表结构

复制代码
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50540
 Source Host           : localhost:3306
 Source Schema         : security_management

 Target Server Type    : MySQL
 Target Server Version : 50540
 File Encoding         : 65001

 Date: 31/10/2020 14:35:33
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for persistent_logins
-- ----------------------------
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins`  (
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `series` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `token` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

-- ----------------------------
-- Records of persistent_logins
-- ----------------------------
INSERT INTO `persistent_logins` VALUES ('admin', 'zaPEuwaT/bdMeJNmVtgY8g==', 'ZkVyYVK/o1RJMt8nOO2k0A==', '2020-10-31 10:39:10');

-- ----------------------------
-- 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_product
-- ----------------------------
DROP TABLE IF EXISTS `t_product`;
CREATE TABLE `t_product`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '商品名称',
  `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品价格',
  `stock` int(11) NULL DEFAULT NULL COMMENT '库存',
  `is_show` tinyint(4) NULL DEFAULT NULL COMMENT '是否展示',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

-- ----------------------------
-- Records of t_product
-- ----------------------------
INSERT INTO `t_product` VALUES (1, '华为mate30', 4500.00, 1001, 0, '2020-10-24 13:53:25');
INSERT INTO `t_product` VALUES (2, '红米10', 3500.00, 100, 1, '2020-10-24 13:53:52');
INSERT INTO `t_product` VALUES (3, '苹果12', 6000.00, 100, 1, '2020-10-24 13:54:24');

-- ----------------------------
-- 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;

配置配置文件

复制代码
server:
  port: 8080

spring:
  # Thymeleaf 模板配置
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: LEGACYHTML5
    servlet:
      content-type: text/html
    encoding: utf-8
    cache: false

  # 静态资源配置
  resources:
    chain:
      strategy:
        content:
          enabled: true
          paths: /**

  # 数据库配置
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3307/security_management
    username: root
    password: 123456

#   Redis 配置(默认注释状态)
  redis:
    database: 7
    host: 127.0.0.1
    port: 6379
    password: 123456

访问网站:http://127.0.0.1:8080/

访问网站:http://127.0.0.1:8080/toLoginPage

SpringSecurity认证基本原理与认证2中方式

过滤链介绍

首先我们需要添加SpringSecurity的maven依赖

复制代码
<!--添加Spring Security 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
  • 在使用SpringSecurity框架时,该框架会默认自动地替我们将系统中的资源进行保护;
  • 每次访问资源的时候都必须经过一层身份校验;
  • 如果通过了则重定向到我们输入的url中,否则访问会被拒绝;
  • SpringSecurity是通过一系列过滤器相互配合完成的,也被称为过滤器链。

Spring Security 默认过滤器链(15 个)

  1. WebAsyncManagerIntegrationFilter 从请求中获取WebAsyncManager,并从中获取 / 注册安全上下文的可调用处理拦截器。
  2. SecurityContextPersistenceFilter 通过SecurityContextRepository在 session 中保存 / 更新SecurityContext(存储当前用户认证、权限信息),并传递给后续过滤器。
  3. HeaderWriterFilter 向请求 Header 中添加指定信息,可通过security:headers配置控制。
  4. CsrfFilter验证所有 POST 请求是否包含系统生成的 CSRF Token,无则拒绝,防止跨域请求伪造攻击。
  5. LogoutFilter 匹配/logout请求,实现用户退出并清除认证信息。
  6. UsernamePasswordAuthenticationFilter 处理表单认证,默认匹配/login且仅支持 POST 请求。
  7. DefaultLoginPageGeneratingFilter未配置自定义认证页面时,生成默认登录页。
  8. DefaultLogoutPageGeneratingFilter生成默认的退出登录页面。
  9. BasicAuthenticationFilter 自动解析 HTTP 请求头中以Basic开头的Authentication信息。
  10. RequestCacheAwareFilter 通过HttpSessionRequestCache缓存HttpServletRequest
  11. SecurityContextHolderAwareRequestFilter 包装ServletRequest,使其具备更丰富的 API。
  12. AnonymousAuthenticationFilterSecurityContextHolder中无认证信息,则创建匿名用户存入,兼容未登录访问。
  13. SessionManagementFilter 通过securityContextRepository限制同一用户的并发会话数量。
  14. ExceptionTranslationFilter位于过滤器链后方,转换链路中出现的异常。
  15. FilterSecurityInterceptor 获取资源访问的授权信息,根据SecurityContextHolder中的用户信息判断是否有权限。

认证方式

  1. HttpBasic认证

登录模式是用户名密码使用Base64模式进行加密;

Base64的加密算法是可逆的,破解不难;

  1. formLogin认证

只是进行了通过携带Http的Header进行简单的登录验证。

表单认证

自定义表单登录页面

首先我们需要添加配置类SecurityConfiguration

复制代码
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()// 开启httpBasic认证
                .and().authorizeRequests().anyRequest().authenticated();// 任何请求,登录后可以访问
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
}

首先我们需要解决重定向问题

这个出现的原因是登录页login.html后配置的是所以请求都登录认证,陷入了死循环,所以login.html不放。

复制代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.httpBasic()// 开启httpBasic认证
//                .and().authorizeRequests().anyRequest().authenticated();// 任何请求,登录后可以访问
        http.formLogin().loginPage("/login.html")// 登录页面
                .and().authorizeRequests()
                .antMatchers("/login.html").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();
    }

接下来我们需要解决访问页面404的问题,因为我们的项目使用了thymeleaf,那么所以的静态页面要放在resource/template下面,所以改为

复制代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.httpBasic()// 开启httpBasic认证
//                .and().authorizeRequests().anyRequest().authenticated();// 任何请求,登录后可以访问
        http.formLogin().loginPage("/toLoginPage")// 登录页面
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();
    }

我们发现我们的css样式没有显示成功,接下来要放行css样式

复制代码
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/fonts/**", "/img/**","/favicon.ico");
    }

SpringSecurity中,安全构建器HttpSecurity和WebSecurity的区别为:

  1. webSecurity不仅控制httpSecurity定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
  2. httpSecurity仅用于定义需要安全控制的请求(当然httpSecurity也可以指定某些请求不需要安全控制);
  3. 可以认为httpSecurity是WebSecurity的一部分。

表单登录

接下来,我们来分析表达登录功能怎么实现,首先我们需要到UsernamePasswordAuthenticationFilter这个过滤器里面

通过观察我们可以发现,表单中的input的name值是username和password,并且表单提交的路径为/login,表单提交的方式为method为post请求。

复制代码
    @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")//修改登录参数
                .successForwardUrl("/")// 登录成功后跳转的页面
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();

        http.csrf().disable();// 关闭csrf
    }

这个时候又出现了新的问题

发现行内框架iframe这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何 Frame页面,设置含义如下:

  • DENY:浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.
  • SAMEORIGIN:frame页面的地址只能为同源域名下的页面

允许加载iframe

复制代码
    @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")//修改登录参数
                .successForwardUrl("/")// 登录成功后跳转的页面
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();

        http.csrf().disable();// 关闭csrf
        http.headers().frameOptions().sameOrigin();// 允许同源iframe访问
    }

基于数据库实现认证功能

  • 之前我们登录时候使用的用户名和密码是源于框架自己本身自动生成的;
  • 那么我们如何实现基于数据库中的用户名和密码功能呢?
  • 要实现这个功能需要security的一个UserDetailsService接口,要重写这个接口里面的loadUserByUsername即可。

首先我们需要定义一个方法去实现UserDetailsService接口,重新loadUserByUsername方法

复制代码
@Service
public class MyUserDetailsServiceImpl implements MyUserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                "{noop}" + user.getPassword(), // 测试用明文密码
                authorities
        );
        return userDetails;
    }
}

然后他springSecurity去指定自定义用户认证

复制代码
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

密码加密认证

  • 前面我们使用数据库登录用户的过程使用的是密码为明文,通过前面加入{noop}前缀,接下来我们需要进行密文加密。
  • Spring Security中PasswordEncoder就是我们对密码进行编码的工具接口,该接口只有两个功能:匹配验证,密码编码。

通过观看源码我们可以知道,我们使用noop表示不加密使用明文密码,现在我们只需要将noop改为bcrypt即可。

复制代码
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }

        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                "{bcrypt}" + user.getPassword(), // 测试用明文密码
                authorities
        );
        return userDetails;
    }

记得要更改数据库里面的密码

复制代码
public class passwordTest {

    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        System.out.println(bCryptPasswordEncoder.encode("123456"));
    }
}

获取当前登录用户

在传统web 项目中,我们将登录成功的用户放到session中,在需要的时候可以从session中获取当前用户,那么我们在spring Security中如何获取呢?

  • SecurityContextHolder:保留系统当前的安全上下文SecurityContext,其中就包含了当前系统使用的用户信息;

  • SecurityContext:安全上下文,获取当前经过身份验证的主体或身份验证请求令牌。

    复制代码
      @RequestMapping("/loginUser")
      @ResponseBody
      public UserDetails getCurrentUser() {
          UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
          return userDetails;
      }

remember me 记住我

大多数网站中,都会实现Remember me这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名和密码。

前端网站:

后端开启remember-me功能

复制代码
    @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")//修改登录参数
                .successForwardUrl("/")// 登录成功后跳转的页面
                .and().rememberMe()// 开启记住我功能
                .tokenValiditySeconds(60 * 60 * 24 * 7).rememberMeParameter("remember-me")
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();

        http.csrf().disable();// 关闭csrf
        http.headers().frameOptions().sameOrigin();// 允许同源iframe访问
    }

持久化token方法

复制代码
    @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")//修改登录参数
                .successForwardUrl("/")// 登录成功后跳转的页面
                .and().rememberMe()// 开启记住我功能
                .tokenValiditySeconds(60 * 60 * 24 * 7).rememberMeParameter("remember-me")
                .tokenRepository(getPersistentTokenRepository())// 设置记住我功能使用的tokenRepository
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();

        http.csrf().disable();// 关闭csrf
        http.headers().frameOptions().sameOrigin();// 允许同源iframe访问
    }

    @Autowired
    private DataSource dataSource ;

    private PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);// 设置数据源
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

我们查看数据库发现其自动生成了一个表

退出登录

前端代码:

后端代码:

复制代码
public class MyAuthenticationServiceImpl implements MyAuthenticationService, AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler {
    
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        redirectStrategy.sendRedirect(httpServletRequest, httpServletResponse, "/toLoginPage");
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出成功后续处理...");

    }
}

    @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")//修改登录参数
                .successForwardUrl("/")// 登录成功后跳转的页面
                .and().rememberMe()// 开启记住我功能
                .tokenValiditySeconds(60 * 60 * 24 * 7).rememberMeParameter("remember-me")
                .tokenRepository(getPersistentTokenRepository())// 设置记住我功能使用的tokenRepository
                .and().logout().logoutUrl("/logout")// 设置退出url
                .logoutSuccessHandler(myAuthenticationService)// 设置退出成功处理器
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll()// 登录页面可以匿名访问
                .anyRequest().authenticated();

        http.csrf().disable();// 关闭csrf
        http.headers().frameOptions().sameOrigin();// 允许同源iframe访问
    }

图片验证码验证

Spring Security生成图片验证码主要是分为三步:

  • 根据随机数生成图片验证码;

  • 将验证码图片显示到登录页面;

  • 认证流程中加入验证码校验。

    @Component
    public class ValidateCodeFilter {

    复制代码
      @Autowired
      private MyAuthenticationService myAuthenticationService;
    
      @Autowired
      private StringRedisTemplate stringRedisTemplate;
    
      protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                      HttpServletResponse httpServletResponse, FilterChain filterChain) throws
              ServletException, IOException {
          //判断是否是登录请求,只有登录请求才去校验验证码
          if (httpServletRequest.getRequestURI().equals("/login")
                  && httpServletRequest.getMethod().equalsIgnoreCase("post")) {
              try {
                  validate(httpServletRequest);
              } catch (ValidateCodeException e) {
                  return;
              }
          }
          //如果不是登录请求,直接调用后面的过滤器
          filterChain.doFilter(httpServletRequest, httpServletResponse);
      }
    
      private void validate(HttpServletRequest request) throws
              ServletRequestBindingException, ValidateCodeException {
          //获取ip
          String remoteAddr = request.getRemoteAddr();
          //拼接redis的key
          String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + remoteAddr;
          //从redis获取imageCode
          String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
    
          String imageCode = request.getParameter("imageCode");
          if (StringUtils.isEmpty(imageCode)) {
              throw new ValidateCodeException("验证码的值不能为空!");
          }
          if (redisImageCode == null) {
              throw new ValidateCodeException("验证码已过期!");
          }
          if (!redisImageCode.equals(imageCode)) {
              throw new ValidateCodeException("验证码不正确!");
          }
          // 从redis中删除imageCode
          stringRedisTemplate.delete(redisKey);
      }

    }

Session管理

Session库可以做一些简单的配置可以实现会话过期,单点登录等

会话超时

  1. 首先要配置session超时时间,默认时间为30分钟

    server:
    servlet:
    session:
    timeout: 60

  2. 自定义设置session超时后跳转的地址

    http.sessionManagement().invalidSessionUrl("/toLoginPage");// session失效后跳转的页面

并发控制

并发控制即同一个账号同时在线个数,同一个账号在线个数如果为1表示,该账号在同一时间内只能有一个有效登录;

  1. 修改超时时间

    server:
    servlet:
    session:
    timeout: 60

  2. 设置最大会话数量

    复制代码
         http.sessionManagement()// session管理
                 .invalidSessionUrl("/toLoginPage")// session失效后跳转的页面
                 .maximumSessions(1)// 同一个用户最多只能登录一个session
                 .expiredUrl("/toLoginPage");// session失效后跳转的页面

集群session

在实际应用场景中,一个服务至少有两台服务器在使用,我们怎么让这两个session做共享?

  1. 首先我们需要引入redis的session共享依赖
vb 复制代码
`        <!-- 基于redis实现session共享 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>`
  1. 然后设置session存储类型

    spring:
    session:
    store-type: redis

CSRF防护机制

你可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF 能够做的事情包括:以你名义发邮件、发消息、盗取你的账号,甚至于购买商品、虚拟货币转账...... 造成的问题包括:个人隐私泄露以及财产安全。

CSRF 这种攻击方式在 2000 年就被国外的安全人员提出,但在国内,直到 06 年才开始被关注,08 年,国内外的多个大型社区和交互网站分别爆出 CSRF 漏洞,如:Metafilter(一个大型的 BLOG 网站)、YouTube 和百度 HI...... 而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称 CSRF 为 "沉睡的巨人"。

csrf攻击的三步骤:

  1. 登录授信网站A,本地生成cookie;

  2. 在不登出A情况,访问危险网站B;

  3. 触发网站B中的一些元素。

    //开启csrf防护, 可以设置哪些不需要防护
    http.csrf().ignoringAntMatchers("/user/save");

跨域

声明跨域配置源

复制代码
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置允许跨域的站点
        // corsConfiguration.addAllowedOrigin("*");
        // 设置允许跨域的http方法
        corsConfiguration.addAllowedMethod("*");
        // 设置允许跨域的请求头
        corsConfiguration.addAllowedHeader("*");
        // 允许带cookie
        corsConfiguration.setAllowCredentials(true);

        // 对所有的url生效
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

开启跨域支持

复制代码
        http.cors().configurationSource(corsConfigurationSource());
相关推荐
知兀10 分钟前
【MybatisPlus】后端用枚举类,数据库用tinyint,存在枚举类型转换
java
StockTV12 分钟前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
User_芊芊君子15 分钟前
【OpenAI 把 AI 玩明白了】:自主推理 + 动态知识图谱,这 4 个技术突破要颠覆行业
java·人工智能·知识图谱
c++之路1 小时前
C++20概述
java·开发语言·c++20
Championship.23.241 小时前
Linux Top 命令族深度解析与实战指南
java·linux·服务器·top·linux调试
橘子海全栈攻城狮1 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
逻辑驱动的ken1 小时前
Java高频面试考点18
java·开发语言·数据库·算法·面试·职场和发展·哈希算法
冷雨夜中漫步2 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
直奔標竿2 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring
one_love_zfl3 小时前
java面试-微服务组件篇
java·微服务·面试