亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Spring?Security+JWT簡(jiǎn)述(附源碼)

 更新時(shí)間:2023年04月17日 10:12:03   作者:IT自習(xí)小空間  
SpringSecurity是一個(gè)強(qiáng)大的可高度定制的認(rèn)證和授權(quán)框架,對(duì)于Spring應(yīng)用來(lái)說(shuō)它是一套Web安全標(biāo)準(zhǔn),下面這篇文章主要給大家介紹了關(guān)于Spring?Security+JWT簡(jiǎn)述的相關(guān)資料,需要的朋友可以參考下

一. 什么是Spring Security

Spring Security是Spring家族的一個(gè)安全管理框架, 相比于另一個(gè)安全框架Shiro, 它具有更豐富的功能。一般中大型項(xiàng)目都是使用SpringSecurity做安全框架, 而Shiro上手比較簡(jiǎn)單

spring security 的核心功能:

  • 認(rèn)證(你是誰(shuí)): 只有你的用戶名或密碼正確才能訪問(wèn)某些資源
  • 授權(quán)(你能干嘛): 當(dāng)前用戶具有哪些功能, 將資源進(jìn)行劃分, 如在公司中分為普通資料和高級(jí)資料, 只有經(jīng)理用戶以上才能訪文高級(jí)資料, 其他人只能擁有訪問(wèn)普通資料的權(quán)限。

1. 登陸校驗(yàn)的流程

2. SpringSecurity基礎(chǔ)案例

首先創(chuàng)建一個(gè)Springboot的項(xiàng)目

添加依賴

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

創(chuàng)建一個(gè)controller類

@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

啟動(dòng)項(xiàng)目訪問(wèn)http://localhost:8080/login, 發(fā)現(xiàn)頁(yè)面并沒(méi)有hello字符, 下圖是SpringSeurity默認(rèn)的登陸界面, 默認(rèn)用戶名為user, 密碼為啟動(dòng)項(xiàng)目時(shí)在輸出框中的內(nèi)容

在實(shí)際項(xiàng)目中, 顯然不能使用默認(rèn)的登陸界面, 所以我們需要自定義登陸認(rèn)證和授權(quán)

二. Spring Security原理流程

SpringSecurity底層實(shí)現(xiàn)是一系列過(guò)濾器鏈

默認(rèn)自動(dòng)配置的過(guò)濾器

過(guò)濾器作用
WebAsyncManagerIntegrationFilter將WebAsyncManger與SpringSecurity上下文進(jìn)行集成
SecurityContextPersistenceFilter在處理請(qǐng)求之前, 將安全信息加載到SecurityContextHolder中
HeaderWriterFilter處理頭信息假如響應(yīng)中
CsrfFilter處理CSRF攻擊
LogoutFilter處理注銷登錄
UsernamePasswordAuthenticationFilter處理表單登錄
DefaultLoginPageGeneratingFilter配置默認(rèn)登錄頁(yè)面
DefaultLogoutPageGeneratingFilter配置默認(rèn)注銷頁(yè)面
BasicAuthenticationFilter處理HttpBasic登錄
RequestCacheAwareFilter處理請(qǐng)求緩存
SecurityContextHolderAwareRequestFilter包裝原始請(qǐng)求
AnonymousAuthenticationFilter配置匿名認(rèn)證
SessionManagementFilter處理session并發(fā)問(wèn)題
ExceptionTranslationFilter處理認(rèn)證/授權(quán)中的異常
FilterSecurityInterceptor處理授權(quán)相關(guān)

下圖是主要的過(guò)濾器

上圖只畫出了核心的過(guò)濾器

UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理登陸頁(yè)面填寫的用戶名和密碼的登陸請(qǐng)求

ExceptionTranslationFilter: 處理過(guò)濾器鏈中拋出的任何AccessDeniedException和AuthenticationException異常

FilterSecurityInterceptor: 負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器

1. 大致流程

(1) 下面是UsernamePasswordAuthenticationFilter中的attemptAuthentication方法, 該方法會(huì)將前端發(fā)送的用戶名和密碼封裝為UsernamePasswordAuthenticationToken對(duì)象, 該對(duì)象是Authentication對(duì)象的實(shí)現(xiàn)類

注意: attemptAuthentication方法主要處理視圖表單認(rèn)證, 現(xiàn)今都是前后端分離項(xiàng)目導(dǎo)致不能使用該方法進(jìn)行攔截, 所以我們需要自己實(shí)現(xiàn)一個(gè)過(guò)濾器覆蓋或者在UsernamePasswordAuthenticationFilter之前做用戶名和密碼攔截處理.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

(2) 返回getAuthenticationManager.authenticate(authRequest), 將未認(rèn)證的Authentication對(duì)象傳入AuthenticationManager , 進(jìn)入authenticate方法我們看到AuthenticationManager是一個(gè)接口, 該接口主要做認(rèn)證管理, 它的默認(rèn)實(shí)現(xiàn)類是ProviderManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

(3) 在SpringSecurity中, 在項(xiàng)目中支持多種不同方式的認(rèn)證方式, 不同的認(rèn)證方式對(duì)應(yīng)不同的AuthenticationProvider, 多個(gè)AuthenticationProvider 組成一個(gè)列表, 這個(gè)列表由ProviderManager代理, 在ProviderManager中遍歷列表中的每一個(gè)AuthenticationProvider進(jìn)行認(rèn)證

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();
    // 迭代遍歷認(rèn)證列表
    Iterator var8 = this.getProviders().iterator();

    while(var8.hasNext()) {
    	// 取出當(dāng)前認(rèn)證
        AuthenticationProvider provider = (AuthenticationProvider)var8.next();
        // 當(dāng)前認(rèn)證是否支持當(dāng)前的用戶名和密碼信息
        if (provider.supports(toTest)) {
            if (debug) {
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            }

            try {
            	// 開(kāi)始做認(rèn)證處理
                result = provider.authenticate(authentication);
                if (result != null) {
                	// 認(rèn)證成功時(shí)候返回
                    this.copyDetails(authentication, result);
                    break;
                }
            } catch (InternalAuthenticationServiceException | AccountStatusException var13) {
                this.prepareException(var13, authentication);
                throw var13;
            } catch (AuthenticationException var14) {
                lastException = var14;
            }
        }
    }

	// 不支持當(dāng)前認(rèn)證并且parent支持該認(rèn)證
    if (result == null && this.parent != null) {
        try {
            result = parentResult = this.parent.authenticate(authentication);
        } catch (ProviderNotFoundException var11) {
        } catch (AuthenticationException var12) {
            parentException = var12;
            lastException = var12;
        }
    }

    if (result != null) {
        if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
            ((CredentialsContainer)result).eraseCredentials();
        }

        if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
        }

        return result;
    } else {
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }

        if (parentException == null) {
            this.prepareException((AuthenticationException)lastException, authentication);
        }

        throw lastException;
    }
}

拓展:

ProviderManager可以配置一個(gè)AuthenticationManager作為parent, 當(dāng)ProviderManager認(rèn)證失敗后, 可以進(jìn)入parent中再次進(jìn)行認(rèn)證, 通常由ProviderManager來(lái)充當(dāng)parent的角色, 即ProviderManagerProviderManager的parent
ProviderManager可以有多個(gè), 而多個(gè)ProviderManager共用一個(gè)parent

(4) 當(dāng)前AuthenticationProvider支持認(rèn)證時(shí), 會(huì)進(jìn)入AuthenticationProviderauthenticate方法, 而AuthenticationProvider是一個(gè)接口, 它的實(shí)現(xiàn)類是AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
        return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
    });
    // 獲取當(dāng)前authentication的信息
    String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
    boolean cacheWasUsed = true;
    // 在緩存中查看username
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;

        try {
        	// 調(diào)用retrieveUser方法
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        } catch (UsernameNotFoundException var6) {
            this.logger.debug("User '" + username + "' not found");
            if (this.hideUserNotFoundExceptions) {
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }

            throw var6;
        }

        Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        this.preAuthenticationChecks.check(user);
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    } catch (AuthenticationException var7) {
        if (!cacheWasUsed) {
            throw var7;
        }

        cacheWasUsed = false;
        user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
        this.preAuthenticationChecks.check(user);
        // 密碼的加密處理
        this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
    }

    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

(5) retrieveUserAbstractUserDetailsAuthenticationProvider中有retrieveUser方法, 但是實(shí)現(xiàn)該方法的對(duì)象是DaoAuthenticationProvider, 該對(duì)象重寫了retrieveUser方法, 在retrieveUser方法中, 可以看到調(diào)用了UserDetailsServiceloadUserByUsername()方法, 該方法用來(lái)根據(jù)用戶名查詢內(nèi)存或者其他數(shù)據(jù)源中的用戶. 默認(rèn)是基于內(nèi)存查找, 我們可以自定義為數(shù)據(jù)庫(kù)查詢. 查詢后的結(jié)果封裝成UserDetails 對(duì)象, 該對(duì)象包含用戶名、加密密碼、權(quán)限以及賬戶相關(guān)信息. 密碼的加密處理是SpringSecurity幫我們處理

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    this.prepareTimingAttackProtection();

    try {
    	// 調(diào)用該方法返回一個(gè)UserDetails 對(duì)象
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    } catch (UsernameNotFoundException var4) {
        this.mitigateAgainstTimingAttack(authentication);
        throw var4;
    } catch (InternalAuthenticationServiceException var5) {
        throw var5;
    } catch (Exception var6) {
        throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    }
}

三. JWT

1. 什么是JWT?

JWT主要用于用戶登陸鑒權(quán), 在之前可能會(huì)使用session和token認(rèn)證, 下面簡(jiǎn)述三者session和JWT的區(qū)別

Session

用戶向服務(wù)器發(fā)送一個(gè)請(qǐng)求時(shí), 服務(wù)器并不知道該請(qǐng)求是誰(shuí)發(fā)的, 所以在用戶發(fā)送登錄請(qǐng)求時(shí), 服務(wù)器會(huì)將用戶提交的用戶名和密碼等信息保存在session會(huì)話中(一段內(nèi)存空間)。同時(shí)服務(wù)器保存的用戶信息會(huì)生成一個(gè)sessionid(相當(dāng)于用戶信息是一個(gè)value值, 而sessionid是value值的key)返回給客戶端, 客戶端將sessionid保存到cookie中, 等到下一次請(qǐng)求客戶端會(huì)將cookie一同請(qǐng)求給服務(wù)器做認(rèn)證

如果用戶過(guò)多, 必然會(huì)耗費(fèi)大量?jī)?nèi)存, 在cookie中存放sessionid會(huì)存在暴露用戶信息的風(fēng)險(xiǎn)

Token

token是一串隨機(jī)的字符串也叫令牌, 其原理和session類似, 當(dāng)用戶登錄時(shí), 提交的用戶名和密碼等信息請(qǐng)求給服務(wù)端, 服務(wù)端會(huì)根據(jù)用戶名或者其他信息生成一個(gè)token而不是sessionid, 這和sessionid唯一區(qū)別就是, token不再存儲(chǔ)用戶信息, 客戶端下一次請(qǐng)求會(huì)攜帶token, 此時(shí)服務(wù)器根據(jù)此次token進(jìn)行認(rèn)證。

token認(rèn)證時(shí)也會(huì)到數(shù)據(jù)庫(kù)中查詢, 會(huì)造成數(shù)據(jù)庫(kù)壓力過(guò)大。

JWT

JWT將登錄時(shí)所有信息都存在自己身上, 并且以json格式存儲(chǔ), JWT不依賴Redis或者數(shù)據(jù)庫(kù), JWT安全性不太好, 所以不能存儲(chǔ)敏感信息

2. SpringSecurity集成JWT

(1) 認(rèn)證配置

a) 配置SpringSecurity

首先配置一個(gè)SpringSecurity的配置類, 因?yàn)槭腔贘WT進(jìn)行認(rèn)證, 所以需要在配置中禁用session機(jī)制, 并不是禁用整個(gè)系統(tǒng)的session功能

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceImpl userDetailsService;
    @Autowired
    private LoginFilter loginFilter;
    @Autowired
    private AuthFilter authFilter;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用session機(jī)制 
        http.csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authorizeRequests()
                // 指定某些接口不需要通過(guò)驗(yàn)證即可訪問(wèn)。像登陸、注冊(cè)接口肯定是不需要認(rèn)證的
                .antMatchers("/sec/login").permitAll()
                .anyRequest().authenticated()

                // 自定義權(quán)限配置
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(customUrlDecisionManager);
                        o.setSecurityMetadataSource(customFilter);
                        return o;
                    }
                })
                .and()
                // 禁用緩存
                .headers()
                .cacheControl();

        http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定義未授權(quán)和未登陸結(jié)果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthoricationEntryPoint);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean     
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

b) 實(shí)現(xiàn)登錄接口

先按照正常流程, 實(shí)現(xiàn)一個(gè)登錄的接口然后在業(yè)務(wù)層中實(shí)現(xiàn)

@PostMapping("/login")
public Res login(@RequestBody User user, HttpServletRequest request) {
    return userService.login(user, request);
}

在業(yè)務(wù)層中, 首先對(duì)密碼和用戶名進(jìn)行檢驗(yàn), 然后更新security登錄用戶對(duì)象, 在此之前我們先來(lái)認(rèn)識(shí)幾個(gè)在SpringSecurity中重要的變量

  • Authentication: 存儲(chǔ)了認(rèn)證信息, 代表登錄用戶
  • SecurityContext: 上下文對(duì)象, 用來(lái)獲取Authentication(用戶信息)
  • SecurityContextHolder: 上下文管理對(duì)象, 用來(lái)在程序任何地方獲取SecurityContext
  • UserDetails: 存儲(chǔ)了用戶的基本信息, 以及用戶權(quán)限、是否被禁用等

Authentication中的認(rèn)證信息有

  • Principal: 用戶信息
  • Credentials: 用戶憑證, 一般是密碼
  • Authorities: 用戶權(quán)限
@Override
public Res login(User user, HttpServletRequest request) {
    String username = user.getUsername();
    String password = user.getPassword();

    // 登陸 檢測(cè)
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    if(null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
        return Res.error("用戶名或密碼不正確!");
    }
    // 更新security登錄用戶對(duì)象
    UsernamePasswordAuthenticationToken authenticationToken = new
            UsernamePasswordAuthenticationToken(userDetails,
            null, userDetails.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

    // 創(chuàng)建一個(gè)token
    String token = jwtTokenUtil.generateToken(userDetails);
    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("token", token);
    tokenMap.put("tokenHead", tokenHead);
    return Res.success("登陸成功", tokenMap);
}

下面這行代碼主要是在數(shù)據(jù)庫(kù)或者緩存中查詢用戶提交的用戶名以及用戶的權(quán)限信息, 將這些信息保存在userDetails

UserDetails userDetails = userDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken 實(shí)現(xiàn)了Authentication, 也就是說(shuō)此時(shí)將userDetails中的信息以及權(quán)限信息存放在Authentication

創(chuàng)建Token需要JWT的工具類, 在網(wǎng)上隨便找個(gè)都可以, 大致都一樣, 這個(gè)只需要知道就行了

c) 過(guò)濾請(qǐng)求

在原生SpringSecurity中默認(rèn)的攔截在UsernamePasswordAuthenticationFilter這個(gè)類中,該類主要攔截表單提交的用戶名和密碼, 顯然在前后端分離項(xiàng)目中不適用, 而且我們用到了JWT的驗(yàn)證方式, 前端每次請(qǐng)求都需要帶上token, 所以我們需要在后端對(duì)每個(gè)請(qǐng)求進(jìn)行提前過(guò)濾攔截

public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 請(qǐng)求頭中獲取token信息
        String authheader = request.getHeader(tokenHeader);
        // 存在token
        if(null != authheader && authheader.startsWith(tokenHead)) {
            // 去除字段名稱, 獲取真正token
            String authToken = authheader.substring(tokenHead.length());
            // 利用token獲取用戶名
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            // token存在用戶但未登陸
            // SecurityContextHolder.getContext().getAuthentication() 獲取上下文對(duì)象中認(rèn)證信息
            if(null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
                // 自定義數(shù)據(jù)源獲取用戶信息
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                // 驗(yàn)證token是否有效 驗(yàn)證token用戶名和存儲(chǔ)的用戶名是否一致以及是否在有效期內(nèi), 重新設(shè)置用戶對(duì)象
                if(jwtTokenUtil.validateToken(authToken, userDetails)) {
                    // 重新將用戶信息封裝到UsernamePasswordAuthenticationToken
                    UsernamePasswordAuthenticationToken authenticationToken = new
                            UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 將信息存入上下文對(duì)象
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request,response);
    }
}

該過(guò)濾器主要做的是:

  1. 提取前端發(fā)送的請(qǐng)求頭信息, 根據(jù)JWT的工具類獲取用戶名
  2. 如果請(qǐng)求頭具有有效的字符串(也就是擁有用戶信息)并且上下文對(duì)象存在用戶信息(數(shù)據(jù)庫(kù)或者緩存中查的用戶信息)則直接到下一個(gè)過(guò)濾器, 否則請(qǐng)求頭中有信息而當(dāng)前上下文對(duì)象沒(méi)有存儲(chǔ)用戶信息則將請(qǐng)求頭中的用戶在數(shù)據(jù)層驗(yàn)證之后重新放入上下文對(duì)象中(UsernamePasswordAuthenticationToken)。
  3. 如果當(dāng)前用戶沒(méi)有登錄或者沒(méi)有token信息(可能是token過(guò)期), 而當(dāng)前請(qǐng)求的地址符合權(quán)限中包含的地址(也就是數(shù)據(jù)庫(kù)中存在的), 則會(huì)進(jìn)入權(quán)限驗(yàn)證(下面會(huì)講)

當(dāng)然以上的邏輯可以自己自定義, 不管以上什么情況都會(huì)進(jìn)入權(quán)限驗(yàn)證

要讓這個(gè)過(guò)濾器加入到SpringSecurity的過(guò)濾器鏈中, 就需要在SecurityConfig類的configure方法添加下面一條語(yǔ)句, addFilterBefore()jwtAuthencationTokenFilter(), 放在UsernamePasswordAuthenticationFilter之前

http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

(2) 權(quán)限配置

在一個(gè)項(xiàng)目中, 不同的用戶需要具有不同的權(quán)限, 我們?cè)趺磳?duì)用戶進(jìn)行區(qū)分呢?

a) RBAC權(quán)限表

將用戶、角色和權(quán)限綁定,這樣可以知道某個(gè)用戶具有哪些角色, 而某個(gè)角色對(duì)應(yīng)有哪些權(quán)限(能干什么,不能干什么),這樣就知道哪些用戶擁有的角色和權(quán)限信息。

基于以上的想法, 我們需要三張實(shí)體表, 還需要兩張多對(duì)多的關(guān)系表, 這樣就構(gòu)成了RBAC的五張表

b) 授權(quán)流程

在SpringSecurity中授權(quán)的過(guò)濾器是FilterSecurityInterceptor

默認(rèn)的流程

  • 調(diào)用SecurityMetadataSource獲取當(dāng)前請(qǐng)求的鑒權(quán)規(guī)則
  • 接著調(diào)用AccessDecisionManager 來(lái)校驗(yàn)當(dāng)前用戶的是否擁有當(dāng)前權(quán)限
  • 如果有權(quán)限就放行, 否則拋出異常, 該異常則會(huì)被AccessDeniedHandler處理

c) 自定義SecurityMetadataSource

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if(Collections.isEmpty(collection)) {
            return;
        }
        for (ConfigAttribute configAttribute : collection) {
            for (GrantedAuthority authority : authentication.getAuthorities()) {
            	if("ROLE_ANONYMOUS".equals(authority.getAuthority())) {
                    throw new AccessDeniedException("尚未登錄, 請(qǐng)登錄");
                }
                if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("權(quán)限不足, 請(qǐng)聯(lián)系管理員!");
    }

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

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

FilterInvocationSecurityMetadataSource繼承SecurityMetadataSource

getAttributes方法中, o參數(shù)封裝了request的相關(guān)信息, 可以從中獲取請(qǐng)求的方法和URL等信息

然后menus得到的是當(dāng)前數(shù)據(jù)層中所有的權(quán)限路徑, 接著循環(huán)所有的路徑信息與當(dāng)前請(qǐng)求的方法和URL進(jìn)行驗(yàn)證, 如果在數(shù)據(jù)層中沒(méi)有當(dāng)前請(qǐng)求則返回null, 否則將該權(quán)限的在數(shù)據(jù)層中的信息返回

c) 自定義AccessDecisionManager

如果在SecurityMetadataSource中有權(quán)限信息, 則會(huì)進(jìn)入該方法

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if(Collections.isEmpty(collection)) {
            return;
        }
        for (ConfigAttribute configAttribute : collection) {
            for (GrantedAuthority authority : authentication.getAuthorities()) {
            	if("ROLE_ANONYMOUS".equals(authority.getAuthority())) {
                    throw new AccessDeniedException("尚未登錄, 請(qǐng)登錄");
                }
                if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("權(quán)限不足, 請(qǐng)聯(lián)系管理員!");
    }

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

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

首先來(lái)看幾個(gè)變量

  • ConfigAttribute: 這個(gè)是鑒權(quán)的規(guī)則, 根據(jù)自己項(xiàng)目設(shè)定, 我們這里填入的是當(dāng)前請(qǐng)求和數(shù)據(jù)層中相匹配的權(quán)限信息id
  • GrantedAuthority: 當(dāng)前認(rèn)證用戶所擁有的權(quán)限信息

在上述的decide方法中, 主要驗(yàn)證了用戶所擁有的權(quán)限和當(dāng)前請(qǐng)求的權(quán)限信息是否一致

如果不一致, 則拋出異常, 被AccessDeniedHandler處理

d) 自定義AccessDeniedHandler

自定義返回json格式數(shù)據(jù)

@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        Res bean = Res.error("權(quán)限不足, 請(qǐng)聯(lián)系管理員!");
        bean.setCode(403);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}

e) 在SpringSecurity中的配置

在configure方法中, 進(jìn)行了動(dòng)態(tài)權(quán)限配置

.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
        o.setAccessDecisionManager(customUrlDecisionManager);
        o.setSecurityMetadataSource(customFilter);
        return o;
    }
})

插入: 還有一個(gè)認(rèn)證異常處理

  1. 用戶首次登錄且驗(yàn)證成功, 此時(shí)正常用戶權(quán)限授權(quán)
  2. 請(qǐng)求數(shù)據(jù)時(shí), 非首次登錄, 如果沒(méi)有攜帶token(token過(guò)期), 又或者沒(méi)有登錄訪問(wèn)內(nèi)部路徑時(shí), 說(shuō)明沒(méi)有認(rèn)證權(quán)限不能訪問(wèn), 拋出未登錄異常
  3. 請(qǐng)求數(shù)據(jù)時(shí), 有token信息, 而上下文對(duì)象中沒(méi)有用戶信息, 則會(huì)重新將用戶信息放入上下文對(duì)象中, 接著進(jìn)入權(quán)限驗(yàn)證, 如果用戶擁有該權(quán)限則放行, 如果沒(méi)有該權(quán)限則拋出權(quán)限不足異常

在configure中配置未登錄和未授權(quán)異常處理

http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthoricationEntryPoint);

四. 總結(jié)

其實(shí)以上配置還有很多漏洞, 比如token的過(guò)期時(shí)間, 當(dāng)用戶上一秒還在請(qǐng)求數(shù)據(jù), 下一秒token過(guò)期, 則會(huì)造成用戶需要重新登錄, 顯然不合適

這是項(xiàng)目的地址 Github下載

到此這篇關(guān)于Spring Security+JWT簡(jiǎn)述的文章就介紹到這了,更多相關(guān)Spring Security JWT簡(jiǎn)述內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • MyBatis工廠類封裝與簡(jiǎn)化實(shí)現(xiàn)

    MyBatis工廠類封裝與簡(jiǎn)化實(shí)現(xiàn)

    工廠類的目的是將對(duì)象的創(chuàng)建邏輯封裝在一個(gè)類中,以便客戶端代碼無(wú)需了解具體的實(shí)現(xiàn)細(xì)節(jié),本文主要介紹了MyBatis工廠類封裝與簡(jiǎn)化實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-01-01
  • idea如何自動(dòng)生成serialVersionUID

    idea如何自動(dòng)生成serialVersionUID

    這篇文章主要介紹了idea如何自動(dòng)生成serialVersionUID,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-02-02
  • java中的接口能夠被實(shí)例化嗎

    java中的接口能夠被實(shí)例化嗎

    這篇文章主要介紹了java中的接口能夠被實(shí)例化嗎,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • Java實(shí)現(xiàn)俄羅斯方塊游戲的示例代碼

    Java實(shí)現(xiàn)俄羅斯方塊游戲的示例代碼

    俄羅斯方塊是一個(gè)最初由阿列克謝帕吉特諾夫在蘇聯(lián)設(shè)計(jì)和編程的益智類視頻游戲。本文將利用Java實(shí)現(xiàn)這一經(jīng)典的小游戲,感興趣的可以動(dòng)手試一試
    2022-03-03
  • Java弱引用集合WeakHashMap總結(jié)

    Java弱引用集合WeakHashMap總結(jié)

    這篇文章主要介紹了Java弱引用集合WeakHashMap總結(jié),WeakHashMap利用WeakReference的弱引用特性讓用戶在使用的過(guò)程中不會(huì)因?yàn)闆](méi)有釋放Map中的資源而導(dǎo)致內(nèi)存泄露,WeakHashMap實(shí)現(xiàn)了Map接口,使用方式和其他的Map相同,需要的朋友可以參考下
    2023-09-09
  • mybatis?plus更新字段為null處理方法

    mybatis?plus更新字段為null處理方法

    這篇文章主要為大家介紹了將mybatis?plus更新字段為null的處理方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步
    2022-02-02
  • jackson設(shè)置返回null為空字符串的操作

    jackson設(shè)置返回null為空字符串的操作

    這篇文章主要介紹了jackson設(shè)置返回null為空字符串的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-09-09
  • SpringSecurity 手機(jī)號(hào)登錄功能實(shí)現(xiàn)

    SpringSecurity 手機(jī)號(hào)登錄功能實(shí)現(xiàn)

    這篇文章主要介紹了SpringSecurity 手機(jī)號(hào)登錄功能實(shí)現(xiàn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧
    2023-12-12
  • springboot攔截器Interceptor的使用,你都了解嗎

    springboot攔截器Interceptor的使用,你都了解嗎

    springmvc 中的攔截器可以對(duì)請(qǐng)求進(jìn)行判別,在請(qǐng)求到達(dá)控制器之前,把非法的請(qǐng)求給攔截掉下面來(lái)說(shuō)一說(shuō), 它在springboot中的使用,感興趣的朋友一起看看吧
    2021-07-07
  • 關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性

    關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性

    這篇文章主要介紹了關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性,需要的朋友可以參考下
    2023-07-07

最新評(píng)論