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的安全认证的详解说明(附完整代码)相关推荐

  1. C语言实现扫雷完整算法详解~(附完整代码~)

    扫雷是一个常见小游戏,那么如何用C语言实现扫雷呢?学习了二维数组之后,我们可将扫雷的网格区域存储为二维数组,从而使用C语言实现扫雷. 目录 1.算法基本思路 2.算法详解 1.初始化数组与打印数组 2 ...

  2. ECharts 数据各种图自适应 可视化 项目过程详解(附完整代码)

    前言: 本篇文章的学习目的: 1.可视化面板布局适配屏幕 2.利用ECharts 实现柱状图展示 实现的技术栈: 基于 flexible.js +rem 智能大屏适配 VScode cssrem插件 ...

  3. 基于MATLAB的拼图游戏设计(图文详解,附完整代码)

                                                                               基于MATLAB的拼图游戏设计 内容摘要:MATL ...

  4. 幸运数 c++程序(详解,附完整代码)

    标题[蓝桥杯][2013年第四届真题]幸运数 时间限制: 1Sec 内存限制: 128MB 提交: 696 解决: 326 题目描述 幸运数是波兰数学家乌拉姆命名的.它采用与生成素数类似的" ...

  5. Pytorch实现手写体识别(基于CNN卷积神经网络)实操详解(附完整代码free)

    手写体作为深度学习的"Hello world",几乎所有的课程都会讲到这个,今天带着大家一些看看吧. 如果对你有所帮助,点个赞给个小关注,以后一起交流学习. 前言 首先和大家讲讲我 ...

  6. JAVA集合详解(附完整代码)

    Collections(集合) 一.常用的集合相关接口与实现类 ​ 在Java中所有的集合类都源自Iterable接口,Colletion继承Iterable接口,Collection下有三个子接口, ...

  7. 动态规划——矩阵连乘问题算法及实现详解(附完整代码)

    问题分析 矩阵连乘问题是经典的动态规划问题,其主要是n个矩阵进行矩阵乘法运算时,通过括号改变运算的先后顺序,减少运算次数,找到最佳划分方法,求解最少运算次数. 算法分析 矩阵连乘问题中动态规划可以帮助 ...

  8. 二叉树遍历详解(附完整代码)

    二叉树 1.最常见的遍历种类 先序遍历 中序遍历 后序遍历 层次遍历 二:遍历的递归代码实现分别给出Java 代码和C代码实现 2.二叉树的存储结构定义: 1)顺序存储结构 2) 链式存储结构 二叉链 ...

  9. Three.js实例详解___旋转的精灵女孩(附完整代码和资源)(一)

    Three.js实例详解___旋转的精灵女孩(附完整代码和资源)(一) 本文目录: 一.[旋转的精灵女孩]案例运行效果 二.Three.js简介 三.Three.js代码正常运行显示条件 (1)不载入 ...

最新文章

  1. Python能让你上天?带你挖掘隐藏彩蛋~(附代码)
  2. Silverlight学习笔记之使用TranslateTransform控制对象位置
  3. Windows消息机制要点
  4. Spyder常用快捷键
  5. Git和Github实现代码同步
  6. css 使用本地字体
  7. Mapped Statements collection already contains value for com.wen.mapper.ProjectMapper.xxx
  8. 网络技术人员要知道的100个安全工具
  9. 开发用于异构环境的可生存云多机器人框架
  10. 可视化展示——实现论文引用关系动图展示
  11. 如何申请注册163邮箱账号?
  12. PixelMe怎么使用?一文教你制作像素风图片
  13. python统计excel中重复数据_Python中用pandas对标Excel自带功能——去除重复项
  14. java解析excel手机号变成科学计数法形式解决
  15. Linux man page命令后的数字含义
  16. KeyLife富翁笔记
  17. HDU 4883 TIANKENG’s restaurant (贪心)
  18. 物联网开发笔记(84)- 使用Micropython开发ESP32开发板之控制LCD12864液晶屏和AHT10温度传感器
  19. MapReduce实战之倒排索引案例(多job串联)
  20. 标准G726音频解码和与H264视频封装为avi

热门文章

  1. 泛谈Flash文件系统
  2. aix内核是linux,linux和aix内核参数检查
  3. 不同核数cpu php速度,计算机的性能指标完全由CPU决定对吗
  4. gltf介绍及gltf模型免费下载网站推荐
  5. 360lib投影格式介绍(一) - 伪圆柱投影(ERP / EAP / AEP / ECP)
  6. RStudio中更改R包安装位置
  7. 多态与instanceof关键字
  8. 论文写作-结论怎么写
  9. Unity 旧版本下载地址
  10. matlab仿真高阶传递函数,Matlab/Simulink动力学系统建模与仿真(第2版)