- 学习流程
- 服务设计
- 创建sso父工程
- 创建sso-system子工程
- 创建sso-auth子工程
- 创建sso-resource子工程
- 创建sso-gateway子工程
- 创建sso-ui前端业务子工程
- 数据库
- 表结构设计
- sql语句
- 方案一: 单次查询
- 方案二: 嵌套查询
- 方案三: 多表关联查询
- SSO-system业务实现
- 概述
- controller
- service
- UserService
- UserServiceImpl
- pojo
- User
- dao
- UserMapper
- 测试
- SSO-auth业务实现
- 概述一
- pom依赖
- yml配置
- 启动类
- 测试一
- service
- RemoteUserService接口
- UserDetailsServiceImpl实现类
- pojo
- config
- SecurityConfig 配置类
- 测试二
- TokenConfig配置类
- Oauth2config
- 测试三
- SSO-resource业务实现
- 业务描述
- 设计架构
- pom依赖
- bootstrap.yml文件
- 启动类
- controller
- config
- TokenConfig
- ResourceConfig
- 测试
- SSO-gateway业务实现
- pom文件
- bootstrap.yml文件
- 启动类
- 测试一
- 拓展
- 网关配置中心
- 设置限流操作
- 测试二
- pom文件
- 启动类
- 前端页面
- login.html
- resource.html
- 测试
- 用户日志信息处理
- 添加用户日志插入方法
- 1.封装日志属性
- 2. controller层
- 3. service层
- 4.mapper层
- source模块获取用户行为日志
- 1.pojo封装日志属性
- 2..自定义 @RequiredLog注解
- 3. 设计自定义注解@RequiredLog的设计链(日志行为业务处理)
- 4. 使用注解
- 使用feign调用system服务里的insert方法存入数据库
- 1. 创建service服务调用接口
- 2.传递封装的log对象
- 添加父工程pom依赖
- spring-cloud-alibaba-dependencies
- spring-cloud-dependencies
- spring-boot-dependencies
用来写查询用户的业务,
- 添加子工程pom依赖
- mysql-connector-java
- mybatis-plus-boot-starter
- spring-cloud-starter-alibaba-nacos-discovery
- spring-cloud-starter-alibaba-nacos-config
- spring-boot-starter-web
- 创建启动类, controller, service, pojo, dao(mapper), yml
- 配置yml(端口, 数据库, 服务注册/发现和配置)
-
添加子工程pom文件
- spring-boot-starter-web
- spring-cloud-starter-alibaba-nacos-discovery
- spring-cloud-starter-alibaba-nacos-config
- spring-cloud-starter-openfeign
- spring-cloud-starter-oauth2
- Spring Security(提供登录页面) + JWT + OAUTH2
-
创建启动类, service, pojo, config, yml
-
配置yml文件(端口, 服务发现, 配置中心)
- 添加pom依赖
- 创建启动类, controller, config, yml文件
- 配置yml(端口, 服务发现, 配置中心)
- 添加pom
- 创建启动类
- 创建yml
- 添加pom
- 创建启动类
- resource下创建static目录
- static下创建HTML文件
- 用户表(ID username password)
- 用户和角色关系表
- 用户角色和菜单关系表
- 用户菜单权限表
通过用户ID查询到用户的权限
方案一: 单次查询- 基于用户ID查询用户对应的角色ID (查询出的角色ID可能是多个)
select role_id from tb_user_roles where user_id = 1;
- 基于角色ID查询用户对象的菜单ID
select menu_id from tb_role_menus where role_id in (1);
- 基于菜单ID查询菜单权限标识
select permission from tb_menus where id in (1, 2, 3);方案二: 嵌套查询
select permission from tb_menus where id in (select menu_id from tb_role_menus where role_id in (select role_id from tb_user_roles where user_id = 1));方案三: 多表关联查询
select permission from tb_user_roles ur join tb_role_menus rm on ur.role_id = rm.role_id join tb_menus m on rm.menu_id = m.id where ur.user_id = 1;SSO-system业务实现 概述
使用ssm实现用户和用户权限的查询, 并被auth远程调用
controllerUserController
@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/login/{username}")
public User doSelectUserByUsername(@PathVariable("username") String username) {
return userService.selectUserByUsername(username);
}
@GetMapping("/permission/{userId}")
public List doSelectUserPermissions(@PathVariable("userId") Long userId) {
return userService.selectUserPermissions(userId);
}
}
service
UserService
public interface UserService {
User selectUserByUsername(String username);
List selectUserPermissions(Long userId);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
@Override
public List selectUserPermissions(Long userId) {
// 方案一: 在这里可以调用数据层的单表查询方法, 查询三次获取用户信息
// 方案二: 在这里可以调用数据层的多表嵌套或多表关联方式执行1次查询
return userMapper.selectUserPermissions(userId);
}
}
pojo
User
@Data
//@TableName(value = "tb-users")
public class User implements Serializable {
private static final long serialVersionUID = 4831304712151465443L;
// @TableId()
private Long id;
private String username;
private String password;
private String status;
}
dao
UserMapper
@Mapper //底层基于接口创建实现类 public interface UserMapper extends BaseMapper{ @Select("select id,username,password,status " + " from tb_users " + " where username = #{username} ") User selectUserByUsername(String username); @Select("select distinct m.permission " + "from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id" + " join tb_menus m on rm.menu_id=m.id " + "where ur.user_id=#{userId}") List selectUserPermissions(Long userId); }
- 注意yml配置
- 基于用户名查询用户信息
- 基于用户id(这里假设用户id为1)查询用户权限
先进行远程服务调用的实现,
调用system查询用户和用户权限的方法,
确认用户信息正确,
根据用户信息和签名生成令牌,
通过携带的令牌访问其他功能
yml配置org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.cloud spring-cloud-starter-openfeign
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
启动类
- 使用@EnableFeignClients注解进行远程调用
@EnableFeignClients
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
测试一
- 项目启动时,系统会默认生成一个登陆密码
- 打开浏览器输入http://localhost:8071呈现登陆页面
- 默认用户名为user, 执行登陆测试,登陆成功进入如下界面(因为没有定义登陆页面,所以会出现404)
@FeignClient(value = "sso-system", contextId ="remoteUserService" )
public interface RemoteUserService {
@GetMapping("/user/login/{username}")
User selectUserByUsername( @PathVariable("username") String username);
@GetMapping("/user/permission/{userId}")
List selectUserPermissions(@PathVariable("userId") Long userId);
}
UserDetailsServiceImpl实现类
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//基于feign方式获取远程数据并封装
//1.基于用户名获取用户信息
com.jt.auth.pojo.User user = remoteUserService.selectUserByUsername(username);
if(user==null) throw new UsernameNotFoundException("用户不存在");
//2.基于用于id查询用户权限
List permissions = remoteUserService.selectUserPermissions(user.getId());
log.debug("permissions {}",permissions.toString());
//3.对查询结果进行封装并返回
User userInfo = new User(username,
user.getPassword(),
AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
return userInfo;
//返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
}
}
pojo
@Data
public class User implements Serializable {
private static final long serialVersionUID = 3570548663999909287L;
private Long id;
private String username;
private String password;
private String status;
}
config
SecurityConfig 配置类
package com.jt.auth.config;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);//默认所有请求都要认证
//1.禁用跨域攻击(先这么写,不写会报403异常) 登录默认是post请求, 系统底层的跨域攻击设计不允许post请求
http.csrf().disable();
//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
// http.authorizeRequests().mvcMatchers("
//定义认证成功处理器
//登录成功以后返回json数据
@Bean
public AuthenticationSuccessHandler successHandler(){
// 普通写法
// return new AuthenticationSuccessHandler() {
// @Override
// public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//
// }
// }
//lambda 简写形式
return (request,response,authentication)->{
//构建map对象封装到要响应到客户端的数据
Map map=new HashMap<>();
map.put("state",200);
map.put("message", "登陆成功");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
};
}
//定义登录失败处理器
@Bean
public AuthenticationFailureHandler failureHandler(){
return (request,response,exception)->{
//构建map对象封装到要响应到客户端的数据
Map map = new HashMap<>();
map.put("state",500);
map.put("message", "登录失败");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(HttpServletResponse response, Map map) throws IOException {
//将map对象,转换为json
String json = new ObjectMapper().writeValueAsString(map);
//设置响应数据的编码方式
response.setCharacterEncoding("utf-8");
//设置响应数据的类型
response.setContentType("application/json;charset=utf-8");
//将数据响应到客户端
PrintWriter out = response.getWriter();
out.println(json);
out.flush();
}
}
测试二
- 启动sso-system,sso-auth服务,然后基于postman访问网关,执行登录测试
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter Converter = new JwtAccessTokenConverter();
Converter.setSigningKey("auth");
return Converter;
}
//这里的签名key将来可以写到配置中心
private static final String SIGNING_KEY = "auth";
}
Oauth2config
package com.jt.auth.config;
import com.jt.auth.service.UserDetailsServiceImpl;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.http.HttpMethod;
import java.util.Arrays;
@AllArgsConstructor // 全参构造
@EnableAuthorizationServer // 启动认证和授权
@Configuration
public class Oauth2config extends AuthorizationServerConfigurerAdapter {
// @Autowired
private AuthenticationManager authenticationManager;
// @Autowired
private UserDetailsServiceImpl userDetailsService;
// @Autowired
private TokenStore tokenStore;
// @Autowired
private PasswordEncoder passwordEncoder;
// @Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// @Autowired
// public Oauth2config(AuthenticationManager authenticationManager, UserDetailsServiceImpl userDetailsService, TokenStore tokenStore, PasswordEncoder passwordEncoder, JwtAccessTokenConverter jwtAccessTokenConverter) {
// this.authenticationManager = authenticationManager;
// this.userDetailsService = userDetailsService;
// this.tokenStore = tokenStore;
// this.passwordEncoder = passwordEncoder;
// this.jwtAccessTokenConverter = jwtAccessTokenConverter;
// }
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//super.configure(endpoints);
endpoints
//配置由谁完成认证(认证管理器)
.authenticationManager(authenticationManager)
//配置谁负责访问数据库(认证时需要两部分信息:一部分来自客户端,一部分来自数据库)
.userDetailsService(userDetailsService)
//配置进行认证的请求方式(默认支持post方式)
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
//配置认证成功以后令牌生成和存储策略(默认令牌生成UUID.randomUUID(),存储方式为内存)
.tokenServices(tokenService());
}
@Bean
public AuthorizationServerTokenServices tokenService(){
//1.构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
DefaultTokenServices tokenServices = new DefaultTokenServices();
//2.设置令牌生成和存储策略(tokenStore)
tokenServices.setTokenStore(tokenStore);
//3.设置是否支持令牌刷新(访问令牌过期了,是否支持通过令牌刷新机制,延长令牌有效期)
tokenServices.setSupportRefreshToken(true);
//4.设置令牌增强(默认令牌会比较简单,使用的就是UUID, 没有业务数据,
//就是简单随机字符串,但现在希望使用jwt方式)(不写会使用默认生成UUID)
TokenEnhancerChain tokenEnhancer = new TokenEnhancerChain();
tokenEnhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancer);
//5.设置访问令牌有效期
tokenServices.setAccessTokenValiditySeconds(3600);//1小时
//6.设置是否支持刷新令牌(是否支持使用刷新令牌再生成新令牌)
tokenServices.setSupportRefreshToken(true);
//7.设置刷新令牌有效期
tokenServices.setRefreshTokenValiditySeconds(3600*72);//3天
return tokenServices;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//1.定义(公开)要认证的url(permitAll()是官方定义好的)
//公开oauth/token_key端点
.tokenKeyAccess("permitAll()") //return this
//2.定义(公开)令牌检查的url
//公开oauth/check_token端点
.checkTokenAccess("permitAll()")
//3.允许客户端直接通过表单方式提交认证
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//定义客户端的id(客户端提交用户信息进行认证时需要这个id)
.withClient("gateway-client")
//定义客户端密钥(客户端提交用户信息时需要携带这个密钥)
.secret(passwordEncoder.encode("123456"))
//定义作用范围(所有符合规则的客户端)
.scopes("all")
//允许客户端基于密码方式,刷新令牌方式实现认证
.authorizedGrantTypes("password","refresh_token");
}
}
测试三
- 登陆访问测试
- 登陆成功以后,会在postman控制台显示如下格式信息
- 检查token信息
- 请求访问ok,在postman控制台会显示如下格式信息
- 刷新令牌应用测试
- 请求访问ok,在postman控制台会显示如下格式信息
资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,都可以方法,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。
设计架构 pom依赖bootstrap.yml文件org.springframework.boot spring-boot-starter-web com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.cloud spring-cloud-starter-oauth2
server:
port: 8881
spring:
application:
name: sso-resource
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class,args);
}
}
controller
package com.jt.resource.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/resource")
public class ResourceController {
@PreAuthorize("hasAuthority('sys:res:list')")
@GetMapping
public String doSelect(){
return "Select Resource ok";
}
@PreAuthorize("hasAuthority('sys:res:create')")
@PostMapping
public String doCreate(){
return "Create Resource OK";
}
@PreAuthorize("hasAuthority('sys:res:update')")
@PutMapping
public String doUpdate(){
return "Update Resource OK";
}
@DeleteMapping
public String doDelete(){
return "Delete resource ok";
}
}
config
TokenConfig
package com.jt;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
//这里采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=
new JwtAccessTokenConverter();
//JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名)
//这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥,
//这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端)
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥
return jwtAccessTokenConverter;
}
private static final String SIGNING_KEY="auth";
}
ResourceConfig
package com.jt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableResourceServer
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//1.关闭跨域攻击
http.csrf().disable();
//2.放行相关请求
http.authorizeRequests()
.antMatchers("/resource
@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;
// @DateTimeFormat(pattern = "yyyy-MM-dd")
@TableField("createdTime")
private Date createdTime; // = new Date();
private Integer status;
private String error;
}
2. controller层
package com.jt.system.controller;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@CrossOrigin
@RequestMapping("/log")
public class LogController {
@Autowired
private LogService logService;
@PostMapping
public void doInsertLog(@RequestBody Log log) {
logService.insertLog(log);
}
}
3. service层
- service接口
package com.jt.system.service;
import com.jt.system.pojo.Log;
public interface LogService {
void insertLog(Log log);
}
- service实现类
package com.jt.system.service.impl;
import com.jt.system.dao.LogMapper;
import com.jt.system.pojo.Log;
import com.jt.system.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Async
@Override
public void insertLog(Log log) {
String tName = Thread.currentThread().getName();
System.out.println("LogServiceImpl.thread.name: " + tName);
logMapper.insert(log);
}
}
4.mapper层
package com.jt.system.dao; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.jt.system.pojo.Log; import org.apache.ibatis.annotations.Mapper; @Mapper public interface LogMapper extends BaseMapper{ }
- 使用了继承自BaseMapper的insert方法
package com.jt.resource.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class Log implements Serializable {
private static final long serialVersionUID = 3054471551801044482L;
private Long id;
private String username;
private String operation;
private String method;
private String params;
private Long time;
private String ip;
private Date createdTime;
private Integer status;
private String error;
}
2…自定义 @RequiredLog注解
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;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String value() default "";
}
3. 设计自定义注解@RequiredLog的设计链(日志行为业务处理)
package com.jt.resource.aspect;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import com.jt.resource.service.RemoteLogService;
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.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class LogAspect {
// @Pointcut("bean(resourceController)") //不能精确到具体某个方法, Controller里所有方法都是切入点方法
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
// @Pointcut("execution(* com.jt.resource.controller.ResourceController. *(..) )")
public void doLog() {
//此方法中不允许写任何内容, 只负责承载@Pointcut注解
}
@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);
}
}
4. 使用注解
- @RequiredLog注解描述的方法为日志切入点方法
@RestController
@RequestMapping("/resource")
public class ResourceController {
@RequiredLog("查询日志") // 注解中的内容为一个操作名称
@PreAuthorize("hasAuthority('sys:res:list')")
@GetMapping
public String doSelect(){
return "查询成功";
}
使用feign调用system服务里的insert方法存入数据库
1. 创建service服务调用接口
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")
void insertLog(@RequestBody Log log);
}
2.传递封装的log对象
- 在设计的执行链中注入service
@Autowired private RemoteLogService logService;
- 将log对象通过参数传输
logService.insertLog(logInfo);
package com.jt.resource.aspect;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import com.jt.resource.service.RemoteLogService;
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.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class LogAspect {
// @Pointcut("bean(resourceController)") //不能精确到具体某个方法, Controller里所有方法都是切入点方法
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
// @Pointcut("execution(* com.jt.resource.controller.ResourceController. *(..) )")
public void doLog() {
//此方法中不允许写任何内容, 只负责承载@Pointcut注解
}
@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);
//3. 将日志对象通过feign方式传递到远程sso-system服务
logService.insertLog(logInfo);
}
@Autowired
private RemoteLogService logService;
}



