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: 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: 项目启动时运行此方法
*/
@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进行配置
/**
* @ApiNote: SpringSecurity配置信息
* @Author: 陌路
* @Date: 2023/2/18 12:14
* @Tool: Created by IntelliJ IDEA
*/
@Configuration
public class SecurityConfiguration {
@Resource
private TokenAuthorityFilter tokenAuthorityFilter;
@Resource
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
/**
* @apiNote: 注入密码加密工具
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public 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 {
@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);
}
}
核心: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(认证失败,请重新登录)
*/
@Override
public 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 {
@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
的权限校验文章来源:https://www.toymoban.com/news/detail-482422.html
SpringSecurity的权限校验详解说明(附完整代码)
https://blog.csdn.net/qq_51076413/article/details/129106824文章来源地址https://www.toymoban.com/news/detail-482422.html
到了这里,关于SpringSecurity的安全认证的详解说明(附完整代码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!