spring cloud分布式项目 结合vue2 实现双token 单点登陆 详细代码实现

目录

前言:

后端代码:

一.项目结构:

网关结构

 用户模块结构

二.用户模块:

ApplicationContextUtils:

JwtUtils:

ResponseResult:

RsaUtils:

2.SecurityConfig:

LoginSuccessHandler(成功处理器):

UserDetailsServiceImpl:

bootstrap.yml:

 依赖配置:

二.网关:

GatewayConfig:

网关过滤器:

yml:

前端代码:

一.登陆页面:

main.js :

​编辑


前言:

        

本项目采用阿里巴巴的nacos进行分布式项目搭建,本文只讲单点登陆的双token实现,需要会搭项目的同学才能看的懂。 对security不熟悉的可以先看我之前的两篇spring security 前后端分离 进行用户验证 权限登陆的实现代码(看不懂??直接cv)

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

后端代码:

一.项目结构:

网关结构

 用户模块结构

二.用户模块:

ApplicationContextUtils:

package com.dmdd.eduuserservice.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);
    }
}

该工具类提供ioc功能,因为在有些类中,注解形式的ioc无法使用,该工具类在ssm mvc项目中经常用到 

JwtUtils:

package com.dmdd.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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 = 120;

    /**
     * 私钥加密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){
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        String username = (String) body.get(JWT_KEY_USERNAME);
        return username;
    }
}

用于生成token的工具类 

ResponseResult:

package com.dmdd.common.util;

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

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

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

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

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

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

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

    public ResponseResult(ResponseStatus status, T data) {
        this.status = status;
        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> ok(T data,String accessToken,String refreshToken){
        ResponseResult<T> result = new ResponseResult<>(ResponseStatus.OK, data);
        result.setAccessToken(accessToken);
        result.setRefreshToken(refreshToken);
        return result;
    }

    /**
     * 返回错误对象
     * @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();
    }
}

 响应对象,controller响应给前端的对象就用这个

RsaUtils:

package com.dmdd.common.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);
    }
}

用于生成公钥和私钥 

2.SecurityConfig:

package com.dmdd.eduuserservice.config;
import com.dmdd.common.util.ResponseResult;
import com.dmdd.common.util.ResponseStatus;
import com.dmdd.eduuserservice.filter.MyFilter;
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;

/**
 * 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",
                        "/swagger-ui.html","/swagger-resources/**","/webjars/**","/*/api-docs","/user","/users","/**").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()
                .csrf().disable() //停止csrf
                .sessionManagement() //session管理
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //无状态,不使用session
                .and()
                .addFilter(new MyFilter(authenticationManager())) //添加自定义验证过滤器
        ;
    }
}

 讲很多遍了,不讲了,只讲跟之前的区别。

因为是分布式项目 网关自带跨域配置所以我们不需要再配置跨域

LoginSuccessHandler(成功处理器):

package com.dmdd.eduuserservice.config;

import com.dmdd.common.util.JwtUtils;
import com.dmdd.common.util.ResponseResult;
import com.dmdd.common.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;

/**
 * 登录成功处理器
 */
@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();
        //生成access 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 * 5);
        log.info("生成access token:{}",accessToken);
        log.info("生成refresh token:{}",refreshToken);
        //发送token给前端
        ResponseResult.write(httpServletResponse, ResponseResult.ok(user.getUsername(),accessToken,refreshToken));
    }
}

 这回是正儿八经的从数据库查询了权限。之前图方便没查

UserDetailsServiceImpl:

package com.dmdd.eduuserservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import com.dmdd.common.entity.User;
import com.dmdd.eduuserservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 实现自定义用户登录逻辑
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //按用户名查询用户信息
        User user = userService.getOne(new QueryWrapper<User>().lambda().eq(User::getUsername, s));
        if(user == null){

            throw new UsernameNotFoundException("用户名不存在");
        }
        //查询所有用户权限 List<String> --> xx,xxx,xxx,xx --> List<Authory>
        List<String> authList = userService.getPermissionsOfUser(s);
        String auths = String.join(",", authList);
        //把用户信息包装到UserDetails的实现类User中
        return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(auths));
    }
}

 懂得都懂

MyFilter(用户模块的过滤器):

package com.dmdd.eduuserservice.filter;

import com.dmdd.common.util.JwtUtils;
import com.dmdd.common.util.RsaUtils;
import com.dmdd.eduuserservice.service.UserService;
import com.dmdd.eduuserservice.util.ApplicationContextUtils;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class MyFilter  extends BasicAuthenticationFilter {
    @Autowired
    private UserService userService= ApplicationContextUtils.getBean(UserService.class);

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

    public static final String ACCESS_TOKEN_HEADER = "Authorization";
    public static final String REFRESH_TOKEN_HEADER = "RefreshToken";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {


       String username="";
        List<GrantedAuthority> authorities= AuthorityUtils.commaSeparatedStringToAuthorityList("");
        //读取用户信息
        //从请求头获得token
        //从请求头获得token
        String accessToken = request.getHeader(ACCESS_TOKEN_HEADER);
         username = JwtUtils.getUsernameFromToken(accessToken, RsaUtils.publicKey);
        System.out.println("user过滤器得到的用户名是"+username);
        if (!username.isEmpty()) {
            //通过用户名查询该用户所拥有的权限
            List<String> permissionsOfUser = userService.getPermissionsOfUser(username);
            System.out.println(permissionsOfUser.toString());
          authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", permissionsOfUser));
        }

        //创建通行证
        UsernamePasswordAuthenticationToken authToken = new
                UsernamePasswordAuthenticationToken(username,"",authorities);
        //把通行证交给Security
        SecurityContextHolder.getContext().setAuthentication(authToken);
        chain.doFilter(request,response);
    }
}

 读取双token ,从token中得到用户名然后通过用户名获取权限。

bootstrap.yml:

server:
  port: 8085

spring:
  application:
    name: edu-user-service
  #配置中心服务器的配置
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.56.188:8080 # sentinel 控制台的地址
        port: 8719  # sentinel的端口
    nacos:
      discovery:
        server-addr: 192.168.56.188:8848  # nacos的地址,端口默认是8848
      config:
        server-addr: 192.168.56.188:8848 # 配置中心地址
        file-extension: yaml            # 配置文件的后缀
        prefix: edu-user-service        # 配置文件的前缀
  profiles:
    active: dev                         # 使用的profile



  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/edu_user?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true
    username: root
    password: jly720609
  redis:
    host: 192.168.159.128
    port: 6379
mybatis-plus:
  type-aliases-package: com.dmdd.common.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

#hystrix:
#  command:
#    default:
#      execution:
#        isolation:
#          thread:
#            timeoutInMilliseconds: 3000;
#            #熔断器每个方法隔离采用的方式,线程池或者信号量 THREAD\SEMAPHORE
#            strategy: THREAD;



#开启全部端点

management:
  endpoints:
    web:
      exposure:
        include: '*'

分布式项目的yml配置

 依赖配置:

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

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>

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

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <dependency>
            <groupId>com.dmdd</groupId>
            <artifactId>common-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </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>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>

    </dependencies>


二.网关:

GatewayConfig:

package com.dmdd.edugatewayservice.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Data
@Configuration
@ConfigurationProperties(prefix = "edu.gateway")
public class GatewayConfig {

    /**
     * 白名单
     */
    private List<String> whiteList;
}

设置网关放行的白名单 

网关过滤器:

package com.dmdd.edugatewayservice.filter;

import com.alibaba.cloud.commons.io.Charsets;
import com.alibaba.fastjson.JSON;
import com.dmdd.common.util.JwtUtils;
import com.dmdd.common.util.ResponseStatus;
import com.dmdd.common.util.RsaUtils;
import com.dmdd.edugatewayservice.config.GatewayConfig;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 请求验证过滤器
 */
@Slf4j
@Component
public class RequestAuthenticationFilter implements GlobalFilter, Ordered {

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

    //白名单
    @Autowired
    private GatewayConfig gatewayConfig;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //跳过白名单
        List<String> whiteList = gatewayConfig.getWhiteList();
        for (String list : whiteList) {
            if (request.getURI().toString().contains(list)) {
                System.out.println("白名单:"+request.getURI());
                return chain.filter(exchange);
            }
        }
        //从请求头获得token
        String accessToken = request.getHeaders().getFirst(ACCESS_TOKEN_HEADER);
        try {
//            if(1 == 1) {
//                throw  new ExpiredJwtException(new DefaultJwsHeader(new HashMap<>()), Jwts.claims().setSubject(""),"error");
//            }
            //对token进行解析
            String username = JwtUtils.getUsernameFromToken(accessToken, RsaUtils.publicKey);
            log.info("Token解析成功,{}登录成功", username);
            return chain.filter(exchange);
        } catch (ExpiredJwtException ex) {
            //如果access-token过时,则解析refresh-token
            String refreshToken = request.getHeaders().getFirst(REFRESH_TOKEN_HEADER);
            if(StringUtils.isEmpty(refreshToken)){
                log.info("读取不到refreshToken,请求{}被拦截",request.getURI());
            }else {
                try {
                    String username = JwtUtils.getUsernameFromToken(refreshToken, RsaUtils.publicKey);
                    //生成新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包装到响应正文中返回客户端
                    return chain.filter(exchange.mutate().response(responseDecorator(response, accessToken, refreshToken)).build());
                } catch (Exception ex2) {
                    log.error("解析token失败", ex);
                }
            }
        } catch (Exception ex) {
            log.error("解析token失败",ex);
        }
        // 出现错误进行拦截
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //返回验证失败的响应信息给客户端
        DataBuffer wrap = response.bufferFactory().wrap(ResponseStatus.AUTH_ERROR.getMessage().getBytes());
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return -1;
    }

    /**
     * 返回响应装饰器对象,添加token数据
     * @param response
     * @param accessToken
     * @param refreshToken
     * @return
     */
    public ServerHttpResponseDecorator responseDecorator(ServerHttpResponse response,String accessToken,String refreshToken){
        // 缓存数据的工厂
        DataBufferFactory bufferFactory = response.bufferFactory();
        // 拿到响应码
        HttpStatus statusCode = response.getStatusCode();
        // 返回响应装饰器对象
        return new ServerHttpResponseDecorator(response){
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (statusCode.equals(HttpStatus.OK) && body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                        // 读取原有的数据
                        DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                        DataBuffer join = dataBufferFactory.join(dataBuffers);
                        byte[] content = new byte[join.readableByteCount()];
                        join.read(content);
                        DataBufferUtils.release(join);
                        // 流转为字符串
                        String responseData = new String(content, Charsets.UTF_8);
                        Map map = JSON.parseObject(responseData);
                        //处理返回的数据,添加token
                        map.put("accessToken",accessToken);
                        map.put("refreshToken",refreshToken);
                        responseData = JSON.toJSONString(map);
                        log.info("网关过滤响应数据:{}",responseData);
                        //将添加了token的响应数据返回
                        byte[] uppedContent = responseData.getBytes(Charsets.UTF_8);
                        response.getHeaders().setContentLength(uppedContent.length);
                        return bufferFactory.wrap(uppedContent);
                    }));
                } else {
                    log.info("错误的响应码{}",statusCode);
                }
                return super.writeWith(body);
            }
        };
    }
}
//将新的token包装到响应正文中返回客户端
return chain.filter(exchange.mutate().response(responseDecorator(response, accessToken, refreshToken)).build());

采用响应包装流将双token连同响应对象一起发送回前端。

yml:

#服务名称
spring:
  application:
    name: edu-gateway-service
  cloud:
    gateway:
      routes: #路由
        - id: edu-ad-service-route #路由id
          uri: lb://edu-ad-service #路由地址
          #断言 条件
          predicates:
            - Path=/promotionAd/**,/promotionAds/**,/ad/** #配置路径中有某些字符串就路由到该服务
#        - id: product-service-route #路由id
#          uri: lb://product-service #路由地址
#          #断言 条件
#          predicates:
#            - Path=/product/**,/products/** #配置路径中有某些字符串就路由到该服务
        - id: edu-user-service-route #路由id
          uri: lb://edu-user-service #路由地址
          #断言 条件
          predicates:
            - Path=/user/**,/users/**,/login #配置路径中有某些字符串就路由到该服务
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins:
              - "http://localhost:8080"
            allowed-headers:
              - "*"
              - "*"
            allowed-methods: "*"
            allowed-credentials: true

    nacos:
      discovery:
        server-addr: 192.168.56.188:8848  # nacos的地址,端口默认是8848
      config:
        server-addr: 192.168.56.188:8848 # 配置中心地址
        file-extension: yaml            # 配置文件的后缀
        prefix: edu_gateway-service        # 配置文件的前缀        profiles:
      #      active: dev                         # 使用的profile


server:
  port: 9000


edu:
  gateway:
    whiteList:
      - /login
      - /logout
      - /register
      - /user
      - /ad
      - /nacos

前端代码:

一.登陆页面:

security自定义登录需要如上设计 security才能识别

v-model分别为username ,password,提交按钮采用button 

 

main.js :

import Vue from 'vue'
import App from './App.vue'
import router from './router'

/*引入qs   单点登陆需要*/
import qs from "qs";
// 导入ele组件库
import ElementUI from 'element-ui'
// 导入ele组件相关样式
import 'element-ui/lib/theme-chalk/index.css'
// 配置ele插件,将其安装到Vue上
Vue.use(ElementUI);

// 引入axios
import axios from "axios"


//axios.defaults.headers.get['Content-Type']='text/plain'
Vue.prototype.qs = qs;
// 配置axios到Vue
Vue.prototype.axios = axios;
// 配置axios的基础路径为网关
Vue.prototype.axios.defaults.baseURL = "http://localhost:9000";
Vue.config.productionTip = false


//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
    config => {
        let url = config.url;
        if (url.split("/").pop() != "/login") {
            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 if (res.config.headers.Authorization && res.config.headers.RefreshToken) {
            console.log("拦截到的响应accessToken:", res.config.headers.Authorization)
            console.log("拦截到的响应refreshToken:", res.config.headers.RefreshToken)
            localStorage.setItem("access-token", res.config.headers.Authorization)
            localStorage.setItem("refresh-token", res.config.headers.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)
    }
})

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

 自定义登录需要用到qs库的方法,需要导入

/*引入qs   单点登陆需要*/
import qs from "qs";
Vue.prototype.qs = qs;

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值