SpringSecurity(八)【RememberMe记住我】

news2024/10/6 22:19:06

八、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. 请求到达过滤器之后,首先判断 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());     // 令牌存储方式(不建议使用内存的方式存储令牌,如果服务器重启,那么内存将全部失效)
    }

}

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

在这里插入图片描述

在这里插入图片描述

  1. 导入数据库相关依赖
<dependencies>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>
</dependencies>
  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. 创建对应的表结构
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. 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);     // 令牌存储方式
    }
  • 总体配置
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);     // 令牌存储方式(不建议使用内存的方式存储令牌)
    }

}

  • 测试效果

第一次登录

在这里插入图片描述

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

在这里插入图片描述

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

  1. 导入依赖
<dependencies>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--thymeleaf-security-->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    <!--security-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
  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. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
  • 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 + '\'' +
                '}';
    }
}
  • 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. 视图页面
  • 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>
        <!-- value 可选值有:true yes on 1  -->
        记住我: <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>
  • 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. 自定义认证数据源 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. 配置拦截请求 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);     // 令牌存储方式(不建议使用内存的方式存储令牌)
    }

}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

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

  1. 导入依赖pom.xml
    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>

    </dependencies>

  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. 实体类(这里就不说持久层、业务逻辑层了,之前已经写过了,可以往前面章节翻翻~)
  • 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 + '\'' +
                '}';
    }
}
  • 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. 自定义登录过滤器
  • 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. 自定义记住我 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. 认证数据源
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. 过滤器适配器
  • 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. 测试登陆后,将服务停止,再次开启访问系统资源能够正常访问

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

正点原子开拓者FPGA,程序固化下载到板子里面

者电子信息专业硕士毕业&#xff0c;获得过多次电子设计大赛、大学生智能车、数学建模国奖&#xff0c;现就职于南京某半导体芯片公司&#xff0c;从事硬件研发&#xff0c;电路设计研究。对于学电子的小伙伴&#xff0c;深知入门的不易&#xff0c;特开次博客交流分享经验&…

XCode内存和UnityProfiler内存有较大差值

1&#xff09;XCode内存和UnityProfiler内存有较大差值 ​2&#xff09;Dynamic Bone插件和Job System的写法哪个好 3&#xff09;编辑器中iOS平台SoftShadow无效 4&#xff09;Unity 2021中阻止AssetPostprocessor代码改变导致相关资源Reimport 这是第313篇UWA技术知识分享的推…

用DIV+CSS技术设计我的家乡网站(web前端网页制作课作业)南宁绿城之都

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

大一学生《web课程设计》用DIV+CSS技术设计的个人网页(网页制作课作业)

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

视频打马赛克并追踪

每天一个PS/PR小技巧&#xff08;原理实践&#xff09;见&#xff1a; 每天一个PS/PR小技巧&#xff08;原理实践&#xff09;_Dezeming的博客-CSDN博客PS是由Adobe Systems开发和发行的图像处理软件。本文的特色在于快速上手和制作一些生活中会常用的功能&#xff0c;并且解释…

数据库高级 II

数据库高级 II 如何保证二叉排序树中的元素是可比较大小的 让元素所属的类实现接口Comparable 比较器: Comparable: 内比较器,类实现该接口后,需要重写其中的抽象方法,在类的内部定义比较规则.观察String对象之间的比较练习:创建Student类型,其中的属性有:id 学号,name 姓名…

【深度学习】实验4布置:脑部 MRI 图像分割

DL_class 学堂在线《深度学习》实验课代码报告&#xff08;其中实验1和实验6有配套PPT&#xff09;&#xff0c;授课老师为胡晓林老师。课程链接&#xff1a;https://www.xuetangx.com/training/DP080910033751/619488?channeli.area.manual_search。 持续更新中。 所有代码…

【Linux】基础IO —— 缓冲区深度剖析

&#x1f308;欢迎来到Linux专栏~~基础IO (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&a…

(免费分享)基于springboot论坛bbs系统

源码获取&#xff1a;关注文末gongzhonghao&#xff0c;输入010领取下载链接 开发工具IDEA ,数据库mysql5.7 技术&#xff1a;springbootjpashiroredislayui 前台截图&#xff1a; 后台截图&#xff1a; package com.qxczh.admin.service.impl;import com.qxczh.admin.servic…

关于城市旅游的HTML网页设计——中国旅游HTML+CSS+JavaScript 出游旅游主题度假酒店 计划出行网站设计

&#x1f468;‍&#x1f393;静态网站的编写主要是用 HTML DⅣV CSSJS等来完成页面的排版设计&#x1f469;‍&#x1f393;&#xff0c;一般的网页作业需要融入以下知识点&#xff1a;div布局、浮动定位、高级css、表格、表单及验证、js轮播图、音频视频Fash的应用、uli、下拉…

人工智能--k近邻算法2-归一化、交叉验证、网格搜索、数据分割方法总结、两案例实现

人工智能-第三阶段-k近邻算法1-算法理论、kd树、鸢尾花数据 人工智能–k近邻算法2-归一化、交叉验证、网格搜索、数据分割方法总结、两案例实现 1.7 特征工程-特征值预处理 1.7.1 介绍 通过一些转换函数奖特征数据转换为更加适合算法模型的特征数据过程 为什么要进行归一化/…

最新最全面的Spring详解(二)——classpath扫描和组件管理

前言 本文为 【Spring】classpath扫描和组件管理 相关知识&#xff0c;下边将对Component 和及其派生出的其他注解&#xff0c;自动检测类和注册beanDifination&#xff0c;组件命名&#xff0c;为自动检测组件提供scope&#xff0c;使用过滤器自定义扫描&#xff0c;在组件中定…

【Java开发】 Spring 03:云服务器 Docker 环境下安装 MongoDB 并连接 Spring 项目实现简单 CRUD:

接下来介绍一下 NoSQL &#xff0c;相比于 Mysql 等关系型的数据库&#xff0c;NoSQL &#xff08;文档型数据库&#xff09;由于存储的数据之间无关系&#xff0c;因此具备大数据量&#xff0c;高性能等特点&#xff0c;用于解决大规模数据集合多重数据种类带来的挑战&#xf…

Aspose.OMR for .NET 22.11.X Crack

Aspose.OMR for .NET 是一个可靠且通用的编程 API&#xff0c;用于设计和自动识别手填答题卡、调查、测试、选票、SAT 考试表格、保险索赔以及受访者通过随机抽取答案来回答问题的类似文件在圆形或正方形中标记。从成百上千个表单中手动读取和汇总结果的漫长且容易出错的过程归…

深入学习函数(2)

目录 一、函数的嵌套调用和链式访问 1、嵌套调用 2、链式访问 二、函数的声明和定义 1、函数的声明 2、函数的定义 声明和定义的拓展 拆成三个文件的好处 一、函数的嵌套调用和链式访问 当代码写的越来越多时&#xff0c;就会发现&#xff0c;其实一个程序都…

Day802.JVM热点问题 -Java 性能调优实战

JVM部分热点问题 Hi&#xff0c;我是阿昌&#xff0c;今天学习JVM部分热点问题的内容。 1、字符串常量不是在java8中已经被放入到堆中了吗&#xff0c;应该不在方法区中了&#xff0c;咋一些图中还在方法区中&#xff1f; JVM 的内存模型只是一个规范&#xff0c;方法区也是…

Fiddler基础使用

目录预备知识关于web的一些基础知识实验目的实验环境实验步骤一实验步骤二实验步骤三预备知识 关于web的一些基础知识 要分析Fiddler抓取的数据包&#xff0c;我们首先要熟悉HTTP协议。HTTP即超文本传输协议&#xff0c;是一个基于请求/响应模式的、无状态的、应用层的协议&a…

【Python开发】Flask项目的组织架构

Flask项目的组织架构在大型Flask项目中&#xff0c;主要有三种常见的项目组织架构&#xff1a;功能式架构&#xff08;也就是 Bluelog 程序使用的架构&#xff09;、分区式架构和混合式架构。我们将以一个示例程序 myapp 作为示例来介绍这三种架构的特点和区别&#xff0c;这个…

教你用HTML+CSS实现百叶窗动画效果

推荐学习专栏&#xff1a; 【JavaWeb】Web前端JavaWeb学习专栏 文章目录前言1、百叶窗效果2、原理讲解3、制作百叶窗4、资源下载5、完整代码总结前言 我们浏览网页的时候总能看见一些炫酷的特效&#xff0c;比如百叶窗效果&#xff0c;本文我们就用HTMLCSS制作一个百叶窗小项…

副业该怎么选择,适合新手的四个副业项目,零基础也可操作的兼职

副业有可能有时挣得并不多&#xff0c;但它是一个改变未来的好机会。假如玩的开了&#xff0c;盈利并不比你工资少。95%的人自主创业也是从第二职业做起&#xff0c;做着干着就全职的了。 四个全员第二职业&#xff0c;新手如何做到单月9000&#xff0c;深入分析看下文&#xf…