SpringSecurity的安全认证的详解说明(附完整代码)
SpringSecurity
登录认证和请求过滤器以及安全配置详解说明
环境
系统环境:win10
Maven环境:apache-maven-3.8.6
JDK版本:1.8
SpringBoot版本:2.7.8
根据用户名密码登录
根据用户名和密码登录,登录成功后返回
Token
数据,将token放到请求头中
,每次请求后台携带token
数据
认证成功,返回请求数据
携带token请求后台
,后台认证成功,过滤器放行,返回请求数据
认证失败,SpringSecurity拦截请求
携带token请求后台,后台认证失败,请求被拦截
数据表结构
CREATE TABLE `sys_user` (`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
下面是本次
Demo
的项目代码和说明
项目环境依赖
<?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><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.8</version><relativePath/></parent><groupId>cn.molu.security.jwt</groupId><artifactId>SpringSecurity-JWT</artifactId><version>0.0.1-SNAPSHOT</version><name>SpringSecurity-JWT</name><description>SpringSecurity-JWT</description><properties><java.version>1.8</java.version></properties><dependencies><!--SpringSecurity安全框架--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--启用SpringBoot对Web的支持--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--热部署插件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><!--Lombok实体类简化组件--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--lang3对象工具包--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!--hutool工具包,数据加解密,对象判空转换等--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.10</version></dependency><!-- UA解析工具(从request中解析出访问设备信息) --><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency><!--生成token依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- MySQL数据连接驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.27</version></dependency><!--MyBatis-Plus操作数据库--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
项目启动入口
使用
MyBatis-Plus
操作数据库,配置扫描mapper
所在的包
@SpringBootApplication
@MapperScan("cn.molu.security.jwt.mapper")
public class SpringSecurityJwtApplication {public static void main(String[] args) {SpringApplication.run(SpringSecurityJwtApplication.class, args);}
}
项目配置文件
MySQL地址、项目访问端口、token有效期
spring:# 数据库链接配置datasource:url: jdbc:mysql://127.0.0.1:3306/security?characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverapplication:name: SpringSecurity-JWT# 热部署devtools:restart:enabled: trueadditional-paths: src/main/java# 服务端口
server:port: 8090# 测试时将token有效期为5分钟
token:expire: 300000# 用于生成JWT的盐值
jwt:secret: 1234567890
项目启动和关闭日志
项目启动和关闭时控制台打印相关提示信息
/*** @ApiNote: 项目启动和关闭时的日志打印* @Author: 陌路* @Date: 2023/2/18 9:46* @Tool: Created by IntelliJ IDEA*/
@Slf4j
@Component
public class AppStartAndStop implements ApplicationRunner, DisposableBean {@Value("${server.port}")private String port;/*** @apiNote: 项目启动时运行此方法*/@Overridepublic void run(ApplicationArguments args) {log.info("==============项目启动成功!==============");log.info("请访问地址:http://{}:{}", ApiUtils.getHostIp(), port);log.info("=======================================");}/*** @apiNote: 项目关闭时执行* @return: void*/@Overridepublic void destroy() {log.info("=======================================");log.info("============程序已停止运行!============");log.info("=======================================");}
}
封装统一响应实体类
统一返回给前台的数据实体
package cn.molu.security.jwt.vo;import cn.molu.security.jwt.utils.ApiUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.HashMap;/*** @ApiNote: 封装响应实体对象* @Author: 陌路* @Date: 2023/2/10 9:42* @Tool: Created by IntelliJ IDEA.*/
@NoArgsConstructor // 生成无参构造方法
@ToString(callSuper = true) // 重写toString方法
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> extends HashMap<String, Object> implements Serializable {private static final long serialVersionUID = 2637614641937282252L;// 返回结果数据public T result;// 返回成功失败标记public static Boolean flag;// 返回成功状态码public static final Integer SUCCESS = 200;// 返回失败状态码public static final Integer FIELD = 500;/*** @apiNote: 返回数据* @param: code 状态码 [返回给前台的状态码]* @param: msg 提示消息 [返回给前台得消息]* @param: result 响应数据结果[返回给前台得结果]* @param: flag 响应标志[true:成功,false:失败]* @return: Result*/public static Result result(Integer code, String msg, Object result, Boolean flag) {Result r = new Result();r.put("code", code);r.put("msg", msg);r.put("result", result);r.put("flag", flag);r.result = result;Result.flag = flag;return r;}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result ok(Integer code, String msg, Object result) {return result(code, msg, result, true);}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result ok(String msg, Object result) {return result(SUCCESS, msg, result, true);}/*** @apiNote: 返回成功数据* @param: result 响应数据结果* @return: Result*/public static Result ok(Object result) {return result(SUCCESS, null, result, true);}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @return: Result*/public static Result ok(String msg) {return result(SUCCESS, msg, null, true);}/*** @apiNote: 返回成功数据* @return: Result*/public static Result ok() {return result(SUCCESS, null, null, true);}/*** @apiNote: 返回失败数据* @param: msg 错误消息* @param: result 响应数据结果* @return: Result*/public static Result err(Integer code, String msg, Object result) {return result(code, msg, result, false);}/*** @apiNote: 返回失败数据* @param: code 响应状态码* @param: msg 错误消息* @return: Result*/public static Result err(Integer code, String msg) {return result(code, msg, null, false);}/*** @apiNote: 返回失败数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result err(String msg, Object result) {return result(FIELD, msg, result, false);}/*** @apiNote: 返回失败数据* @param: result 响应数据结果* @return: Result*/public static Result err(Object result) {return result(FIELD, null, result, false);}/*** @apiNote: 返回失败数据* @param: msg 错误消息* @return: Result*/public static Result err(String msg) {return result(FIELD, msg, null, false);}/*** @apiNote: 返回失败数据* @return: Result*/public static Result err() {return result(FIELD, null, null, false);}/*** @apiNote: 返回数据* @param: [code, result, msg, flag]* @return: cn.molu.api.vo.Result*/public static Result res(Integer code, Object result, String msg, boolean flag) {return result(code, msg, result, flag);}/*** @apiNote: 返回数据* @param: [flag, result]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, Object result) {return result(flag ? SUCCESS : FIELD, null, result, flag);}/*** @apiNote: 返回数据* @param: [flag, result]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, String msg, Object result) {return result(flag ? SUCCESS : FIELD, msg, result, flag);}/*** @apiNote: 返回数据* @param: [flag, msg]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, String msg) {return result(flag ? SUCCESS : FIELD, msg, null, flag);}/*** @apiNote: 返回数据* @param: [flag, msg]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag) {return result(flag ? SUCCESS : FIELD, null, null, flag);}/*** @apiNote: 重写HashMap的put方法* @param: [key, value]* @return: Result*/@Overridepublic Result put(String key, Object value) {super.put(key, value);return this;}public <T> T getResult() {return ApiUtils.getObj(this.result, null);}public void setRes(boolean flag, T result) {this.flag = flag;this.result = result;put("flag", flag);put("result", result);}
}
封装对象工具类
封装静态方法工具类,便于在项目中使用
/*** @ApiNote: api通用工具类* @Author: 陌路* @Date: 2023/2/10 9:26* @Tool: Created by IntelliJ IDEA.*/
public class ApiUtils {/*** @apiNote: 获取设备ip* @return: String*/public static String getHostIp() {try {return InetAddress.getLocalHost().getHostAddress();} catch (UnknownHostException e) {return "127.0.0.1";}}/*** @apiNote: 将对象转为字符串数据* @param: [obj:带转换对象]* @return: java.lang.String*/public static String getStr(Object obj) {String str = Objects.nonNull(obj) ? String.valueOf(obj).trim().replaceAll("\\s*|\r|\n|\t", "") : "";return "null".equalsIgnoreCase(str) ? "" : str;}/*** @apiNote: 将对象转为字符串数据, obj为空时返回defaultVal值* @param: [obj, defaultVal]* @return: java.lang.String*/public static String getStr(Object obj, String defaultVal) {final String str = getStr(obj);return StringUtils.isBlank(str) ? defaultVal : str;}/*** @apiNote: 当对象obj为空时返回defaultVal值* @param: [obj, defaultVal]* @return: java.lang.Object*/public static <T> T getObj(Object obj, Object defaultVal) {final String str = getStr(obj);if (StringUtils.isBlank(str) && ObjUtil.isNull(defaultVal)) {return null;}return (T) (StringUtils.isBlank(str) ? defaultVal : obj);}/*** @apiNote: 校验数据是否为空* @param: [msg, val]* @return: void*/public static void hasText(String msg, Object... val) {if (ObjUtil.hasNull(val) || !ObjUtil.isAllNotEmpty(val) || val.length == 0 || StringUtils.isBlank(getStr(val))) {Assert.hasText(null, msg);}}/*** @apiNote: 向前台输出数据* @param: [obj, response]* @return: void*/public static void printJsonMsg(Object obj, HttpServletResponse response) {if (ObjUtil.isAllNotEmpty(obj, response)) {response.reset();response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");try (final PrintWriter writer = response.getWriter()) {writer.print(obj);writer.flush();} catch (IOException ignored) {}}}/*** @apiNote: 校验数据是否未空,为空则抛出异常* @param: tipMsg:异常提示信息* @param: params:需要校验的参数值*/public static void checkParamsIsEmpty(String tipMsg, Object... params) {if (ObjUtil.isNull(params) || !ObjUtil.isAllNotEmpty(params)) {throw new RuntimeException(getStr(tipMsg, "校验失败:参数值为空!"));}}
}
Token工具类
封装
token
工具类,用于生成token
和解析token
数据
/*** @ApiNote: token工具类* @Author: 陌路* @Date: 2023/02/10 16:00* @Tool: Created by IntelliJ IDEA*/
@Component
public class TokenUtils {@Resourceprivate ContextLoader contextLoader;@Value("${jwt.secret}")private String secret;/*** @apiNote: 生成token* @param: userId 用户id* @param: timeMillis 时间戳,每次生成的Token都不一样* @return: token*/public String createToken(Long userId, Long timeMillis) {ApiUtils.checkParamsIsEmpty("生成Token失败,userId不能为空!", userId);timeMillis = timeMillis == null ? System.currentTimeMillis() : timeMillis;String token = Jwts.builder().claim("userId", userId).claim("timeMillis", timeMillis).signWith(SignatureAlgorithm.HS256, secret).compact();contextLoader.setCache(userId + "_KEY", token);return token;}/*** @apiNote: 解析token数据* @param: token* @return: map*/public Map<String, Object> verifyToken(String token) {return StringUtils.isEmpty(token) ? new HashMap<>() : ApiUtils.getObj(Jwts.parser().setSigningKey(secret).parse(token).getBody(), new HashMap<>());}/*** @apiNote: 根据token获取userId* @param: token* @return: userId*/public String getUserId(String token) {return ApiUtils.getStr(verifyToken(token).get("userId"));}
}
通过MyBatis-Plus操作数据库
/*** @ApiNote: userMapper$* @Author: 陌路* @Date: 2023/2/18 11:13* @Tool: Created by IntelliJ IDEA*/
@Mapper
public interface UserMapper extends BaseMapper<User> {}
封装缓存工具类
封装数据缓存类,用于缓存数据(
项目中使用redis做数据缓存
)
一般数据缓存是用redis
来做的,为了简便我这里就用了Map
/*** @ApiNote: 初始化缓存加载类* @Author: 陌路* @Date: 2023/2/10 9:29* @Tool: Created by IntelliJ IDEA.* @Desc: 正式开发中缓存数据应该放到redis中*/
@Component
public class ContextLoader {// 缓存用户数据public static final Map<String, LoginUser> CACHE_USER = new HashMap<>(2);// 缓存参数数据public static final Map<String, Object> CACHE_PARAM = new HashMap<>(4);// 数据有效时长@Value("${token.expire}")private long expire;/*** @apiNote: 根据token获取用户数据* @param: [token]* @return: cn.molu.api.pojo.User*/public LoginUser getCacheUser(String token) {if (StringUtils.isNotEmpty(token) && CACHE_USER.containsKey(token)) {final LoginUser loginUser = ApiUtils.getObj(CACHE_USER.get(token), new LoginUser());Long expire = ApiUtils.getObj(loginUser.getExpire(), 0);long currentTimeMillis = System.currentTimeMillis();if ((expire > currentTimeMillis)) {if (expire - currentTimeMillis <= this.expire) {setCacheUser(token, loginUser);}return loginUser;}CACHE_USER.remove(token);}return new LoginUser();}/*** @apiNote: 添加缓存数据到CACHE_USER中* @param: [token, user]* @return: cn.molu.api.pojo.User*/public void setCacheUser(String token, LoginUser loginUser) {if (StringUtils.isNotEmpty(token)) {loginUser.setExpire(System.currentTimeMillis() + expire);CACHE_USER.put(token, loginUser);}}/*** @apiNote: 向CACHE_PARAM中添加缓存数据* @param: [key, val]* @return: void*/public void setCache(String key, Object val) {if (StringUtils.isNotEmpty(key)) {CACHE_PARAM.put(key, val);}}/*** @apiNote: 删除CACHE_USER中的用户数据* @param: key* @return: void*/public void deleteUser(String key) {if (StringUtils.isNotBlank(key) && this.CACHE_USER.containsKey(key)) {this.CACHE_USER.remove(key);}}/*** @apiNote: 删除CACHE_PARAM中的数据* @param: key* @return: void*/public void deleteParam(String key) {if (StringUtils.isNotEmpty(key) && this.CACHE_PARAM.containsKey(key)) {this.CACHE_PARAM.remove(key);}}
}
用户对象实体类
用户对象,对应数据库中的
sys_user
表
/***@ApiNote: 用户对象实体类,对应数表sys_user*@Author: 陌路*@Date: 2023/2/18 20:46*@Tool: Created by IntelliJ IDEA*/
@Data
@NoArgsConstructor
@TableName("sys_user")
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class User implements Serializable {private static final long serialVersionUID = -40356785423868312L;@TableIdprivate Long id;//主键private String userName;//用户名private String nickName;//昵称private String password;//密码private String status;//账号状态(0正常 1停用)private String email;// 邮箱private String phone;//手机号private String sex;//用户性别(0男,1女,2未知)private String avatar;//头像private String userType;//用户类型(0管理员,1普通用户)private Long createBy;//创建人的用户idprivate Date createTime;//创建时间private Long updateBy;//更新人private Date updateTime;//更新时间private Integer delFlag;//删除标志(0代表未删除,1代表已删除)
}
==SpringSecurity核心内容==
核心:用户认证(登录)
SpringSecurity
:登录业务需要实现SpringSecurity
接口(UserDetailsService
)中提供的方法(loadUserByUsername
)并返回SpringSecurity
提供的UserDetails
接口对象
/*** @ApiNote: 用户数据认证* @Author: 陌路* @Date: 2023/2/18 11:34* @Tool: Created by IntelliJ IDEA*/
@Service("userDetailsImpl")
public class UserDetailsImpl implements UserDetailsService {@Resourceprivate UserMapper userMapper;/*** @apiNote: 根据用户名获取用户数据* @param: username 用户名* @return: UserDetails*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名查询用户数据User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));ApiUtils.checkParamsIsEmpty("未获取到用户数据,请检查用户名和密码是否正确!", user);// 根据用户信息查询相关权限// TODO: 权限相关配置后面实现,目前先做认证 // 将用户数据封装到LoginUser中并返回return new LoginUser(user);}
}
核心:实现接口封装用户数据
SpringSecurity
:存储当前登录用户数据,需要实现SpringSecurity
提供的接口对象(UserDetails
),通过LoginUser
对象来接收loadUserByUsername
返回的用户登录数据
/*** @ApiNote: 封装登录用户数据* @Author: 陌路* @Date: 2023/2/18 11:55* @Tool: Created by IntelliJ IDEA*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LoginUser implements UserDetails {// 实现SpringSecurity提供的UserDetails接口来管理用户数据private User user; // 用户数据对象private Long expire; // 过期时间private String token; // token// 构造方法public LoginUser(User user) {this.user = user;}/*** @apiNote: 获取当前登录用户信息*/public static LoginUser getLoginUser() {LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return ApiUtils.getObj(loginUser, new LoginUser());}/*** @apiNote: 用户权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}/*** @apiNote: 获取用户密码*/@Overridepublic String getPassword() {return user.getPassword();}/*** @apiNote: 获取用户名*/@Overridepublic String getUsername() {return user.getUserName();}/*** @apiNote: 是否未过期(true:未过期,false:已过期)*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** @apiNote: 是否锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** @apiNote: 是否超时(true:未超时,false:已超时)*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** @apiNote: 当前用户是否可用(true:可用,false:不可用)*/@Overridepublic boolean isEnabled() {return true;}
}
核心:SpringSecurity配置类
SpringSecurity
:核心配置类,用于配置自定义过滤器、拦截和放行用户请求
WebSecurityConfigurerAdapter
:此方法已过时,可使用SecurityFilterChain
来配置,以下有说明
/*** @ApiNote: SpringSecurity配置信息* @Author: 陌路* @Date: 2023/2/18 12:14* @Tool: Created by IntelliJ IDEA*/
//@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 注入自定义的过滤器,在用户名和密码认证之前执行(UsernamePasswordAuthenticationFilter之前)@Resourceprivate TokenAuthorityFilter tokenAuthorityFilter;/*** @apiNote: 注入密码加密工具*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** @apiNote: 注入AuthenticationManager对象来实现登录逻辑管理*/@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}/*** @apiNote: 配置请求认证和拦截*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 关闭Security的CSRF功能防御http.csrf().disable()// 不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 允许所有用户访问登录路径.antMatchers("/user/login").anonymous()//匿名访问(未登录未认证的)// 除以上请求路径外,其他所有请求都必须经过认证才能访问成功.anyRequest().authenticated();// 添加自定义的请求过滤器(tokenAuthorityFilter)并定义在指定哪个过滤器(UsernamePasswordAuthenticationFilter)执行前执行http.addFilterBefore(tokenAuthorityFilter, UsernamePasswordAuthenticationFilter.class);}// 测试密码的加密和密码的验证public static void main(String[] args) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();// 加密后的密文,每次加密结果都不一样,因为加密时会生成随机盐值String encode = passwordEncoder.encode("123456");// 校验用户输入的密码和加密后的密码是否一样,一样返回true,否则返回falseboolean matches = passwordEncoder.matches("123456", encode);System.out.println("encode = " + encode);System.out.println("matches = " + matches);}
}
以上对SpringSecurity配置的方法已过时
可以使用以下方法对SpringSecurity进行配置
/*** @ApiNote: SpringSecurity配置信息* @Author: 陌路* @Date: 2023/2/18 12:14* @Tool: Created by IntelliJ IDEA*/
@Configuration
public class SecurityConfiguration {@Resourceprivate TokenAuthorityFilter tokenAuthorityFilter;@Resourceprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManager() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}/*** @apiNote: 注入密码加密工具*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 允许所有用户访问登录路径:anonymous(匿名访问,即允许未登录时访问,登录时则不允许访问).antMatchers("/user/login").anonymous()// 除以上请求路径外,其他所有请求都必须经过认证才能访问成功.anyRequest().authenticated().and()// 添加自定义的请求过滤器(tokenAuthorityFilter)并定义在指定哪个过滤器(UsernamePasswordAuthenticationFilter)执行前执行.addFilterBefore(tokenAuthorityFilter, UsernamePasswordAuthenticationFilter.class);// 添加异常处理器http.exceptionHandling()// 认证异常处理器.authenticationEntryPoint(authenticationEntryPoint);// 运行跨域配置//http.cors();return http.build();}
}
核心:自定义请求过滤器
SpringSecurity
:自定义请求过滤器需要继承OncePerRequestFilter
类,并重写里面的doFilterInternal
方法来实现具体的业务逻辑
/*** @ApiNote: 请求过滤器:是否认证是否有权访问* @Author: 陌路* @Date: 2023/2/18 13:04* @Tool: Created by IntelliJ IDEA*/
@Component
public class TokenAuthorityFilter extends OncePerRequestFilter {@Resourceprivate TokenUtils tokenUtils;@Resourceprivate ContextLoader contextLoader;/*** @apiNote: 请求过滤器*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 获取token数据String authorityToken = ApiUtils.getStr(request.getHeader("Authorization"));// token为空直接放行if (StringUtils.isBlank(authorityToken)) {filterChain.doFilter(request, response);return;}// 解析token数据得到userIdString userId = tokenUtils.getUserId(authorityToken);// 从缓存中获取用户信息LoginUser loginUser = contextLoader.getCacheUser(userId + "_TOKEN_" + authorityToken);ApiUtils.checkParamsIsEmpty("请求失败,认证已过期!", loginUser, loginUser.getUser());// 将用户信息封装到SecurityContextHolder中//principal:用户数据,credentials:,authenticated:权限信息UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}
}
核心:SpringSecurity异常处理
认证失败:
- 实现
SpringSecurity
提供的AuthenticationEntryPoint
接口中的commence
方法来处理认证失败后的业务- 统一处理:统一返回JSON异常提示信息
- 在
SpringSecurity
配置类(SecurityConfiguration
)中添加http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
即可
/*** @ApiNote: 认证失败处理类* @Author: 陌路* @Date: 2023/2/19 12:25* @Tool: Created by IntelliJ IDEA*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {/*** @apiNote: 认证失败处理* @return: JSON(认证失败,请重新登录)*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String authExceptionMessage = authException.getMessage();authExceptionMessage = StringUtils.isBlank(ApiUtils.getStr(authExceptionMessage)) ? "认证失败,请重新登录!" : authExceptionMessage;String jsonStr = JSONUtil.toJsonStr(Result.err(HttpStatus.UNAUTHORIZED.value(), authExceptionMessage));ApiUtils.printJsonMsg(jsonStr, response);}
}
后台请求接口
用户请求后台接口:登录接口、查询用户信息接口、注销登录接口
/*** @ApiNote: 请求接口控制器* @Author: 陌路* @Date: 2023/2/18 9:53* @Tool: Created by IntelliJ IDEA*/
@RestController
@RequestMapping("/user/*")
public class IndexController {@Resourceprivate UserService userService;/*** @apiNote: 获取用户列表* @return: Result*/@GetMapping("getUserList")public Result getUserList() {return Result.ok(userService.queryList());}/*** @apiNote: 用户登录接口* @param: User对象实体* @return: Result*/@PostMapping("login")public Result login(@RequestBody User user) {return userService.login(user);}/*** @apiNote: 用户退出登录* @return: Result*/@GetMapping("logout")public Result logout() {return Result.res(userService.logout());}
}
请求接口实现类
用户请求接口实现类型:登录、获取用户数据、注销登录
/*** @ApiNote: userService$* @Author: 陌路* @Date: 2023/2/18 11:28* @Tool: Created by IntelliJ IDEA*/
@Service("userService")
public class UserServiceImpl implements UserService {@Value("${token.expire}")private long expire;@Resourceprivate UserMapper userMapper;@Resourceprivate TokenUtils tokenUtils;@Resourceprivate ContextLoader contextLoader;@Resourceprivate AuthenticationManager authenticationManager;/*** @apiNote: 查询所有用户数据*/public List<User> queryList() {return userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getDelFlag, 0));}/*** @apiNote: 用户登录:缓存用户数据* @param: User* @return: Result*/public Result login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);ApiUtils.checkParamsIsEmpty("登录失败!", authenticate);LoginUser loginUser = ApiUtils.getObj(authenticate.getPrincipal(), new LoginUser());long currentTimeMillis = System.currentTimeMillis();String token = tokenUtils.createToken(loginUser.getUser().getId(), currentTimeMillis);loginUser.setToken(token);loginUser.setExpire(currentTimeMillis + expire);contextLoader.setCacheUser(loginUser.getUser().getId() + "_TOKEN_" + token, loginUser);return Result.ok("登录成功!", token);}/*** @apiNote: 用户退出登录,删除用户缓存数据*/public boolean logout() {LoginUser loginUser = LoginUser.getLoginUser();Long id = loginUser.getUser().getId();String token = loginUser.getToken();contextLoader.deleteUser(id + "_TOKEN_" + token);contextLoader.deleteParam(id + "_KEY");return true;}
}
项目接口调用实例
在请求体中输入用户名和密码进行登录(登录时请求头不需要携带token)请求
/user/login
接口,登录成功!
请求头中携带
token
,请求/user/getUserList
接口,获取用户列表数据,请求成功!
请求头中携带
token
请求/user/logout
接口退出登录,请求成功!
退出登录后,携带
toekn
再次访问/user/getUserList
获取用户列表接口,可以看到请求被拒绝访问,后台校验失败,提示请求失败,认证已过期!
到此SpringSecurity
登录认证部分已结束,希望这篇文章对您有所帮助
下一篇SpringSecurity
的权限校验
SpringSecurity的权限校验详解说明(附完整代码)
https://blog.csdn.net/qq_51076413/article/details/129106824
相关文章:

SpringSecurity的安全认证的详解说明(附完整代码)
SpringSecurity登录认证和请求过滤器以及安全配置详解说明 环境 系统环境:win10 Maven环境:apache-maven-3.8.6 JDK版本:1.8 SpringBoot版本:2.7.8 根据用户名密码登录 根据用户名和密码登录,登录成功后返回Token数据…...

详解制造业业务数据模型
业务数据在企业数字化转型或单体应用的开发中都是至关重要的。站在跨业务跨部门的企业数字化转型角度,离不开业务架构的设计,详细的业务领域和业务数据模型是后续应用架构和数据架构的必要输入。站在单部门单场景的信息化角度,应用程序的需求…...
BigDecimal使用注意避坑
目录一. BigDecimal的初始化精度丢失问题二. BigDecimal在进行除法运算时需设置精度,否则对于除不尽的情况会抛出异常三. 不要使用BigDecimal的equals方法比较大小, 否则可能会因为精度问题导致比较结果和预期的不一致在java.math包中提供了对大数字的操作类,用于进…...

windows环境下,vue启动项目后打开chrome浏览器
前言:关于vue启动后打开chrome浏览器,我查了很多资料,方案如下: 1、增加环境变量BROWSER为chrome(试了没效果) 2、设置系统的默认浏览器为chrome(应该可以,但没试;因为…...
SpringBoot2.X整合ClickHouse项目实战-从零搭建整合(三)
一、ClickHouseSpringBoot2.XMybatisPlus整合搭建 二、需求描述和数据库准备 三、ClickHouse统计SQL编写实战和函数使用 四、ClickHouseSpringBoot2.X案例-基础模块搭建 controller/request层 mapper层 model层 service层 五、ClickHouseSpringBoot2.X案例-数据统计接口 …...

学海记录项目测试报告
⭐️前言⭐️ 本篇文章是博主基于学海记录的个人项目所做的测试报告,用于总结运用自动化测试技术,应用于自己的项目。 🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁 🍉博主将持续更新学习记录…...

【1792. 最大平均通过率】
来源:力扣(LeetCode) 描述: 一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] [passi, totali] ,表示你…...

言简意赅+图解 函数传参问题(传值、传地址 500字解决战斗)
1、传值 2、传地址 不论是传值,还是传地址,形参都是对于实参的一份拷贝 下图为按值传递进行交换: 形参left拷贝一块新空间,形参right拷贝一块新空间 下图为按指针传递进行交换 形参left拷贝一块新的空间,形参right…...

UML-时序图以及PlantUML绘制
介绍 时序图(Sequence Diagram),又名序列图、循序图,是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。它可以表示用例的行为顺序,当执行一个用例行为时,其中的每条消息…...

【Redis】Redis 有序集合 Zset 操作 ( 简介 | 查询操作 | 增加操作 | 删除操作 | 修改操作 )
文章目录一、有序集合 Zset二、查询操作1、查询 Zset 所有数据2、查询 Zset 所有数据和评分3、查询指定评分范围的 Zset 数据4、查询指定评分范围的 Zset 数据并从大到小排序5、统计指定评分范围的 Zset 数据个数6、查询指定元素在 Zset 有序集合中的排名三、增加操作1、向 Red…...

Java特性之设计模式【策略模式】
一、策略模式 概述 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式 在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略…...
IR-CUT 保证摄像机成像效果的滤镜
IR-CUT双滤镜是指在摄像头镜头组里内置了一组滤镜,当镜头外的红外感应点侦测到光线的强弱变化后,内置的IR-CUT自动切换滤镜能够根据外部光线的强弱随之自动切换,使图像达到最 佳效果。也就是说,在白天或黑夜下,双滤光片…...

openpnp - 普通航空插头和PCB的连接要使用线对板连接器
文章目录openpnp - 普通航空插头和PCB的连接要使用线对板连接器概述改进实际效果总结ENDopenpnp - 普通航空插头和PCB的连接要使用线对板连接器 概述 和同学讨论问题, 准备将航空插头连接到PCB上. 航空插头选用GX12-4公头, 拧到开孔的铁板上. 然后航空插头公头再与PCB连接. 铁…...

Python3 错误和异常实例及演示
作为 Python 初学者,在刚学习 Python 编程时,经常会看到一些报错信息,在前面我们没有提及,这章节我们会专门介绍。 Python 有2种错误很容易辨认:语法错误和异常。 Python assert(断言)用于判断…...
Android 9.0第三方app根据包名设置为横屏显示
1.概述 在android9.0的系统rom定制化开发中,在某些横屏的设备比如平板电脑,tv智能电视,广告机等等设备中,通常系统是默认横批显示的,但是在安装一些竖屏app的时候, 就会旋转为竖屏,这个时候操作app也不方便,所以产品需求要求竖屏也需要根据包名横屏显示出来,这就需要在…...
MySQL会导致索引失效的情况与解决索引失效的方法
什么情况会导致索引失效 索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些: 1.使用 SELECT * 进行查询;2.创建了组合索引,但查询条件未准守最左匹配原则;3.在索引列上进行计算、函数、类型转换等操作;4.以 % 开头的 L…...

使用nginx单独部署Vben应用
前言 本文主要介绍Vben使用nginx单独部署的方式,其实前端发展到现在已经不是当年的jsp,asp必须要和后端一起部署了。单独部署调试的工具也很多,比如vue-cli-service 和 Vben中用到的vite ,当然这些我们一般用在开发的工程中。正式…...

ES6新特性详解
文章目录1. let和const1.1 let声明变量1.2 const声明常量2. 模板字符串3. 解构赋值3.1 数组的解构赋值3.2 对象的解构赋值4. 函数扩展4.1 参数默认值4.2 剩余参数4.3 箭头函数5. 对象扩展5.1 对象简写5.2 属性名表达式5.3 扩展运算符6. Symbol7. Iterator和Generator7.1 Iterat…...

Ubuntu下安装 ntfs-3g
目录1.FAT32、NTFS和exFAT2.ubuntu 安装 ntfs-3g2.1 直接安装2.2 源码安装1.FAT32、NTFS和exFAT U盘在格式化的时候都会有三种格式分别是FAT32、NTFS和exFAT。 FAT32格式 FAT32格式硬盘分区的最大容量为2TB,虽然U盘做不到,但是现在1xTB硬盘都有了&…...
【专业认知】抖音就业 / 保研北大教育学 / 留学南加州EE / 微软就业
2023.2.18 一. 周金辉学长分享——本科经验分享 0 简介 计算机农大本硕 硕士毕业后在抖音公司工作 1 行业前景:计算机专业能做什么? 1.1 计算机行业发展路线 远古时代: 二战开始,计算机技术发展,出现互联网 包…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...

客户案例 | 短视频点播企业海外视频加速与成本优化:MediaPackage+Cloudfront 技术重构实践
01技术背景与业务挑战 某短视频点播企业深耕国内用户市场,但其后台应用系统部署于东南亚印尼 IDC 机房。 随着业务规模扩大,传统架构已较难满足当前企业发展的需求,企业面临着三重挑战: ① 业务:国内用户访问海外服…...