SpringSecurity的安全认证的详解说明(附完整代码)

news2024/9/23 11:27:08

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=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: SpringSecurity-JWT

 # 热部署
  devtools:
    restart:
      enabled: true
      additional-paths: src/main/java

# 服务端口
server:
  port: 8090

# 测试时将token有效期为5分钟
token:
  expire: 500000

# 用于生成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: 项目启动时运行此方法
     */
    @Override
    public void run(ApplicationArguments args) {
        log.info("==============项目启动成功!==============");
        log.info("请访问地址:http://{}:{}", ApiUtils.getHostIp(), port);
        log.info("=======================================");
    }

    /**
     * @apiNote: 项目关闭时执行
     * @return: void
     */
    @Override
    public 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
     */
    @Override
    public 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 {
    @Resource
    private 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;
    @TableId
    private 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;//创建人的用户id
    private 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 {
    @Resource
    private UserMapper userMapper;

    /**
     * @apiNote: 根据用户名获取用户数据
     * @param: username 用户名
     * @return: UserDetails
     */
    @Override
    public 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: 用户权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * @apiNote: 获取用户密码
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * @apiNote: 获取用户名
     */
    @Override
    public String getUsername() {
        return user.getUserName();
    }

    /**
     * @apiNote: 是否未过期(true:未过期,false:已过期)
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * @apiNote: 是否锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * @apiNote: 是否超时(true:未超时,false:已超时)
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * @apiNote: 当前用户是否可用(true:可用,false:不可用)
     */
    @Override
    public 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之前)
    @Resource
    private TokenAuthorityFilter tokenAuthorityFilter;

    /**
     * @apiNote: 注入密码加密工具
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * @apiNote: 注入AuthenticationManager对象来实现登录逻辑管理
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * @apiNote: 配置请求认证和拦截
     */
    @Override
    protected 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,否则返回false
        boolean matches = passwordEncoder.matches("123456", encode);
        System.out.println("encode = " + encode);
        System.out.println("matches = " + matches);
    }
}

以上对SpringSecurity配置的方法已过时
可以使用以下方法对SpringSecurity进行配置

@Configuration
public class SecurityConfiguration {

    @Resource
    private TokenAuthorityFilter tokenAuthorityFilter;
    @Resource
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 允许所有用户访问登录路径
                .antMatchers("/user/login").anonymous()
                // 除以上请求路径外,其他所有请求都必须经过认证才能访问成功
                .anyRequest().authenticated()
                .and()
                // 添加自定义的请求过滤器(tokenAuthorityFilter)并定义在指定哪个过滤器(UsernamePasswordAuthenticationFilter)执行前执行
                .addFilterBefore(tokenAuthorityFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

核心:自定义请求过滤器

SpringSecurity:自定义请求过滤器需要继承OncePerRequestFilter类,并重写里面的doFilterInternal方法来实现具体的业务逻辑

/**
 * @ApiNote: 请求过滤器:是否认证是否有权访问
 * @Author: 陌路
 * @Date: 2023/2/18 13:04
 * @Tool: Created by IntelliJ IDEA
 */
@Component
public class TokenAuthorityFilter extends OncePerRequestFilter {
    @Resource
    private TokenUtils tokenUtils;
    @Resource
    private ContextLoader contextLoader;

    /**
     * @apiNote: 请求过滤器
     */
    @Override
    protected 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数据得到userId
        String 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);
    }
}

后台请求接口

用户请求后台接口:登录接口、查询用户信息接口、注销登录接口

/**
 * @ApiNote: 请求接口控制器
 * @Author: 陌路
 * @Date: 2023/2/18 9:53
 * @Tool: Created by IntelliJ IDEA
 */
@RestController
@RequestMapping("/user/*")
public class IndexController {

    @Resource
    private 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;
    @Resource
    private UserMapper userMapper;
    @Resource
    private TokenUtils tokenUtils;
    @Resource
    private ContextLoader contextLoader;
    @Resource
    private 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的权限校验



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355626.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

狂神聊Redis复习笔记一

目录目前一个基本的互联网项目&#xff01;NoSQL 特点Redis 是什么&#xff1f;Redis 能干嘛&#xff1f;特性测试性能基础的知识Redis 是单线程的&#xff01;Redis 为什么单线程还这么快&#xff1f;五大数据类型Redis-KeyString&#xff08;字符串&#xff09;List&#xff…

[软件工程导论(第六版)]第3章 需求分析(复习笔记)

文章目录3.1 需求分析的任务3.2 与用户沟通获取需求的方法3.3 分析建模与规格说明3.4 实体-联系图&#xff08;E-R图&#xff09;3.5 数据规范化3.6 状态转换图3.7 其他图形工具3.8 验证软件需求需求分析是软件定义时期的最后一个阶段&#xff0c;需求分析的基本任务是准确的回…

EASYui+C#web

第一步创建一个web应用程序。 选择web应用程序。 第二步选择mvc框架 创建完成项目目录。 如图引入easyui包。 记住复制到content文件夹&#xff0c;否则无法识别。 easyui下载&#xff0c;官网。 如何用 引入jscss文件 <link rel"stylesheet" type"text…

Guitar Pro8手机电脑免费版吉他软件下载

Guitar Pro8是专业的吉他软件&#xff0c;具有可视化的五线谱编辑器&#xff0c;涵盖常用的乐器和特殊乐器单元&#xff0c;内置海量吉他音色效果和1000多个乐器音色&#xff0c;成为一个小型音乐站&#xff0c;制作出动听的音乐&#xff0c;支持边看边听&#xff0c;添加音频轨…

【论文阅读】 Few-shot object detection via Feature Reweighting

Few-shot object detection的开山之作之一 ~~ 特征学习器使用来自具有足够样本的基本类的训练数据来 提取 可推广以检测新对象类的meta features。The reweighting module将新类别中的一些support examples转换为全局向量&#xff0c;该全局向量indicates meta features对于检…

使用MindSpore20.0的API快速实现深度学习模型之数据变换

文章目录前言一. 实验环境二. 安装ubuntu虚拟机2. 1.下载ubuntu镜像2.2 配置虚拟机2.3 安装操作系统三. 安装MindSpore20.0-alpha3.1 下载需要的安装程序脚本3.2 安装MindSpore 2.0.0-alpha和Python 3.73.3 开始手动安装3.4. 安装gcc3. 5.安装MindSpore3.6. 验证是否成功&#…

【opencv源码解析0.3】调试opencv源码的两种方式

调试opencv源码的两种方式 上两篇我们分别讲了如何配置opencv环境&#xff0c;以及如何编译opencv源码方便我们阅读。但我们还是无法调试我们的代码&#xff0c;无法以我们的程序作为入口来一步一步单点调试看opencv是如何执行的。 【opencv源码解析0.1】VS如何优雅的配置ope…

有效提升英语论文写作的方法

这里结合我的一些经验&#xff0c;分享一些练习英语论文写作的方法。 论文是由单词&#xff0c;句子&#xff0c;还有逻辑三部分组成&#xff0c;每个部分的要求都不太一样&#xff0c;这里分成三个部分练习讨论: 1炼词, 2 炼句,3 炼逻辑. 炼词 首先&#xff0c;炼词就是首先…

发布npm库遇到的

&#xff08;1&#xff09;首先首先要检查源是npm还是淘宝镜像源&#xff0c;因为本地设置了淘宝镜像源的原因&#xff0c;会导致npm login出错&#xff0c;需要修改&#xff1a; npm config set registry https://registry.npmjs.org 查看是否修改成功&#xff1a;得到上面源…

SpringCloud - Eureka注册发现

目录 提供者与消费者 Eureka原理分析 搭建Eureka服务 服务注册 服务发现 提供者与消费者 服务提供者&#xff1a; 一次业务中&#xff0c;被其它微服务调用的服务(提供接口给其它微服务)服务消费者&#xff1a; 一次业务中&#xff0c;调用其它微服务的服务(调用其它微服务…

基于jeecgboot的flowable流程增加节点自动跳过功能

为了满足有时候需要在某个节点没有人员处理的时候需要自动跳过&#xff0c;所以增加了这个功能。 一、FlowComment意见里增加一个类型8&#xff0c;跳过流程 /** * 流程意见类型 * */ public enum FlowComment { /** * 说明 */ NORMAL("1", "…

Java8新特性全面

文章目录一、函数式接口二、Java内置函数式接口三、接口内允许添加默认实现的方法四、Lambda表达式五、方法引用六、Stream API七、Stream API示例1. Filter过滤2. Sorted排序3. Map转换4. Match匹配5. Count计数6. Reduce7. Parallel-Streams并行流8. Map集合八、新时间日期接…

天津+发展得到的权重

添加评分添加评分的依据高德地图找到如图数据&#xff0c;容积率体现楼层高度近似反映人口密度&#xff0c;繁华程度近似等价霓虹灯照明时长住房容积率具体是指在城市规划区的某一宗地内&#xff0c;房屋的总建筑面积与宗地面积的比值&#xff0c;分为实际容积率和规划容积率两…

链表学习基础

链表 通过指针串联在一起的线性结构&#xff0c;每个节点由数据域和指针域两部分组成。链表节点在内存中的存储通常不是连续的&#xff0c;各节点通过指针连接在一起&#xff0c;其内存分布大致如下图所示。 定义 单链表 struct ListNode {// DATATYPE 可以是任意存放数据的…

HTTP基础知识

关键字&#xff1a;一问一答用于和服务器交互什么是HTTPHTTP是个应用层协议&#xff0c;是HTTP客户端和HTTP服务器之间的交互数据格式。所以这里有个实例&#xff1a;在浏览网页的时候&#xff0c;浏览器会向服务器发送一个HTTP请求&#xff0c;告诉服务器我想访问什么..然后服…

图论学习03

图神经网络模型介绍 将图神经网络分为基于谱域上的模型和基于空域上的模型&#xff0c;并按照发展顺序详解每个类别中的重要模型。 基于谱域的图神经网络 谱域上的图卷积在图学习迈向深度学习的发展历程上起到了关键性的作用。三个具有代表性的谱域图神经网络 谱图卷积网络切…

c++中超级详细的一些知识,新手快来

目录 2.文章内容简介 3.理解虚函数表 3.1.多态与虚表 3.2.使用指针访问虚表 4.对象模型概述 4.1.简单对象模型 4.2.表格驱动模型 4.3.非继承下的C对象模型 5.继承下的C对象模型 5.1.单继承 5.2.多继承 5.2.1一般的多重继承&#xff08;非菱形继承&#xff09; 5.2…

奥地利、爱沙尼亚重要机构或正成为俄黑客目标

近日&#xff0c;网络安全公司Sekoia有一项新发现&#xff1a;由俄罗斯政府支持的黑客组织“图拉”&#xff08;Turla&#xff09;正在对奥地利经济商会、北约平台、波罗的海国防学院&#xff08;Baltic Defense College&#xff09;发动一系列攻击。这是Sekoia公司基于Google …

One Fuzzing Strategy to Rule Them All

One Fuzzing Strategy to Rule Them All 文章目录One Fuzzing Strategy to Rule Them All相关链接概述背景实验测试RQ1&#xff1a;RQ2相关工作总结最后相关链接 One Fuzzing Strategy to Rule Them All 参考链接 概述 在本文中作者提出了对变异策略havoc的研究&#xff0c…

linux异步IO编程实例分析

在Direct IO模式下&#xff0c;异步是非常有必要的&#xff08;因为绕过了pagecache&#xff0c;直接和磁盘交互&#xff09;。linux Native AIO正是基于这种场景设计的&#xff0c;具体的介绍见&#xff1a;KernelAsynchronousI/O (AIO)SupportforLinux。下面我们就来分析一下…