1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 45319字,需瀏覽 91分鐘

         ·

        2021-09-13 22:02

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

        前言

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

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

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

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

        一、五個handler一個filter兩個User

        5個handler,分別是

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

        1.1 AuthenticationEntryPoint

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

        /**
         * 匿名未登錄的時候訪問,需要登錄的資源的調(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)碼,返回錯誤信息等
             ...
                ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
            }
        }

        1.2 AuthenticationSuccessHandler

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

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

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

        @Slf4j
        @Component
        public class CustomerAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
             //簡單的說就是獲取當(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

        有登陸成功就有登錄失敗

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

        /**
         * 登錄賬號密碼錯誤等情況下,會調(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)碼,返回錯誤信息等
             ....
                ResponseUtil.out(401, ResultUtil.failure(ErrorCodeConstants.LOGIN_UNMATCH_ERROR));
            }

        }

        1.4 LogoutSuccessHandler

        登出注銷的時候調(diào)用,Jwt有個缺點就是無法主動控制失效,可以采用Jwt+session的方式,比如刪除存儲在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)限的資源會調(diào)用。

        /**
         * 沒有權(quán)限,被拒絕訪問時的調(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 一個過濾器OncePerRequestFilter

        這里算是一個小重點。

        上面我們在登錄成功后,返回了一個token,那怎么使用這個token呢?

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

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

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

        這是因為SpringSecurity會自己去做登錄的認(rèn)證和權(quán)限的校驗,靠的就是我們放在SpringSecurity上下文中的SecurityContextHolder.getContext().setAuthentication(authentication);,沒有拿到authentication,放行了,SpringSecurity還是會走到認(rèn)證和校驗,這個時候就會發(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;

        /**
         * 過濾器,在請求過來的時候,解析請求頭中的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 {
                
             //請求頭為 accessToken
             //請求體為 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 實現(xiàn)UserDetails擴充字段

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

        于是我們將自己的數(shù)據(jù)庫的User作為拓展,自己實現(xiàn)這個接口。繼承的是數(shù)據(jù)庫對應(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 實現(xiàn)UserDetailsService

        SpringSecurity在登錄的時候,回去數(shù)據(jù)庫(或其他來源),根據(jù)username獲取正確的user信息,就會根據(jù)這個service類,拿到用戶的信息和權(quán)限。我們自己實現(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 自己實現(xiàn)UserDetailService,用與SpringSecurity獲取用戶信息
         */

        @Service
        @Slf4j
        public class CustomerUserDetailService implements UserDetailsService {

          @Autowired
          private IUserService userService;

          /**
           * 獲取用戶信息,然后交給spring去校驗權(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,注冊到SpringSecurity。同時配置一些放行的url

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

        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)證用戶信息獲取的來源、密碼校驗的規(guī)則
           *
           * @param auth
           * @throws Exception
           */

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

            //如果需要改變認(rèn)證的用戶信息來源,我們可以實現(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,來解析過來的請求是否有token
            http
                    .addFilterBefore(customerJwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


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


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

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

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


        }

        三、其他

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

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

        3.1 不足之處

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

        • 控制token銷毀?

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

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

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

        2、 將返回的jwtToken設(shè)置短一點的過期時間,redis再存這個token,過期時間設(shè)置長一點。如果請求過來token過期,查詢redis,如果redis還存在,返回新的token。(為什么redis的過期時間大于token的?因為redis的過期是可控的,手動可刪除,以redis的為準(zhǔn))

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

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

        3.2 解決

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

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

        為什么這樣?

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

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

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

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

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

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

        不過這個還是有細(xì)節(jié)方面問題,并發(fā)下同時刷新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;

        /**
         * 過濾器,在請求過來的時候,解析請求頭中的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 {
                
             //請求頭為 accessToken
             //請求體為 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未過期,比對時間戳是否一致,是則重新頒發(fā)token
                            if (isSameTimestampToken(username,e.getClaims())){
                                userTokenManager.awardAccessToken(userDetails,true);
                            }
                        }
                    }
                    //避免每次請求都請求數(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解析的時間戳和session保存的一致
                                UsernamePasswordAuthenticationToken authentication =
                                        new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                                SecurityContextHolder.getContext().setAuthentication(authentication);
                            }
                        }
                    }
                }
                chain.doFilter(request, response);
            }

            /**
             * 判斷是否同一個時間戳
             * @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 確定時間戳,保存在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);
          }
        }

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

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

        /**
         * 過濾器,在請求過來的時候,解析請求頭中的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 {
                
             //請求頭為 accessToken
             //請求體為 Bearer token

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

                if (authHeader != null && authHeader.startsWith(SecurityConstants.TOKEN_SPLIT)) {
                    //請求頭有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未過期,比對時間戳是否一致,是則重新頒發(fā)token
                            if (isSameTimestampToken(username,e.getClaims())){
                                userTokenManager.awardAccessToken(userDetails,true);
                                //直接設(shè)置響應(yīng)碼為201,直接返回
                                return;
                            }else{
                                //時間戳不一致.無效token,無法刷新token,響應(yīng)碼401,前端跳轉(zhuǎn)登錄頁
                                ResponseUtil.out(HttpStatus.UNAUTHORIZED.value(),ResultUtil.failure(ErrorCodeConstants.REQUIRED_LOGIN_ERROR));
                                return;
                            }
                        }else{
                            //直接放行,交給后面的handler處理,如果當(dāng)前請求是需要訪問權(quán)限,則會由CustomerRestAccessDeniedHandler處理
                            chain.doFilter(request, response);
                            return;
                        }
                    }

                    //避免每次請求都請求數(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解析的時間戳和session保存的一致
                                UsernamePasswordAuthenticationToken authentication =
                                        new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
                                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

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

            /**
             * 判斷是否同一個時間戳
             * @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);
            }
        }

        END


        順便給大家推薦一個GitHub項目,這個 GitHub 整理了上千本常用技術(shù)PDF,絕大部分核心的技術(shù)書籍都可以在這里找到,

        GitHub地址:https://github.com/javadevbooks/books

        Gitee地址:https://gitee.com/javadevbooks/books

        電子書已經(jīng)更新好了,你們需要的可以自行下載了,記得點一個star,持續(xù)更新中..



        瀏覽 31
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            继攵攵稚嫩亲女乱挺进h动漫 | 成人一二区 | 深爱五月激情五月 | 欧美性按摩 | 美女被 免费网站 | 成人大香交 | 欧美日韩在线视频观看 | 嫩草影院在线观看91麻豆 | 奶大灬舒服灬又爽灬高潮 | 中文字幕在线中文 |