文章目录
- 网关工程设计及实现
- 项目创建和初始化
- UI工程设计及实现
- 项目创建和初始化
- 创建资源展现页面
- 日志存储设计
- 步骤
- 1.Pojo逻辑实现:
- 2. dao层逻辑实现:
- 3. service逻辑实现和实现类:
- 4. Controller逻辑实现
- 5. 基于postman进行访问测试
- 行为日志操作
- 1. 这里的Pojo可以采用上面的,但是serialVersionUID需要重新生成
- 2. 切入点注解定义
- 3.AOP方式获取并记录日志
- 启动服务进行访问测试
- 通过Feign方式将日志传递给系统
- 1.确保sso-resource工程中添加了openfeign依赖
- 2.确保sso-resource工程的启动上添加了@EnableFeignClients注解
- 3.定义日志远程服务调用接口
- 4.在LogAspect类中注入RemoteLogService对象
- 5.依次启动服务进行访问
网关工程设计及实现
项目创建和初始化
- 创建目录
- 初始化pom文件内容
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
</dependencies>
- 创建bootstrap.yml配置文件并进行路由定义
server:
port: 9000
spring:
application:
name: sso-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
sentinel:
transport:
dashboard: localhost:8180
eager: true
gateway:
routes:
- id: router01
uri: lb://sso-resource
predicates:
- Path=/sso/resource/**
filters:
- StripPrefix=1
- id: router02
uri: lb://sso-auth
predicates:
- Path=/sso/oauth/**
filters:
- StripPrefix=1
globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
allowCredentials: true
- 定义启动类
- 通过postman进行访问
【先通过登录访问获取到token,然后在进行资源访问(要在postman的Headers中输入获取到的token)】
UI工程设计及实现
项目创建和初始化
- 创建目录
- 加载依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 第一步:在resource目录下创建static目录
第二步:在static目录下创建登陆页面login.html
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>login</title>
</head>
<body>
<div class="container"id="app">
<h3>Please Login</h3>
<form>
<div class="mb-3">
<label for="usernameId" class="form-label">Username</label>
<input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="passwordId" class="form-label">Password</label>
<input type="password" v-model="password" class="form-control" id="passwordId">
</div>
<button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var vm=new Vue({
el:"#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
data:{ //此对象中定义页面上要操作的数据
username:"",
password:""
},
methods: {//此位置定义所有业务事件处理函数
doLogin() {
//1.定义url
let url = "http://localhost:9000/sso/oauth/token"
//2.定义参数
let params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
params.append('client_id',"gateway-client");
params.append('client_secret',"123456");
params.append('grant_type',"password");
//3.发送异步请求
axios.post(url, params)
.then((response) => {//ok
alert("login ok")
let result=response.data;
console.log("result",result);
//将返回的访问令牌存储到浏览器本地对象中
localStorage.setItem("accessToken",result.access_token);
location.href="/resource.html";
//启动一个定时器,一个小时以后,向认证中心发送刷新令牌
})
.catch((e)=>{
console.log(e);
})
}
}
});
</script>
</body>
</html>
创建资源展现页面
- 在UI工程的static目录下创建resource.html
- 设计页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>The Resource Page</h1>
<button onclick="doSelect()">查询我的资源</button>
<button onclick="doUpdate()">修改我的资源</button>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
function doSelect(){
let url="http://localhost:9000/sso/resource";
//获取登录后,存储到浏览器客户端的访问令牌
let token=localStorage.getItem("accessToken");
//发送请求时,携带访问令牌
axios.get(url,{headers:{"Authorization":"Bearer "+token}})
.then(function (response){
alert("select ok")
console.log(response.data);
})
.catch(function (e){//失败时执行catch代码块
if(e.response.status==401){
alert("请先登录");
location.href="/login.html";
}else if(e.response.status==403){
alert("您没有权限")
}
console.log("error",e);
})
}
function doUpdate(){
let url="http://localhost:9000/sso/resource";
//获取登录后,存储到浏览器客户端的访问令牌
let token=localStorage.getItem("accessToken");
console.log("token",token);
//发送请求时,携带访问令牌
axios.put(url,"",{headers:{"Authorization":"Bearer "+token}})
.then(function (response){
alert("update ok")
console.log(response.data);
})
.catch(function (e){//失败时执行catch代码块
console.log(e);
if(e.response.status==401){
alert("请先登录");
location.href="/login.html";
}else if(e.response.status==403){
alert("您没有权限")
}
console.log("error",e);
})
}
</script>
</body>
</html>
- 进行访问即可
日志存储设计
步骤
1.Pojo逻辑实现:
package com.jt.system.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 基于此对象封装用户行为日志?
* 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
*/
@Data
@TableName("tb_logs")
public class Log implements Serializable {
private static final long serialVersionUID = 3054471551801044482L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
@TableField("createdTime")
private Date createdTime;
private Integer status;
private String error;
}
2. dao层逻辑实现:
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}
3. service逻辑实现和实现类:
public interface LogService {
/**
* 保存用户行为日志.
* @param log
*/
void insertLog(Log log);
//.....
}
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
/**
* @Async描述的方法底层会异步执行(不由web服务线程执行,
* 而是交给spring自带的线程池中的线程去执行)但是@Async注解的
* 应用有个前提,需要启动类上启动异步执行(添加@EnableAsync注解描述).
* 优点:不会长时间阻塞web服务(例如tomcat)线程
*/
@Async
@Override
public void insertLog(Log log) {
logMapper.insert(log);
}
}
4. Controller逻辑实现
@RestController
@RequestMapping("/log")
public class LogController {
@Autowired
private LogService logService;
@PostMapping
public void doInsertLog(@RequestBody Log log){
logService.insertLog(log);
}
}
5. 基于postman进行访问测试
行为日志操作
1. 这里的Pojo可以采用上面的,但是serialVersionUID需要重新生成
2. 切入点注解定义
package com.jt.resource.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 定义RequiredLog注解,通过此注解对需要
* 进行日志记录的方法进行描述
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "";
}
3.AOP方式获取并记录日志
package com.jt.resource.aspect;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.codehaus.jackson.map.ObjectMapper;
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 java.lang.reflect.Method;
import java.util.Date;
/**
* @Aspect 注解描述的类型为一个切面类型,在此类中可以定义:
* 1)切入点(切入扩展逻辑的位置~例如权限控制,日志记录,事务处理的位置),在
* @Aspect描述的类中,通常使用@Pointcut注解进行定义.使用切入点描述的方法为切入点方法.
*
* 2)通知方法(在切入点对应的目标方法执行前后要执行逻辑需要写到这样的方法中),在
* @Aspect描述的类中,通过@Before,@After,@Aroud,@AfterReturning,@AfterThrowing
* 这样的注解进行描述.
* a: @Before切入点方法执行之前执行
* b: @After 切入点方法执行之后执行(不管切入点方法是否执行成功了,它都会执行)
* c: @Aroud 切入点方法执行之前和之后都可以执行(最重要)
* d: @AfterReturning 切入点方法成功执行之后执行
* e: @AfterThrowing 切入点方法执行时出了异常会执行
*/
@Aspect
@Component
public class LogAspect {
/**
* @Pointcut 注解用于定义切入点,此注解中的内容为切入点表达式
* @annotation 为注解方式的切入点表达式,此方式的表达式为一种细粒度的切入点表达式,
* 因为它可以精确到方法,例如我们现在使用RequiredLog注解描述方法时,由它描述的方法
* 就是一个切入点方法.
*/
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
public void doLog(){
//此方法中不需要写任何内容,只负责承载@Pointcut注解
}
/**
* @Around 注解描述的方法为Aspect中的一个环绕通知方法,在此方法
* 内部可以控制对目标方法的调用.
* @param joinPoint 连接点对象,此对象封装了你要执行的切入点方法信息,可以基于
* 此对象对切入点方法进行反射调用
* @return 目标执行链中切入点方法的返回值.
* @throws Throwable
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
int status=1;//状态
String error=null;//错误信息
long time=0l;//执行时长
long t1=System.currentTimeMillis();
try {
//手动调用目标执行链(这个执行链中包含切入点方法~目标方法)
Object result = joinPoint.proceed();
long t2=System.currentTimeMillis();
time=t2-t1;
return result;
}catch (Throwable e){
long t3=System.currentTimeMillis();
time=t3-t1;
status=0;
error=e.getMessage();
throw e;
}finally {
saveLog(joinPoint,time,status,error);
}
}
//存储用户行为日志
private void saveLog(ProceedingJoinPoint joinPoint,long time,
int status,String error)throws Throwable{
//1.获取用户行为日志
//1.1获取目标对象类型(切入点方法所在类的类型)
Class<?> targetClass = joinPoint.getTarget().getClass();
//1.2.获取目标方法
//1.2.1获取方法签名(包含方法信息,....)
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//1.2.2获取方法对象
Method targetMethod=
targetClass.getDeclaredMethod(signature.getName(),signature.getParameterTypes());
//1.3获取方法上的RequiredLog注解内容
//1.3.1获取目标方法上注解
RequiredLog requiredLog=targetMethod.getAnnotation(RequiredLog.class);
//1.3.2获取注解中的内容(这个内容为我们定义的操作名)
String operation=requiredLog.value();
//1.4获取目标方法名(类名+方法名)
String targetMethodName=targetClass.getName()+"."+targetMethod.getName();
//1.5获取目标方法执行时传入的参数
String params=new ObjectMapper().writeValueAsString(joinPoint.getArgs());
//1.6获取登录用户名(参考了Security官方的代码)
String username=(String)
SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
//1.7获取ip地址(从当前线程获取request对象,然后基于request获取ip地址)
//String ip="192.168.1.100";
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ip=requestAttributes.getRequest().getRemoteAddr();
//2.将用户行为日志,封装到Log对象
Log logInfo=new Log();
logInfo.setIp(ip);//后续获取
logInfo.setUsername(username);
logInfo.setOperation(operation);
logInfo.setMethod(targetMethodName);
logInfo.setParams(params);
logInfo.setTime(time);
logInfo.setStatus(status);
logInfo.setError(error);
logInfo.setCreatedTime(new Date());
System.out.println("logInfo="+logInfo);
}
}
启动服务进行访问测试
依次启动nacos,sso-system,sso-auth,sso-resource,sso-gateway,sso-ui工程服务,然后执行登陆,登陆成功后查询我的资源,检测日志输出。
通过Feign方式将日志传递给系统
1.确保sso-resource工程中添加了openfeign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.确保sso-resource工程的启动上添加了@EnableFeignClients注解
3.定义日志远程服务调用接口
package com.jt.resource.service;
import com.jt.resource.pojo.Log;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(value = "sso-system",contextId = "remoteLogService")
public interface RemoteLogService {
@PostMapping("/log")
public void insertLog(@RequestBody Log log);
}
4.在LogAspect类中注入RemoteLogService对象
@Autowired
private RemoteLogService remoteLogService;
remoteLogService.insertLog(log);
5.依次启动服务进行访问