一、核心概念

SpringSecurity的核心包括认证和授权两个部分。

认证

认证过程主要是实现AuthenticationManager, AuthenticationManager最重要的实现类是ProviderManager, 通过ProviderManager,可以管理AuthenticationProvider, 每个AuthenticationProvider都对应一种认证方式, 当所有认证方式不支持时,调用ProviderManager的parent认证。

SpringSecurity提供的认证其实是一种接口,他允许你自定义认证方式,这种设计更像是一种规范设计。

授权

AccessDecisionVoter 是一个投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票;
AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许。

过滤链

开发者所见到的 Spring Security 提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。

需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中,如下图所示。
在这里插入图片描述

二、Springboot整合SpringSecurity核心流程

在将SpringSecurity整合到Springboot之前,我们需要知道整体的流程以便于我们更好的理解验证过程。

  1. 引入spring-boot-starter-security依赖
  2. 创建SecurityConfig配置文件
  3. 重写UserDetailsService

第2点中,又可以拆分为登录、登出、错误处理、自动登录、过滤器等配置,如果完全不配置,SpringSecurity也会为你提供一套默认配置,从引入依赖开始就能够正常工作了。

第3点中,重写UserDetailsService.loadUserByUsername(String username)方法可以让你自定义验证的过程。如验证时,你希望通过查询MySQL数据库,如果数据库中存在该用户并且密码验证正确,就返回SpringSecurity中的User对象,这些过程都可以自定义实现。

三、Springboot整合SpringSecurity

3.1 引入依赖

在pom.xml文件中引入依赖后,SpringSecurity在Springboot启动时就能开始正常工作了。

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

3.2 创建SecurityConfig配置文件

如果什么都不配置,那么该配置文件的内容应该这样的,剩下的事情SpringSecurity会帮你设置默认配置

package com.example.demo.config.security;

@Configuration
public class SecurityConfig {
	@Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { }
}

如果想要自定义配置,如自定义登录界面、自定义验证过程,或者想要整合一些工具,如Jwt,这个时候需要在SecurityFilterChain中配置。

比较简单的配置:

package com.example.demo.config.security;

@Configuration
public class SecurityConfig {
	@Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // 登录页为/login.html,请求/login路径验证登录信息,成功后重定向到/index.html页
    	httpSecurity.formLogin()
                .loginProcessingUrl("/login")
            	.loginPage("/login.html")
            	.defaultSuccessUrl("/index.html");
        // 允许登录页和登录的url不需要验证即可访问,因为那个时候还没有登录信息
        httpSecurity.authorizeRequests()
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated();
        httpSecurity.csrf().disable();
        return httpSecurity.build();
    }
}

更全的配置:

其中,SpringSecurity整合JwtToken可以看另一篇文章。

package com.example.demo.config.security;

/**
 * @Author : HuangJiajian
 * @create 2022/10/18 17:49
 */
@Configuration
public class SecurityConfig {
    @Autowired
    UserDetailServiceImpl userDetailService;
    @Autowired
    PersistentTokenRepository persistentTokenRepository;
    @Autowired
    private JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // 登录,自定义登录界面为/login.html,通过请求/login路径验证身份
        httpSecurity.formLogin()
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new AuthenticationSuccessConfig())
                .failureHandler(new AuthenticationFailConfig());
        // 退出,自定义退出成功时的操作,如响应的数据
        httpSecurity.logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new LogoutSuccessConfig());
        // 过滤器,除了/test/*和/login*的url请求不需要验证身份外,其他的请求都需要身份验证
        httpSecurity.authorizeRequests()
                .antMatchers("/test/*").permitAll()
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated();
        // 错误处理,自定义认证失败的错误处理,如403返回权限不足等
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(new AccessDeniedConfig())
                .authenticationEntryPoint(new AuthenticationEntryPointConfig());
		// 整合JwtToken验证身份
        httpSecurity.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        httpSecurity.csrf().disable();
        return httpSecurity.build();
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        // 选择非对称加密为SpringSecurity的加密方法
        return new BCryptPasswordEncoder();
    }
}

3.3 重写UserDetailsService

Spring Security 中定义了 UserDetails 接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已经固定的系统集成到 Spring Security 认证体系中。而负责提供用户数据源的接口是 UserDetailsService, UserDetailsService 中只有一个查询用户的方法,那就是loadUserByUsername。

换句话说,我们可以使用UserDetailsService.loadUserByUsername(String username)方法来自定义查询用户的过程。这部分的代码,只需要关心loadUserByUsername的自定义逻辑即可。注意这里的User类是来自于SpringSecurity的,而非自己在Entity中定义的User类。

package com.example.demo.service.impl;

/**
 * @Author : HuangJiajian
 * @create 2022/10/19 10:03
 */
@Service
@Component
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    UserRoleMapper userRoleMapper;
    @Autowired
    RoleMapper roleMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * @Author HJJ
     * @Date 2022-12-27 9:43
     * @Params String username
     * @Return User
     * @Description SpringSecurity方法,验证并获取用户权限信息,并将User数据写入Redis缓存
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String cacheUser = stringRedisTemplate.opsForValue().get("username::" + username);
        // 如果有缓存,直接返回,如果没有缓存,否则需要查询数据库并写入Redis缓存
        if (!Objects.isNull(cacheUser)) {
            return JSON.parseObject(cacheUser, UserDetails.class);
        } else {
            UserEntity userEntity = getUserEntityByUsername(username);
            User user = new User(username, userEntity.getUserPassword(), getAuthoritiesByUserId(userEntity.getUserId()));
            String strUser = JSONObject.toJSONString(user);
            System.out.println(username+"查询了数据库并写入Redis缓存:" + strUser);
        	// 将User对象转换成String并写入缓存
            stringRedisTemplate.opsForValue().set("username::" + username, strUser, 60, TimeUnit.SECONDS);
            return user;
        }
    }

    /**
     * @Author HJJ
     * @Date 2022-12-27 9:42
     * @Params
     * @Return
     * @Description 查询数据库,获取用户信息
     */
    public UserEntity getUserEntityByUsername(String username) {
        QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userName", username);
        UserEntity userEntity = userMapper.selectOne(queryWrapper);
        if (Objects.isNull(userEntity)) {
            System.out.println("没有此用户:" + username);
            throw new UsernameNotFoundException("没有此用户");
        }
        return userEntity;
    }

    /**
     * @Author HJJ
     * @Date 2022-12-27 9:55
     * @Params
     * @Return
     * @Description 查询数据库,获取用户权限信息
     */
    public List<GrantedAuthority> getAuthoritiesByUserId(int userId) {
        StringBuilder roles = new StringBuilder();
        QueryWrapper<UserRole> queryWrapper1 = new QueryWrapper<>();
        queryWrapper1.eq("userId", userId);
        List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper1);
        for (int i = 0; i < userRoles.size(); i++) {
            UserRole userRole = userRoles.get(i);
            int roleId = userRole.getRoleId();
            QueryWrapper<Role> queryWrapper2 = new QueryWrapper<>();
            queryWrapper2.eq("roleId", roleId);
            Role role = roleMapper.selectOne(queryWrapper2);
            String roleName = role.getRoleName();
            if (Objects.equals(userRoles.size() - 1, i)) {
                roles.append(roleName);
                break;
            }
            roles.append(roleName).append(",");
        }
        return AuthorityUtils.commaSeparatedStringToAuthorityList(roles.toString());
    }
}

以上即是Springboot整合SpringSecurity的整体过程。

参考文献

  1. 深入浅出SpringSecurity-王松
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐