SpringSecurity之整合JWT

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, 歡迎參考和交流!

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