天天看點

SpringBoot整合SpringSecurity(進階篇)一、資料庫二、引入依賴三、SpringSecurity 配置四、各種處理器五、加載資料庫中的權限資訊六、通路決策管理器七、從資料庫中查找使用者資訊八、菜單資訊補充

SpringBoot整合SpringSecurity(進階篇)

  • 一、資料庫
  • 二、引入依賴
  • 三、SpringSecurity 配置
  • 四、各種處理器
    • 1.AuthenticationEntryPointImpl
    • 2.AccessDeniedHandlerImpl
    • 3.AuthenticationSuccessHandlerImpl
    • 4.AuthenticationFailHandlerImpl
    • 5.LogoutSuccessHandlerImpl
  • 五、加載資料庫中的權限資訊
  • 六、通路決策管理器
  • 七、從資料庫中查找使用者資訊
  • 八、菜單資訊
  • 補充

“風丶宇個人部落格”這個項目使用了SpringSecurity去管理認證授權。SpringSecurity的内容是非常多的,要想完全駕馭隻看本篇是不夠用的,建議去看B站程式設計不良人的SpringSecurity,内容介紹較為全面,我願稱為B站最強的SpringSecurity教程。

下面開始介紹如何在項目中使用:

一、資料庫

俗話說完事開頭難,是以要給資料庫建好。這裡我就不給建表語句了,我給出表的關系圖,大家可以更具自己的情況來建表。
SpringBoot整合SpringSecurity(進階篇)一、資料庫二、引入依賴三、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實作),可以在任何地方進行調用,友善使用。

注意:該部落格根據 風丶宇個人部落格項目 進行編寫的,内容中可能出現各種常量和未提及的方法,精力有限請見諒。建議參考源代碼進行學習。