SpringBoot integrates SpringSecurity (advanced)

The "Feng Yu Personal Blog" project uses Spring Security to manage authentication and authorization. The content of SpringSecurity is very large. If you want to fully control it, it is not enough to just read this article. It is recommended to read SpringSecurity by bad programmers at station B. The content is more comprehensive. I would like to call it the strongest SpringSecurity tutorial at station B.
Let's start to introduce how to use it in the project:

1. Database

As the saying goes, it is difficult to finish the work at the beginning, so it is necessary to build the database. Here I will not give the table creation statement, but I will give the relationship diagram of the table, and you can build the table according to your own situation.

table relationship

2. Introducing dependencies

Introduce spring security dependencies

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

Three, Spring Security configuration

First create WebSecurityConfig inherits WebSecurityConfigurerAdapter, which contains most of the configuration of 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()); // 自定义会话存储

    }
}

Four, various processors

1.AuthenticationEntryPointImpl

Unauthenticated processing

/**
 * 用户未登录处理
 * @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

Insufficient Privilege Processor

/**
 * 用户权限不通过处理
 *
 * @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

Authentication success handler

/**
 * 登录成功处理器
 * @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

authentication failure handler

/**
 * 认证失败处理器
 * @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

logout handler

/**
 * 退出登录处理器
 * @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()));
    }
}

5. Load the permission information in the database

Load all paths and their corresponding role information from the database, compare with the requested path, and return all the roles corresponding to the path if the comparison is successful. If the path does not correspond to any role, arbitrarily returns a role without it, which means the path is inaccessible. If the request path does not match any path, it means that the request can be accessed anonymously and allowed directly.

/**
 * 接口拦截规则
 * @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);
    }
}

6. Access Decision Manager

Compare the roles required by the access path with the roles owned by the user, and the user can pass as long as he has any corresponding role.

/**
 * 访问决策管理器
 * @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;
    }
}

7. Find user information from the database

Find the user's username, password, roles and other information from the database.

/**
 * @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();
    }
}

8. Menu information

After the user logs in, the front end requests the menu information owned by the user, and the back end queries according to the user's Id, and then responds to the front end. The front end displays the corresponding menu according to the menu information responded by the back end. Complete authority control.
Because I haven't seen the front end yet, so I won't introduce too much here, and I will improve it after I read it.

Replenish

After the security is authenticated, the user information will be stored (implemented with ThreadLocal), which can be called anywhere for easy use.

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

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

Guess you like

Origin blog.csdn.net/weixin_52067659/article/details/128459183