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 计算机行业发展路线 远古时代: 二战开始,计算机技术发展,出现互联网 包…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
ubuntu22.04有线网络无法连接,图标也没了
今天突然无法有线网络无法连接任何设备,并且图标都没了 错误案例 往上一顿搜索,试了很多博客都不行,比如 Ubuntu22.04右上角网络图标消失 最后解决的办法 下载网卡驱动,重新安装 操作步骤 查看自己网卡的型号 lspci | gre…...
如何在Windows本机安装Python并确保与Python.NET兼容
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)
旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据!该数据集源自2025年4月发表于《地理学报》的论文成果…...
VSCode 使用CMake 构建 Qt 5 窗口程序
首先,目录结构如下图: 运行效果: cmake -B build cmake --build build 运行: windeployqt.exe F:\testQt5\build\Debug\app.exe main.cpp #include "mainwindow.h"#include <QAppli...
