Spring Security

Spring Security

概述

Spring Security 是一个功能强大且高度可定制的身份验证访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义要求。

特点

  • 对身份验证和授权的全面且可扩展的支持
  • 防止会话固定、点击劫持、跨站点请求伪造等攻击
  • Servlet API 集成
  • 与 Spring Web MVC 的可选集成。

入门案例

  1. 创建一个SpringBoot项目

  2. 导入以下依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Spring Security 安全框架依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  3. 修改 application.yaml

    server:
      port: 9000
    
    spring:
      application:
        name: Spring-Security
    
  4. 业务类

    package com.wyx.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    public class HelloController {
        @GetMapping("/hello")
        public String hello(){
            return "hello,Security!";
        }
    }
    
  5. 启动程序,访问:http://localhost:9000/hello,发现我们的请求会被拦截并且给我们跳转到登录页面,如下

这个页面是Spring Security自带的一个登录页面。默认用户名是:user 密码会在控制台中打印出来,登录后才能访问我们的请求。

底层加载原理

Spring Sevurity 本质是一个过滤器链。因为使用SpringBoot,所以我们使用过滤我们不需要配置,如果使用Spring,那么我们就必须配置一个名叫DelegatingFilterPoxy的类。

源码分析:查看DelegatingFilterPoxy,它继承了GenericFilterBean的抽象类,然后GenericFilterBean实现了Filter接口。在DelegatingFilterPoxy也重写了Filter接口的doFilter方法。

于是我们查看doFilter方法,发现经过一些判断后调用了initDelegate方法。查看这个方法,它通过getBeans()获取了一个类名为springSecurityFilterChain的类,如下

springSecurityFilterChain是通过WebSecurityConfiguration这个配置类注入的,查看源码如下。

这个类的主要功能就是通过迭代器满足条件的过滤器通过建造者模式将它构建出来,方便后面过滤器过滤的时候执行。到此,他的底层加载过程结束。

简单来说,就是通过加载很多的过滤器,然后通过过滤器链来完成授权和认证功能。

授权接口

UserDetailsService接口

我们什么也没有配置时,用户名和密码,是由Spring Security自动生成,但是在实际的开发中,用户名和密码都是由我们走数据库中查出来,并且需要自定义登录逻辑。

但是登录逻辑已经在一个名叫WebSecurityConfigurerAdapter类中实现,我们要实现自定义登录逻辑,就需要继承这个类,然后重新方法。

简单来说,想要实现自定义登录逻辑

  1. 那么我们就必须创建一个类来实现UserDetailsService,然后利用这个类来查询数据库并且将返回的结果封装到一个Spring Security自带的一个User类。(负责查数据库)
  2. 创建一个类继承WebSecurityConfigurerAdapter,并且来实现他的几个方法来实现自定义登录逻辑

PasswordEncoder接口

这个接口的主要功能就是为了数据的安全,给我们的密码加密的一个接口。我们可以实现这个接口自定义加密规则。也可以用它封装好的BCryptPasswordEncoder的加密类。这个接口还有几个实现类,和抽象类,感兴趣的可以自行研究。

自定义用户授权

Spring Security中,它虽然默认给我们配置了密码但是并不常用。下面我们来看一下它的自定义用户名密码功能。

Spring Security中,主要有三种方式来设置,分别是配置文件配置配置类配置自定义实现类(上面说的UserDetailsService接口和UsernamePasswordAuthenticationFilter类)

配置文件

配置文件虽然简单,但是在实际开发中并不常用。

spring:
  security:
    user:
      name: admin   # 用户名
      password: 970699  # 密码
      roles: admin  # 权限

配置类配置

编写配置类,这个方法也不常用。

package com.wyx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


@Configuration
public class UserAuthConfig extends WebSecurityConfigurerAdapter {
    // 编写配置类,继承他默认的实现类WebSecurityConfigurerAdapter
    // 重写它的一个方法 configure,主要这个方法的参数AuthenticationManagerBuilder,别写错方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // new 一个密码加密的类,
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123456");
        // 设置登录用户和密码和权限,不常用。
        auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
    }
    
    // 注入它的加密方式
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
}

自定义实现类

  1. 编写实现UserDetailsService接口的类

    package com.wyx.service;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service("userDetailsService")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 设置权限,通过逗号分割
            List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admin");
            return new User("user",new BCryptPasswordEncoder().encode("123456"),authorityList);
        }
       
    }
    
  2. 编写配置类继承 WebSecurityConfigurerAdapter

    package com.wyx.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    
    @Configuration
    public class UserAuthConfig extends WebSecurityConfigurerAdapter {
        // 注入我们编写的实现类
        @Autowired
        @Qualifier("userDetailsService")
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 将实现类中配置的用户名密码权限,放进来,并设置密码加密方式
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        // 注入它的加密方式
        @Bean
        PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    

连接数据库实现自定义授权(重点,常用)

环境准备,创建一个数据库(Test)并创建一个user表,sql语句如下

DROP TABLE IF EXISTS `User`;
CREATE TABLE `User`  (
  `id` bigint(30) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(16) CHARACTER SET utf8 COLLATE utf8_unicode_ci  DEFAULT NULL COMMENT '姓名',
  `age` int(4)  DEFAULT NULL COMMENT '年龄',
  `sex` int(2)  DEFAULT NULL COMMENT '性别(男:1,女:0)',
  `email` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci  DEFAULT NULL COMMENT '邮件地址',
  `phone` bigint(30)  DEFAULT NULL COMMENT '电话号码',
  `password` varchar(30) CHARACTER SET utf8 COLLATE utf8_unicode_ci  DEFAULT NULL COMMENT '密码',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制',
  `deleted` int(1) NOT NULL DEFAULT 1 COMMENT '逻辑删除(存在:1,删除:0)',
  PRIMARY KEY (`id`) 
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci;

insert into User(id, name, age, sex, email, phone, password, create_time, update_time, version, deleted) VALUE (180501501084,'admin',18,1,'309597117@qq.com',17384206580,'123456',now(),now(),1,1)
  1. 导入依赖

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    
  2. 配置数据库连接

    spring:
      datasource:
        url: jdbc:mysql://47.97.218.81:3306/Test?useSSL=true&useUnicode=true&charterEncoding=utf8&serverTimezone=GMT%2B8
        password: 970699
        username: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        hikari:
          pool-name: DateHikariCP # 连接池名
          minimum-idle: 5      # 最小空闲连接数
          idle-timeout: 18000  # 空闲连接最大存活时间 单位毫秒
          maximum-pool-size: 10 # 最大连接数
          auto-commit: true     # 是否自动提交(返回连接池时)
          max-lifetime: 18000   # 连接最大存活时间 单位毫秒
          connection-timeout: 3000  # 连接超时时间 单位毫秒
          connection-test-query: SELECT 1 # 测试连接是否可以用的查询语句
    
  3. 创建实体类

    package com.wyx.pojo;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.Date;
    
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        
        private Long id;
        
        private String name;
        
        private Integer age;
        
        private int sex;
        
        private String email;
        
        private Long phone;
        
        private String password;
        
        private Date createTime;
        
        private Date updateTime;
        
        private int version;
        
        private int deleted;
        
    }
    
  4. 创建UserMapper接口

    package com.wyx.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.wyx.pojo.User;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface UserMapper extends BaseMapper<User> {
    
    }
    
  5. 在主启动类上添加注解

    @MapperScan("com.wyx.mapper")
    
  6. 编写实现UserDetailsService接口的类

    package com.wyx.service;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.wyx.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service("userDetailsService")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        // 注入操作数据库User表的对象
        @Autowired
        UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //操作数据库查询密码
            QueryWrapper<com.wyx.pojo.User> wrapper = new QueryWrapper<>();
            wrapper.eq("name",username);
            com.wyx.pojo.User user = userMapper.selectOne(wrapper);
            if (user==null){
                throw  new UsernameNotFoundException("用户名不存在!");
            }
            // 设置权限,通过逗号分割
            List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admin");
            return new User(user.getName(),new BCryptPasswordEncoder().encode(user.getPassword()),authorityList);
        }
    }
    
  7. 配置实现类

    package com.wyx.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    
    @Configuration
    public class UserAuthConfig extends WebSecurityConfigurerAdapter {
        // 注入我们编写的实现类
        @Autowired
        @Qualifier("userDetailsService")
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 将实现类中配置的用户名密码权限,放进来,并设置密码加密方式
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
        // 注入它的加密方式
        @Bean
        PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    

自定义登录页面

  1. 还是在继承WebSecurityConfigurerAdapter的类(UserAuthConfig)中,重写configure方法,切记这里参数为 http 的方法。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()    // 自定义登录页面设置
            .loginPage("/login.html")    //选择登录页面设置
            .loginProcessingUrl("/login")   //登录访问路径(即提交表单时的路径),切记好表单提交的一样,可以不用实现这个Controller
            .defaultSuccessUrl("/index").permitAll()   // 登录成功后访问的路径,并且将它的权限打开,这个controller要实现
            //                .successForwardUrl() 和 defaultSuccessUrl 功能相同,但是这个是转发,defaultSuccessUrl是重定向
            .and().authorizeRequests()
            .antMatchers("/","login").permitAll()     //设置哪些路径不用授权也可以直接登录,
            .anyRequest().authenticated()       // 配置访问除了不用授权路径外其他路径都需要授权
            .and().csrf().disable(); //关闭 csrf防护,后面会解释
    }
    
  2. 编写对应的登录页面,放在SpringBoot 默认可以扫描到的静态资源的地方

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <form action="/login" method="post">
    <!--用户名,密码的name 必须规定,因为源码中规定了,请求方法必须时post,路径记得对应登录路径-->
        <span>用户名:<input type="text" name="username"></span>
        <br>
        <span>密码:<input type="password" name="password"><br></span>
        <input type="submit" value="登录">
    </form>
    
    </body>
    </html>
    
  3. 实现登录成功跳转的Controller

    @GetMapping("/index")
    @ResponseBody
    public String index(){
        return "首页";
    }
    

访问测试即可

授权控制访问

基于权限

当要访问某个请求时,他必须具有一定权限(我们设定的)才能访问,可以使用hasAuthority和hasAnyAuthority来设置权限。

hasAuthorityhasAnyAuthority 的都是设置权限,区别在于一个可以设置多个用逗号隔开,一个只能设置一个。

下面的方法来至于继承WebSecurityConfigurerAdapter的类(UserAuthConfig)中。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()    // 自定义登录页面设置
        .loginPage("/login.html")    //选择登录页面设置
        .loginProcessingUrl("/login")   //登录访问路径(即提交表单时的路径),切记好表单提交的一样,可以不用实现这个Controller
        .defaultSuccessUrl("/index").permitAll()   // 登录成功后访问的路径,并且将它的权限打开,这个controller要实现
        //                .successForwardUrl() 和 defaultSuccessUrl 功能相同,但是这个是转发,defaultSuccessUrl是重定向
        .and().authorizeRequests()
        .antMatchers("/","login").permitAll()     //设置哪些路径不用授权也可以直接登录,
        .antMatchers("/hello").hasAuthority("admin")	// 对路径设置权限
        .antMatchers("/hello").hasAnyAuthority("admin,role")	// 对路径设置权限可以通过,设置多个
        .anyRequest().authenticated()       // 配置访问除了不用授权路径外其他路径都需要授权
        .and().csrf().disable(); //关闭 csrf防护,后面会解释
}

那么对于登录的用户我们如何授权给它呢?

我们可以在实现 UserDetailsService的接口中的返回 Spring Security中的User对象时添加。通过AuthorityUtilscommaSeparatedStringToAuthorityList方法添加。

package com.wyx.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wyx.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    // 注入操作数据库User表的对象
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //操作数据库查询密码
        QueryWrapper<com.wyx.pojo.User> wrapper = new QueryWrapper<>();
        wrapper.eq("name",username);
        com.wyx.pojo.User user = userMapper.selectOne(wrapper);
        if (user==null){
            throw  new UsernameNotFoundException("用户名不存在!");
        }
        // 设置权限,通过逗号分割
        List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admins");
        return new User(user.getName(),new BCryptPasswordEncoder().encode(user.getPassword()),authorityList);
    } 
}

基于角色

基于角色授权登录,也是两种方法hasRolehasAnyRole。和上面一样,hasRolehasAnyRole 的都是设置权限,区别在于一个可以设置多个用逗号隔开,一个只能设置一个。

下面的方法来至于继承WebSecurityConfigurerAdapter的类(UserAuthConfig)中。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()    // 自定义登录页面设置
        .loginPage("/login.html")    //选择登录页面设置
        .loginProcessingUrl("/login")   //登录访问路径(即提交表单时的路径),切记好表单提交的一样,可以不用实现这个Controller
        .defaultSuccessUrl("/index").permitAll()   // 登录成功后访问的路径,并且将它的权限打开,这个controller要实现
        //                .successForwardUrl() 和 defaultSuccessUrl 功能相同,但是这个是转发,defaultSuccessUrl是重定向
        .and().authorizeRequests()
        .antMatchers("/","login").permitAll()     //设置哪些路径不用授权也可以直接登录,
        //.antMatchers("/hello").hasAuthority("admin")
        //.antMatchers("/hello").hasAnyAuthority("admin,role")
        .antMatchers("/hello").hasRole("user")     // 角色授权登录
        .antMatchers("/hello").hasAnyRole("user,admin")
        .anyRequest().authenticated()       // 配置访问除了不用授权路径外其他路径都需要授权
        .and().csrf().disable(); //关闭 csrf防护,后面会解释

}

但是如何基于给用户分配角色呢?

还是和基于权限一样,但是在分配的权限中要添加前缀ROLE_,因为在底层的源码中,基于角色的权限,他在前面拼接了ROLE_。我们可以在实现 UserDetailsService的接口中的返回 Spring Security中的User对象时添加。通过AuthorityUtilscommaSeparatedStringToAuthorityList方法添加。

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //操作数据库查询密码
    QueryWrapper<com.wyx.pojo.User> wrapper = new QueryWrapper<>();
    wrapper.eq("name",username);
    com.wyx.pojo.User user = userMapper.selectOne(wrapper);
    if (user==null){
        throw  new UsernameNotFoundException("用户名不存在!");
    }
    // 设置权限,通过逗号分割
    List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role,admins,ROLE_user");
    // 分配角色时需要加是 ROLE_ 的前缀
    return new User(user.getName(),new BCryptPasswordEncoder().encode(user.getPassword()),authorityList);
}

自定义403页面

  1. 配置403页面后要走的请求,方法来至于继承WebSecurityConfigurerAdapter的类(UserAuthConfig)中。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().accessDeniedPage("/unAuth"); //403页面后要走的Controller,可以是页面
    }
    
  2. 编写配置的Controller

    @GetMapping("/unAuth")
    @ResponseBody
    public String unAuth(){
        return "没有权限访问!!";
    }
    

基于注解的权限认证

@Secure

@Secure用户具有某个 角色,可以访问方法。

  1. 在主启动类上添加开启注解支持

    @EnableGlobalMethodSecurity(securedEnabled = true)
    
  2. 在Controller的方法上添加注解开启某种权限

    @Secured({"ROLE_user","ROLE_admin"})
    @GetMapping("/hello")
    @ResponseBody
    public String hello(){
        return "hello,Security!";
    }
    // 存在以上两个角色的任意一种,都可以访问该方法,否则不能。
    

@PreAuthorize和@PostAuthorize

@PreAuthorize:在进入某个方法前进行权限认证

@PostAuthorize:用法一样,在执行某个方法之后权限认证,多用于有返回值的类型。

  1. 开机注解支持

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    
  2. 在Controller的方法上添加注解开启某种权限

    //两种权限认证方法都可以,这里多个权限用:隔开
    //@PreAuthorize("hasAnyRole('ROLE_user')")
    @PreAuthorize("hasAnyAuthority('admin:role')")
    @GetMapping("/hello")
    @ResponseBody
    public String hello(){
        return "hello,Security!";
    }
    

@PreFilter和@PostFilter

对请求参数进行过滤:

@PreFilter:对传入的参数进行过滤,传入的参数为XXX不能访问

@PostFilter:对返回的参数进行过滤,返回的参数值为XXX不能访问

Spring Security自动登录(记住密码)

在传统的Selvlet中,实现记住密码功能,那么我们可以使用 cookie技术实现,但是如果使用Cookies记住密码,用户可以查看 不安全,于是在Spring Security中实现这个功能同时使用了cookie数据库技术

原理图:

简单理解

  1. 浏览器登录并进行授权,如果有记住密码功能,并且勾选了记住密码。
  2. 假设认证成功,那么服务器将会生成一个令牌。
  3. 将令牌存入数据库,并且将令牌用cookies发送给浏览器
  4. 下一次在请求时,利用cookeis的令牌,然后查询数据库的令牌。(为了防止服务器宕机,访问大的数据库最好用缓存来做)。因为保存过来的数据会一直存在(自己做测试)使用缓存时,推荐使用RedisSet类型数据保存。

实现过程

  1. 配置数据源,上面我们已经配置了数据源这里不在配置

  2. 创建表

    DROP TABLE IF EXISTS `persistent_logins`;
    CREATE TABLE `persistent_logins`  (
      `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `last_used` timestamp(0) NOT NULL,
      PRIMARY KEY (`series`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
  3. 注入数据源

    //注入数据源
    @Autowired
    private DataSource dataSource;
    
    //注入操作数据库的对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);      //给对象注入数据源
        // 自动生成建表语句,不推荐使用,因为第二次重启会启动失败,建议自己在数据库中创建
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
    
  4. 配置记住我功能

    @Autowired
    @Qualifier("userDetailsService")
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() 
            .and().rememberMe()     // 开启记住我的功能
            .tokenRepository(persistentTokenRepository)     //令牌生成数据库操作
            .tokenValiditySeconds(60)       // 设置过期时间(本次登录且浏览器不关闭一直可以访问,但是浏览器关闭后在规定时间依然可以登录(时间过了不能访问,需要重新登录))
            .userDetailsService(userDetailsService)     // 设置用户登录方式
    }
    
  5. 修改登录页面

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    
    <form action="/login" method="post">
    <!--用户名,密码的name 必须规定,因为源码中规定了,请求方法必须时post,路径记得对应登录路径-->
        <span>用户名:<input type="text" name="username"></span>
        <br>
        <span>密码:<input type="password" name="password"><br></span>
        <!--remember-me 是不能改变的,因为源码中已经写死了-->
        <span><input type="checkbox" name="remember-me" title="记住密码">记住密码</span>
        <input type="submit" value="登录">
    </form>
    
    </body>
    </html>
    
  6. 测试即可

退出功能实现

退出登录:就是当用户退出登录后,对一切操作不允许,需要重新登录后才允许。

  1. 修改登录成功进入的页面

    http.formLogin()    // 自定义登录页面设置
        .loginPage("/login.html")    //选择登录页面设置
        .loginProcessingUrl("/login")   //登录访问路径(即提交表单时的路径),切记好表单提交的一样,可以不用实现这个Controller
        //.defaultSuccessUrl("/index").permitAll()   // 登录成功后访问的路径,并且将它的权限打开,这个controller要实现
        .defaultSuccessUrl("/success.html").permitAll()   // 登录成功后访问的路径,并且将它的权限打开,这个controller要实现
    
  2. 创建sucess.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>登录成功</h1>
    <a href="/logout">退出登录</a>
    </body>
    </html>
    
  3. 配置退出登录功能

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.logout().logoutUrl("/logout")
                    .logoutSuccessUrl("/login.html").permitAll();
    }
    

CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

这个功能相对于:假设你不开启,那么你对这个网站的cookie信息,在同一个浏览器中,其他网站,也能拿到你在本网站的cookie信息,他可以利用你的cookies信息,就是那么网站,通过一些恶意手段给你方法一些东西,比如购买商品等等。

Spring Security 4.0 后默认开启,会针对 PATCHPOSTPUTDELETE请求经行保护。

原理图:

  1. 第一次登录认证时,会生成一个CSRF令牌(就是加密过的密码)
  2. 通过Cookies存放到浏览器,并通过Session 存放到服务器
  3. 下一次发送请求时(不是登录),利用Cookies和Session来比较要不要接收请求

底层源码参考 CsrfFilterdoFilterInternal方法。

实现CSRF

  1. 引入thymeleaf与SpringSecurity整合依赖

    <dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
    
  2. 注释掉配置中的

    .and().csrf().disable(); //关闭 csrf防护,后面会解释
    
  3. 在需要提交csrf验证的表单中添加如下代码

    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    
posted @ 2021-07-30 19:25  橘子有点甜  阅读(296)  评论(0编辑  收藏  举报