• SpringSecurity(八)【RememberMe记住我】


    八、RememberMe


    简介

    RememberMe 这个功能非常常见,无论是在 QQ、邮箱…都有这个选项。提到 RememberMe,往往会有一些误解,认为 RememberMe 功能就是把 用户名/密码 用 Cookie 保存在浏览器中,下次登陆时不用再次输入 用户名/密码。这个理解显然是不对的。我们这里所说的 RememberMe 是一种服务器端的行为。传统的登录方式基于 Session 会话,一旦用户的会话超时过期,就要再次登录,这样太过于繁琐。如果有一种机制,让用户会话过期之后,还能继续保持认证状态,就会方便很多。RememberMe 就是为了解决这一需求而生

    在这里插入图片描述

    具体的实现思路就是通过 Cookie 来记录当前用户身份。当用户登陆成功之后,会通过一定算法,将用户信息、时间戳等进行加密,加密完成之后,通过响应头带回给前端存储在 Cookie 中,当浏览器会话过期之后,如果再次访问该网站,会自动将 Cookie 中的信息发送给服务器,服务器对 Cookie 中考的信息进行校验分析,进而确定出用户的身份,Cookie 中所保存的用户信息也是有效的,例如三天、一周等

    在这里插入图片描述

    8.1 基本使用

    开启记住我

    在这里插入图片描述

    8.2 原理分析

    RememberMeAuthenticationFilter

    在这里插入图片描述

    从上图中,当在 SecurityConfig 配置中开启了"记住我"功能之后,在进行认证时如果勾选了"记住我"选项,此时打开浏览器控制台,查看network 中的请求头信息。首先我们登陆时,在登陆请求中多了一个 remember-me 的参数

    在这里插入图片描述

    很显然,这个参数就是告诉服务器应该开启 RememberMe 这个功能的。如果自定义登陆页面开启 Remember 功能应该多加入一个一样的请求参数就可以了。请求最终会被 RememberMeAuthenticationFilter 进行拦截,然后自动登录具体参见源码

    记住我: <input type="checkbox" name="remember-me">
    
    • 1

    在这里插入图片描述

    • 流程
      1. 请求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没有值的话表示用户尚未登陆,此时调用 autoLogin() 方法进行自动登录
      2. 当自动登录成功后返回的rememberMeAuth不为null时,表示自动登陆成功,此时调用 authenticate() 方法对 key 进行校验,并将登陆成功的用户信息保存到 SecurityContextHolder 对象中,然后发布登录成功事件,调用登陆成功回调。需要关注的是,登陆成功的回调并不包含 RememberMeServices 中的 loginSuccess() 方法
      3. 如果自动登陆失败,则调用 rememberMeServices.loginFail 方法处理登陆失败的回调。onUnSuccessfulAuthentication 和 onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现,这就是 RememberMeAuthenticationFilter 过滤器所做的事情,成功将 RememberMeServices 的服务集成进来

    RememberMeServices

    RememberMeServices 一共定义了三个方法

    • autoLogin:可以从请求中提取需要的参数,完成自动登录功能
    • loginFail:方法是自动登陆失败的回调
    • loginSuccess:方法是自动登录成功的回调

    在这里插入图片描述

    TokenBasedRememberMeServices

    在开启记住我后,如果没有加入额外配置默认实现就是由 TokenBasedRememberMeServices 进行实现的。查看这个类源码中 processAutoLoginCookie() 方法实现(用于使用 Cookie 进行自动登录)

    在这里插入图片描述

    processAutoLoginCookie() 方法主要用来验证 Cookie 中的令牌信息是否合法

    • 流程
      1. 首先判断 cookieTokens 长度是否为3,如果不为3说明格式不符合,直接抛出异常
      2. 从 cookieTokens 数组中提取出第 1 项,也就是过期时间,判断令牌是否过期,如果已经过期,则抛出异常
      3. 根据用户名(cookieTokens 数组的第0项)查询当前用户的对象
      4. 调用 makeTokenSignature 方法生成一个签名,签名生成的过程如下
        • 首先将用户名、令牌过期时间、用户密码以及 key 组成一个字符串,中间用:隔开
        • 然后通过 MD5 消息摘要算法对该字符串进行加密,并将密码结果转为一个字符串返回
      5. 判断第4步生成的签名和通过 Cookie 传来的签名是否相等(即 cookieTokens 数组的第2项),如果相等,表示令牌合法,则直接返回用户对象,否则抛出异常

    在这里插入图片描述

    • 成功登录回调过程

      1. 在这个回调中,首先获取用户名和密码信息,如果用户密码在用户登录成功后从 successfulAuthentication 对象中擦除,则从数据库中重新加载出用户密码
      2. 计算出令牌的过期时间,令牌默认有效期是两周
      3. 根据令牌的过期时间、用户名以及用户密码,计算出一个签名
      4. 调用 setCookie() 方法设置 Cookie,参数是一个字符串数组,数组中一共包含三项。用户名、过期时间以及签名,在 setCookie() 方法中会将数组转为字符串,并进行 Base64 编码后响应给前端
    • 生成 token

    在这里插入图片描述

    • 登陆认证成功之后的操作

    在这里插入图片描述

    在这里插入图片描述

    • 将生成的 token 存储到 Cookie 中

    在这里插入图片描述

    • 对传递的 token 进行编码后存入当前 cookie 中

    在这里插入图片描述

    总结

    当用户通过 用户名/密码 的形式登录成功后,系统会根据用户的用户名、密码以及令牌的过期时间计算出一个签名,这个签名使用 MD5 消息摘要算法生成,是不可逆的。然后再将用户名,令牌过期时间以及签名拼接成一个字符串,中间用:隔开,对拼接好的字符串进行 Base64 编码,然后将编码后的结果返回到前端,也就是我们在浏览器中看到的令牌。当关闭浏览器再次打开,访问系统资源时会自动携带上 Cookie 中的令牌,服务端拿到 Cookie 中的令牌后,先进行 Bae64 解码,解码后分别提取出令牌中的三项数据:接着根据令牌中的数据判断令牌是否已经过期,如果没有过期,则根据令牌中的用户名查询出用户信息;接着再计算出一个签名和令牌中的签名进行对比,如果一致,表示会牌是合法令牌,自动登录成功,否则自动登录失败

    在这里插入图片描述

    在这里插入图片描述

    8.3 内存令牌

    PersistentTokenBasedRememberMeServices

    在这里插入图片描述

    • 流程
      1. 不同于 TokonBasedRememberMeServices 中的 processAutologinCookie 方法,这里cookieTokens数组的长度为2,第一项是 series,第二项是 token
      2. cookieTokens 数组中分到提取出seriestoken然后根据series去内存中查询出一个 PersistentRememberMeToken 对象。如果查询出来的对象为 null,表示内存中并没有series对应的值,本次自动登录失败。如果查询出来的token和从cookieTokens中解析出来的token不相同,说明自动登录会牌已经泄漏(恶意用户利用令牌登录后,内存中的token变了),此时移除当前用户的所有自动登录记录并抛出异常
      3. 根据数据库中查询出来的结果判断令牌是否过期。如果过期就抛出异常
      4. 生成一个新的 PersistentRememberMeToken 对象,用户名和series不变,token重新生成,date也使用当前时间。 newToken 生成后,根据series去修改内存中的tokendate(即每次自动登录后都会产生新的token和date)
      5. 调用addCookie()方法添加 Cookie,在addCookie()方法中,会调用到我们前面所说的setCookie()方法,但是要注意第一个数组参数中只有两项:seriestoken(即返回到前端的令牌是通过对seriestoken进行Base64编码得到的)
      6. 最后将根据用户名查询用户对象并返回

    使用内存中令牌实现

    package com.vinjcent.config.security;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    
    import java.util.UUID;
    
    
    /**
     *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
     */
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 构造注入使用@Autowired,set注入使用@Resource
        private final DivUserDetailsService userDetailsService;
    
        // UserDetailsService
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
        // AuthenticationManager
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 拦配置http拦截
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/toLogin").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/toLogin")
                    .loginProcessingUrl("/login")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .defaultSuccessUrl("/toIndex", true)
                    .failureUrl("/toLogin")
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/toLogin")
                    .and()
                    .rememberMe()
                    .rememberMeServices(rememberMeServices())
                    // .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数
                    // .alwaysRemember(true)   // 总是记住我,只针对服务后台设置
                    .and()
                    .csrf()
                    .disable();
        }
    
        // 指定记住我的实现
        @Bean
        public RememberMeServices rememberMeServices() {
            return new PersistentTokenBasedRememberMeServices(
                    UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
                    userDetailsService,     // 认证数据源
                    new InMemoryTokenRepositoryImpl());     // 令牌存储方式(不建议使用内存的方式存储令牌,如果服务器重启,那么内存将全部失效)
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    8.4 持久化令牌(就如Shiro中的session缓存)

    在这里插入图片描述

    在这里插入图片描述

    1. 导入数据库相关依赖
    <dependencies>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 配置数据源
    spring:
      # 数据源
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    mybatis:
      # 注意 mapper 映射文件必须使用"/"
      type-aliases-package: com.vinjcent.pojo
      mapper-locations: com/vinjcent/mapper/**/*.xml
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 创建对应的表结构
    CREATE TABLE `persistent_logins` 
    (username VARCHAR(64) NOT NULL, 
    series VARCHAR(64) PRIMARY KEY, 
    token VARCHAR(64) NOT NULL, 
    last_used TIMESTAMP NOT NULL
    ) ENGINE=INNODB DEFAULT CHARSET=utf8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. RememberMeServices 进行持久化配置
        // 指定记住我的实现
        @Bean
        public RememberMeServices rememberMeServices() {
            // 配置 token 数据源,保证服务重启之后仍然有存储记录
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            // 配置数据源
            tokenRepository.setDataSource(dataSource);
            // 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
            // tokenRepository.setCreateTableOnStartup(true);
    
            return new PersistentTokenBasedRememberMeServices(
                    UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
                    userDetailsService,     // 认证数据源
                    tokenRepository);     // 令牌存储方式
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 总体配置
    package com.vinjcent.config.security;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    
    import javax.sql.DataSource;
    import java.util.UUID;
    
    
    /**
     *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
     */
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 构造注入使用@Autowired,set注入使用@Resource
        private final DivUserDetailsService userDetailsService;
        // token 存储数据源
        private final DataSource dataSource;
    
        // UserDetailsService
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
            this.userDetailsService = userDetailsService;
            this.dataSource = dataSource;
        }
    
        // AuthenticationManager
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 拦配置http拦截
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/toLogin").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/toLogin")
                    .loginProcessingUrl("/login")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .defaultSuccessUrl("/toIndex", true)
                    .failureUrl("/toLogin")
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/toLogin")
                    .and()
                    .rememberMe()
                    .rememberMeServices(rememberMeServices())
                    // .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数
                    // .alwaysRemember(true)   // 总是记住我,只针对服务后台设置
                    .and()
                    .csrf()
                    .disable();
        }
    
        // 指定记住我的实现
        @Bean
        public RememberMeServices rememberMeServices() {
            // 配置 token 数据源,保证服务重启之后仍然有存储记录
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            // 配置数据源
            tokenRepository.setDataSource(dataSource);
            // 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
            // tokenRepository.setCreateTableOnStartup(true);
    
            return new PersistentTokenBasedRememberMeServices(
                    UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
                    userDetailsService,     // 认证数据源
                    tokenRepository);     // 令牌存储方式(不建议使用内存的方式存储令牌)
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 测试效果

    第一次登录

    在这里插入图片描述

    重启服务测试,发现依然可以自动登录

    在这里插入图片描述

    8.5 自定义记住我(传统web版)

    1. 导入依赖
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
        
        <dependency>
            <groupId>org.thymeleaf.extrasgroupId>
            <artifactId>thymeleaf-extras-springsecurity5artifactId>
            <version>3.0.4.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>2.2.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.2.8version>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    1. application.yml配置文件
    # 端口号
    server:
      port: 3035
      servlet:
        session:
          # 设置session过期时间
          timeout: 1
    # 服务应用名称
    spring:
      application:
        name: SpringSecurity08
      # 关闭thymeleaf缓存(用于修改完之后立即生效)
      thymeleaf:
        cache: false
        # thymeleaf默认配置
        prefix: classpath:/templates/
        suffix: .html
        encoding: UTF-8
        mode: HTML
      # 数据源
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    mybatis:
      # 注意 mapper 映射文件必须使用"/"
      type-aliases-package: com.vinjcent.pojo
      mapper-locations: com/vinjcent/mapper/**/*.xml
    
    # 日志处理,为了展示 mybatis 运行 sql 语句
    logging:
      level:
        com:
          vinjcent:
            debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    1. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
    • Role
    package com.vinjcent.pojo;
    
    import java.io.Serializable;
    
    public class Role implements Serializable {
    
        private Integer id;
        private String name;
        private String nameZh;
    
        public Role() {
        }
    
        public Role(Integer id, String name, String nameZh) {
            this.id = id;
            this.name = name;
            this.nameZh = nameZh;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getNameZh() {
            return nameZh;
        }
    
        public void setNameZh(String nameZh) {
            this.nameZh = nameZh;
        }
    
        @Override
        public String toString() {
            return "Role{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", nameZh='" + nameZh + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • User
    package com.vinjcent.pojo;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.*;
    
    // 自定义用户User
    public class User implements UserDetails {
    
        private Integer id; // 用户id
        private String username;    // 用户名
        private String password;    // 密码
        private boolean enabled;    // 是否可用
        private boolean accountNonExpired;  // 账户过期
        private boolean accountNonLocked;   // 账户锁定
        private boolean credentialsNonExpired;  // 凭证过期
        private List<Role> roles = new ArrayList<>();   // 用户角色信息
    
    
        // 返回权限信息
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Set<SimpleGrantedAuthority> authorities = new HashSet<>();
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
                authorities.add(simpleGrantedAuthority);
            });
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    
        public void setAccountNonExpired(boolean accountNonExpired) {
            this.accountNonExpired = accountNonExpired;
        }
    
        public void setAccountNonLocked(boolean accountNonLocked) {
            this.accountNonLocked = accountNonLocked;
        }
    
        public void setCredentialsNonExpired(boolean credentialsNonExpired) {
            this.credentialsNonExpired = credentialsNonExpired;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
        public Integer getId() {
            return id;
        }
    
        public List<Role> getRoles() {
            return roles;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    1. 视图页面
    • login.html
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录页面title>
    head>
    <body>
    
        <h1>用户登录h1>
        <form th:action="@{/login}" method="post">
            用户名: <input type="text" name="uname"> <br>
            密码: <input type="password" name="passwd"> <br>
            
            记住我: <input type="checkbox" name="remember-me" value="true">
            <input type="submit" value="登录">
        form>
    <h3>
        <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}">div>
    h3>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • index.html
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <title>系统主页title>
    head>
    <body>
    <h1>欢迎<span sec:authentication="principal.username">span>,进入我的主页!h1>
    
    <hr>
    <h1>获取认证用户信息h1>
    <ul>
        <li sec:authentication="principal.username">li>
        <li sec:authentication="principal.authorities">li>
        <li sec:authentication="principal.accountNonExpired">li>
        <li sec:authentication="principal.accountNonLocked">li>
        <li sec:authentication="principal.credentialsNonExpired">li>
    ul>
    
    
    <a th:href="@{/logout}">退出登录a>
    
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 自定义认证数据源 UserDetailsService
    package com.vinjcent.config.security;
    
    import com.vinjcent.pojo.Role;
    import com.vinjcent.pojo.User;
    import com.vinjcent.service.RoleService;
    import com.vinjcent.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    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.Component;
    import org.springframework.util.ObjectUtils;
    
    import java.util.List;
    
    @Component
    public class DivUserDetailsService implements UserDetailsService {
    
        // dao ===> springboot + mybatis
        private final UserService userService;
    
        private final RoleService roleService;
    
        @Autowired
        public DivUserDetailsService(UserService userService, RoleService roleService) {
            this.userService = userService;
            this.roleService = roleService;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1.查询用户
            User user = userService.queryUserByUsername(username);
            if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
            // 2.查询权限信息
            List<Role> roles = roleService.queryRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 配置拦截请求 WebSecurityConfigurerAdapter
    package com.vinjcent.config.security;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    
    import javax.sql.DataSource;
    import java.util.UUID;
    
    
    /**
     *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
     */
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 构造注入使用@Autowired,set注入使用@Resource
        private final DivUserDetailsService userDetailsService;
        // token 存储数据源
        private final DataSource dataSource;
    
        // UserDetailsService
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
            this.userDetailsService = userDetailsService;
            this.dataSource = dataSource;
        }
    
        // AuthenticationManager
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 拦配置http拦截
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/toLogin").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/toLogin")
                    .loginProcessingUrl("/login")
                    .usernameParameter("uname")
                    .passwordParameter("passwd")
                    .defaultSuccessUrl("/toIndex", true)    // 重定向
                    .failureUrl("/toLogin")     // 失败重定向
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/toLogin")
                    .and()
                    .rememberMe()
                    .rememberMeServices(rememberMeServices())
                    // .rememberMeParameter("remember-me") // 用来接受请求中哪个参数作为开启记住我的参数,注意前端传递的参数
                    // .alwaysRemember(true)   // 总是记住我,只针对服务后台设置,无论前端是否点击"记住我"都默认使用记住我
                    .and()
                    .csrf()
                    .disable();
        }
    
        // 指定记住我的实现
        @Bean
        public RememberMeServices rememberMeServices() {
            // 配置 token 数据源,保证服务重启之后仍然有存储记录
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            // 配置数据源
            tokenRepository.setDataSource(dataSource);
            // 设置第一次启动时,创建表结构(当对http请求的配置中不设置rememberMeServices()时,该设置生效,不然会报错)
            // tokenRepository.setCreateTableOnStartup(true);
    
            return new PersistentTokenBasedRememberMeServices(
                    UUID.randomUUID().toString(), // 自定义一个生成令牌 key,默认 UUID
                    userDetailsService,     // 认证数据源
                    tokenRepository);     // 令牌存储方式(不建议使用内存的方式存储令牌)
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    8.6 自定义记住我(前后端分离

    在这里插入图片描述

    在根据之前源码分析中,发现是根据 remember-me 设置记住我的参数,但是如果使用前后端分离,请求中的类型为 JSON 数据,又如何提取出来 remember-me 的参数呢?而又要如何在 Cookie 中设置我们的 token 令牌呢?

    对于登录认证成功之后的操作,见如下图

    在这里插入图片描述

    这里调用了 rememberMeRequested()方法,传递的是一个 HttpServletRequest 和 String 类型的参数,而这个 rememberMeRequested()函数是在 AbstractRememberMeServices 抽象类中的,所以我们需要对其进行重写

    1. 导入依赖pom.xml
        <dependencies>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
            
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>2.2.2version>
            dependency>
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>8.0.22version>
            dependency>
            
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>1.2.8version>
            dependency>
    
        dependencies>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    1. application.yml配置文件
    # 端口号
    server:
      port: 3035
      servlet:
        session:
          # 设置session过期时间
          timeout: 1
    # 服务应用名称
    spring:
      application:
        name: SpringSecurity09security
      # 数据源
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/spring?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    mybatis:
      # 注意 mapper 映射文件必须使用"/"
      type-aliases-package: com.vinjcent.pojo
      mapper-locations: com/vinjcent/mapper/**/*.xml
    
    # 日志处理,为了展示 mybatis 运行 sql 语句
    logging:
      level:
        com:
          vinjcent:
            debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    1. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
    • Role
    package com.vinjcent.pojo;
    
    import java.io.Serializable;
    
    public class Role implements Serializable {
    
        private Integer id;
        private String name;
        private String nameZh;
    
        public Role() {
        }
    
        public Role(Integer id, String name, String nameZh) {
            this.id = id;
            this.name = name;
            this.nameZh = nameZh;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getNameZh() {
            return nameZh;
        }
    
        public void setNameZh(String nameZh) {
            this.nameZh = nameZh;
        }
    
        @Override
        public String toString() {
            return "Role{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", nameZh='" + nameZh + '\'' +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • User
    package com.vinjcent.pojo;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.*;
    
    // 自定义用户User
    public class User implements UserDetails {
    
        private Integer id; // 用户id
        private String username;    // 用户名
        private String password;    // 密码
        private boolean enabled;    // 是否可用
        private boolean accountNonExpired;  // 账户过期
        private boolean accountNonLocked;   // 账户锁定
        private boolean credentialsNonExpired;  // 凭证过期
        private List<Role> roles = new ArrayList<>();   // 用户角色信息
    
    
        // 返回权限信息
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Set<SimpleGrantedAuthority> authorities = new HashSet<>();
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
                authorities.add(simpleGrantedAuthority);
            });
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    
        public void setAccountNonExpired(boolean accountNonExpired) {
            this.accountNonExpired = accountNonExpired;
        }
    
        public void setAccountNonLocked(boolean accountNonLocked) {
            this.accountNonLocked = accountNonLocked;
        }
    
        public void setCredentialsNonExpired(boolean credentialsNonExpired) {
            this.credentialsNonExpired = credentialsNonExpired;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
        public Integer getId() {
            return id;
        }
    
        public List<Role> getRoles() {
            return roles;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    1. 自定义登录过滤器
    • LoginFilter
    package com.vinjcent.filter;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
    import org.springframework.util.ObjectUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Map;
    
    /**
     * 自定义前后端分离的 Filter,重写 UsernamePasswordAuthenticationFilter
     */
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
        // 用于指定请求类型
        private boolean postOnly = true;
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
    
            if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
                // 如果是json格式,需要转化成对象并从中获取用户输入的用户名和密码进行认证 {"username": "root", "password": "123", "remember-me": "true"}
                try {
                    Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                    String username = userInfo.get(getUsernameParameter());
                    String password = userInfo.get(getPasswordParameter());
                    // 可以进行修改,使其成为动态参数
                    String rememberMe = userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
                    // 如果 rememberMe 不为空
                    if (!ObjectUtils.isEmpty(rememberMe)) {
                        // 将其存储request作用域
                        request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberMe);
                    }
                    System.out.println("用户名: " + username + " 密码: " + password +  " 是否记住我: " + rememberMe);
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
                    setDetails(request, token);
                    return this.getAuthenticationManager().authenticate(token);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return super.attemptAuthentication(request, response);
        }
    
        @Override
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    1. 自定义记住我 services 实现类
    • DivPersistentTokenBasedRememberMeServices
    package com.vinjcent.config.security;
    
    import org.springframework.core.log.LogMessage;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * 自定义记住我 services 实现类
     */
    public class DivPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
    
    
        /**
         * 自定义前后端分离获取 rememberMe 请求参数
         * @param request 请求
         * @param rememberMe 记住我参数
         * @return 返回boolean
         */
        @Override
        protected boolean rememberMeRequested(HttpServletRequest request, String rememberMe) {
            String paramValue = (String) request.getAttribute(rememberMe);
            if (paramValue != null) {
                if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                        || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
                    return true;
                }
            }
            this.logger.debug(
                    LogMessage.format("Did not send remember-me cookie (principal did not set parameter '%s')", paramValue));
            return false;
        }
    
        public DivPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
            super(key, userDetailsService, tokenRepository);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    1. 认证数据源
    package com.vinjcent.config.security;
    
    import com.vinjcent.pojo.Role;
    import com.vinjcent.pojo.User;
    import com.vinjcent.service.RoleService;
    import com.vinjcent.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    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.Component;
    import org.springframework.util.ObjectUtils;
    
    import java.util.List;
    
    @Component
    public class DivUserDetailsService implements UserDetailsService {
    
        // dao ===> springboot + mybatis
        private final UserService userService;
    
        private final RoleService roleService;
    
        @Autowired
        public DivUserDetailsService(UserService userService, RoleService roleService) {
            this.userService = userService;
            this.roleService = roleService;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1.查询用户
            User user = userService.queryUserByUsername(username);
            if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
            // 2.查询权限信息
            List<Role> roles = roleService.queryRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    1. 过滤器适配器
    • WebSecurityConfiguration
    package com.vinjcent.config.security;
    
    import com.vinjcent.filter.LoginFilter;
    import com.vinjcent.handler.DivAuthenticationFailureHandler;
    import com.vinjcent.handler.DivAuthenticationSuccessHandler;
    import com.vinjcent.handler.DivLogoutSuccessHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.RememberMeServices;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
    import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
    
    import javax.sql.DataSource;
    import java.util.UUID;
    
    
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        // 注入数据源认证
        private final DivUserDetailsService userDetailsService;
        // 注入数据源
        private final DataSource dataSource;
    
        @Autowired
        public WebSecurityConfiguration(DivUserDetailsService userDetailsService, DataSource dataSource) {
            this.userDetailsService = userDetailsService;
            this.dataSource = dataSource;
        }
    
    
        // 自定义AuthenticationManager(自定义需要暴露该bean)
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        // 暴露AuthenticationManager,使得这个bean能在组件中进行注入
        @Override
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public LoginFilter loginFilter() throws Exception {
            // 1.创建自定义的LoginFilter对象
            LoginFilter loginFilter = new LoginFilter();
            // 2.设置登陆操作的请求
            loginFilter.setFilterProcessesUrl("/login");
            // 3.动态设置传递的参数key
            loginFilter.setUsernameParameter("uname");  // 指定 json 中的用户名key
            loginFilter.setPasswordParameter("passwd"); // 指定 json 中的密码key
            // 4.设置自定义的用户认证管理者
            loginFilter.setAuthenticationManager(authenticationManager());
            // 5.配置认证成功/失败处理(前后端分离)
            loginFilter.setAuthenticationSuccessHandler(new DivAuthenticationSuccessHandler());  // 认证成功处理
            loginFilter.setAuthenticationFailureHandler(new DivAuthenticationFailureHandler());  // 认证失败处理
            // 6.设置认证成功时使用自定义 rememberMeServices
            // 下面也设置了一次,因为第一次认证需要生成token传递给客户端,第二次是因为,当session过期之后,能够从数据库中去查找对应的持久化记录(二者缺一不可)
            loginFilter.setRememberMeServices(rememberMeServices());
            return loginFilter;
        }
    
        // 自定义rememberMeServices
        @Bean
        public RememberMeServices rememberMeServices() {
            // 使用持久化存储数据
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            // 设置持久化数据源
            tokenRepository.setDataSource(dataSource);
            return new DivPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService, tokenRepository);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .rememberMe()   // 开启记住我功能
                    // 1.认证成功之后根据记住我,将 cookie 保存到客户端
                    // 2.只有 cookie 写入到客户端成功才能实现自动登录功能
                    .rememberMeServices(rememberMeServices())   // 设置自动登录使用哪个 rememberMeServices
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessHandler(new DivLogoutSuccessHandler())
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(((req, resp, ex) -> {
                        resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        resp.setStatus(HttpStatus.UNAUTHORIZED.value());
                        resp.getWriter().println("请认证之后再操作!");
                    }))
                    .and()
                    .csrf()
                    .disable();
    
            // 替换原始 UsernamePasswordAuthenticationFilter 过滤器
            http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
            /**
                http.addFilter();   // 添加一个过滤器
                http.addFilterAt(); // at: 添加一个过滤器,将过滤链中的某个过滤器进行替换
                http.addFilterBefore(); // before: 添加一个过滤器,追加到某个具体过滤器之前
                http.addFilterAfter();  // after: 添加一个过滤器,追加到某个具体过滤器之后
             */
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    1. 测试登陆后,将服务停止,再次开启访问系统资源能够正常访问
  • 相关阅读:
    环境变量基础
    课程的概述
    File类和IO流的相关面试(二)
    【Flink】事务型Sink连接器、kafka连接器、写入到redis、mysql
    github desktop上传代码
    react antd下拉选择框选项内容换行
    漏洞复现--时空智友企业流程化管控系统敏感信息泄露(POC)
    《uni-app》表单组件-Button按钮
    linux网络编程之System V 共享内存 和 系列函数
    centos6服务器升级glibc失败后操作系统命令不能执行处理
  • 原文地址:https://blog.csdn.net/Wei_Naijia/article/details/127898284