注意:在在登陆时需要把生成的 token 返回!
第一步,建立网关模块,导入网关,jwt 相关坐标
<dependencies>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- <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>
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
第二步,进行网关的配置 application.yml
解释:
-
路由id:路由的唯一标示
-
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡(lb://服务名称,这样也可以,可以负载均衡,但是需要导入相关坐标,我的导不进去。。。。)
-
路由断言(predicates):判断路由的规则,就是前缀,自定义,建议和服务名称一致,(我最开始以为是controller里面的路径,其实不是!!)/MyModule 在访问时 地址为 localhost:10010/MyModule/my/…
-
路由过滤器(filters):对请求或响应做处理
server:
port: 10010 # ????
spring:
application:
name: gateway # ????
cloud:
nacos:
server-addr: localhost:8848 # nacos??
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes: # ??????
- id: login
uri: http://127.0.0.1:8081
predicates:
- Path=/usr/**
filters:
- StripPrefix= 1
- id: MyModule
uri: http://127.0.0.1:8084
predicates:
- Path=/MyModule/**
filters:
- StripPrefix= 1
3.建立过滤器,拦截判断请求
package com.isoft.mygateway.filter;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.isoft.mygateway.utils.AppJwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//2.判断是否是登录
if (request.getURI().getPath().contains("/login")) {
//放行
return chain.filter(exchange);
}
//3.获取token
String token = request.getHeaders().getFirst("token");
//4.判断token是否存在
if (StringUtils.isBlank(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//5.判断token是否有效
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
//是否是过期
int result = AppJwtUtil.verifyToken(claimsBody);
if (result == 1 || result == 2) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 网关进行token解析后,把解析后的用户信息存储到header
//这里的 id 要和登陆时 存入的 名称一致,一定是id 不能是其他名字 ids 啥的,要不然取不出来 !!
Object userId = claimsBody.get("id");
// 在header中添加新的信息
ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
httpHeaders.add("userId", userId + "");
}).build();
// 重置header
exchange.mutate().request(serverHttpRequest).build();
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//6.放行
return chain.filter(exchange);
}
/**
* 优先级设置 值越小 优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
如何在多个微服务之间使用ThreadLocal获取当前登录用户的id
1.首先在网关中 需要将token信息在网关进行token解析,然后把解析后的用户信息存储到header中,上述网关有相关代码,下面只是部分
// 网关进行token解析后,把解析后的用户信息存储到header
Object userId = claimsBody.get("id");
// 在header中添加新的信息
ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
httpHeaders.add("userId", userId + "");
}).build();
// 重置header
exchange.mutate().request(serverHttpRequest).build();
2.其他微服务(需要使用到登录用户id的)使用拦截器获取到header中的的用户信息,并放入到threadlocal中
package com.isoft.mymodule.interceptor;
import com.heima.utils.common.BaseContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;
@Slf4j
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("userId");
Optional<String> optional = Optional.ofNullable(userId);
if (optional.isPresent()) {
// 把id存入threadLocal
Long id = Long.valueOf(userId);
BaseContext.setCurrentId(id);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
BaseContext.clear();
}
}
3.进行过滤器配置,拦截所有请求
package com.isoft.mymodule.config;
import com.isoft.mymodule.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
//配置类
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
//这样就不用写controller而实现页面跳转
// 参数传入就是controller中的URL,setViewName中传入的参数就是原return的页面
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/hello.html").setViewName("index");
}
//映射静态资源目录
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**");
}
jwt工具栏类和thread工具类
package com.heima.utils.common;
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class AppJwtUtil {
// TOKEN的有效期一天(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 加密KEY
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 最小刷新间隔(S)
private static final int REFRESH_TIME = 300;
// 生产ID
public static String getToken(Long id){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id",id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) //签发时间
.setSubject("system") //说明
.setIssuer("heima") //签发者信息
.setAudience("app") //接收用户
.compressWith(CompressionCodecs.GZIP) //数据压缩方式
.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //cla信息
.compact();
}
/**
* 获取token中的claims信息
*
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息
*
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
/**
* 获取hearder body信息
*
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
return getJws(token).getHeader();
}
/**
* 是否过期
*
* @param claims
* @return -1:有效,0:有效,1:过期,2:过期
*/
public static int verifyToken(Claims claims) {
if(claims==null){
return 1;
}
try {
claims.getExpiration()
.before(new Date());
// 需要自动刷新TOKEN
if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
return -1;
}else {
return 0;
}
} catch (ExpiredJwtException ex) {
return 1;
}catch (Exception e){
return 2;
}
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static void main(String[] args) {
/* Map map = new HashMap();
map.put("id","11");*/
System.out.println(AppJwtUtil.getToken(1102L));
Jws<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
Claims claims = jws.getBody();
System.out.println(claims.get("id"));
}
}
ThreadLocal工具类 。。。
package com.heima.utils.common;
/**
* 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void clear() {
threadLocal.remove();
}
}
前端部分
1.首先在登录代码中需要将后端传过来的token存储到浏览器本地或者浏览器cookie中,我是存储到本地中 ,之后就可以在浏览器调试台–应用中查看
function doLogin() {
const username = $("#username").val();
const password = $("#password").val();
let data = {
username: username,
password: password
}
$.ajax({
url: "/usr/user/user/login",
type: "post",
data: JSON.stringify(data),
contentType: "application/json", //这里注意 异常原因:
// 后端接口API需要的参数格式为json,但我们前端提交的数据格式为form表单。
dataType: 'json',
success: function(res) {
console.log(res);
if (res.code != 0) {
// alert(user.state);
alert("success login ");
//把token、存储到本地
localStorage.setItem('token',res.data.token);
// window.location = './layuimini/index.html';
window.location.href='./layuimini/index.html';
return false;
} else {
alert("fail login ");
}
}
});
event.preventDefault();//阻止表单默认提交
}
2.其他需要认证的请求 从本地中去取
const token = localStorage.getItem('token')
// 将token设置到请求头headers中
$.ajaxSetup({
headers: {
'token': token
}
})
3.ok