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