SpringBoot整合SpringSecurity(进阶篇)

“风丶宇个人博客”这个项目使用了SpringSecurity去管理认证授权。SpringSecurity的内容是非常多的,要想完全驾驭只看本篇是不够用的,建议去看B站编程不良人的SpringSecurity,内容介绍较为全面,我愿称为B站最强的SpringSecurity教程。
下面开始介绍如何在项目中使用:

一、数据库

俗话说完事开头难,所以要给数据库建好。这里我就不给建表语句了,我给出表的关系图,大家可以更具自己的情况来建表。

表关系

二、引入依赖

引入springsecurity依赖

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

三、SpringSecurity 配置

首先创建WebSecurityConfig继承了WebSecurityConfigurerAdapter,里面包含了Security的大部分配置。

/**
 * chenjiayan
 * 2022/12/23
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    // 未认证认证处理器
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    // 权限不足处理器
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;
    // 认证成功处理器
    @Autowired
    private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
    // 认证失败处理器
    @Autowired
    private AuthenticationFailHandlerImpl authenticationFailHandler;
    // 退出登录处理器
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    @Bean
    public FilterInvocationSecurityMetadataSource securityMetadataSource(){
    
    
        // 接口拦截规则
        return new FilterInvocationSecurityMetadataSourceImpl();
    }

    @Bean
    public AccessDecisionManager accessDecisionManager(){
    
    
        // 访问决策管理器
        return new AccessDecisionManagerImpl();
    }

    @Bean
    public SessionRegistry sessionRegistry(){
    
    
        // 会话注册(方法)使用本地缓存保存 TODO 可替换为redis实现
        return new SessionRegistryImpl();
    }

    /**
     * 监听会话的创建和过期,过期移除 
     * @return HttpSessionEventPublisher
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher(){
    
    
        return new HttpSessionEventPublisher();
    }

    /**
     * 密码加密
     * @return {@link PasswordEncoder} 加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
    
    
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        // 认证
        http.formLogin()
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailHandler)
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(logoutSuccessHandler);
        // 授权
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
    
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
    
    
                        // 自定义权限设置
                        o.setSecurityMetadataSource(securityMetadataSource());
                        // 自定义权限校验
                        o.setAccessDecisionManager(accessDecisionManager());
                        return o;
                    }
                })
                .anyRequest().permitAll()
                .and()
                // 关闭跨站请求防护
                .csrf().disable()
                // 异常处理
                .exceptionHandling()
                // 未登录处理
                .authenticationEntryPoint(authenticationEntryPoint)
                // 权限不足处理
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                .sessionManagement() // 开启会话管理
                .maximumSessions(20) // 设置最大会话数
                .sessionRegistry(sessionRegistry()); // 自定义会话存储

    }
}

四、各种处理器

1.AuthenticationEntryPointImpl

未认证处理

/**
 * 用户未登录处理
 * @author chenjiayan
 * @date 2022/12/23
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
    
    
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(JSON.toJSONString(Result.fail(StatusCodeEnum.NO_LOGIN)));
    }
}

2.AccessDeniedHandlerImpl

权限不足处理器

/**
 * 用户权限不通过处理
 *
 * @author chenjiayan
 * @date 2022/12/23
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
    
    
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(JSON.toJSONString(Result.fail("权限不足")));
    }
}

3.AuthenticationSuccessHandlerImpl

认证成功处理器

/**
 * 登录成功处理器
 * @author chenjiayan
 * @date 2022/12/25
 */
@Component
@Slf4j
@EnableAsync(proxyTargetClass=true) // 开启异步任务
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    
    
    @Autowired
    private UserAuthServiceImpl userAuthService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
    
    
        // 返回登录信息 //TODO 优化 直接从authentication中获取用户信息
        UserInfoDTO userInfoDTO = BeanCopyUtils.copyObject(UserUtils.getLoginUser(),UserInfoDTO.class);
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(JSON.toJSONString(Result.ok(userInfoDTO)));
        log.info("登录成功!");
        // 更新用户id和最近登录时间
        updateUserInfo();
    }

    /**
     * 异步更新用户
     * TODO 使用自己创建的线程池
     */
    @Async
    public void updateUserInfo() {
    
    
        UserAuth userAuth = UserAuth.builder()
                .id(UserUtils.getLoginUser().getId())
                .ipAddress(UserUtils.getLoginUser().getIpAddress())
                .ipSource(UserUtils.getLoginUser().getIpSource())
                .lastLoginTime(UserUtils.getLoginUser().getLastLoginTime())
                .build();
        userAuthService.updateById(userAuth);
    }
}

4.AuthenticationFailHandlerImpl

认证失败处理器

/**
 * 认证失败处理器
 * @author chenjiayan
 * @date 2022/12/25
 */
@Component
public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {
    
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException{
    
    
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(JSON.toJSONString(Result.fail(e.getMessage())));
    }
}

5.LogoutSuccessHandlerImpl

退出登录处理器

/**
 * 退出登录处理器
 * @author chenjiayan
 * @date 2022/12/25
 */
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    
    
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(JSON.toJSONString(Result.ok()));
    }
}

五、加载数据库中的权限信息

从数据库加载所有路径及其对应的角色信息,和请求的路径进行比对,比对成功把该路径对应的所有角色返回。如果该路径没有对应任何角色,任意返回一个没有的角色,代表该路径不可访问。若请求路径没有匹配任何一个路径,说明可以匿名访问,直接放行。

/**
 * 接口拦截规则
 * @author chenjiayan
 * @date 2022/12/25
 */
@Component
public class FilterInvocationSecurityMetadataSourceImpl implements FilterInvocationSecurityMetadataSource {
    
    
    /**
     * 资源角色列表
     */
    private static List<ResourceRoleDTO> resourceRoleList;

    @Autowired
    private RoleMapper roleMapper;

    @PostConstruct // 构造函数执行后执行(初始化)
    public void loadDateSource(){
    
    
        resourceRoleList = roleMapper.listResourceRoles();
    }

    /**
     * 清空接口角色信息
     */
    public void clearDataSource(){
    
    
        resourceRoleList = null;
    }


    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    
    
        // 修改接口角色关系后重新加载
        if(CollectionUtils.isEmpty(resourceRoleList)){
    
    
            this.loadDateSource();
        }
        FilterInvocation fi = (FilterInvocation) object;
        // 获取用户请求方式
        String method = fi.getRequest().getMethod();
        // 获取用户请求Url
        String url = fi.getRequest().getRequestURI();
        // 路径匹配
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) {
    
    
            if(antPathMatcher.match(resourceRoleDTO.getUrl(),url)&&resourceRoleDTO.getRequestMethod().equals(method)){
    
    
                List<String> roleList = resourceRoleDTO.getRoleList();
                if(CollectionUtils.isEmpty(roleList)){
    
    
                    return SecurityConfig.createList("disable");
                }
                return SecurityConfig.createList(roleList.toArray(new String[]{
    
    }));
            }
        }
        // 方法返回 null 的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
    
    
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
    
    
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

六、访问决策管理器

将访问路径所需的角色和用户拥有的角色进行比对,用户只要拥有任何一个对应的角色即可放行。

/**
 * 访问决策管理器
 * @author chenjiayan
 * @date 2022/12/25
 */
@Component
public class AccessDecisionManagerImpl implements AccessDecisionManager {
    
    
    /**
     * 决策
     * @param authentication 用户认证信息
     * @param object
     * @param configAttributes 权限信息
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
    
    
        // 获取用户权限列表
        List<String> permissionList = authentication.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        for(ConfigAttribute item :configAttributes){
    
    
            if(permissionList.contains(item.getAttribute())){
    
    
                // 用户权限包含该操作权限
                return ;
            }
        }
        throw new AccessDeniedException("没有操作权限");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
    
    
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
    
    
        return true;
    }
}

七、从数据库中查找用户信息

从数据库中查找该用户的用户名、密码、拥有的角色等信息。

/**
 * @author chenjiayan
 * @date 2022/12/25
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    
    @Autowired
    private UserAuthService userAuthService;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private RedisService redisService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        if(StringUtils.isBlank(username)){
    
    
            throw new BizException("用户名不能为空!");
        }
        // 查询账号是否存在
        UserAuth userAuth = userAuthService.getOne(new LambdaQueryWrapper<UserAuth>()
                .select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
                .eq(UserAuth::getUsername, username));
        if(Objects.isNull(userAuth)){
    
    
            throw new BizException("用户名不存在!");
        }

        // 封装登录信息
        return convertUserDetail(userAuth, request);
    }

    /**
     * 封装用户登录信息
     * @param userAuth 用户账号
     * @param request 请求
     * @return {@link UserDetails} 用户登录信息
     */
    private UserDetails convertUserDetail(UserAuth userAuth, HttpServletRequest request) {
    
    
        // 查询账号信息
        UserInfo userInfo = userInfoService.getById(userAuth.getUserInfoId());
        // 查询账号角色信息
        List<String> roleList = roleMapper.listRolesByUserInfoId(userInfo.getId());
        // 查询账号点赞信息
        // 文章
        Set<Object> articleLikeSet = redisService.sMembers(ARTICLE_USER_LIKE + userInfo.getId());
        // 评论
        Set<Object> commentLikeSet = redisService.sMembers(COMMENT_USER_LIKE + userInfo.getId());
        // 说说
        Set<Object> talkLikeSet = redisService.sMembers(TALK_USER_LIKE + userInfo.getId());
        // 获取设备信息
        String ipAddress = IpUtils.getIpAddress(request);
        String ipSource = IpUtils.getIpSource(ipAddress);
        UserAgent userAgent = IpUtils.getUserAgent(request);
        // 封装权限集合
        return UserDetailDTO.builder()
                .id(userAuth.getId())
                .loginType(userAuth.getLoginType())
                .userInfoId(userInfo.getId())
                .username(userAuth.getUsername())
                .password(userAuth.getPassword())
                .email(userInfo.getEmail())
                .roleList(roleList)
                .nickname(userInfo.getNickname())
                .avatar(userInfo.getAvatar())
                .intro(userInfo.getIntro())
                .webSite(userInfo.getWebSite())
                .articleLikeSet(articleLikeSet)
                .commentLikeSet(commentLikeSet)
                .talkLikeSet(talkLikeSet)
                .ipAddress(ipAddress)
                .ipSource(ipSource)
                .isDisable(userInfo.getIsDisable())
                .os(userAgent.getOperatingSystem().getName())
                .lastLoginTime(LocalDateTime.now(ZoneId.of(SHANGHAI.getZone())))
                .build();
    }
}

八、菜单信息

用户登录后前端请求该用户拥有的菜单信息,后端根据用户的Id进行查询,然后响应给前端。前端根据后端响应的菜单信息显示相应的菜单。完成权限的控制。
因为我还没看到前端,所有这里先不过多介绍,等我看完再进行完善。

补充

security进行认证后会将用户信息存储起来(用ThreadLocal实现),可以在任何地方进行调用,方便使用。

(UserDetailDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

注意:该博客根据 风丶宇个人博客项目 进行编写的,内容中可能出现各种常量和未提及的方法,精力有限请见谅。建议参考源代码进行学习。

猜你喜欢

转载自blog.csdn.net/weixin_52067659/article/details/128459183