基于spring security实现vue2前后端分离的双token刷新机制(完整代码详解,含金量拉满!)

目录

一.前言:

核心功能概要: 

通过加密算法创建一个用户:

二.后端 代码详解:

1.代码整体结构:

 2.所需依赖:

 3.UserDetailServiceImpl拦截用户登陆:

4.所需工具类

4.1ApplicationContextUtils:

 4.2JwtUtils:

4.3ResponseResult

4.4ResponseStatus

4.5RsaUtils:

4.6.SecurityContextUtil

5.SecurityConfig:

6.LoginSuccessHandler登陆成功处理器:

7.RequestAuthenticationFilter:

8. RefreshTokenAspect:

 9.application.yml配置:

三.前端核心代码:

2.main.js

四.模拟登陆访问流程逐步讲解(重点!重点!) :


一.前言:

       网上许多博主,放张图片讲讲双token原理就发出来,没有含金量,还耽误大伙的时间。但是我不一样,我将会把代码的每一步骤都讲明白,并把详细代码放出来,让每一位看到的人都能跟着一步步自己搞,弄明白每一步的执行流程。要是觉得我跟那些博主一样,也是水文章,互相抄袭,请在评论区直接开骂。如果是第一次接触security请看这篇spring security单token前后端分离详细配置

     不过我们还是要走流程,下图是双token执行步骤的图片:

核心功能概要: 

当客户端发送请求给服务器,发现accessToken过期了,如果是单Token就只能重新登陆,双token则需要重新生成token发送给前端,同时还需要正常访问接口。采取的措施是将重新生成的双token保存到redis中,并且通过aop切入接口的方法,让接口正常运行后将redis中的token一起返回给前端。

通过加密算法创建一个用户:

 只有通过特定算法进行加密后的用户才可以进行匹配,再此先教大家如何创建一个加密的用户,代码如下。

package com.dmdd.userservice;

import com.dmdd.common.entity.SysUser;
import com.dmdd.userservice.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCrypt;

import java.nio.charset.StandardCharsets;

@SpringBootTest
class UserServiceApplicationTests {
@Autowired
private UserService userService;

    @Test
    void contextLoads() {
        String gensalt = BCrypt.gensalt();
        String password="123456";
        String hashpw = BCrypt.hashpw(password, gensalt);
        SysUser sysUser = new SysUser();
        sysUser.setUsername("jin");
        sysUser.setPassword(hashpw);
        userService.save(sysUser);

    }

}

二.后端 代码详解:

1.代码整体结构:

 2.所需依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.dmdd</groupId>
        <artifactId>springcloud_demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dmdd</groupId>
    <artifactId>user-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-service</name>
    <description>user-service</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.9.9</version>
    </dependency>


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

可以直接拿去用,另外解释其中部分依赖

1.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

spring security所需要的依赖

2.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

该依赖用于开启spring boot的aop功能

3.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

该依赖用于开启redis

 3.UserDetailServiceImpl拦截用户登陆:

package com.dmdd.userservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dmdd.common.entity.SysUser;
import com.dmdd.userservice.mapper.UserMapper;
import com.dmdd.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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;
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        SysUser user = userService.getOne(new QueryWrapper<SysUser>().lambda().eq(SysUser::getUsername, s));
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");

        }
        //设置权限字符串为null
        String auths = "gg";
        return new User(user.getUsername(), user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(auths));
    }
}

该模块 从数据库查询登陆的用户信息以及该用户所拥有的权限,然后交给security管理

进行一系列的安全验证。详细执行流程如下

1.用户输入用户名和密码点击登陆

2.该方法会拦截到请求查询到数据库的用户和该用户的权限字符串

3.交给security进行验证。

注:该方法调用了工具类的方法,记得导入工具类!

4.所需工具类

4.1ApplicationContextUtils:

package com.dmdd.userservice.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 应用程序上下文工具
 * 程序启动后,会创建ApplicationContext对象
 * ApplicationContextAware能感知到ApplicationContext对象
 * 自动调用setApplicationContext方法
 */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

    //系统的IOC容器
    private static ApplicationContext applicationContext = null;

    //感知到上下文后,自动调用,获得上下文
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextUtils.applicationContext = applicationContext;
    }

    //返回对象
    public static <T> T getBean(Class<T> tClass){
        return applicationContext.getBean(tClass);
    }
}
 

 4.2JwtUtils:

 
package com.dmdd.userservice.util;

import io.jsonwebtoken.*;
import org.joda.time.DateTime;

import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * JWT工具类
 */
public class JwtUtils {

    public static final String JWT_KEY_USERNAME = "username";
    public static final int EXPIRE_MINUTES = 600;

    /**
     * 私钥加密token
     */
    public static String generateToken(String username, PrivateKey privateKey, int expireMinutes) {

        return Jwts.builder()
                .claim(JWT_KEY_USERNAME, username)
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 从token解析用户
     *
     * @param token
     * @param publicKey
     * @return
     * @throws Exception
     */
    public static String getUsernameFromToken(String token, PublicKey publicKey){
        String username = "";
        Claims body = null;
        try {
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
            body = claimsJws.getBody();
        }catch (ExpiredJwtException e){
            body = e.getClaims();
        }
        username = (String) body.get(JWT_KEY_USERNAME);
        return username;
    }
}

4.3ResponseResult

package com.dmdd.userservice.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 响应数据封装对象
 */
@Data
public class ResponseResult<T> {

    /**
     * 访问token
     */
    private String accessToken;

    /**
     * 刷新token
     */
    private String refreshToken;


    /**
     * 状态信息
     */
    private ResponseStatus status;

    /**
     * 数据
     */
    private T data;

    public ResponseResult(ResponseStatus status, T data) {
        this.status = status;
        this.data = data;
    }

    public ResponseResult(ResponseStatus status,T data,String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.data = data;
    }

    /**
     * 返回成功对象
     * @param data
     * @return
     */
    public static <T>  ResponseResult<T> ok(T data){
        return new ResponseResult<>(ResponseStatus.OK, data);
    }

    public static <T>  ResponseResult<T> okTwo(T data,String accessToken,String refreshToken){
        return new ResponseResult<>(ResponseStatus.OK,data,accessToken,refreshToken);
    }
    /**
     * 返回错误对象
     * @param status
     * @return
     */
    public static ResponseResult<String> error(ResponseStatus status){
        return new ResponseResult<>(status,status.getMessage());
    }

    /**
     * 返回错误对象
     * @param status
     * @return
     */
    public static ResponseResult<String> error(ResponseStatus status, String msg){
        return new ResponseResult<>(status,msg);
    }

    /**
     * 向流中输出结果
     * @param resp
     * @param result
     * @throws IOException
     */
    public static void write(HttpServletResponse resp, ResponseResult result) throws IOException {
        //设置返回数据的格式
        resp.setContentType("application/json;charset=UTF-8");
        //jackson是JSON解析包,ObjectMapper用于解析 writeValueAsString 将Java对象转换为JSON字符串
        String msg = new ObjectMapper().writeValueAsString(result);
        //用流发送给前端
        resp.getWriter().print(msg);
        resp.getWriter().close();
    }

    public static void writeTwo(HttpServletResponse resp, ResponseResult result) throws IOException {
        //设置返回数据的格式
        resp.setContentType("application/json;charset=UTF-8");
        //jackson是JSON解析包,ObjectMapper用于解析 writeValueAsString 将Java对象转换为JSON字符串
        String msg = new ObjectMapper().writeValueAsString(result);
        //用流发送给前端
        resp.getWriter().print(msg);
        resp.getWriter().close();
    }

}

4.4ResponseStatus

package com.dmdd.userservice.util;

/**
 * 响应状态枚举
 */
public enum ResponseStatus {
    /**
     * 内置状态
     */
    OK(20000,"操作成功"),
    INTERNAL_ERROR(500000,"系统错误"),
    BUSINESS_ERROR(500001,"业务错误"),
    LOGIN_ERROR(500002,"账号或密码错误"),
    NO_DATA_ERROR(500003,"没有找到数据"),
    PARAM_ERROR(500004,"参数格式错误"),
    AUTH_ERROR(401,"没有权限,需要登录");

    //响应代码
    private Integer code;
    //响应消息
    private String message;

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    ResponseStatus(Integer status, String message) {
        this.code = status;
        this.message = message;
    }
}

4.5RsaUtils:

package com.dmdd.userservice.util;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA工具类
 */
public class RsaUtils {

    public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
    public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/";//秘钥保存位置
    public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa";//公钥路径
    public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa";//私钥路径

    public static PublicKey publicKey; //公钥
    public static PrivateKey privateKey; //私钥

    /**
     * 类加载后,生成公钥和私钥文件
     */
    static {
        try {
            File rsa = new File(RSA_PATH);
            if (!rsa.exists()) {
                rsa.mkdirs();
            }
            File pubKey = new File(RSA_PUB_KEY_PATH);
            File priKey = new File(RSA_PRI_KEY_PATH);
            //判断公钥和私钥如果不存在就创建
            if (!priKey.exists() || !pubKey.exists()) {
                //创建公钥和私钥文件
                RsaUtils.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
            }
            //读取公钥和私钥内容
            publicKey = RsaUtils.getPublicKey(RSA_PUB_KEY_PATH);
            privateKey = RsaUtils.getPrivateKey(RSA_PRI_KEY_PATH);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

4.6.SecurityContextUtil

package com.dmdd.userservice.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SecurityContextUtil {
    public static String getCurrentUsername(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username;
        Object principal = authentication.getPrincipal();
        if (principal instanceof UserDetails) {
            username = ((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }
}

5.SecurityConfig:

package com.dmdd.userservice.config;
import com.dmdd.userservice.filter.RequestAuthenticationFilter;
import com.dmdd.userservice.util.ResponseResult;
import com.dmdd.userservice.util.ResponseStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * SpringSecurity的核心配置
 */
//启动权限控制的注解
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
//启动Security的验证
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //提供密码编码器
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    //配置验证用户的账号和密码
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //数据库用户验证
        auth.userDetailsService(userDetailsService);
    }

    //配置访问控制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //给请求授权
        http.authorizeRequests()
                //给登录相关的请求放行
                .antMatchers("/login","/logout","/").permitAll()
                //访问控制
                //其余的都拦截
                .anyRequest().authenticated()
                .and()
                //配置自定义登录
                .formLogin()
                .successHandler(loginSuccessHandler)//成功处理器
                .failureHandler(((httpServletRequest, httpServletResponse, e) -> { //登录失败处理器
                    ResponseResult.write(httpServletResponse, ResponseResult.error(ResponseStatus.LOGIN_ERROR));
                }))
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(((httpServletRequest, httpServletResponse, e) -> { //未验证处理
                    ResponseResult.write(httpServletResponse,ResponseResult.error(ResponseStatus.AUTH_ERROR));
                }))
                .and()
                .logout() //配置注销
                .logoutSuccessHandler(((httpServletRequest, httpServletResponse, authentication) -> { //注销成功
                    ResponseResult.write(httpServletResponse,ResponseResult.ok(ResponseStatus.OK));
                }))
                .clearAuthentication(true) //清除验证信息
                .and()
                .cors() //配置跨域
                .configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable() //停止csrf
                .sessionManagement() //session管理
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //无状态,不使用session
                .and()
                .addFilter(new RequestAuthenticationFilter(authenticationManager())) //添加自定义验证过滤器
        ;
    }

    /**
     * 跨域配置对象
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        //配置允许访问的服务器域名
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

6.LoginSuccessHandler登陆成功处理器:

package com.dmdd.userservice.config;

import com.dmdd.common.entity.VO.UserTokenVO;
import com.dmdd.userservice.util.JwtUtils;
import com.dmdd.userservice.util.ResponseResult;
import com.dmdd.userservice.util.RsaUtils;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 登录成功处理器11133
 */
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    //登录成功的回调
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //获得用户名
        User user = (User) authentication.getPrincipal();
        //生成token字符串
        String accessToken = JwtUtils.generateToken(user.getUsername(), RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES);
        //生成refresh token字符串
        String refreshToken = JwtUtils.generateToken(user.getUsername(), RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES * 50);
        log.info("生成refresh token:{}",refreshToken);
        log.info("生成accessToken:{}",accessToken);
        //发送token给前端11
        ResponseResult.write(httpServletResponse,ResponseResult.okTwo   (user.getUsername(),accessToken,refreshToken));
    }
}

7.RequestAuthenticationFilter:

package com.dmdd.userservice.filter;

import com.dmdd.userservice.service.UserService;
import com.dmdd.userservice.util.ApplicationContextUtils;
import com.dmdd.userservice.util.JwtUtils;
import com.dmdd.userservice.util.RsaUtils;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * 请求验证过滤器
 */
@Slf4j
public class RequestAuthenticationFilter extends BasicAuthenticationFilter {

    public static final String ACCESS_TOKEN_HEADER = "Authorization";
    public static final String REFRESH_TOKEN_HEADER = "RefreshToken";

    //通过工具类获得service对象
    private UserService userService = ApplicationContextUtils.getBean(UserService.class);

    //使用redis
    private StringRedisTemplate redisTemplate = ApplicationContextUtils.getBean(StringRedisTemplate.class);

    public RequestAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    //请求的过滤
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //从请求头获得token
        System.out.println("发送回来的请求是" + request + "\n" + "**************************************************");
        String accessToken = request.getHeader(ACCESS_TOKEN_HEADER);
        System.out.println("accessToken是" + accessToken);
        if (StringUtils.isEmpty(accessToken)) {
            //从请求参数获得token
            accessToken = request.getParameter(accessToken);
        }
        //如果读取不到,就拦截
        if (StringUtils.isEmpty(accessToken)) {
            log.info("读取不到token,请求{}被拦截", request.getRequestURL());
            chain.doFilter(request, response);
            return;
        }
        try {
            parseToken(accessToken);
//            throw new ExpiredJwtException(new DefaultJwsHeader(new HashMap<>()), Jwts.claims().setSubject(""),"error");
        }
        //token过期
        catch (ExpiredJwtException ejex) {
            //如果access-token过时,则解析refresh-token
            String refreshToken = request.getHeader(REFRESH_TOKEN_HEADER);
            if (StringUtils.isEmpty(refreshToken)) {
                log.info("读取不到refreshToken,请求{}被拦截", request.getRequestURL());
                chain.doFilter(request, response);
                return;
            }
            try {
                String username = parseToken(refreshToken);
                //生成新access-token,refresh-token
                accessToken = JwtUtils.generateToken(username, RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES);
                refreshToken = JwtUtils.generateToken(username, RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES * 5);
                log.info("重新生成access token:{}", accessToken);
                log.info("重新生成refresh token:{}", refreshToken);
                //将token保存到redis中
                redisTemplate.opsForValue().set("access-token:" + username, accessToken);
                redisTemplate.opsForValue().set("refresh-token:" + username, refreshToken);
            } catch (Exception ex) {
                log.error("解析token失败", ex);
            }
        } catch (Exception ex) {
            log.error("解析token失败", ex);
        }
        chain.doFilter(request, response);
    }

    /**
     * 对token进行解析然后通行
     */
    private String parseToken(String token) {
        //对token进行解析
        String username = JwtUtils.getUsernameFromToken(token, RsaUtils.publicKey);
        //将用户的权限查询出来
//            List<String> authList = userService.getAuthoritiesByUsername(username);
        List<String> authList = new ArrayList<String>();
        authList.add("dmdd");
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", authList));
        //创建通行证
        UsernamePasswordAuthenticationToken authToken = new
                UsernamePasswordAuthenticationToken(username, "", authorities);
        //把通行证交给Security
        SecurityContextHolder.getContext().setAuthentication(authToken);
        return username;
    }


}

客户端登陆成功后,发送的请求将会被该类进行过滤,读取请求中的token,判断token是否过期。讲解一下其中重要的操作

1.

public static final String ACCESS_TOKEN_HEADER = "Authorization";
String accessToken = request.getHeader(ACCESS_TOKEN_HEADER);

从请求的请求头中获取"Authorization"保存的token

 2.

accessToken = request.getParameter(ACCESS_TOKEN_HEADER);

从请求体中读取token

3.

throw new ExpiredJwtException(new DefaultJwsHeader(new HashMap<>()), Jwts.claims().setSubject(""),"error");

手动抛出token过期异常,用于测试双token刷新机制。

4.
//生成新access-token,refresh-token
accessToken = JwtUtils.generateToken(username, RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES);
refreshToken = JwtUtils.generateToken(username, RsaUtils.privateKey, JwtUtils.EXPIRE_MINUTES * 50);

 调用工具类生成新的token

5.

//将token保存到redis中
redisTemplate.opsForValue().set("access-token:" + username, accessToken);
redisTemplate.opsForValue().set("refresh-token:" + username, refreshToken);

 将双token保存到redis中

6.

List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", authList));

 将查询到的权限字符串集合转换成security能够接收的类型。

8. RefreshTokenAspect:

package com.dmdd.userservice.aspect;

import com.dmdd.userservice.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;


@Slf4j
@Aspect
@Component
public class RefreshTokenAspect {
    public static final String ACCESS_TOKEN_HEADER = "Authorization";
    public static final String REFRESH_TOKEN_HEADER = "RefreshToken";


    @Autowired
    private StringRedisTemplate redisTemplate;

    //切所有返回值类型为ResponseResult的控制器方法
    @Around("execution(com.dmdd.userservice.util.ResponseResult com.dmdd.userservice.controller.*Controller.*(..))")
    public Object addToken(ProceedingJoinPoint joinPoint) {
        log.info("当前执行的方法:" + joinPoint.getSignature().getName());
        try {
            //执行原有方法
            ResponseResult result = (ResponseResult) joinPoint.proceed();
//                    MyResultEntity responseEntity= (MyResultEntity) result;
            //从redis读取token
            ValueOperations<String, String> ops = redisTemplate.opsForValue();
            //从security读用户名
            String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            //判断redis中是否有token 只有第一次过期后才会创建token到redis中 第一次过期前因为没有存入token到redis中会死循环读取不到
            if (redisTemplate.hasKey("access-token:" + username) && redisTemplate.hasKey("refresh-token:" + username)) {
                System.out.println("redis 中的权限数据是:*******************" + "\n" + ops.get("access-token:" + username));
                result.setAccessToken(ops.get("access-token:" + username));
                result.setRefreshToken(ops.get("refresh-token:" + username));
                log.info("添加token:" + result);
                return result;
            }
            //从请求头读取token
            else {
                RequestAttributes ra = RequestContextHolder.getRequestAttributes();
                ServletRequestAttributes sra = (ServletRequestAttributes) ra;
                HttpServletRequest request = sra.getRequest();
                String accessToken = request.getHeader(ACCESS_TOKEN_HEADER);
                String refreshToken = request.getHeader(REFRESH_TOKEN_HEADER);
                //从请求体中获取token
                if (accessToken.isEmpty() && refreshToken.isEmpty()) {
                    accessToken = request.getParameter(ACCESS_TOKEN_HEADER);
                    refreshToken = request.getParameter(REFRESH_TOKEN_HEADER);
                }
                result.setAccessToken(accessToken);
                result.setRefreshToken(refreshToken);
                return result;
            }
        } catch (Throwable throwable) {
            log.error("出现异常{}", throwable);
        }

        return null;
    }
}

1.
//切所有返回值类型为ResponseResult的控制器方法
@Around("execution(com.dmdd.userservice.util.ResponseResult com.dmdd.userservice.controller.*Controller.*(..))")

切入所有返回值为ResponseResult的Controller层方法

2.

//判断redis中是否有token 只有第一次过期后才会创建token到redis中 第一次过期前因为没有存入token到redis中会死循环读取不到
if (redisTemplate.hasKey("access-token:" + username) && redisTemplate.hasKey("refresh-token:" + username)) {
    System.out.println("redis 中的权限数据是:*******************" + "\n" + ops.get("access-token:" + username));
    result.setAccessToken(ops.get("access-token:" + username));
    result.setRefreshToken(ops.get("refresh-token:" + username));
    log.info("添加token:" + result);
    return result;
}

 判断redis中是否有token,有的话就将redis中的token设置到返回结果中。

 9.application.yml配置:

server:
  port: 8066

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/sys_order?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true
    username: root
    password: jly720609
  application:
    name: product-service
  redis:
    host: 192.168.56.188
    port: 6379
mybatis-plus:
  type-aliases-package: com.blb.product_service.entity
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
    cache-enabled: true
    use-deprecated-executor: false

#redis缓存配置


#spring.redis.database=0
#spring.redis.jedis.pool.max-active=100
#spring.redis.jedis.pool.max-wait=100ms
#spring.redis.jedis.pool.max-idle=100
#spring.redis.jedis.pool.min-idle=10

eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    serviceUrl:
      defaultZone: http://127.0.0.1:8000/eureka,http://127.0.0.1:8011/eureka,http://127.0.0.1:8012/eureka

三.前端核心代码:

1.自定义登陆页面:

<template>
  <!-- Login -->
  <div id="login">
    <div id="login-form">
      <h1>登陆界面</h1>
      <label for="name"><i class="el-icon-user-solid" style="color: #c1c1c1"></i></label>
      <input type="text" name="username" placeholder="用户名" id="name" autocapitalize="off"aria-autocomplete="off"
             v-model="user.username">
      <p style="visibility: hidden">用户名为必填选项</p>
      <label for="password"><i class="el-icon-right" style="color: #c1c1c1"></i></label>
      <input type="password" name="password" placeholder="密码" id="password" autocapitalize="off"
             v-model="user.password">
      <p style="visibility: hidden">密码为必填选项</p>
      <div>
        <el-button type="primary" v-on:click="login">登录</el-button>
        <el-button type="info" v-on:click="resetInfo">重置</el-button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "LoginView",
  data() {
    return {
      //登陆用户
      user: {"username": "", "password": ""},
      msg: {}//登陆错误
    }
  },
  methods: {
    resetInfo(){},
    login() {
      console.log(this.user);
      //使用axios向后台发送一个post请求
      // this.axios.post("http://192.168.56.188:8088/login",
      this.axios.post("http://localhost:8066/login",
          this.qs.stringify(this.user))
          .then(result => {
            console.log("登陆返回结果*******************:\n" +result);
              if (result.data.status == null) {
              //把两个token保存到本地
              localStorage.setItem("access-token", result.data.accessToken);
              localStorage.setItem("refresh-token", result.data.refreshToken);
              //跳转到权限管理
              // location.href = "permission.html";
              this.$router.push({path: '/order', query: {username: result.data.data.username}});
            }
          })
          .catch(err => {
            console.log("出现错误:" + JSON.stringify(err));
          });
    },
    //测试访问后台接口
    testGet() {
      //访问后台接口时,需要在请求头中携带token
      this.axios.get("http://192.168.56.188:8088/hello1", {"headers": {"Authorization": localStorage.getItem("token")}})
          .then(result => {
            console.log("测试返回结果*******************:\n" + JSON.stringify(result));
          });
    }
  }

}
</script>
<style lang="less" scoped>
#login {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  position: relative;
  //背景图片样式
  background: url("E:\steam\steamapps\workshop\content\431960\1948961570\preview.jpg");
  //background-repeat: no-repeat;
  background-attachment: fixed;
  background-position: center;
  background-size: cover;
}

#login-form {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 50vw;
  min-width: 300px;
  max-width: 400px;
  display: flex;
  flex-direction: column;
  background-color: rgba(0, 0, 0, 0.7);
  border-radius: 15px;
  // 表单 box-shadow 样式 好看
  box-shadow: 0 15px 25px rgba(0, 0, 0, .5);

  h1 {
    width: 60%;
    margin: 50px auto 0;
    color: #c1c1c1;
    text-align: center;
  }

  input {
    width: 60%;
    margin: 0 auto;
    // 注意 border outline 默认值
    outline: none;
    border: none;
    padding: 10px;
    border-bottom: 1px solid #fff;
    background: transparent;
    color: white;
  }

  label {
    width: 60%;
    margin: 0 auto;
    position: relative;
    top: 30px;
    left: 120px;
  }

  div {
    width: 60%;
    margin: 10px auto;
    display: flex;
    justify-content: center;
    align-content: center;
  }

  button {
    // rgba
    background-color: rgba(9, 108, 144, 0.5);
    margin: 10px 25px 80px 25px;
  }

  p {
    width: 60%;
    margin: 8px auto;
    position: relative;
    left: -15px;
    color: #ff0000;
    font-size: 8px;
  }
}
// 浏览器兼容 , 针对谷歌浏览器 默认设置的 奇怪样式
input {
  -webkit-text-fill-color: #ffffff !important;
  transition: background-color 5000s ease-in-out ,width 1s ease-out!important;
}

</style>
login() {
  console.log(this.user);
  //使用axios向后台发送一个post请求
  // this.axios.post("http://192.168.56.188:8088/login",
  this.axios.post("http://localhost:8066/login",
      this.qs.stringify(this.user))
      .then(result => {
        console.log("登陆返回结果*******************:\n" + result);
        if (result.data.status == null) {
          //把两个token保存到本地
          localStorage.setItem("access-token", result.data.accessToken);
          localStorage.setItem("refresh-token", result.data.refreshToken);
          //跳转到权限管理
          // location.href = "permission.html";
          this.$router.push({path: '/order', query: {username: result.data.data.username}});
        }
      })
      .catch(err => {
        console.log("出现错误:" + JSON.stringify(err));
      });
},

访问服务器的/login接口,该接口为security的接口,用于进行登陆验证

2.main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
/*引入element-ui*/
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
/*引入axios*/
import axios from "axios";
/*引入qs*/
import qs from "qs";

//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
    config => {
      let accessToken = localStorage.getItem("access-token");
      let refreshToken= localStorage.getItem("refresh-token");
      console.log("在发送请求前进行拦截  main.js里的 access-token:\n" + accessToken);
      console.log("在发送请求前进行拦截  main.js里的 refresh-token:\n"+refreshToken);
      if (accessToken && refreshToken) {
        //把localStorage的token放在Authorization里
        config.headers.Authorization = accessToken;
        config.headers.RefreshToken = refreshToken;
      }
      return config;
    },
    function(err) {
      console.log("失败信息" + err);
    }
);

//错误响应拦截
axios.interceptors.response.use(res => {
  console.log('拦截响应');
  console.log(res);
  if (res.status ===200) {
      //判断响应头是否有权限
      //如果响应内容有token,就保存
      if(res.data.accessToken && res.data.refreshToken){
          console.log("拦截到的响应accessToken:" ,res.data.accessToken )
          console.log("拦截到的响应refreshToken:" ,res.data.refreshToken )
          localStorage.setItem("access-token",res.data.accessToken)
          localStorage.setItem("refresh-token",res.data.refreshToken)
      }else{
          localStorage.setItem("access-token","")
          localStorage.setItem("refresh-token","")
      }
    return res;
  }
  if (res.data.data === '验证错误,需要登录') {
    console.log('验证错误,需要登录')
    // window.location.href = '/'
    MessageBox.alert('没有权限,需要登录', '权限错误', {
      confirmButtonText: '跳转登录页面',
      callback: action => {
        window.location.href = '/'
      }
    })
  } else {
    Message.error(res.data.data)
  }
})

Vue.use(ElementUI)
// 配置axios,qs到Vue
Vue.prototype.axios = axios;
Vue.prototype.qs = qs;
Vue.config.productionTip = false

new Vue({
  router,
  render: function (h) { return h(App) }
}).$mount('#app')

 1.

//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
    config => {
      let accessToken = localStorage.getItem("access-token");
      let refreshToken= localStorage.getItem("refresh-token");
      console.log("在发送请求前进行拦截  main.js里的 access-token:\n" + accessToken);
      console.log("在发送请求前进行拦截  main.js里的 refresh-token:\n"+refreshToken);
      if (accessToken && refreshToken) {
        //把localStorage的token放在Authorization里
        config.headers.Authorization = accessToken;
        config.headers.RefreshToken = refreshToken;
      }
      return config;
    },
    function(err) {
      console.log("失败信息" + err);
    }
);

客户端每次发送请求前都会执行该方法,用于将双token存入请求头中。

2.
//错误响应拦截
axios.interceptors.response.use(res => {
  console.log('拦截响应');
  console.log(res);
  if (res.status ===200) {
      //判断响应头是否有权限
      //如果响应内容有token,就保存
      if(res.data.accessToken && res.data.refreshToken){
          console.log("拦截到的响应accessToken:" ,res.data.accessToken )
          console.log("拦截到的响应refreshToken:" ,res.data.refreshToken )
          localStorage.setItem("access-token",res.data.accessToken)
          localStorage.setItem("refresh-token",res.data.refreshToken)
      }else{
          localStorage.setItem("access-token","")
          localStorage.setItem("refresh-token","")
      }
    return res;
  }
  if (res.data.data === '验证错误,需要登录') {
    console.log('验证错误,需要登录')
    // window.location.href = '/'
    MessageBox.alert('没有权限,需要登录', '权限错误', {
      confirmButtonText: '跳转登录页面',
      callback: action => {
        window.location.href = '/'
      }
    })
  } else {
    Message.error(res.data.data)
  }
})

每次服务器响应数据给客户端的时候都会先执行该方法,核心功能就是将响应中的token保存到本地

localStorage.setItem("access-token",res.data.accessToken)
localStorage.setItem("refresh-token",res.data.refreshToken)

四.模拟登陆访问流程逐步讲解(重点!重点!) :

我将会演示完整的代码执行流程并且逐步讲解,每一步骤所调用的方法。

1. 点击登陆按钮,调用登陆方法向后台发送请求

2.在发送请求前会先执行main.js里的请求拦截方法,用于读取本地保存的token,因为是第一次请求,服务器还未生成token所以第一次读取不到

 3.进入SecurityConfig类中的方法进行Security验证

 4.通过前端传过来的用户名查询数据库对应的数据,将用户信息交给Security

 5.登陆成功将执行成功处理器的方法,生成token响应给前端。

 6.进入main.js的拦截过滤器方法,读取响应端的token并保存到前端本地。

 7.执行前端登陆方法的获取响应后的步骤,我的后续步骤是跳转到另一个页面

 8.进入跳转后的页面

 9.执行跳转后的页面的mounted方法

10.main.js里的请求拦截先进行拦截,读取本地的双token,并将双token保存到请求头中

 11.服务器调用过滤方法,获得请求头中的双token,读取token中的用户名,通过用户名查询该用户所拥有的权限,将权限交给security管理,并进行判断token是否过期,如果只是一个token过期将生成新的双token保存到redis中去。并

 

 12.调用aop的切入方法,不仅可以访问,原有接口还可以读取token并保存到响应中一起返回给前端,因为我的token还没有过期所有redis中还没有token,所以读取的是请求头中的token

 13.main.js里的响应拦截读取token,并保存到本地

14.执行该方法后续响应步骤

 15. 大家休息一下看看我的控制台打印结果ε≡٩(๑>₃<)۶ 

 以上操作是token未过期的情况,redis中还没有存入双token

 17.开始演示token过期的情况,将后端过滤器的该段代码取消注释,手动抛出token过期异常

 18.(✪ω✪)∑(っ°Д°;)っ卧槽,redis有数据了!!

 ─=≡Σ(((つ•̀ω•́)つ

(*^o^)人(^o^*)

d=====( ̄▽ ̄*)b 顶

 依然可以正常运行

  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值