欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringSecurity進行認證與授權(quán)的示例代碼

 更新時間:2024年06月13日 14:52:21   作者:不應該熱愛  
SpringSecurity是Spring家族中的一個安全管理框架,而認證和授權(quán)也是SpringSecurity作為安全框架的核心功能,本文主要介紹了SpringSecurity進行認證與授權(quán)的示例代碼,感興趣的可以了解一下

一、SpringSecurity簡介

Spring Security 是 Spring 家族中的一個安全管理框架。相比與另外一個安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。
一般來說中大型的項目都是使用SpringSecurity 來做安全框架。小項目有Shiro的比較多,因為相比與SpringSecurity,Shiro的上手更加的簡單。
一般Web應用的需要進行認證和授權(quán)。

  • 認證:驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶
  • 授權(quán):經(jīng)過認證后判斷當前用戶是否有權(quán)限進行某個操作

而認證和授權(quán)也是SpringSecurity作為安全框架的核心功能。

1.1 入門Demo

依賴如下:

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

引入依賴后我們在嘗試去訪問之前的接口就會自動跳轉(zhuǎn)到一個SpringSecurity的默認登陸頁面,默認用戶名是user,密碼會輸出在控制臺。

必須登陸之后才能對接口進行訪問。

訪問 localhost:8080/logout 這個鏈接可以 對其進行退出操作。

Ps:

以上過程了解即可,因為我們實際Web項目中,一般采用我們自定義的登錄驗證授權(quán)方案,不會采取SpringSecurity框架提供的默認方案。

二、認證

登錄校驗流程:

為了實現(xiàn)以上這種過程,我們需要先對SpringSecurity默認的流程進行了解,才可以對其進行修改,實現(xiàn)我們自定義的方案。

2.1 SpringSecurity完整流程

SpringSecurity的原理其實就是一個過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器:

圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示:

  • UsernamePasswordAuthenticationFilter:負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。入門案例的認證工作主要有它負責。
  • ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
  • FilterSecurityInterceptor:負責權(quán)限校驗的過濾器。我們可以通過Debug查看當前系統(tǒng)中SpringSecurity過濾器鏈中有哪些過濾器及它們的順序。

如果想查看所有的過濾器,可以通過獲取Spring容器,Debug方式來查看:

2.2 認證流程詳解

箭頭代表該方法屬于這個實現(xiàn)類的。 

概念速查:

  • Authentication接口: 它的實現(xiàn)類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
  • AuthenticationManager接口:定義了認證Authentication的方法
  • UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。
  • UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。

 2.3 自定義認證實現(xiàn)

登錄

①自定義登錄接口

調(diào)用ProviderManager的方法進行認證 如果認證通過生成jwt 把用戶信息存入redis中

②自定義UserDetailsService

在這個實現(xiàn)類中去查詢數(shù)據(jù)庫

校驗

①定義Jwt 認證過濾器

獲取token 解析token獲取其中的userid

從redis中獲取用戶信息

存入SecurityContextHolder

這里為什么要存入 SecurityContextHolder中呢?

我們自定義的JWT過濾器的時候,肯定是需要將這個JWT過濾器放在UsernamePasswordAuthenticationFilter前的,這時我們將從redis獲取的用戶信息存入SecurityContextHolder才行,否則后續(xù)過濾器在進行校驗的時候,可能會因為SecurityContextHolder中沒有對應的值而判斷當前訪問用戶驗證不通過。

2.3.1 數(shù)據(jù)庫校驗用戶

定義Mapper接口

package com.example.springsecurity_demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecurity_demo.domain.User;

public interface UserMapper extends BaseMapper<User> {
    
}

定義User實體類

package com.example.springsecurity_demo.domain;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;



@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
    private static final long serialVersionUID = -40356785423868312L;
    
    /**
    * 主鍵
    */
    @TableId
    private Long id;
    /**
    * 用戶名
    */
    private String userName;
    /**
    * 昵稱
    */
    private String nickName;
    /**
    * 密碼
    */
    private String password;
    /**
    * 賬號狀態(tài)(0正常 1停用)
    */
    private String status;
    /**
    * 郵箱
    */
    private String email;
    /**
    * 手機號
    */
    private String phonenumber;
    /**
    * 用戶性別(0男,1女,2未知)
    */
    private String sex;
    /**
    * 頭像
    */
    private String avatar;
    /**
    * 用戶類型(0管理員,1普通用戶)
    */
    private String userType;
    /**
    * 創(chuàng)建人的用戶id
    */
    private Long createBy;
    /**
    * 創(chuàng)建時間
    */
    private Date createTime;
    /**
    * 更新人
    */
    private Long updateBy;
    /**
    * 更新時間
    */
    private Date updateTime;
    /**
    * 刪除標志(0代表未刪除,1代表已刪除)
    */
    private Integer delFlag;
}

配置Mapper掃描

package com.example.springsecurity_demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@MapperScan("com.example.springsecurity_demo.mapper")
public class SpringSecurityDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args);
        System.out.println(1);
    }

}

核心代碼實現(xiàn)

package com.example.springsecurity_demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springsecurity_demo.domain.LoginUser;
import com.example.springsecurity_demo.domain.User;
import com.example.springsecurity_demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查詢用戶信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        // 如果沒有查詢到用戶就拋出異常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用戶名或者密碼錯誤");
        }
        //TODO 查詢對應的權(quán)限信息

        return new LoginUser(user);
    }
}

因為UserDetailsService方法的返回值是UserDetails(接口):

所以需要定義一個類,實現(xiàn)該接口,把用戶信息封裝在其中。

package com.example.springsecurity_demo.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:如果要測試,需要往用戶表中寫入用戶數(shù)據(jù),并且如果你想讓用戶的密碼是明文存儲,需要在密碼前加{noop}。例如:

這樣登陸的時候就可以用fox作為用戶名,123作為密碼來登陸了。

2.3.2 密碼加密存儲

實際項目中我們不會把密碼明文存儲在數(shù)據(jù)庫中。
默認使用的PasswordEncoder要求數(shù)據(jù)庫中的密碼格式為:{id}password 。它會根據(jù)id去判斷密碼的加密方式。但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該
PasswordEncoder來進行密碼校驗。
我們可以定義一個SpringSecurity的配置類:

低版本配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

以下是高版本的SpringSecurity(SpringBoot 3 用以下配置):

package com.example.springsecurity_demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.3.3 登錄接口實現(xiàn)

接下來我們需要自定義登錄接口,這里我們需要讓SpringSecurity對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。(畢竟登錄接口如果還需要權(quán)限訪問,那么就很奇怪了)

在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。

認證成功的話要生成一個JWT,放入響應中返回,并且為了讓用戶下回請求時需要通過JWT識別出具體的是哪個用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。

 Contorller類如下:

package com.example.springsecurity_demo.controller;

import com.example.springsecurity_demo.domain.ResponseResult;
import com.example.springsecurity_demo.domain.User;
import com.example.springsecurity_demo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
    @Autowired
    private LoginService loginService;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginService.login(user);

    }

}

Ps:

雖然字段聲明的類型是 LoginService,但實際上注入的是 LoginServiceImpl。這是因為 LoginServiceImpl 實現(xiàn)了 LoginService 接口,因此它被視為 LoginService 的一種類型。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //創(chuàng)建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //關(guān)閉csrf
                .csrf().disable()
                //不通過Session獲取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 對于登錄接口 允許匿名訪問
                .antMatchers("/user/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有請求全部需要鑒權(quán)認證
                .anyRequest().authenticated();
//
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

 實現(xiàn)類如下:

import com.sangeng.domain.LoginUser;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.User;
import com.sangeng.service.LoginServcie;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        //AuthenticationManager authenticate進行用戶認證
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果認證沒通過,給出對應的提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登錄失敗");
        }
        //如果認證通過了,使用userid生成一個jwt jwt存入ResponseResult返回

        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userid = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userid);
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用戶信息存入redis  userid作為key
        redisCache.setCacheObject("login:"+userid,loginUser);
        return new ResponseResult(200,"登錄成功",map);
    }

  
}

分析:

這里創(chuàng)建UsernamePasswordAuthenticationToken對象是因為調(diào)用authenticationManager.authenticate方法 需要傳入Authentication,但是Authentication又是一個接口,所以需要傳入其實現(xiàn)類。

2.3.4 認證過濾器

我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進行解析取出其中的
userid。
使用userid去redis中獲取對應的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder。

import com.sangeng.domain.LoginUser;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //獲取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //從redis中獲取用戶信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用戶未登錄");
        }
        //存入SecurityContextHolder
        //TODO 獲取權(quán)限信息封裝到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

分析: 

這里先設置放行最后再使用return是因為,如果該請求無含token,那么就對其進行放行,讓請求進入下一個攔截器,后續(xù)Security框架還有很多攔截器可以對其進行驗證,而使用return是因為后續(xù)在進行參數(shù)返回的時候,不需要再執(zhí)行以下代碼。

簡單來說:圖中代碼紅線以上部分是參數(shù)請求時候所執(zhí)行的部分,紅線以下是返回響應體時候所執(zhí)行的部分。

還有個細節(jié)是:這里必須調(diào)用三個參數(shù)的構(gòu)造方法,而不是兩個,這是因為只有調(diào)用三個構(gòu)造方法的時候,才能保證該主體是認證過的,否則框架檢查時候還是會報錯:

 SecurityConfig如下:

import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //創(chuàng)建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //關(guān)閉csrf
                .csrf().disable()
                //不通過Session獲取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 對于登錄接口 允許匿名訪問
                .antMatchers("/user/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有請求全部需要鑒權(quán)認證
                .anyRequest().authenticated();

        //添加過濾器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置異常處理器
        http.exceptionHandling()
                //配置認證失敗處理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允許跨域
        http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

2.3.5 退出登錄 

我們只需要定義一個登陸接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數(shù)據(jù)即可。

import com.sangeng.domain.LoginUser;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.User;
import com.sangeng.service.LoginServcie;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginServcie {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult logout() {
        //獲取SecurityContextHolder中的用戶id
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        //刪除redis中的值
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"注銷成功");
    }
}

分析:
這里并不需要刪除SecurityContextHolder中的信息,只需要刪除redis中所存儲的即可,因為在進行認證的時候,需要先在SecurityContextHolder中拿到信息后,再從redis中獲取對應信息。

每個請求都對應一個SecurityContextHolder,所以刪除SecurityContextHolder中的信息是無效的,需要刪除redis中所存儲的信息。

三、授權(quán)

3.1 權(quán)限系統(tǒng)作用

例如一個學校圖書館的管理系統(tǒng),如果是普通學生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應該就能看到并使用添加書籍信息,刪除書籍信息等功能。

總結(jié)起來就是不同的用戶可以使用不同的功能。這就是權(quán)限系統(tǒng)要去實現(xiàn)的效果。
我們不能只依賴前端去判斷用戶的權(quán)限來選擇顯示哪些菜單哪些按鈕。因為如果只是這樣,如果有人知道了對應功能的接口地址就可以不通過前端,直接去發(fā)送請求來實現(xiàn)相關(guān)功能操作。

所以我們還需要在后臺進行用戶權(quán)限的判斷,判斷當前用戶是否有相應的權(quán)限,必須具有所需權(quán)限才能進行相應的操作。

3.2 授權(quán)基本流程

在SpringSecurity中,會使用默認的FilterSecurityInterceptor來進行權(quán)限校驗。在
FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的
權(quán)限信息。當前用戶是否擁有訪問當前資源所需的權(quán)限。

所以我們在項目中只需要把當前登錄用戶的權(quán)限信息也存入Authentication。
然后設置我們的資源所需要的權(quán)限即可。

如何將權(quán)限信息存入Authentication呢?

回顧之前的代碼,我們需要在自定義的認證過濾器中,將Authentication存入SecurityContextHolder中,那么Authentication的信息又是從哪里來呢?

:是從redis中獲取到的。

也就是我們當初應該往redis中存入權(quán)限信息,而redis中存儲的是loginUser,

loginUser是當初我們UserDetail的自定義實現(xiàn)類中所查詢到的用戶信息(也就是:從數(shù)據(jù)庫中查詢的)。

而最終這個loginUser對象會返回給這個authenticate對象,那么authenticate這個對象就會存入redis中。

以上就是整個流程,所以我們最終只需要完成兩個步驟即可:

  • 在查詢數(shù)據(jù)庫的時候獲取對應的權(quán)限信息。
  • 在實現(xiàn)認證過濾器的時候,需要獲取當前用戶的權(quán)限信息,并存入到SecurityContextHolder中。

也就是需要完善上述圖片中兩個 TODO 標識的代碼片段。

3.3 授權(quán)實現(xiàn)

3.3.1 限制訪問資源所需權(quán)限

SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權(quán)限。
但是要使用它我們需要先開啟相關(guān)配置,即在關(guān)于SpringSecurity的配置類中添加以下代碼:

@EnableGlobalMethodSecurity(prePostEnabled = true)

設置完之后,就可以使用對應的注解:@PreAuthorize。

@RestController
public class HelloController {
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

分析:

這里的test只供測試,test使用單引號是因為外層已經(jīng)有了雙引號,所以使用單引號來標識這是個字符串,實際上我們通過idea的提示(ctrl+p),也可以知道,這個hasAuthority所需要的參數(shù)也是String類型:

3.3.2 封裝權(quán)限信息

我們前面在寫UserDetailsServiceImpl的時候說過,在查詢出用戶后還要獲取對應的權(quán)限信息,封裝到UserDetails中返回。

我們先直接把權(quán)限信息寫死封裝到UserDetails中進行測試。
我們之前定義了UserDetails的實現(xiàn)類LoginUser,想要讓其能封裝權(quán)限信息就要對其進行修改。

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對象
       authorities = new ArrayList<>();
        for (String permission : permissions) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }
//        authorities = permissions.stream()
//                .map(SimpleGrantedAuthority::new)
//                .collect(Collectors.toList());
        return authorities;
    }


    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

分析:

這里我們重寫了getAuthorities方法, permission這個屬性存儲的是權(quán)限信息。

在getAuthorities方法中,由于返回值是一個Collection類型,所以我們這里選擇List集合。

又因為泛型要求是GrantedAuthority的子類,但是其是一個接口,所以我們通過查找其實現(xiàn)類(ctrl+alt+鼠標左鍵):

選擇了SimpleGrantedAuthority,因為看它名字有個simple,再觀察其構(gòu)造方法,發(fā)現(xiàn)只需要傳入一個字符串即可,于是我們就需要將我們類屬性的permission全部都通過構(gòu)造方法存入SimpleGrantedAuthority屬性中,然后將其一個個遍歷,放入list集合中。

還有一個小細節(jié)是這里加了@JSONField(serialize = false) 注解防止redis存儲loginUser時候序列化出錯(報異常),因為SimpleGrantedAuthority是Spring中提供的類。

雖然我們這里不讓這個List集合序列化,但是并不影響后續(xù)操作,因為在取出來反序列化的時候,我們自定義的permission屬性是可以被正常序列化的,那個時候通過它就可以讓程序正常運行。

3.3.3 從數(shù)據(jù)庫查詢權(quán)限信息

3.3.3.1 RBAC權(quán)限模型

RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權(quán)限模型。

3.3.3.2 代碼實現(xiàn)

 Menu類如下:

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

/**
 * 菜單表(Menu)實體類
 *
 * @author makejava
 * @since 2021-11-24 15:30:08
 */
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
    private static final long serialVersionUID = -54979041104113736L;
    
    @TableId
    private Long id;
    /**
    * 菜單名
    */
    private String menuName;
    /**
    * 路由地址
    */
    private String path;
    /**
    * 組件路徑
    */
    private String component;
    /**
    * 菜單狀態(tài)(0顯示 1隱藏)
    */
    private String visible;
    /**
    * 菜單狀態(tài)(0正常 1停用)
    */
    private String status;
    /**
    * 權(quán)限標識
    */
    private String perms;
    /**
    * 菜單圖標
    */
    private String icon;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    /**
    * 是否刪除(0未刪除 1已刪除)
    */
    private Integer delFlag;
    /**
    * 備注
    */
    private String remark;
}

mapper如下:

我們只需要根據(jù)用戶id去查詢到其所對應的權(quán)限信息即可。
所以我們可以先定義個mapper,其中提供一個方法可以根據(jù)userid查詢權(quán)限信息。

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sangeng.domain.Menu;

import java.util.List;

public interface MenuMapper extends BaseMapper<Menu> {

    List<String> selectPermsByUserId(Long userid);
}

尤其是自定義方法,所以需要創(chuàng)建對應的mapper文件,定義對應的sql語句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.fox.mapper.MenuMapper">


    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
            LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{userid}
            AND r.`status` = 0
            AND m.`status` = 0
    </select>
</mapper>

然后我們可以在UserDetailsServiceImpl中去調(diào)用該mapper的方法查詢權(quán)限信息封裝到LoginUser對象中即可:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sangeng.domain.LoginUser;
import com.sangeng.domain.User;
import com.sangeng.mapper.MenuMapper;
import com.sangeng.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //查詢用戶信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        // 如果沒有查詢到用戶就拋出異常
        if(Objects.isNull(user)){
            throw new RuntimeException("用戶名或者密碼錯誤");
        }
//        List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
        List<String> list = menuMapper.selectPermsByUserId(user.getId());
        //把數(shù)據(jù)封裝成UserDetails返回
        return new LoginUser(user,list);
    }
}

 四、自定義失敗處理

我們還希望在認證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應進行統(tǒng)一的處理。要實現(xiàn)這個功能我們需要知道SpringSecurity的異常處理機制。
在SpringSecurity中,如果我們在認證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權(quán)失敗出現(xiàn)的異常。

如果是認證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用
AuthenticationEntryPoint對象的方法去進行異常處理。
如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進行異常處理。

所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和
AccessDeniedHandler然后配置給SpringSecurity即可。

4.1 創(chuàng)建自定義實現(xiàn)類

也就是說,我們只需要創(chuàng)建一個自定義的實現(xiàn)類然后分別去實現(xiàn)AccessDeniedHandler接口和AccessDeniedHandler接口即可,代碼如下。

認證失敗自定義實現(xiàn)類如下:

import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用戶認證失敗請查詢登錄");
        String json = JSON.toJSONString(result);
        //處理異常
        WebUtils.renderString(response,json);
    }
}

 授權(quán)失敗自定義實現(xiàn)類如下:

import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的權(quán)限不足");
        String json = JSON.toJSONString(result);
        //處理異常
        WebUtils.renderString(response,json);
    }
}

 涉及到的工具類如下:

由于response對象是較為原生的,所以我們需要進行書寫狀態(tài)碼,ContentType等。所以我們需要使用工具類對其進行修改。

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WebUtils
{
    /**
     * 將字符串渲染到客戶端
     * 
     * @param response 渲染對象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}

4.2 將實現(xiàn)類配置給SpringSecurity

 注入對應處理器:

 然后我們可以使用HttpSecurity對象的方法去配置:

配合類代碼如下:

import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //創(chuàng)建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //關(guān)閉csrf
                .csrf().disable()
                //不通過Session獲取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 對于登錄接口 允許匿名訪問
                .antMatchers("/user/login").anonymous()
//                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有請求全部需要鑒權(quán)認證
                .anyRequest().authenticated();

        //添加過濾器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置異常處理器
        http.exceptionHandling()
                //配置認證失敗處理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允許跨域
        http.cors();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

五、跨域問題解決方案 

瀏覽器出于安全的考慮,使用 XMLHttpRequest對象發(fā)起 HTTP請求時必須遵守同源策略,否則就是跨域的HTTP請求,默認情況下是被禁止的。 同源策略要求源相同才能正常進行通信,即協(xié)議、域名、端口號都完全一致。

前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。
所以我們就要處理一下,讓前端能進行跨域請求。

①先對SpringBoot配置,運行跨域請求

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // 設置允許跨域的路徑
        registry.addMapping("/**")
                // 設置允許跨域請求的域名
                .allowedOriginPatterns("*")
                // 是否允許cookie
                .allowCredentials(true)
                // 設置允許的請求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 設置允許的header屬性
                .allowedHeaders("*")
                // 跨域允許時間
                .maxAge(3600);
    }
}

②開啟SpringSecurity的跨域訪問

由于我們的資源都會收到SpringSecurity的保護,所以想要跨域訪問還要讓SpringSecurity運行跨域訪問。

六、其他權(quán)限校驗方法 

我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。
SpringSecurity還為我們提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。

這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學習其他方法你就更容易理解,而不是死記硬背區(qū)別。并且我們也可以選擇定義校驗方法,實現(xiàn)我們自己的校驗邏輯。
hasAuthority方法實際是執(zhí)行到了SecurityExpressionRoot的hasAuthority,大家只要斷點調(diào)試既可知道它內(nèi)部的校驗原理。

它內(nèi)部其實是調(diào)用authentication的getAuthorities方法獲取用戶的權(quán)限列表。然后判斷我們存入的方法參數(shù)數(shù)據(jù)在權(quán)限列表中。

hasAnyAuthority方法可以傳入多個權(quán)限,只有用戶有其中任意一個權(quán)限都可以訪問對應資源。

    @RequestMapping("/hello")
    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    public String hello(){
        return "hello";
    }

hasRole要求有對應的角色才可以訪問,但是它內(nèi)部會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權(quán)限也要有 ROLE_ 這個前綴才可以。

    @RequestMapping("/hello")
    @PreAuthorize("hasRole('system:dept:list')")
    public String hello(){
        return "hello";
    }

hasAnyRole 有任意的角色就可以訪問。它內(nèi)部也會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權(quán)限也要有 ROLE_ 這個前綴才可以。

    @RequestMapping("/hello")
    @PreAuthorize("hasAnyRole('admin','system:dept:list')")
    public String hello(){
        return "hello";
    }

七、自定義權(quán)限校驗方法

我們也可以定義自己的權(quán)限校驗方法,在@PreAuthorize注解中使用我們的方法。

import com.fox.domain.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("ex")
public class FoxExpressionRoot {

    public boolean hasAuthority(String authority){
        //獲取當前用戶的權(quán)限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判斷用戶權(quán)限集合中是否存在authority
        return permissions.contains(authority);
    }
}

在SPEL表達式中使用 @ex相當于獲取容器中bean的名字未ex的對象。然后再調(diào)用這個對象的
hasAuthority方法:

    @RequestMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    public String hello(){
        return "hello";
    }

八、基于配置的權(quán)限控制

我們也可以在配置類中使用使用配置的方式對資源進行權(quán)限控制。

到此這篇關(guān)于SpringSecurity進行認證與授權(quán)的示例代碼的文章就介紹到這了,更多相關(guān)SpringSecurity 認證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • java開發(fā)Dubbo負載均衡與集群容錯示例詳解

    java開發(fā)Dubbo負載均衡與集群容錯示例詳解

    這篇文章主要為大家介紹了java開發(fā)Dubbo負載均衡與集群容錯示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2021-11-11
  • spring?kafka?@KafkaListener詳解與使用過程

    spring?kafka?@KafkaListener詳解與使用過程

    這篇文章主要介紹了spring-kafka?@KafkaListener詳解與使用,本文結(jié)合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-02-02
  • maven多profile 打包下 -P參和-D參數(shù)的實現(xiàn)

    maven多profile 打包下 -P參和-D參數(shù)的實現(xiàn)

    這篇文章主要介紹了maven多profile 打包下 -P參和-D參數(shù)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-11-11
  • @FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決

    @FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決

    這篇文章主要介紹了@FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • java通過DelayQueue實現(xiàn)延時任務

    java通過DelayQueue實現(xiàn)延時任務

    本文主要介紹了java通過DelayQueue實現(xiàn)延時任務,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07
  • Springbootadmin與security沖突問題及解決

    Springbootadmin與security沖突問題及解決

    這篇文章主要介紹了Springbootadmin與security沖突問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • 通過實例了解Java jdk和jre的區(qū)別

    通過實例了解Java jdk和jre的區(qū)別

    這篇文章主要介紹了通過實例了解Java jdk和jre的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-05-05
  • 自定義的Troop<T>泛型類( c++, java和c#)的實現(xiàn)代碼

    自定義的Troop<T>泛型類( c++, java和c#)的實現(xiàn)代碼

    這篇文章主要介紹了自定義的Troop<T>泛型類( c++, java和c#)的實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下
    2017-05-05
  • 解決idea中@Data標簽getset不起作用的問題

    解決idea中@Data標簽getset不起作用的問題

    這篇文章主要介紹了解決idea中@Data標簽getset不起作用的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • C++ 歸并排序(merge sort)案例詳解

    C++ 歸并排序(merge sort)案例詳解

    這篇文章主要介紹了C++ 歸并排序(merge sort)案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-08-08

最新評論