SpringSecurity之整合JWT

山人西來發表於2020-11-27

SpringSecurity之整合JWT

1. 寫在前面的話

  • 首先, 本文依舊是筆者學習SpringSecurity遇到的坑的一些感悟, 因此, 不會去介紹一些基本概念, 如有需求, 請百度!

  • 其次, 本文有些做法可能存在問題, 希望大家不吝指教

  • 最後, 本文也是通過參考網上的博文再通過自己整合完成, 因此有相同的程式碼請諒解!

2. JWT依賴以及工具類的編寫

本文是在前幾篇的SpringSecurity專案上編寫的, 因此, 本文只重點說一下新增的功能

本文使用的JWT是 JJWT, 當然還有其他的選擇

<!--Jwt, 這裡用的是JJWT-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
  • JWT 工具類

    package com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil;
    
    import com.wang.spring_security_framework.common.SecurityConstant;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    //JWT工具類
    @Component
    public class JWTUtil {
        private static final String CLAIM_KEY_CREATED = "created";
        private static final String CLAIM_KEY_USERNAME = "sub";
    
        //生成JWT
        public String JWTCreator(Authentication authResult) {
            //獲取登入使用者的角色
            Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
            StringBuffer stringBuffer = new StringBuffer();
            for (GrantedAuthority authority : authorities) {
                stringBuffer.append(authority.getAuthority()).append(",");
            }
            String username = authResult.getName();
            //自定義屬性
            Map<String, Object> claims = new HashMap<>();
            //自定義屬性, 放入使用者擁有的許可權
            claims.put(SecurityConstant.AUTHORITIES, stringBuffer);
            //自定義屬性, 放入建立時間
            claims.put(CLAIM_KEY_CREATED, new Date());
            //自定義屬性, 放入主題, 即使用者名稱
            claims.put(CLAIM_KEY_USERNAME, username);
    
            return Jwts.builder()
                    //自定義屬性
                    .setClaims(claims)
                    //過期時間
                    .setExpiration(new Date(System.currentTimeMillis() + SecurityConstant.EXPIRATION_TIME))
                    //簽名
                    .signWith(SignatureAlgorithm.HS256, SecurityConstant.JWT_SIGN_KEY)
                    .compact();
        }
    
        //生成Token的Claims, 呼叫下面的方法, 返回一個JWT
        public String generateToken(User userDetails) {
            Map<String, Object> claims = new HashMap<>();
            //獲取使用者名稱, 使用sub作為key和設定subject是一樣的
            claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
            //獲取登入使用者的角色
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            StringBuffer stringBuffer = new StringBuffer();
            for (GrantedAuthority authority : authorities) {
                stringBuffer.append(authority.getAuthority()).append(",");
            }
            claims.put(SecurityConstant.AUTHORITIES, stringBuffer);
            //獲取建立時間
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    
        //根據Claims生成JWT
        public String generateToken(Map<String, Object> claims) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(new Date(System.currentTimeMillis() + SecurityConstant.EXPIRATION_TIME))
                    .signWith(SignatureAlgorithm.HS256, SecurityConstant.JWT_SIGN_KEY)
                    .compact();
        }
    
        //解析JWT, 獲得Claims
        private Claims getClaimsFromToken(String token) {
            Claims claims;
            try {
                claims = Jwts.parser()
                        .setSigningKey(SecurityConstant.JWT_SIGN_KEY)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (ExpiredJwtException e) {
                //如果過期,在異常中呼叫, 返回claims, 否則無法解析過期的token
                claims = e.getClaims();
            } catch (Exception e) {
                claims = null;
            }
            return claims;
        }
    
        //從JWT中獲得使用者名稱
        public String getUsernameFromToken(String token) {
            try {
                return getClaimsFromToken(token).getSubject();
            } catch (ExpiredJwtException e) {
                //如果過期, 需要在此處異常呼叫如下的方法, 否則拿不到使用者名稱
                return e.getClaims().getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
    //        catch (Exception e) {
    //            username = null;
    //        }
        }
    
        //從JWT中獲取建立時間 ==> 在自定義區域內
        public Date getCreatedDateFromToken(String token) {
            Date created;
            try {
                Claims claims = getClaimsFromToken(token);
                created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
            } catch (Exception e) {
                created = null;
            }
            return created;
        }
    
        //從JWT中獲取過期時間
        public Date getExpirationDateFromToken(String token) {
            Date expiration;
            try {
                Claims claims = getClaimsFromToken(token);
                expiration = claims.getExpiration();
            } catch (Exception e) {
                expiration = null;
            }
            return expiration;
        }
    
        //判斷JWT是否過期
        private Boolean isTokenExpired(String token) {
            Date expiration = getExpirationDateFromToken(token);
            //判斷過期時間是否在當前時間之前
            return expiration.before(new Date());
        }
    
        //JWT是否可以被重新整理(過期就可以被重新整理)
        public Boolean canTokenBeRefreshed(String token) {
            return isTokenExpired(token);
        }
    
        //重新整理JWT
        public String refreshToken(String token) {
            String refreshedToken;
            try {
                //獲得Token的Claims, 由於在生成JWT的時候會根據當前時間更新過期時間, 我們只需要手動修改
                //放在自定義屬性中的建立時間就可以了
                Claims claims = getClaimsFromToken(token);
                claims.put(CLAIM_KEY_CREATED, new Date());
                //利用修改後的claims再次生成token, 就不需要我們每次都去查使用者的資訊和許可權了
                refreshedToken = generateToken(claims);
            } catch (Exception e) {
                refreshedToken = null;
            }
            return refreshedToken;
        }
    
        //判斷Token是否合法
        public Boolean validateToken(String token, UserDetails userDetails) {
            User user = (User) userDetails;
            String username = getUsernameFromToken(token);
            return (
                    //如果使用者名稱與token一致且token沒有過期, 則認為合法
                    username.equals(user.getUsername())
                    && !isTokenExpired(token)
                    );
        }
    
    }
    

    這裡需要說明的是, 注意 getClaimsFromToken() 方法, 由於 JWT對於處於過期時間之外的TOKEN不會解析, 而會丟擲異常, 因此我們不能使用統一的異常來返回空指標, 這樣會導致我們無法進行TOKEN的重新整理 (因為無法將過期的token中的使用者名稱與我們長期儲存, 如快取中的使用者名稱進行比對, 從而確定token的重新整理策略生效)

  • 快取倉庫

    package com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil;
    
    import org.springframework.security.core.userdetails.User;
    import org.springframework.stereotype.Component;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 存入user token,可以引用快取系統,存入到快取。
     */
    @Component
    public class UserRepository {
    
        private static final Map<String, User> userMap = new HashMap<>();
    
        public User findByUsername(final String username) {
            return userMap.get(username);
        }
    
        public User insert(User user) {
            userMap.put(user.getUsername(), user);
            return user;
        }
    
        public void remove(String username) {
            userMap.remove(username);
        }
    }
    

    此處偷懶, 寫死在了程式碼裡, 實際上我們可以使用Redis儲存, 設定一個較長的過期時間

3. JWT過濾器

由於我們使用了JWT作為認證和授權, 因此每次請求都會受到一個前端請求的token, 我們這裡把所有的請求都過一遍我們的JWT過濾器

package com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityFilter;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.wang.spring_security_framework.common.SecurityConstant;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil.JWTUtil;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

//JWT校驗過濾器
public class JwtFilter extends OncePerRequestFilter {

    @Autowired
    private JWTUtil jwtUtil;
    @Autowired
    private UserRepository userRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //從header中獲取JWT
        String jwtToken = request.getHeader(SecurityConstant.HEADER);
        if (StrUtil.isNotBlank(jwtToken) && jwtToken.startsWith(SecurityConstant.TOKEN_SPLIT)) {
            jwtToken = jwtToken.substring(SecurityConstant.TOKEN_SPLIT.length());
            //如果去掉頭部的"Bearer "後不為空
            if (StrUtil.isNotBlank(jwtToken)) {
                //獲得當前使用者名稱
                String username = jwtUtil.getUsernameFromToken(jwtToken);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    //從已有的user快取中取了出user資訊
                    User user = userRepository.findByUsername(username);

                    //token相關資訊的map
                    Map<String, String> resultMap = new HashMap<>();
                    //檢查token是否有效
                    if (jwtUtil.validateToken(jwtToken, user)) {
                        //建立一個識別符號, 表示此時Token有效, 不需要更新
                        resultMap.put("needRefresh", "false");
                        request.setAttribute("auth", resultMap);
                        //建立一個UsernamePasswordAuthenticationToken
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        //設定使用者登入狀態 ==> 放到當前的Context中
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    } else if (username.equals(user.getUsername())) {
                        //如果使用者名稱相同但是過期了, 重新整理token (和快取中的比較)
                        if (jwtUtil.canTokenBeRefreshed(jwtToken)) {
                            //TODO 將更新後的token更新到前端
                            String refreshedToken = jwtUtil.refreshToken(jwtToken);
                            resultMap.put("refreshedToken", refreshedToken);
                            //需要更新
                            resultMap.put("needRefresh", "true");
                            //將更新後的Token放到request中, 我們寫一個controller, 從中取出後就可以更新了
                            request.setAttribute("auth", resultMap);
                            //建立一個UsernamePasswordAuthenticationToken
                            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            //設定使用者登入狀態 ==> 放到當前的Context中
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                        }
                    }
                }
            }
        }
//        //建立一個UsernamePasswordAuthenticationToken
//        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
//        //放到當前的Context中
//        SecurityContextHolder.getContext().setAuthentication(token);
        //繼續過濾器鏈的請求
        filterChain.doFilter(request, response);
    }
}

這裡需要注意

SpringSecurity的上下文用於儲存使用者的資訊, 但是會在一個過濾器鏈執行完畢後就銷燬

預設的是使用Session, SpringSecurity會從Session中拿到使用者資訊並放在上下文中, 我們這裡用token, 放在使用者本地, 因此每次校驗之後都要生成一個上下文

在判斷使用者不為空且上下文為空可以保證我們位於一條新的過濾器鏈中(大概是為了防止併發搶佔過濾器鏈)

4. 登入成功結果處理器

登入成功後, 與之前的不同的是, 我們要給前端傳遞一個生成的JWT

package com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityHandler;

import com.alibaba.fastjson.JSON;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil.JWTUtil;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityConfigUtil.UserRepository;
import com.wang.spring_security_framework.service.CaptchaService;
import com.wang.spring_security_framework.service.serviceImpl.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

//登入成功處理
//我們不能在這裡獲得request了, 因為我們已經在前面自定義了認證過濾器, 做完後SpringSecurity會關閉inputStream流
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    CaptchaService captchaService;
    @Autowired
    JWTUtil jwtUtil;
    @Autowired
    UserRepository userRepository;
    @Autowired
    UserDetailServiceImpl userDetailsService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        //我們從自定義的認證過濾器中拿到的authInfo, 接下來做驗證碼校驗和跳轉, 以及JWT的生成
        Map<String, String> authInfo = (Map<String, String>) request.getAttribute("authInfo");
        System.out.println(authInfo);
        System.out.println("success!");
        String token = authInfo.get("token");
        String inputCode = authInfo.get("inputCode");

        //校驗驗證碼
        Boolean verifyResult = captchaService.versifyCaptcha(token, inputCode);
        System.out.println(verifyResult);

        Map<String, String> result = new HashMap<>();
        //驗證碼正確, 則生成JWT
        if (verifyResult) {
            //成功的跳轉頁面
            String VerifySuccessUrl = "/newPage";
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            result.put("code", "200");
            result.put("msg", "認證成功!");
            result.put("url", VerifySuccessUrl);
            //JWT生成
            String jwt = jwtUtil.JWTCreator(authentication);
            result.put("jwt", jwt);
            //使用者資訊放入快取 ==> 從userDetailsService的實現類中根據使用者名稱切除User類
            userRepository.insert((User) userDetailsService.loadUserByUsername(authentication.getName()));
            PrintWriter writer = response.getWriter();
            writer.write(JSON.toJSONString(result));
        } else {
            String VerifyFailedUrl = "/toLoginPage";
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            result.put("code", "201");
            result.put("msg", "驗證碼輸入錯誤!");
            result.put("url", VerifyFailedUrl);
            PrintWriter writer = response.getWriter();
            writer.write(JSON.toJSONString(result));
        }
    }
}

5. SpringSecurity配置類

package com.wang.spring_security_framework.config;

import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityFilter.JwtFilter;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityFilter.MyCustomAuthenticationFilter;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityHandler.LoginFailHandler;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityHandler.LoginSuccessHandler;
import com.wang.spring_security_framework.config.SpringSecurityConfig.SpringSecurityHandler.LogoutHandler;
import com.wang.spring_security_framework.service.UserService;
import com.wang.spring_security_framework.service.serviceImpl.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
import org.springframework.security.web.authentication.logout.LogoutFilter;

//SpringSecurity設定
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;
    @Autowired
    UserDetailServiceImpl userDetailServiceImpl;
    @Autowired
    LoginSuccessHandler loginSuccessHandler;
    @Autowired
    LoginFailHandler loginFailHandler;
    @Autowired
    LogoutHandler logoutHandler;

    //授權
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //指定自定義的登入頁面, 表單提交的url, 以及成功後的處理器
        http.
                formLogin()
                .loginPage("/toLoginPage")
                .and().csrf().disable().cors();

        //退出登入
        http
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(logoutHandler)
                //退出時讓Session無效
                .invalidateHttpSession(true);

        //設定過濾器鏈, 新增自定義過濾器
        http
                .addFilter(myCustomAuthenticationFilter())
                .addFilterBefore(jwtFilter(), LogoutFilter.class);

        //允許iframe
        http
                .headers().frameOptions().sameOrigin();

        //授權
        http
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyAuthority("p2")
                .antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")
                .antMatchers("/r/**").authenticated().anyRequest().permitAll();

        http
                // 基於token,所以不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    }

    //註冊自定義過濾器
    @Bean
    MyCustomAuthenticationFilter myCustomAuthenticationFilter() throws Exception {

        MyCustomAuthenticationFilter filter = new MyCustomAuthenticationFilter();

        //設定過濾器認證管理
        filter.setAuthenticationManager(super.authenticationManagerBean());
        //設定filter的url
        filter.setFilterProcessesUrl("/login");
        //設定登入成功處理器
        filter.setAuthenticationSuccessHandler(loginSuccessHandler);
        //設定登入失敗處理器
        filter.setAuthenticationFailureHandler(loginFailHandler);

        return filter;
    }

    //註冊JWT過濾器
    @Bean
    public JwtFilter jwtFilter() throws Exception {
        return new JwtFilter();
    }

    //密碼使用鹽值加密 BCryptPasswordEncoder
    //BCrypt.hashpw() ==> 加密
    //BCrypt.checkpw() ==> 密碼比較
    //我們在資料庫中儲存的都是加密後的密碼, 只有在網頁上輸入時是明文的
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

在配置類中, 我們主要新增了以下的工作

  • 註冊JWT過濾器

  • 禁用session, 同時由於禁用了session, 不會有csrf攻擊, 我們就大膽的禁用防csrf攻擊吧

  • 註冊JWT過濾器到過濾器鏈中

    • 這裡需要說明的是, 我們將 JWT 過濾器放在了logout過濾器之前, 這是由原始碼的過濾器鏈決定的

      image-20201127164107179

      ​ 可以看到, logout過濾器甚至比Username校驗過濾器還靠前, 因此我們將其註冊在logout過濾器之前

6. 新增Controller

  • 由於我們採用本地儲存token的策略, 因此不能從session獲得使用者的資訊了, 而SpringSecurity整合Thymeleaf的方言是從Session獲得使用者的資訊的, 因此我們要寫一個傳送使用者名稱的Controller

    @RequestMapping("/username")
    public String userName() {
        return JSON.toJSONString(getUserName());
    }
    
  • 此處採用的策略是後臺發請求返回判斷JWT是否需要重新整理 (其實更合理的方法是在前端的所有請求都非同步的走一遍下面的url)

    package com.wang.spring_security_framework.controller;
    
    import com.alibaba.fastjson.JSON;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    
    //用於重新整理
    @RestController
    public class RefreshController {
        //重新整理token ==> 如果JWT過期
        @RequestMapping("/refreshJWT")
        public String refreshJWT(HttpServletRequest request) {
            return JSON.toJSONString(request.getAttribute("auth"));
        }
    }
    

7. 後端總結

至此, 我們完成了後端對於JWT的處理

筆者得到的最重要的一個體會就是, SpringSecurity過濾器鏈如果想附加什麼東西上去, 用addAttribute加到request上就好了, 我們在Controller中取出對應的getAttribute的值

後端的結構如下

image-20201127165128476

image-20201127165138907

8. 前端程式碼

筆者學習過程中, 發現大部分的程式碼都是在postman中測試了介面, 很少有人將前端整合寫出, 因此筆者踩坑也花了不少精力, 來看看吧!

1. 寫在前面的話

首先, 我們使用JWT應該把它放在header裡, 這就有一個問題, 我們如何將每個請求都設定header

其中最棘手的是ajax的回撥函式, 經過一天的嘗試, 筆者發現有以下三種解決方法

  • axios ==> 強烈推薦

  • 寫xhr

  • 使用ajaxhook(github有開源的文件)

筆者採用的是前兩種

JWT 請求頭前要加一個 "Bearer " ==> 這是JWT的規定, 其實不加也可以~~

本文采用的是儲存在本地的 localstorage 中, 本地儲存的策略有很多, 筆者只是一個後端萌新, 前端一竅不通, 就不在此處深究了

2. 採用axios統一處理請求頭

function logout() {
    layui.use('layer', function () {
        //退出登入
        layer.confirm('確定要退出麼?', {icon: 3, title: '提示'}, function (index) {
            //do something
            let url = '/logout';

            //新增request攔截器, 為所有請求新增header
            axios.interceptors.request.use(function (config) {
                //請求頭要這樣新增
                config.headers['accessToken'] = "Bearer " + localStorage.getItem("jwt");
                return config;
            }, function (error) {
               return Promise.reject(error);
            });

            //注意, axios的回撥函式和ajax不一樣, response是一個封裝的結果, 要用response.data.xxx才能得到結果
            axios({
                method: 'post',
                url: url,
                responseType: 'json',
                responseEncoding: 'utf8',
                headers: {
                    "accessToken": ("Bearer " + localStorage.getItem("jwt"))
                }
            })
            .then(function (response) {
                alert("進入success---");
                let code = response.data.code;
                let url = response.data.url;
                let msg = response.data.msg;
                if (code === '203') {
                    alert(msg);
                    //清除jwt
                    localStorage.removeItem("jwt");
                    //清除username
                    localStorage.removeItem("username");
                    //跳轉
                    window.location.href = url;
                } else {
                    alert("未知錯誤!");
                }
            })
            .catch(function (error) {
                if (error.response) {
                    // The request was made and the server responded with a status code
                    // that falls out of the range of 2xx
                    console.log(error.response.data);
                    console.log(error.response.status);
                    console.log(error.response.headers);
                } else if (error.request) {
                    // The request was made but no response was received
                    // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
                    // http.ClientRequest in node.js
                    console.log(error.request);
                } else {
                    // Something happened in setting up the request that triggered an Error
                    console.log('Error', error.message);
                }
                console.log(error.config);
            });
            layer.close(index);
        });
    });
}

注意, axios和ajax不一樣, 引數是封裝好的, 切記!

同時注意請求頭的寫法

這裡的思路是使用攔截器, 但是攔截器只能攔截axios的then和catch回撥函式, 不能攔截全部請求

退出登入記得清除本地不需要的內容

3. 採用xhr統一處理請求頭

$.ajaxSetup({
    type: "post",
    dataType: "json",
    contentType: "application/json;charset=utf-8",
    success: function (data) {
        let code = data.code;
        let url = data.url;
        let msg = data.msg;
        if (code == 204) {
            alert(msg);
            let xhr = new XMLHttpRequest();
            xhr.open("get", url);
            xhr.setRequestHeader("accessToken", "Bearer " + localStorage.getItem("jwt"));
            // window.location.href = url;
            xhr.send(null);
            //回撥函式, 這裡一定要同時判斷兩個狀態, 否則會多次執行(與xhr的原理有關)
            xhr.onreadystatechange = function () {
                //判斷xhr的狀態以及http的狀態, 傳送完畢且200才執行
                if(xhr.readyState == 4 && xhr.status == 200) {
                    data = xhr.responseText;
                    alert(data);
                } else if (xhr.readyState == 4 && xhr.status == 403){
                    //403 ==> 沒有許可權的響應
                    alert("沒有許可權!")
                }
            }

        } else {
            alert("未知錯誤!" + code + url);
        }
    },
    error: function (xhr, textStatus, errorThrown) {
        alert("進入error---");
        alert("狀態碼:" + xhr.status);
        alert("狀態:" + xhr.readyState); //當前狀態,0-未初始化,1-正在載入,2-已經載入,3-資料進行互動,4-完成。
        alert("錯誤資訊:" + xhr.statusText);
        alert("返回響應資訊:" + xhr.responseText);//這裡是詳細的資訊
        alert("請求狀態:" + textStatus);
        alert(errorThrown);
        alert("請求失敗");
    },
    //請求頭中放入JWT
    beforeSend: function (request) {
        request.setRequestHeader("accessToken", "Bearer " + localStorage.getItem("jwt"));
    },
});

function btnClick1() {
    let url = "/toR1";
    $.ajax({
        url: url
    });
}

function btnClick2() {
    let url = "/toR2";
    $.ajax({
        url: url
    });
}

function btnClick3() {
    let url = "/toR3";
    $.ajax({
        url: url
    });
}

//頁面載入時就呼叫, 將使用者名稱存放在localstorage中(注意語法)
$(function () {
    $.ajax({
        type: "post",
        dataType: "json",
        contentType: "application/json;charset=utf-8",
        url: '/r/username',
        //請求頭中放入JWT
        beforeSend: function (request) {
            request.setRequestHeader("accessToken", "Bearer " + localStorage.getItem("jwt"));
        },
        success: function (data) {
            localStorage.username = data;
        }
    });
});

注意

使用 xhr 新增請求頭, 一定要寫全open和send, 而且位置不能錯, 否則無效

這種做法本質上是原生的ajax(不是jQuery封裝後的)

要注意, 設定了響應的格式為 JSON, 後端不要傳錯, 否則會走到error的回撥函式中

ajax無法使用頁面後端跳轉, 因為他是區域性重新整理的, 要ajax從前端跳轉**

我們在頁面一載入就去請求使用者名稱, 並放在本地, 這樣我們就不需要頻繁的去取了

4. 重新整理token

此處在頁面載入完畢後執行兩個操作

  • 渲染username
  • 請求後端介面, 看JWT是否過期, 過期就放一個新的到本地 (每500ms一次)
//頁面載入完畢後執行, 顯示username ==> 從localstorage中取
$(window).load(function () {
    let username = localStorage.getItem("username");
   $("#showUsername").text(username);
   JWTRefreshCheck();
});

//定時詢問JWT是否過期 ==> 每500毫秒傳送請求
function JWTRefreshCheck() {
    setInterval(function () {
            $.ajax({
                type: "post",
                dataType: "json",
                contentType: "application/json;charset=utf-8",
                url: '/refreshJWT',
                //請求頭中放入JWT
                beforeSend: function (request) {
                    request.setRequestHeader("accessToken", "Bearer " + localStorage.getItem("jwt"));
                },
                success: function (data) {
                    if (data.needRefresh == true) {
                        localStorage.setItem("jwt", data.refreshedToken);
                    }
                }
            });
    },
    500);
}

9. 寫在最後的話

終於, 筆者的SpringSecurity學習可以告一段落了, 關於認證, 授權以及驗證碼的生成可以在前面的文章中找到

程式碼位於github, 歡迎參考和交流!

https://github.com/hello-world-cn/My-SpringSecurity-Framework

相關文章