登录实战
前言
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架 Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的主要进行 认证 和 授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
授权:经过认证后判断当前用户是否有权限进行某个操作。
而认证和授权也是 SpringSecurity 作为安全框架的核心功能。
登录流程:
先进行登录,登录后带着生成的token进行访问,再给予对相应的权限进行操作。
在使用Spring Security构建的Web应用程序中,登录流程涉及多个关键组件,下面详细介绍这些组件及其在登录过程中扮演的角色:
1. Filter Chain(过滤器链)
SecurityContextPersistenceFilter:维护安全上下文,确保线程间的安全信息传递。
UsernamePasswordAuthenticationFilter:负责处理基于表单的登录请求,收集用户名和密码,调用AuthenticationManager进行验证。
ConcurrentSessionFilter:管理并发会话,避免同一账号多处登录。
ExceptionTranslationFilter:捕获并处理认证或授权失败的异常。
FilterSecurityInterceptor:执行访问决策,依据用户权限判断是否允许访问特定资源。
2. AuthenticationManager(认证管理器)
负责处理认证请求,它接受一个Authentication对象(包含用户凭证),并返回一个经过完全填充的(已验证的或未经验证的)Authentication对象。对于基于用户名和密码的登录,Spring Security提供了DaoAuthenticationProvider,它使用UserDetailsService来检索用户信息。
3. UserDetailsService(用户详情服务)
接口定义了一个方法loadUserByUsername(String username),用于根据用户名加载用户信息。开发者需要实现这个接口,通常从数据库中查询用户信息。返回的是UserDetails对象,它包含用户的用户名、密码(通常是加密的)、权限等安全相关信息。
4. UserDetails(用户详情)
表示用户安全信息的核心接口,包含用户名、密码、账号是否过期、凭证是否过期、账号是否锁定以及赋予用户的权限集合。一个典型的实现是org.springframework.security.core.userdetails.User。
登录流程步骤(结合Spring Security组件):
请求登录页面:用户访问登录页面,该页面由Spring Security默认或自定义的登录页面处理。
提交登录信息:用户提交用户名和密码,这些信息通过UsernamePasswordAuthenticationFilter被封装成一个UsernamePasswordAuthenticationToken,并转发给AuthenticationManager。
验证用户凭证:
AuthenticationManager委托给配置的AuthenticationProvider(如DaoAuthenticationProvider)处理。
DaoAuthenticationProvider调用实现UserDetailsService的服务来加载用户详情(UserDetails)。
使用PasswordEncoder比较提交的密码与数据库中存储的密码哈希值,验证密码是否正确。
认证成功:
如果验证成功,AuthenticationProvider返回一个完全填充的Authentication对象,其中包含用户的角色和权限信息。
AuthenticationManager将此认证对象设置到安全上下文中,使得后续请求能够访问用户信息和权限。
通常会生成一个会话或JWT,并将其发送给客户端,用于后续请求的认证。
认证失败:
认证失败时,抛出异常,由ExceptionTranslationFilter捕获并处理,可能重定向到登录页面显示错误消息,或响应HTTP 401 Unauthorized。
访问控制:
用户携带令牌访问受保护资源时,FilterSecurityInterceptor基于用户的角色和权限进行访问决策,决定是否允许访问。
如上:Spring Security提供了一个强大且灵活的安全认证与授权框架,确保了登录过程的严谨性和安全性。
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xinzhi</groupId>
<artifactId>SpringSecurityDemo2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringSecurityDemo2</name>
<description>SpringSecurityDemo2</description>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-plus的springboot支持-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<artifactId>javax.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties配置文件
# ????
spring.application.name=SpringSecurityDemo
# ???
server.port=8081
#-----------------------------Mysql??
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#mybatis.config-location=classpath:mybatis-config.xml
mybatis-plus.type-aliases-package=com.xinzhi.model
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.configuration.map-underscore-to-camel-case=true
#-----------------------------Mysql
spring.datasource.url=jdbc:mysql://localhost:3306/jdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
server.servlet.session.timeout=1m
建表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, '/admin/**');
INSERT INTO `menu` VALUES (2, '/user/**');
INSERT INTO `menu` VALUES (3, '/guest/**');
-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rid` int(11) NULL DEFAULT NULL,
`mid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES (1, 1, 1);
INSERT INTO `menu_role` VALUES (2, 1, 2);
INSERT INTO `menu_role` VALUES (3, 2, 2);
INSERT INTO `menu_role` VALUES (4, 3, 3);
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'product', '商品管理员');
INSERT INTO `role` VALUES (2, 'admin', '系统管理员');
INSERT INTO `role` VALUES (3, 'user', '用户管理员');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`enabled` tinyint(1) NULL DEFAULT NULL,
`accountNonExpired` tinyint(1) NULL DEFAULT NULL,
`accountNonLocked` tinyint(1) NULL DEFAULT NULL,
`credentialsNonExpired` tinyint(1) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
INSERT INTO `user` VALUES (3, 'han', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
实体类
权限表
package com.xinzhi.model;
import java.util.List;
/**
* 权限表
*/
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
result
package com.xinzhi.model;
import java.io.Serializable;
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功标志
*/
private boolean success = true;
/**
* 返回处理消息
*/
private String message = "操作成功!";
/**
* 返回代码
*/
private Integer code = 0;
/**
* 返回数据对象 data
*/
private T result;
public static long getSerialVersionUID() {
return serialVersionUID;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
/**
* 时间戳
*/
private long timestamp = System.currentTimeMillis();
public Result() {
}
public Result<T> success(String message) {
this.message = message;
this.code = 200;
this.success = true;
return this;
}
public static Result<Object> ok() {
Result<Object> r = new Result<Object>();
r.setSuccess(true);
r.setCode(200);
r.setMessage("成功");
return r;
}
public static Result<Object> ok(String msg) {
Result<Object> r = new Result<Object>();
r.setSuccess(true);
r.setCode(200);
r.setMessage(msg);
return r;
}
public static Result<Object> ok(Object data) {
Result<Object> r = new Result<Object>();
r.setSuccess(true);
r.setCode(200);
r.setResult(data);
return r;
}
public static Result<Object> error(String msg) {
return error(500, msg);
}
public static Result<Object> error(int code, String msg) {
Result<Object> r = new Result<Object>();
r.setCode(code);
r.setMessage(msg);
r.setSuccess(false);
return r;
}
public Result<T> error500(String message) {
this.message = message;
this.code = 500;
this.success = false;
return this;
}
/**
* 无权限访问返回结果
*/
public static Result<Object> noauth(String msg) {
return error(555, msg);
}
}
角色信息表
package com.xinzhi.model;
/**
* 角色信息
*/
public class Role {
/**
* id
*/
private Integer id;
/**
* 角色名
*/
private String name;
/**
* 角色说明
*/
private String content;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content == null ? null : content.trim();
}
}
用户信息表
package com.xinzhi.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* 用户信息
*/
public class User implements UserDetails {
private Integer id;
private String username; //用户名
private String password; //密码
private boolean accountNonExpired; //是否没过期
private boolean accountNonLocked; //是否没被锁定
private boolean credentialsNonExpired; //密码是否没过期
private boolean enabled; //账号是否可用
private Collection<? extends GrantedAuthority> authorities; //用户的权限集合
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true; //暂时未用到,直接返回true,表示账户未过期
}
@Override
public boolean isAccountNonLocked() {
return true; //暂时未用到,直接返回true,表示账户未被锁定
}
@Override
public boolean isCredentialsNonExpired() {
return true; //暂时未用到,直接返回true,表示账户密码未过期
}
@Override
public boolean isEnabled() {
return enabled;
}
}
dao接口
package com.xinzhi.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xinzhi.model.Menu;
import com.xinzhi.model.Role;
import com.xinzhi.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
User selectByUsername(String username);
List<Role> selectRolesByUserId(Integer id);
Integer updatePassword(@Param("username") String username, @Param("password") String password);
List<Menu> selectMenuByRoleIds(@Param("roleIds")List<Integer> roleIds);
}
usermapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xinzhi.dao.UserMapper" >
<sql id="Base_Column_List" >
id, username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
</sql>
<update id="updatePassword">
update user set password=#{password} where username=#{username}
</update>
<select id="selectByUsername" resultType="com.xinzhi.model.User">
select
<include refid="Base_Column_List" />
from user
where username = #{username}
</select>
<select id="selectRolesByUserId" resultType="com.xinzhi.model.Role">
select
r.name,r.id,r.content
from
role r
left join
user_role ur
on r.id = ur.rid
where ur.uid=#{uid}
</select>
<select id="selectMenuByRoleIds" resultType="com.xinzhi.model.Menu">
select
m.id,m.pattern
from menu m
left join menu_role rm on m.id=rm.mid
where
<foreach collection="roleIds" open="rm.rid in(" item="id" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper>
Service层
package com.xinzhi.service;
import com.xinzhi.dao.UserMapper;
import com.xinzhi.model.Menu;
import com.xinzhi.model.Role;
import com.xinzhi.model.User;
import lombok.extern.slf4j.Slf4j;
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.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 权限
*/
@Service
@Slf4j
public class SpringUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByUsername(username);
// 判断用户是否存在
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
// 根据用户ID查找用户的角色列表
List<Role> roles = userMapper.selectRolesByUserId(user.getId());
// 获取角色ID列表
List<Integer> roleIds = roles.stream().map(s -> s.getId()).collect(Collectors.toList());
// 根据角色ID列表查找权限
List<Menu> menus = userMapper.selectMenuByRoleIds(roleIds);
// 获取角色名称列表并加上前缀 "ROLE_"
List<String> collect = roles.stream().map(s -> "ROLE_" + s.getName()).collect(Collectors.toList());
// 获取菜单权限模式
List<String> collect1 = menus.stream().map(m -> m.getPattern()).collect(Collectors.toList());
// 合并菜单权限和角色权限
Collections.addAll(collect1,collect.stream().toArray(String[] ::new));
// 将权限列表设置到用户对象中
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",",collect1)));
// 返回包含用户详情的User对象
return user;
}
}
config配置类
package com.xinzhi.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xinzhi.filter.CaptchaCodeFilter;
import com.xinzhi.service.SpringUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CaptchaCodeFilter captchaCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单认证
http.formLogin()
.loginProcessingUrl("/login")
// .successForwardUrl("/index")
// .failureForwardUrl("/fail") //登录失败跳转地址
//认证成功处理
.successHandler(((request, response, authentication) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "认证成功");
map.put("status", 200);
map.put("用户信息", authentication.getPrincipal());
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
}))
// 认证失败处理
.failureHandler(((request, response, exception) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "认证失败");
map.put("status", 500);
map.put("异常信息", exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
}))
.loginPage("/login.html");
// 拦截
http.authorizeHttpRequests()
.antMatchers("/login.html", "/kaptcha", "/authentication", "/refreshtoken").permitAll()
.antMatchers("/fail.html").permitAll() //fail.html 不需要被认证
.antMatchers("/invalidSession.html").permitAll()
.anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
String requestURI = requestAuthorizationContext.getRequest().getRequestURI();
SimpleGrantedAuthority simpleGrantedAuthority
= new SimpleGrantedAuthority(requestURI);
boolean contains = authorities.contains(simpleGrantedAuthority);
return new AuthorizationDecision(contains);
});
// .antMatchers("/user/detail").hasAuthority("/user/**")
// .antMatchers("/index.html").hasRole("super")
// .anyRequest().authenticated();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(((request, response, accessDeniedException) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "禁止访问");
map.put("status", 403);
map.put("异常信息", accessDeniedException.getMessage());
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
}));
// session管理
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
//.invalidSessionUrl("/invalidSession.html") //非法超时session跳转页面
.maximumSessions(1) // 允许登录的最多用户数量
.maxSessionsPreventsLogin(false) // true表示已经登录就不予许再次登录, false表示允许再次登录但是之前的登录账户会被踢下线
.expiredSessionStrategy(new CustomExpiredSessionStrategy());
/**
* 退出登录
*/
http.logout().logoutSuccessHandler(((request, response, authentication) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "退出成功");
map.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
}));
// 验证码
http.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class);
//
/**
* 记住我
*/
http.rememberMe().tokenValiditySeconds(7 * 24 * 3600);
//跨域
http.csrf().disable();
// 跨域
// http.cors(c -> {
// CorsConfigurationSource source = request -> {
// CorsConfiguration config = new CorsConfiguration();
// config.setAllowedOrigins(Arrays.asList("*"));
// config.setAllowedMethods(Arrays.asList("*"));
// config.addAllowedHeader("*");
// return config;
// };
// c.configurationSource(source);
// });
// CSRF 跨站攻击
// http.csrf()
// .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// .ignoringAntMatchers("/authentication");
}
/**
* 注入加密类
* @return
*/
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Resource
private SpringUserDetailsService userDetailsService;
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
// 加密方式认证
builder.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
Controller层
package com.xinzhi.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/user")
public class TestController {
@RequestMapping("/hello")
@ResponseBody
public String test(){
return "security";
}
@RequestMapping("/index")
public String index(){
return "redirect:/index.html";
}
@PostMapping("/fail")
public String fail(){
return "redirect:/fail.html";
}
@GetMapping("/detail")
public String detail(){
return "detail";
}
@GetMapping("/add")
@ResponseBody
public String add(){
return "add";
}
@GetMapping("/delete")
@ResponseBody
public String delete(){
return "delete";
}
@GetMapping("/update")
@ResponseBody
public String update(){
return "update";
}
@GetMapping("/select")
@ResponseBody
public String select(){
return "select";
}
}
页面
static
login.heml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title></head>
<body>
<!--不想要jwt是把authentication换成login就是普通的了-->
<form action="/authentication" method="post">
用户名:<input type="text" name="username"/> <br>
密码: <input type="password" name="password"/> <br>
验证码:<input type="text" name="captchaCode"/><img src="/kaptcha" id="kaptcha" width="110px" height="40px"/> <br>
<input type="checkbox" name="remember-me" value="on"/>记住密码 <br>
<input type="submit" value="提交"/>
</form>
<script>
window.onload=function(){
var kaptchaImg = document.getElementById("kaptcha");
kaptchaImg.onclick = function(){
kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
}
}
</script>
</body>
</html>
templates
detail.html 退出页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/logout" >退出</a>
详情页
</body>
</html>
fail.html 超时页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>error</title>
</head>
<body>
操作失败,请重新登录. <a href="/login.html">跳转</a>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
首页
</body>
</html>
测试(页面上)
http://localhost:8080/login.html
记住我
在配置类里有相对应的配置类就可已了直接用就好了
在login页面里也有先对应的代码直接用就好了
退出登录
退出页面有相对应的代码
配置文件也有对应的代码
验证码
pom有对应的依赖了他的依赖是
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<artifactId>javax.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
实体类
创建一个vo
CaptchaImageVO
package com.xinzhi.model.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 验证码的实体类
*/
@Data
public class CaptchaImageVO {
//验证码文字
private String code;
//验证码失效时间
private LocalDateTime expireTime;
public CaptchaImageVO(String code, int expireAfterSeconds){
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
}
//验证码是否失效
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
public String getCode() {
return code;
}
}
kaptcha.properties配置文件
#????????
kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100
kaptcha.image.height=45
kaptcha.session.key=code
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=35
kaptcha.textproducer.char.length=4
kaptcha.textproducer.font.names=??,??,????
配置类CaptchaConfig
package com.xinzhi.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import java.util.Properties;
//验证码的配置文件
@Component
@PropertySource(value = {"classpath:kaptcha.properties"})
public class CaptchaConfig {
@Value("${kaptcha.border}")
private String border;
@Value("${kaptcha.border.color}")
private String borderColor;
@Value("${kaptcha.textproducer.font.color}")
private String fontColor;
@Value("${kaptcha.image.width}")
private String imageWidth;
@Value("${kaptcha.image.height}")
private String imageHeight;
@Value("${kaptcha.textproducer.char.length}")
private String charLength;
@Value("${kaptcha.textproducer.font.names}")
private String fontNames;
@Value("${kaptcha.textproducer.font.size}")
private String fontSize;
@Value("${kaptcha.session.key}")
private String sessionKey;
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", border);
properties.setProperty("kaptcha.border.color", borderColor);
properties.setProperty("kaptcha.textproducer.font.color", fontColor);
properties.setProperty("kaptcha.image.width", imageWidth);
properties.setProperty("kaptcha.image.height", imageHeight);
properties.setProperty("kaptcha.session.key", sessionKey);
properties.setProperty("kaptcha.textproducer.char.length", charLength);
properties.setProperty("kaptcha.textproducer.font.names", fontNames);
properties.setProperty("kaptcha.textproducer.font.size",fontSize);
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
Controller层里
CaptchaController
package com.xinzhi.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.xinzhi.model.vo.CaptchaImageVO;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
/**
* 获取验证码
*/
@RestController
public class CaptchaController extends Throwable {
@Resource
DefaultKaptcha captchaProducer;
/**
* 获取验证码
*/
@RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
public void kaptcha(HttpSession session, HttpServletResponse response) throws Exception {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String capText = captchaProducer.createText();
System.out.println(capText);
CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
//将验证码存到session
session.setAttribute("code", capText);
//将图片返回给前端
try(ServletOutputStream out = response.getOutputStream();) {
BufferedImage bi = captchaProducer.createImage(capText);
ImageIO.write(bi, "jpg", out);
out.flush();
}//使用try-with-resources不用手动关闭流
}
}
创建一个Filter包
package com.xinzhi.filter;
import com.alibaba.druid.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
/**
* 验证码
*/
@Component
public class CaptchaCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 必须是登录的post请求才能进行验证,其他的直接放行
if(StringUtils.equals("/login",request.getRequestURI())
&& StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
//1.验证谜底与用户输入是否匹配
if (!validate(new ServletWebRequest(request), response)){
return;
}
}
//通过校验,就放行
filterChain.doFilter(request,response);
}
private Boolean validate(ServletWebRequest request,HttpServletResponse response) throws ServletRequestBindingException, IOException {
HttpSession session = request.getRequest().getSession();
//获取用户登录界面输入的captchaCode
String codeInRequest = ServletRequestUtils.getStringParameter(
request.getRequest(),"captchaCode");
if(StringUtils.isEmpty(codeInRequest)){
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "验证码不能为空");
map.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
return false;
}
// 获取session池中的验证码谜底
String codeInSession = (String)
session.getAttribute("code");
// 请求验证码校验
if(!StringUtils.equals(codeInSession, codeInRequest)) {
HashMap<String, Object> map = new HashMap<>();
map.put("msg", "验证码不匹配");
map.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(map);
response.getWriter().println(s);
return false;
}
return true;
}
}
过滤器替换 前端代码对应的里面都有不用在添加了
9 配置类中验证码路径放行
.antMatchers("/login.html", "/fail.html", "/invalidSession.html","/kaptcha").permitAll()
也有这段代码
整合JTW
-
引入依赖 不用管也有配置类里
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
工具类
package com.xinzhi.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Data
@Component
public class JwtTokenUtil {
private String secret="xinzhi";
private Long expiration=3600000L;
private String header="JWTHeaderName";
/**
* 生成token令牌
*
* @param userDetails 用户
* @return 令token牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
//获取用户名
claims.put("sub", userDetails.getUsername());
// 创建时间
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
/**
* 从claims生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
异常
创建一个exception包
package com.xinzhi.exception;
/**
* jwt异常
*/
public class CustomException extends RuntimeException {
//异常错误编码
private int code ;
//异常信息
private String message;
private CustomException(){}
public CustomException(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
service
JwtAuthService
package com.xinzhi.service;
import com.xinzhi.exception.CustomException;
import com.xinzhi.utils.JwtTokenUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class JwtAuthService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
public UserDetails login(String username, String password) throws CustomException {
try{
// 通过手机号查到用户信息
//使用用户名密码进行登录验证
UsernamePasswordAuthenticationToken upToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch(AuthenticationException e){
throw new CustomException(500, "用户名或密码不正确");
}
//返回了一个用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername( username );
return userDetails;
}
public String refreshToken(String oldToken) {
if (!jwtTokenUtil.isTokenExpired(oldToken)) {
return jwtTokenUtil.refreshToken(oldToken);
}
return null;
}
}
Controller
package com.xinzhi.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.xinzhi.model.vo.CaptchaImageVO;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
/**
* 获取验证码
*/
@RestController
public class CaptchaController extends Throwable {
@Resource
DefaultKaptcha captchaProducer;
/**
* 获取验证码
*/
@RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
public void kaptcha(HttpSession session, HttpServletResponse response) throws Exception {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String capText = captchaProducer.createText();
System.out.println(capText);
CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
//将验证码存到session
session.setAttribute("code", capText);
//将图片返回给前端
try(ServletOutputStream out = response.getOutputStream();) {
BufferedImage bi = captchaProducer.createImage(capText);
ImageIO.write(bi, "jpg", out);
out.flush();
}//使用try-with-resources不用手动关闭流
}
}
Filter
package com.xinzhi.filter;
import com.xinzhi.service.SpringUserDetailsService;
import com.xinzhi.utils.JwtTokenUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* jwt
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private SpringUserDetailsService myUserDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String jwtToken = request.getHeader(jwtTokenUtil.getHeader());
if(!StringUtils.isEmpty(jwtToken)){
String username = jwtTokenUtil.getUsernameFromToken(jwtToken);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//如果可以正确的从JWT中提取用户信息,并且该用户未被授权
if(username != null){
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
if(jwtTokenUtil.validateToken(jwtToken,userDetails)){
//给使用该JWT令牌的用户进行授权
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request,response);
}
}
后台日志记录
依赖 已经有了
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
代码
package com.xinzhi.aspest;
import com.xinzhi.model.User;
import lombok.Data;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Component("myAspect")
@Aspect
@Data
public class MyAspect {
// 打印日志
private final static Logger logger= LoggerFactory.getLogger(MyAspect.class);
//@Before指在切点方法之前执行,也就是在Controller层方法执行之前执行,这里可以通过JoinPoint获取一些有关方法的信息,在这里也可以修改参数的值
//@Before()括号里设置的是切点方法的名称
@Before("execution(public * com.xinzhi.controller..*.*(..))")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal.equals("anonymousUser")){
logger.info("匿名用户");
return;
}
User user =(User)(principal);
// 记录下请求内容
logger.info("URL : " + request.getRequestURI().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
logger.info("用户 : " + user.getUsername());
}
}