目录
前言:
本项目采用阿里巴巴的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;