Bootstrap

阐述Spring Security概念及其运用于实战

Spring Security(安全校验)

1. 概述

Spring Security是Spring项目组提供的安全服务框架,核心功能包括认证授权.为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作.
在如今开发模式中,Spring Security已经成为Java程序员必备的一项技术,简化认证和授权开发的首选项,
其核心为认证和授权,我们先来了解一下何为认证和授权,理解其概念.

2. 认证

认证是指系统判断用户的身份是否合法,合法可继续访问,不合法则会拒绝访问.在生活中我们很常见认证的方式,例如人脸识别认证,指纹认证,用户名密码的登录,手机短信的登录等方式
认证的目的是保护系统的隐私数据和资源,用户的身份合法 才能访问资源.
注: SpringSecurity的依赖为(基于Spring Boot)

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.1 内存认证

内存认证的概念,及用户的数据存放在内存当中,数据在代码中写死,项目启动便加载到内存当中.代码如下

@Configuration
public class SpringSecurityConfig{

		@Bean
    public UserDetailsService userDetailsService(){
    //1.创建内存管理对象
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
	//2.创建权限对象
        UserDetails user1 = User.withUsername("zhangsan").password("123").authorities("admin").build();
        UserDetails user2 = User.withUsername("lisi").password("123").authorities("admin").build();
	//3.封装到manager当中
        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }
}

这种认证方式很明显,不灵活,代码写死,用户信息仅有代码所包含部分.这不符合我们的业务开发,应该连接于数据库.所以Spring Security提供了另一套配置.我们需要创建一个配置类去实现UsersDetailsService接口.(这里我使用的是Mybatis-Plus去操作数据库)
代码如下:

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.查询数据库,是否存在对象
        Users users = usersMapper
        .selectOne(new QueryWrapper<Users>().lambda()
                .eq(Users::getUsername, username));
     
        //2.判断用户是否为空
        if(users == null){
            //2.1.1 为空直接返回
            return null;
        }
        //3.封装成userDetails对象
        UserDetails userDetails = User.withUsername(users.getUsername())
                .password(users.getPassword())
                .authorities("admin")
                .build();
        //5.返回
        return userDetails;
    }
}

2.2 密码编码

为了数据的安全性,通常我们存储到数据库的密码是经过转码的,即密文存储,所以我们需要配置转码的配置类,校验时才能检查对应的密码是否一致
Spring Security提供了BCryptPasswordEncoder编码配置类,我们注入一下即可.

/**
     * 密码解码配置
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

2.3 认证成功与失败的处理

当认证成功之后,我们可能要对用户做一些信息上的配置或者其他的调整,需要介入java代码,同理失败也得控制用户的信息,防止用户恶意破坏信息.
我们需要配置SecurityFilterChain,由名字也可知道这是经过SpringSecurity框架的执行链.我们可以在这配置我们的业务需求.代码如下(包含前端路径的可自行配置,这里我使用的是thymeleaf,在resource下创建templates目录,编写对应页面就可)
注: 别忘了配置第二步允许静态资源的访问

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        //1.配置登录表单
        httpSecurity.formLogin(form ->{
            form.loginPage("/login.html") //自定义登录页面
                    .loginProcessingUrl("/login")   //登录路径,表单向该路径提交,提交后自动执行MyUserDetailService的方法
                    .usernameParameter("username")  //表单中用户名项
                    .passwordParameter("password")  //表单中的密码项
                    .successHandler(new MyLoginSuccessHandler())    //登录成功跳转页面
                    .failureHandler(new MyLoginFailHandler());   //登录失败跳转页面
        });
        //2.配置需要认证和不需要认证的资源
        httpSecurity.authorizeHttpRequests(resp->{
            resp.requestMatchers("/login.html","/fail.html").permitAll(); //不需要认证的资源
            resp.requestMatchers("/css/*.css","/js/*.js","/img/**").permitAll();    //静态资源
            resp.anyRequest().authenticated();  //其余所有请求都需要认证
        });
        //3.关闭csrf防护(若不关闭资源无法访问)
        httpSecurity.csrf(csrf->{
            csrf.disable();
        });
}

认证成功的处理器

public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //拿到登录用户的信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();

        System.out.println("用户名:"+userDetails.getUsername());
        System.out.println("登录之后的其余操作");
        //重定向到主页
        response.sendRedirect("/main");
    }
}

认证失败的处理器

public class MyLoginFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.err.println(exception.getMessage());
        System.out.println("登录失败");
        response.sendRedirect("/fail.html");
    }
}

2.4 退出登录

用户需要退出登录时,我们需要清除他的会话信息及授权信息.配置如下(依旧配置在上述配置类SpringSecurityConfig的方法当中)

//配置退出登录功能
        httpSecurity.logout(out->{
//                out.logoutUrl("/logout")
            out.logoutSuccessHandler(new MyLoginLogOutSuccessHandler())
                    .invalidateHttpSession(true)    //清除session记录,默认为true
                    .clearAuthentication(true);    //清除授权记录,默认为true
        });

2.5 记住我

在许多登录场景当中,往往会有记住我这个选项.可以在一段时间内记住用户的登录信息,用户下次访问时便可以不再需要登录.我们可以思考一下怎么去做到这个事情.大家应该都接触过JWT令牌,对token字段我们并不陌生.我们可以在用户使用记住我这个功能时,生成一个Token令牌,保存到数据库当中,下次用户再来访问我们去数据库查询是否有这样一个Token即可.
SpringSecurity帮我们实现了这样一个功能,需要我们配置一下即可.其功能演示大概为:

首先我们得有这样一个存放token的表,利用SpringSecurity配置生成表(别忘了在配置文件中配置DataSource哦)

@Configuration
public class RememberMeConfig {
    @Autowired
    private DataSource dataSource;
    //生成令牌
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository(){
        // 为Spring Security 自带的令牌控制器设置数据源
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        // 开启自动建表,第一次启动时需要,第二次就注释掉
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        //返回
        return jdbcTokenRepository;
    }
}

注: 第一次启动运行时可以直接这样写,第二次启动记得把jdbcTokenRepository.setCreateTableOnStartup(true);给注释掉,若忘记啦也没有关系,控制台会报错(表已经存在),根据原因我们再来处理也可

再回到SpringSecurityConfig当中,将这个配置类生效

//注入这两个类
	@Autowired
    private PersistentTokenRepository persistentTokenRepository;
    @Autowired
    private MyUserDetailService myUserDetailService;
//配置记住我
        httpSecurity.rememberMe(remember->{
            remember.tokenRepository(persistentTokenRepository) //配置持久化token仓库
                    .userDetailsService(myUserDetailService)	//配置权限处理对象,即认证逻辑对象
                    .tokenValiditySeconds(30); //配置token的保持时间,即多久以后失效
        });
;