SpringSecurity深度解析与实践(2)

发布时间:2023年12月21日


在这里插入图片描述

引言

上篇网址

1.Springboot结合SpringSecurity用户认证流程

1.1 配置pom文件

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

1.2.配置application.yml

server:
    port: 8080
spring:
    freemarker:
        # 设置freemarker模板后缀
        suffix: .ftl
        # 设置freemarker模板前缀
        template-loader-path: classpath:/templates/
        enabled: true
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/bookshop

导入表sys_user
mybatisplus生成

package com.yuan.springsecurity1.config;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Slf4j
public class MySQLGenerator {

    private final static String URL = "jdbc:mysql://localhost:3306/bookshop";
    private final static String USERNAME = "root";
    private final static String PASSWORD = "123456";

    private final static DataSourceConfig.Builder DATA_SOURCE_CONFIG =
            new DataSourceConfig.Builder(URL, USERNAME, PASSWORD);

    public static void main(String[] args) {
        FastAutoGenerator.create(DATA_SOURCE_CONFIG)
                .globalConfig(
                        (scanner, builder) ->
                                builder.author(scanner.apply("请输入作者名称?"))
                                        .outputDir(System.getProperty("user.dir") + "\\src\\main\\java")
                                        .commentDate("yyyy-MM-dd")
                                        .dateType(DateType.TIME_PACK)
                                        .enableSwagger()
                )
                .packageConfig((builder) ->
                        builder.parent("com.yuan.springsecurity1")
                                .entity("pojo")
                                .service("service")
                                .serviceImpl("service.impl")
                                .mapper("mapper")
                                .xml("mapper.xml")
                                .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "\\src\\main\\resources\\mapper"))
                )
                .injectionConfig((builder) ->
                        builder.beforeOutputFile(
                                (a, b) -> log.warn("tableInfo: " + a.getEntityName())
                        )
                )
                .strategyConfig((scanner, builder) ->
                        builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
                                .addTablePrefix("tb_", "t_", "lay_", "meeting_", "sys_", "t_medical_")
                                .entityBuilder()
                                .enableChainModel()
                                .enableLombok()
                                .enableTableFieldAnnotation()
                                .controllerBuilder()
                                .enableRestStyle()
                                .enableHyphenStyle()
                                .build()
                )
                .templateEngine(new FreemarkerTemplateEngine())
                .execute();
    }

    protected static List<String> getTables(String tables) {
        return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
    }

}

实体类

package com.yuan.springsecurity1.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * <p>
 * 用户信息表
 * </p>
 *
 * @author yuan
 * @since 2023-12-21
 */
@Getter
@Setter
@Accessors(chain = true)
@TableName("sys_user")
public class User implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @TableField("username")
    private String username;

    @TableField("password")
    private String password;

    @TableField("real_name")
    private String realName;

    @TableField(exist = false)
    private List<GrantedAuthority> authorities;

    @TableField("account_non_expired")
    private boolean accountNonExpired;
    @TableField("account_non_locked")
    private boolean accountNonLocked;
    @TableField("credentials_non_expired")
    private boolean credentialsNonExpired;
    @TableField("enabled")
    private boolean enabled;


}

实现类

package com.yuan.springsecurity1.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.yuan.springsecurity1.pojo.User;
import com.yuan.springsecurity1.mapper.UserMapper;
import com.yuan.springsecurity1.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 用户信息表 服务实现类
 * </p>
 *
 * @author yuan
 * @since 2023-12-21
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return getOne(new QueryWrapper<User>().eq("username",username));
    }
}

security配置类

package com.yuan.springsecurity1.config;

import com.yuan.springsecurity1.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
//开启SpringSecurity的默认行为
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private UserServiceImpl userService;
    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用(基于数据库方式)
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        //创建DaoAuthenticationProvider
        DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
        //设置userDetailsService,基于数据库方式进行身份认证
        provider.setUserDetailsService(userService);
        //配置密码编码器
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception{
        http.authorizeRequests()
                    // 开放接口访问权限,不需要登录就可以访问
                    .antMatchers("/toLogin").permitAll()
                    //访问路径有admin的方法时,需要有ADMIN的身份
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .antMatchers("/user/**").hasAnyRole("ADMIN","USER")
                    // 其余所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                .and()
                .formLogin()
                // 设置登录页面的 URL
                .loginPage("/toLogin")
                // 设置登录请求的 URL,即表单提交的 URL
                .loginProcessingUrl("/userLogin")
                // 设置登录表单中用户名字段的参数名,默认为username
                .usernameParameter("username")
                // 设置登录表单中密码字段的参数名,默认为password
                .passwordParameter("password")
                //成功后的处理
                .successHandler((req,resp,auth)->{
                    resp.sendRedirect("/index");
                })
                //登录失败的处理
                .failureHandler((req,resp,ex)->{
                    req.setAttribute("msg",ex.getMessage());
                    req.getRequestDispatcher("/toLogin").forward(req,resp);
                })
                .and()
                .exceptionHandling().accessDeniedPage("/noAccess")
                .and()
                .logout()
                // 设置安全退出的URL路径
                .logoutUrl("/logout")
                // 设置退出成功后跳转的路径
                .logoutSuccessUrl("/toLogin") ;
        http.csrf().disable();
    	return http.build();
    }
}

login.ftl

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<h1>用户登录</h1>
<form action="/userLogin" method="post">
    <p>
        <label>用户:<input type="text" name="username"/></label>
    </p>
    <p>
        <label>密码:<input type="password" name="password"/></label>
    </p>
    <input type="submit" value="登录"/>
</form>
<p>${msg!}</p>
</body>
</html>

2.自定义MD5加密

创建自定义MD5加密类并实现PasswordEncoder

public class CustomMd5PasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        //对密码进行 md5 加密
        String md5Password = DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
        System.out.println(md5Password);
        return md5Password;
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // 通过md5校验
        System.out.println(rawPassword);
        System.out.println(encodedPassword);
        return encode(rawPassword).equals(encodedPassword);
    }
}

修改SecurityConfig配置类,更换密码编码器:

@Bean
public PasswordEncoder passwordEncoder(){
    // 自定义MD5加密方式:
    return new CustomMd5PasswordEncoder();
}

数据库中的用户密码也需要更换成对应自定义MD5加密密码:

//MD5自定义加密方式:
String pwd =  DigestUtils.md5DigestAsHex("123456".getBytes());
System.out.println(pwd);

最后,将生成的MD5加密密码保存到数据库表中。

3.BCryptPasswordEncoder密码编码器

BCryptPasswordEncoderSpring Security中一种基于bcrypt算法的密码加密方式。bcrypt算法是一种密码哈希函数,具有防止彩虹表攻击的优点,因此安全性较高。

使用BCryptPasswordEncoder进行密码加密时,可以指定一个随机生成的salt值,将其与原始密码一起进行哈希计算。salt值可以增加密码的安全性,因为即使两个用户使用相同的密码,由于使用不同的salt值进行哈希计算,得到的哈希值也是不同的。

Spring Security中,可以通过在SecurityConfig配置类中添加以下代码来使用BCryptPasswordEncoder进行密码加密:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

这样就可以在Spring Security中使用BCryptPasswordEncoder进行密码加密了。

4.RememberMe记住我的实现

在实际开发中,为了用户登录方便常常会启用记住我(Remember-Me)功能。如果用户登录时勾选了“记住我”选项,那么在一段有效时间内,会默认自动登录,免去再次输入用户名、密码等登录操作。该功能的实现机理是根据用户登录信息生成 Token 并保存在用户浏览器的 Cookie 中,当用户需要再次登录时,自动实现校验并建立登录态的一种机制。

Spring Security提供了两种 Remember-Me 的实现方式:

  • 简单加密 Token:用散列算法加密用户必要的登录系信息并生成 Token 令牌。
  • 持久化 Token:数据库等持久性数据存储机制用的持久化 Token 令牌。

基于持久化Token配置步骤如下:

  • 创建数据库表 persistent_logins,用于存储自动登录信息
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) PRIMARY KEY,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL
);

该步骤可以不做,在后续的配置过程中可以交由SpringSecurity自动生成。

基于持久化Token配置,修改SecurityConfig配置类:

package com.yuan.springsecurity1.config;

import com.yuan.springsecurity1.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

@Configuration
//开启SpringSecurity的默认行为
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private DataSource dataSource;

    /**
     * 配置持久化Token方式,注意tokenRepository.setCreateTableOnStartup()配置
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 设置为true要保障数据库该表不存在,不然会报异常哦
        // 所以第二次打开服务器应用程序的时候得把它设为false
        tokenRepository.setCreateTableOnStartup(false);
        return tokenRepository;
    }

    @Autowired
    private UserServiceImpl userService;
    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用(基于数据库方式)
     * @param
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        //创建DaoAuthenticationProvider
        DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
        //设置userDetailsService,基于数据库方式进行身份认证
        provider.setUserDetailsService(userService);
        //配置密码编码器
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception{
        http.authorizeRequests()
                    // 开放接口访问权限,不需要登录就可以访问
                    .antMatchers("/toLogin").permitAll()
                    //访问路径有admin的方法时,需要有ADMIN的身份
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .antMatchers("/user/**").hasAnyRole("ADMIN","USER")
                    // 其余所有请求全部需要鉴权认证
                    .anyRequest().authenticated()
                .and()
                .formLogin()
                // 设置登录页面的 URL
                .loginPage("/toLogin")
                // 设置登录请求的 URL,即表单提交的 URL
                .loginProcessingUrl("/userLogin")
                // 设置登录表单中用户名字段的参数名,默认为username
                .usernameParameter("username")
                // 设置登录表单中密码字段的参数名,默认为password
                .passwordParameter("password")
                //成功后的处理
                .successHandler((req,resp,auth)->{
                    resp.sendRedirect("/index");
                })
                //登录失败的处理
                .failureHandler((req,resp,ex)->{
                    req.setAttribute("msg",ex.getMessage());
                    req.getRequestDispatcher("/toLogin").forward(req,resp);
                })
                .and()
                .exceptionHandling().accessDeniedPage("/noAccess")
                .and()
                .logout()
                // 设置安全退出的URL路径
                .logoutUrl("/logout")
                // 设置退出成功后跳转的路径
                .logoutSuccessUrl("/toLogin")
                .and()
                .rememberMe()
                // 指定 rememberMe 的参数名,用于在表单中携带 rememberMe 的值。
                .rememberMeParameter("remember-me")
                // 指定 rememberMe 的有效期,单位为秒,默认2周。
                .tokenValiditySeconds(600)
                // 指定 rememberMe 的 cookie 名称。
                .rememberMeCookieName("remember-me-cookie")
                // 指定 rememberMe 的 token 存储方式,可以使用默认的 PersistentTokenRepository 或自定义的实现。
                .tokenRepository(persistentTokenRepository())
                // 指定 rememberMe 的认证方式,需要实现 UserDetailsService 接口,并在其中查询用户信息。
                .userDetailsService(userService);
        http.csrf().disable();
    	return http.build();
    }
}

登录界面添加

 <input type="checkbox" name="remember-me"/>记住我<br/>

5.CSRF防御

5.1.什么是CSRF

CSRFCross-Site Request Forgery,跨站请求伪造)是一种利用用户已登录的身份在用户不知情的情况下发送恶意请求的攻击方式。攻击者可以通过构造恶意链接或者伪造表单提交等方式,让用户在不知情的情况下执行某些操作,例如修改密码、转账、发表评论等。

为了防范CSRF攻击,常见的做法是在请求中添加一个CSRF Token(也叫做同步令牌、防伪标志),并在服务器端进行验证。CSRF Token是一个随机生成的字符串,每次请求都会随着请求一起发送到服务器端,服务器端会对这个Token进行验证,如果Token不正确,则拒绝执行请求。

修改SecurityConfig配置类:把这个删掉

http.csrf().disable();

login.ftl中添加

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

总的来说,不带 name=“${_csrf.parameterName}” 这个的话会,就像是没带身份证一样,不能提交表单,反之,者可通过验证,提交表单

文章来源:https://blog.csdn.net/2201_75869073/article/details/135138670
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。