<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot+SpringSecurity前后端分離+Jwt的權(quán)限認(rèn)證(改造記錄)

          共 45369字,需瀏覽 91分鐘

           ·

          2021-09-07 18:12

          來源:blog.csdn.net/zzzgd_666/article/details/96444829

          前言

          一般來說,我們用SpringSecurity默認(rèn)的話是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity還自帶login登錄頁,還讓你配置登出頁,錯(cuò)誤頁。

          但是現(xiàn)在前后端分離才是正道,前后端分離的話,那就需要將返回的頁面換成Json格式交給前端處理了

          SpringSecurity默認(rèn)的是采用Session來判斷請(qǐng)求的用戶是否登錄的,但是不方便分布式的擴(kuò)展,雖然SpringSecurity也支持采用SpringSession來管理分布式下的用戶狀態(tài),不過現(xiàn)在分布式的還是無狀態(tài)的Jwt比較主流。

          所以下面說下怎么讓SpringSecurity變成前后端分離,采用Jwt來做認(rèn)證的

          一、五個(gè)handler一個(gè)filter兩個(gè)User

          5個(gè)handler,分別是

          • 實(shí)現(xiàn)AuthenticationEntryPoint接口,當(dāng)匿名請(qǐng)求需要登錄的接口時(shí),攔截處理
          • 實(shí)現(xiàn)AuthenticationSuccessHandler接口,當(dāng)?shù)卿洺晒?該處理類的方法被調(diào)用
          • 實(shí)現(xiàn)AuthenticationFailureHandler接口,當(dāng)?shù)卿浭『?該處理類的方法被調(diào)用
          • 實(shí)現(xiàn)AccessDeniedHandler接口,當(dāng)?shù)卿浐?訪問接口沒有權(quán)限的時(shí)候,該處理類的方法被調(diào)用
          • 實(shí)現(xiàn)LogoutSuccessHandler接口,注銷的時(shí)候調(diào)用

          1.1 AuthenticationEntryPoint

          匿名未登錄的時(shí)候訪問,遇到需要登錄認(rèn)證的時(shí)候被調(diào)用

          /**
           * 匿名未登錄的時(shí)候訪問,需要登錄的資源的調(diào)用類
           * @author zzzgd
           */

          @Component
          public class CustomerAuthenticationEntryPoint implements AuthenticationEntryPoint {
              @Override
              public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
               //設(shè)置response狀態(tài)碼,返回錯(cuò)誤信息等
               ...
                  ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
              }
          }

          1.2 AuthenticationSuccessHandler

          這里是我們輸入的用戶名和密碼登錄成功后,調(diào)用的方法

          簡(jiǎn)單的說就是獲取用戶信息,使用JWT生成token,然后返回token

          /**
           * 登錄成功處理類,登錄成功后會(huì)調(diào)用里面的方法
           * @author Exrickx
           */

          @Slf4j
          @Component
          public class CustomerAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


              @Override
              public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
               //簡(jiǎn)單的說就是獲取當(dāng)前用戶,拿到用戶名或者userId,創(chuàng)建token,返回
                  log.info("登陸成功...");
                  CustomerUserDetails principal = (CustomerUserDetails) authentication.getPrincipal();
                  //頒發(fā)token
                  Map<String,Object> emptyMap = new HashMap<>(4);
                  emptyMap.put(UserConstants.USER_ID,principal.getId());
                  String token = JwtTokenUtil.generateToken(principal.getUsername(), emptyMap);
                  ResponseUtil.out(ResultUtil.success(token));
              }
          }

          1.3 AuthenticationFailureHandler

          有登陸成功就有登錄失敗

          登錄失敗的時(shí)候調(diào)用這個(gè)方法,可以在其中做登錄錯(cuò)誤限制或者其他操作,我這里直接就是設(shè)置響應(yīng)頭的狀態(tài)碼為401,返回

          /**
           * 登錄賬號(hào)密碼錯(cuò)誤等情況下,會(huì)調(diào)用的處理類
           * @author Exrickx
           */

          @Slf4j
          @Component
          public class CustomerAuthenticationFailHandler implements AuthenticationFailureHandler {


              @Override
              public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
              //設(shè)置response狀態(tài)碼,返回錯(cuò)誤信息等
               ....
                  ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));
              }

          }

          1.4 LogoutSuccessHandler

          登出注銷的時(shí)候調(diào)用,Jwt有個(gè)缺點(diǎn)就是無法主動(dòng)控制失效,可以采用Jwt+session的方式,比如刪除存儲(chǔ)在Redis的token

          這里需要注意,如果將SpringSecurity的session配置為無狀態(tài),或者不保存session,這里authentication為null??!,注意空指針問題。(詳情見下面的配置WebSecurityConfigurerAdapter)

          /**
           * 登出成功的調(diào)用類
           * @author zzzgd
           */

          @Component
          public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {
              @Override
              public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                  ResponseUtil.out(ResultUtil.success("Logout Success!"));
              }
          }

          1.5 AccessDeniedHandler

          登錄后,訪問缺失權(quán)限的資源會(huì)調(diào)用。

          /**
           * 沒有權(quán)限,被拒絕訪問時(shí)的調(diào)用類
           * @author Exrickx
           */

          @Component
          @Slf4j
          public class CustomerRestAccessDeniedHandler implements AccessDeniedHandler {

              @Override
              public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
                  ResponseUtil.out(403, ResultUtil.failure(ErrorCodeConstants.PERMISSION_DENY));
              }

          }

          1.6 一個(gè)過濾器OncePerRequestFilter

          這里算是一個(gè)小重點(diǎn)。

          上面我們?cè)诘卿洺晒?,返回了一個(gè)token,那怎么使用這個(gè)token呢?

          前端發(fā)起請(qǐng)求的時(shí)候?qū)oken放在請(qǐng)求頭中,在過濾器中對(duì)請(qǐng)求頭進(jìn)行解析。

          • 如果有accessToken的請(qǐng)求頭(可以自已定義名字),取出token,解析token,解析成功說明token正確,將解析出來的用戶信息放到SpringSecurity的上下文中
          • 如果有accessToken的請(qǐng)求頭,解析token失?。o效token,或者過期失效),取不到用戶信息,放行
          • 沒有accessToken的請(qǐng)求頭,放行

          這里可能有人會(huì)疑惑,為什么token失效都要放行呢?

          這是因?yàn)镾pringSecurity會(huì)自己去做登錄的認(rèn)證和權(quán)限的校驗(yàn),靠的就是我們放在SpringSecurity上下文中的SecurityContextHolder.getContext().setAuthentication(authentication);,沒有拿到authentication,放行了,SpringSecurity還是會(huì)走到認(rèn)證和校驗(yàn),這個(gè)時(shí)候就會(huì)發(fā)現(xiàn)沒有登錄沒有權(quán)限。

          舊版本, 最新在底部

          package com.zgd.shop.web.config.auth.filter;

          import com.zgd.shop.common.constants.SecurityConstants;
          import com.zgd.shop.common.util.jwt.JwtTokenUtil;
          import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
          import lombok.extern.slf4j.Slf4j;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
          import org.springframework.security.core.context.SecurityContextHolder;
          import org.springframework.security.core.userdetails.UserDetails;
          import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
          import org.springframework.stereotype.Component;
          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;

          /**
           * 過濾器,在請(qǐng)求過來的時(shí)候,解析請(qǐng)求頭中的token,再解析token得到用戶信息,再存到SecurityContextHolder中
           * @author zzzgd
           */

          @Component
          @Slf4j
          public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

              @Autowired
              CustomerUserDetailService customerUserDetailService;

              @Override
              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                  
               //請(qǐng)求頭為 accessToken
               //請(qǐng)求體為 Bearer token

               String authHeader = request.getHeader(SecurityConstants.HEADER);

                  if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {

                      final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());
                      String username = JwtTokenUtil.parseTokenGetUsername(authToken);
                      if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                          UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                          if (userDetails != null) {
                              UsernamePasswordAuthenticationToken authentication =
                                      new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                              authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                              SecurityContextHolder.getContext().setAuthentication(authentication);
                          }
                      }
                  }
                  chain.doFilter(request, response);
              }
          }

          1.7 實(shí)現(xiàn)UserDetails擴(kuò)充字段

          這個(gè)接口表示的用戶信息,SpringSecurity默認(rèn)實(shí)現(xiàn)了一個(gè)User,不過字段寥寥無幾,只有username,password這些,而且后面獲取用戶信息的時(shí)候也是獲取的UserDetail。學(xué)習(xí)資料:Java進(jìn)階視頻資源

          于是我們將自己的數(shù)據(jù)庫的User作為拓展,自己實(shí)現(xiàn)這個(gè)接口。繼承的是數(shù)據(jù)庫對(duì)應(yīng)的User,而不是SpringSecurity的User

          package com.zgd.shop.web.config.auth.user;

          import com.zgd.shop.common.constants.UserConstants;
          import com.zgd.shop.dao.entity.model.User;
          import org.springframework.security.core.GrantedAuthority;
          import org.springframework.security.core.userdetails.UserDetails;

          import java.util.Collection;

          /**
           * CustomerUserDetails
           *
           * @author zgd
           * @date 2019/7/17 15:29
           */

          public class CustomerUserDetails extends User implements UserDetails {

            private Collection<? extends GrantedAuthority> authorities;

            public CustomerUserDetails(User user){
              this.setId(user.getId());
              this.setUsername(user.getUsername());
              this.setPassword(user.getPassword());
              this.setRoles(user.getRoles());
              this.setStatus(user.getStatus());
            }

            public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
              this.authorities = authorities;
            }

            /**
             * 添加用戶擁有的權(quán)限和角色
             * @return
             */

            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
              return this.authorities;
            }

            /**
             * 賬戶是否過期
             * @return
             */

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

            /**
             * 是否禁用
             * @return
             */

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

            /**
             * 密碼是否過期
             * @return
             */

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

            /**
             * 是否啟用
             * @return
             */

            @Override
            public boolean isEnabled() {
              return UserConstants.USER_STATUS_NORMAL.equals(this.getStatus());
            }
          }

          1.8 實(shí)現(xiàn)UserDetailsService

          SpringSecurity在登錄的時(shí)候,回去數(shù)據(jù)庫(或其他來源),根據(jù)username獲取正確的user信息,就會(huì)根據(jù)這個(gè)service類,拿到用戶的信息和權(quán)限。我們自己實(shí)現(xiàn)

          package com.zgd.shop.web.config.auth.user;

          import com.alibaba.fastjson.JSON;
          import com.zgd.shop.dao.entity.model.User;
          import com.zgd.shop.service.IUserService;
          import lombok.extern.slf4j.Slf4j;
          import org.apache.commons.collections.CollectionUtils;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.security.core.authority.SimpleGrantedAuthority;
          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.List;

          /**
           * @author zgd
           * @date 2019/1/16 16:27
           * @description 自己實(shí)現(xiàn)UserDetailService,用與SpringSecurity獲取用戶信息
           */

          @Service
          @Slf4j
          public class CustomerUserDetailService implements UserDetailsService {

            @Autowired
            private IUserService userService;

            /**
             * 獲取用戶信息,然后交給spring去校驗(yàn)權(quán)限
             * @param username
             * @return
             * @throws UsernameNotFoundException
             */

            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
              //獲取用戶信息
              User user = userService.getUserRoleByUserName(username);
              if(user == null){
                throw new UsernameNotFoundException("用戶名不存在");
              }
              CustomerUserDetails customerUserDetails = new CustomerUserDetails(user);

              List<SimpleGrantedAuthority> authorities = new ArrayList<>();
              //用于添加用戶的權(quán)限。只要把用戶權(quán)限添加到authorities 就萬事大吉。
              if (CollectionUtils.isNotEmpty(user.getRoles())){
                user.getRoles().forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_"+r.getRoleName())));
              }
              customerUserDetails.setAuthorities(authorities);
              log.info("authorities:{}", JSON.toJSONString(authorities));
              
              //這里返回的是我們自己定義的UserDetail
              return customerUserDetails;
            }
          }

          二、配置WebSecurityConfigurerAdapter

          我們需要將上面定義的handler和filter,注冊(cè)到SpringSecurity。同時(shí)配置一些放行的url

          這里有一點(diǎn)需要注意:如果配置了下面的SessionCreationPolicy.STATELESS,則SpringSecurity不會(huì)保存session會(huì)話,在/logout登出的時(shí)候會(huì)拿不到用戶實(shí)體對(duì)象。

          http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

          如果登出注銷不依賴SpringSecurity,并且session交給redis的token來管理的話,可以按上面的配置。

          package com.zgd.shop.web.config;

          import com.zgd.shop.web.config.auth.encoder.MyAesPasswordEncoder;
          import com.zgd.shop.web.config.auth.encoder.MyEmptyPasswordEncoder;
          import com.zgd.shop.web.config.auth.handler.*;
          import com.zgd.shop.web.config.auth.filter.CustomerJwtAuthenticationTokenFilter;
          import com.zgd.shop.web.config.auth.user.CustomerUserDetailService;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
          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.builders.WebSecurity;
          import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
          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.authentication.UsernamePasswordAuthenticationFilter;

          /**
           * @Author: zgd
           * @Date: 2019/1/15 17:42
           * @Description:
           */

          @Configuration
          @EnableWebSecurity
          @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)// 控制@Secured權(quán)限注解
          public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

            /**
             * 這里需要交給spring注入,而不是直接new
             */

            @Autowired
            private PasswordEncoder passwordEncoder;
            @Autowired
            private CustomerUserDetailService customerUserDetailService;
            @Autowired
            private CustomerAuthenticationFailHandler customerAuthenticationFailHandler;
            @Autowired
            private CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;
            @Autowired
            private CustomerJwtAuthenticationTokenFilter customerJwtAuthenticationTokenFilter;
            @Autowired
            private CustomerRestAccessDeniedHandler customerRestAccessDeniedHandler;
            @Autowired
            private CustomerLogoutSuccessHandler customerLogoutSuccessHandler;
            @Autowired
            private CustomerAuthenticationEntryPoint customerAuthenticationEntryPoint;


           
            /**
             * 該方法定義認(rèn)證用戶信息獲取的來源、密碼校驗(yàn)的規(guī)則
             *
             * @param auth
             * @throws Exception
             */

            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              //auth.authenticationProvider(myauthenticationProvider)  自定義密碼校驗(yàn)的規(guī)則

              //如果需要改變認(rèn)證的用戶信息來源,我們可以實(shí)現(xiàn)UserDetailsService
              auth.userDetailsService(customerUserDetailService).passwordEncoder(passwordEncoder);
            }


            @Override
            protected void configure(HttpSecurity http) throws Exception {
              /**
               * antMatchers: ant的通配符規(guī)則
               * ? 匹配任何單字符
               * * 匹配0或者任意數(shù)量的字符,不包含"/"
               * ** 匹配0或者更多的目錄,包含"/"
               */

              http
                      .headers()
                      .frameOptions().disable();

              http
                      //登錄后,訪問沒有權(quán)限處理類
                      .exceptionHandling().accessDeniedHandler(customerRestAccessDeniedHandler)
                      //匿名訪問,沒有權(quán)限的處理類
                      .authenticationEntryPoint(customerAuthenticationEntryPoint)
              ;

              //使用jwt的Authentication,來解析過來的請(qǐng)求是否有token
              http
                      .addFilterBefore(customerJwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


              http
                      .authorizeRequests()
                      //這里表示"/any"和"/ignore"不需要權(quán)限校驗(yàn)
                      .antMatchers("/ignore/**""/login""/**/register/**").permitAll()
                      .anyRequest().authenticated()
                      // 這里表示任何請(qǐng)求都需要校驗(yàn)認(rèn)證(上面配置的放行)


                      .and()
                      //配置登錄,檢測(cè)到用戶未登錄時(shí)跳轉(zhuǎn)的url地址,登錄放行
                      .formLogin()
                      //需要跟前端表單的action地址一致
                      .loginProcessingUrl("/login")
                      .successHandler(customerAuthenticationSuccessHandler)
                      .failureHandler(customerAuthenticationFailHandler)
                      .permitAll()

                      //配置取消session管理,又Jwt來獲取用戶狀態(tài),否則即使token無效,也會(huì)有session信息,依舊判斷用戶為登錄狀態(tài)
                      .and()
                      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                      //配置登出,登出放行
                      .and()
                      .logout()
                      .logoutSuccessHandler(customerLogoutSuccessHandler)
                      .permitAll()
                      
                      .and()
                      .csrf().disable()
              ;
            }


          }

          三、其他

          大概到這就差不多了,啟動(dòng),localhost:8080/login,使用postman,采用form-data,post提交,參數(shù)是username和password,調(diào)用,返回token。

          將token放在header中,請(qǐng)求接口。學(xué)習(xí)資料:Java進(jìn)階視頻資源

          3.1 不足之處

          上面是最簡(jiǎn)單的處理,還有很多優(yōu)化的地方。比如

          • 控制token銷毀?

          使用redis+token組合,不僅解析token,還判斷redis是否有這個(gè)token。注銷和主動(dòng)失效token:刪除redis的key

          • 控制token過期時(shí)間?如果用戶在token過期前1秒還在操作,下1秒就需要重新登錄,肯定不好

          1、考慮加入refreshToken,過期時(shí)間比token長(zhǎng),前端在拿到token的同時(shí)獲取過期時(shí)間,在過期前一分鐘用refreshToken調(diào)用refresh接口,重新獲取新的token。

          2、 將返回的jwtToken設(shè)置短一點(diǎn)的過期時(shí)間,redis再存這個(gè)token,過期時(shí)間設(shè)置長(zhǎng)一點(diǎn)。如果請(qǐng)求過來token過期,查詢r(jià)edis,如果redis還存在,返回新的token。(為什么redis的過期時(shí)間大于token的?因?yàn)閞edis的過期是可控的,手動(dòng)可刪除,以redis的為準(zhǔn))

          • 每次請(qǐng)求都會(huì)被OncePerRequestFilter 攔截,每次都會(huì)被UserDetailService中的獲取用戶數(shù)據(jù)請(qǐng)求數(shù)據(jù)庫

          可以考慮做緩存,還是用redis或者直接保存內(nèi)存中

          3.2 解決

          這是針對(duì)上面的2.2說的,也就是redis時(shí)間久一點(diǎn),jwt過期后如果redis沒過期,頒發(fā)新的jwt。

          不過更推薦的是前端判斷過期時(shí)間,在過期之前調(diào)用refresh接口拿到新的jwt。

          為什么這樣?

          如果redis過期時(shí)間是一周,jwt是一個(gè)小時(shí),那么一個(gè)小時(shí)后,拿著這個(gè)過期的jwt去調(diào),就可以想創(chuàng)建多少個(gè)新的jwt就創(chuàng)建,只要沒過redis的過期時(shí)間。當(dāng)然這是在沒對(duì)過期的jwt做限制的情況下,如果要考慮做限制,比如對(duì)redis的value加一個(gè)字段,保存當(dāng)前jwt,刷新后就用新的jwt覆蓋,refresh接口判斷當(dāng)前的過期jwt是不是和redis這個(gè)一樣。

          總之還需要判斷刷新token的時(shí)候,過期jwt是否合法的問題。總不能去年的過期token也拿來刷新吧。

          而在過期前去刷新token的話,至少不會(huì)發(fā)生這種事情

          不過我這里自己寫demo,采用的還是2.2的方式,也就是過期后給個(gè)新的,思路如下:

          • 登錄后頒發(fā)token,token有個(gè)時(shí)間戳,同時(shí)以u(píng)sername拼裝作為key,保存這個(gè)時(shí)間戳到緩存(redis,cache)
          • 請(qǐng)求來了,過濾器解析token,沒過期的話,還需要比較緩存中的時(shí)間戳和token的時(shí)間戳是不是一樣 ,如果時(shí)間戳不一樣,說明該token不能刷新。無視
          • 注銷,清除緩存數(shù)據(jù)

          這樣就可以避免token過期后,我還能拿到這個(gè)token無限制的refresh。

          不過這個(gè)還是有細(xì)節(jié)方面問題,并發(fā)下同時(shí)刷新token這些并沒有考慮,部分代碼如下

          舊版本, 最新在底部

          package com.zgd.shop.web.auth.filter;

          import com.zgd.shop.common.constants.SecurityConstants;
          import com.zgd.shop.common.util.jwt.JwtTokenUtil;
          import com.zgd.shop.web.auth.user.CustomerUserDetailService;
          import com.zgd.shop.web.auth.user.CustomerUserDetails;
          import com.zgd.shop.web.auth.user.UserSessionService;
          import com.zgd.shop.web.auth.user.UserTokenManager;
          import io.jsonwebtoken.Claims;
          import io.jsonwebtoken.ExpiredJwtException;
          import lombok.extern.slf4j.Slf4j;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
          import org.springframework.security.core.context.SecurityContextHolder;
          import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
          import org.springframework.stereotype.Component;
          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;

          /**
           * 過濾器,在請(qǐng)求過來的時(shí)候,解析請(qǐng)求頭中的token,再解析token得到用戶信息,再存到SecurityContextHolder中
           * @author zzzgd
           */

          @Component
          @Slf4j
          public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

              @Autowired
              CustomerUserDetailService customerUserDetailService;
              @Autowired
              UserSessionService userSessionService;
              @Autowired
              UserTokenManager userTokenManager;

              @Override
              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                  
               //請(qǐng)求頭為 accessToken
               //請(qǐng)求體為 Bearer token

               String authHeader = request.getHeader(SecurityConstants.HEADER);

                  if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {

                      final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());

                      String username;
                      Claims claims;
                      try {
                          claims = JwtTokenUtil.parseToken(authToken);
                          username = claims.getSubject();
                      } catch (ExpiredJwtException e) {
                          //token過期
                          claims = e.getClaims();
                          username = claims.getSubject();
                          CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                          if (userDetails != null){
                              //session未過期,比對(duì)時(shí)間戳是否一致,是則重新頒發(fā)token
                              if (isSameTimestampToken(username,e.getClaims())){
                                  userTokenManager.awardAccessToken(userDetails,true);
                              }
                          }
                      }
                      //避免每次請(qǐng)求都請(qǐng)求數(shù)據(jù)庫查詢用戶信息,從緩存中查詢
                      CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                      if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
          //                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                          if (userDetails != null) {
                              if(isSameTimestampToken(username,claims)){
                                  //必須token解析的時(shí)間戳和session保存的一致
                                  UsernamePasswordAuthenticationToken authentication =
                                          new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                                  authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                                  SecurityContextHolder.getContext().setAuthentication(authentication);
                              }
                          }
                      }
                  }
                  chain.doFilter(request, response);
              }

              /**
               * 判斷是否同一個(gè)時(shí)間戳
               * @param username 
               * @param claims
               * @return
               */

              private boolean isSameTimestampToken(String username, Claims claims){
                  Long timestamp = userSessionService.getTokenTimestamp(username);
                  Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);
                  return timestamp.equals(jwtTimestamp);
              }
          }
          package com.zgd.shop.web.auth.user;

          import com.google.common.collect.Maps;
          import com.zgd.shop.common.constants.SecurityConstants;
          import com.zgd.shop.common.constants.UserConstants;
          import com.zgd.shop.common.util.ResponseUtil;
          import com.zgd.shop.common.util.jwt.JwtTokenUtil;
          import com.zgd.shop.core.result.ResultUtil;
          import com.zgd.shop.web.config.auth.UserAuthProperties;
          import org.apache.commons.collections.MapUtils;
          import org.checkerframework.checker.units.qual.A;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.http.HttpStatus;
          import org.springframework.stereotype.Component;

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

          /**
           * UserTokenManager
           * token管理
           *
           * @author zgd
           * @date 2019/7/19 15:25
           */

          @Component
          public class UserTokenManager {

            @Autowired
            private UserAuthProperties userAuthProperties;
            @Autowired
            private UserSessionService userSessionService;

            /**
             * 頒發(fā)token
             * @param principal
             * @author zgd
             * @date 2019/7/19 15:34
             * @return void
             */

            public void awardAccessToken(CustomerUserDetails principal,boolean isRefresh) {
              //頒發(fā)token 確定時(shí)間戳,保存在session中和token中
              long mill = System.currentTimeMillis();
              userSessionService.saveSession(principal);
              userSessionService.saveTokenTimestamp(principal.getUsername(),mill);

              Map<String,Object> param = new HashMap<>(4);
              param.put(UserConstants.USER_ID,principal.getId());
              param.put(SecurityConstants.TIME_STAMP,mill);

              String token = JwtTokenUtil.generateToken(principal.getUsername(), param,userAuthProperties.getJwtExpirationTime());
              HashMap<String, String> map = Maps.newHashMapWithExpectedSize(1);
              map.put(SecurityConstants.HEADER,token);
              int code = isRefresh ? 201 : 200;
              ResponseUtil.outWithHeader(code,ResultUtil.success(),map);
            }
          }

          針對(duì)token解析的過濾器做了優(yōu)化:

          • 如果redis的session沒過期, 但是請(qǐng)求頭的token過期了, 判斷時(shí)間戳一致后, 頒發(fā)新token并返回
          • 如果redis的session沒過期, 但是請(qǐng)求頭的token過期了, 時(shí)間戳不一致, 說明當(dāng)前請(qǐng)求的token無法刷新token, 設(shè)置響應(yīng)碼為401返回
          • 如果請(qǐng)求頭的token過期了, 但是redis的session失效或未找到, 直接放行, 交給后面的權(quán)限校驗(yàn)處理(也就是沒有給上下文SecurityContextHolder設(shè)置登錄信息, 后面如果判斷這個(gè)請(qǐng)求缺少權(quán)限會(huì)自行處理)
          package com.zgd.shop.web.auth.filter;

          import com.zgd.shop.common.constants.SecurityConstants;
          import com.zgd.shop.common.util.ResponseUtil;
          import com.zgd.shop.common.util.jwt.JwtTokenUtil;
          import com.zgd.shop.core.error.ErrorCodeConstants;
          import com.zgd.shop.core.result.ResultUtil;
          import com.zgd.shop.web.auth.user.CustomerUserDetailService;
          import com.zgd.shop.web.auth.user.CustomerUserDetails;
          import com.zgd.shop.web.auth.user.UserSessionService;
          import com.zgd.shop.web.auth.user.UserTokenManager;
          import io.jsonwebtoken.Claims;
          import io.jsonwebtoken.ExpiredJwtException;
          import lombok.extern.slf4j.Slf4j;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.http.HttpStatus;
          import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
          import org.springframework.security.core.context.SecurityContextHolder;
          import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
          import org.springframework.stereotype.Component;
          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;

          /**
           * 過濾器,在請(qǐng)求過來的時(shí)候,解析請(qǐng)求頭中的token,再解析token得到用戶信息,再存到SecurityContextHolder中
           * @author zzzgd
           */

          @Component
          @Slf4j
          public class CustomerJwtAuthenticationTokenFilter extends OncePerRequestFilter {

              @Autowired
              CustomerUserDetailService customerUserDetailService;
              @Autowired
              UserSessionService userSessionService;
              @Autowired
              UserTokenManager userTokenManager;

              @Override
              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                  
               //請(qǐng)求頭為 accessToken
               //請(qǐng)求體為 Bearer token

               String authHeader = request.getHeader(SecurityConstants.HEADER);

                  if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {
                      //請(qǐng)求頭有token
                      final String authToken = authHeader.substring(SecurityConstants.TOKEN_SPLIT.length());

                      String username;
                      Claims claims;
                      try {
                          claims = JwtTokenUtil.parseToken(authToken);
                          username = claims.getSubject();
                      } catch (ExpiredJwtException e) {
                          //token過期
                          claims = e.getClaims();
                          username = claims.getSubject();
                          CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                          if (userDetails != null){
                              //session未過期,比對(duì)時(shí)間戳是否一致,是則重新頒發(fā)token
                              if (isSameTimestampToken(username,e.getClaims())){
                                  userTokenManager.awardAccessToken(userDetails,true);
                                  //直接設(shè)置響應(yīng)碼為201,直接返回
                                  return;
                              }else{
                                  //時(shí)間戳不一致.無效token,無法刷新token,響應(yīng)碼401,前端跳轉(zhuǎn)登錄頁
                                  ResponseUtil.out(HttpStatus.UNAUTHORIZED.value(),ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
                                  return;
                              }
                          }else{
                              //直接放行,交給后面的handler處理,如果當(dāng)前請(qǐng)求是需要訪問權(quán)限,則會(huì)由CustomerRestAccessDeniedHandler處理
                              chain.doFilter(request, response);
                              return;
                          }
                      }

                      //避免每次請(qǐng)求都請(qǐng)求數(shù)據(jù)庫查詢用戶信息,從緩存中查詢
                      CustomerUserDetails userDetails = userSessionService.getSessionByUsername(username);
                      if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
          //                UserDetails userDetails = customerUserDetailService.loadUserByUsername(username);
                          if (userDetails != null) {
                              if(isSameTimestampToken(username,claims)){
                                  //必須token解析的時(shí)間戳和session保存的一致
                                  UsernamePasswordAuthenticationToken authentication =
                                          new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                                  authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                                  SecurityContextHolder.getContext().setAuthentication(authentication);
                              }
                          }
                      }
                  }
                  chain.doFilter(request, response);
              }

              /**
               * 判斷是否同一個(gè)時(shí)間戳
               * @param username
               * @param claims
               * @return
               */

              private boolean isSameTimestampToken(String username, Claims claims){
                  Long timestamp = userSessionService.getTokenTimestamp(username);
                  Long jwtTimestamp = (Long) claims.get(SecurityConstants.TIME_STAMP);
                  return timestamp.equals(jwtTimestamp);
              }
          }

          瀏覽 44
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产视频第一区 | 亚洲区免费| 五月天色婷婷综合 | 亚洲免费视频在线看 | 欧美少妇在线 |