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
相关文章:
![](https://img-blog.csdnimg.cn/bb98f93411ea4d3081f0c23447c4369e.png)
SpringSecurity的安全认证的详解说明(附完整代码)
SpringSecurity登录认证和请求过滤器以及安全配置详解说明 环境 系统环境:win10 Maven环境:apache-maven-3.8.6 JDK版本:1.8 SpringBoot版本:2.7.8 根据用户名密码登录 根据用户名和密码登录,登录成功后返回Token数据…...
![](https://img-blog.csdnimg.cn/0e7ff294f2bf42738398e889aa5dab6f.png)
详解制造业业务数据模型
业务数据在企业数字化转型或单体应用的开发中都是至关重要的。站在跨业务跨部门的企业数字化转型角度,离不开业务架构的设计,详细的业务领域和业务数据模型是后续应用架构和数据架构的必要输入。站在单部门单场景的信息化角度,应用程序的需求…...
![](https://www.ngui.cc/images/no-images.jpg)
BigDecimal使用注意避坑
目录一. BigDecimal的初始化精度丢失问题二. BigDecimal在进行除法运算时需设置精度,否则对于除不尽的情况会抛出异常三. 不要使用BigDecimal的equals方法比较大小, 否则可能会因为精度问题导致比较结果和预期的不一致在java.math包中提供了对大数字的操作类,用于进…...
![](https://img-blog.csdnimg.cn/4569f5d1b6a24c3b8ad60789371c8902.png)
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案例-数据统计接口 …...
![](https://img-blog.csdnimg.cn/505bfdad61e746389519a7d39b6bf99f.gif#pic_center)
学海记录项目测试报告
⭐️前言⭐️ 本篇文章是博主基于学海记录的个人项目所做的测试报告,用于总结运用自动化测试技术,应用于自己的项目。 🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁 🍉博主将持续更新学习记录…...
![](https://img-blog.csdnimg.cn/9eb661455602407886e00e0eb7465049.png)
【1792. 最大平均通过率】
来源:力扣(LeetCode) 描述: 一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] [passi, totali] ,表示你…...
![](https://img-blog.csdnimg.cn/afe1a03db90b4af7b4b4a791c5ea765d.png)
言简意赅+图解 函数传参问题(传值、传地址 500字解决战斗)
1、传值 2、传地址 不论是传值,还是传地址,形参都是对于实参的一份拷贝 下图为按值传递进行交换: 形参left拷贝一块新空间,形参right拷贝一块新空间 下图为按指针传递进行交换 形参left拷贝一块新的空间,形参right…...
![](https://img-blog.csdnimg.cn/9e9f0b65a57440379fb6c96af9d4b90a.png)
UML-时序图以及PlantUML绘制
介绍 时序图(Sequence Diagram),又名序列图、循序图,是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。它可以表示用例的行为顺序,当执行一个用例行为时,其中的每条消息…...
![](https://img-blog.csdnimg.cn/60e0a2a45e5d43b7bc7253a7f945c2b5.png)
【Redis】Redis 有序集合 Zset 操作 ( 简介 | 查询操作 | 增加操作 | 删除操作 | 修改操作 )
文章目录一、有序集合 Zset二、查询操作1、查询 Zset 所有数据2、查询 Zset 所有数据和评分3、查询指定评分范围的 Zset 数据4、查询指定评分范围的 Zset 数据并从大到小排序5、统计指定评分范围的 Zset 数据个数6、查询指定元素在 Zset 有序集合中的排名三、增加操作1、向 Red…...
![](https://img-blog.csdnimg.cn/55a3dd1bb4324fcd921f392b5236dd5f.png#pic_center)
Java特性之设计模式【策略模式】
一、策略模式 概述 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式 在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略…...
![](https://www.ngui.cc/images/no-images.jpg)
IR-CUT 保证摄像机成像效果的滤镜
IR-CUT双滤镜是指在摄像头镜头组里内置了一组滤镜,当镜头外的红外感应点侦测到光线的强弱变化后,内置的IR-CUT自动切换滤镜能够根据外部光线的强弱随之自动切换,使图像达到最 佳效果。也就是说,在白天或黑夜下,双滤光片…...
![](https://img-blog.csdnimg.cn/abb37a674d974308837aab46ae72cc41.png)
openpnp - 普通航空插头和PCB的连接要使用线对板连接器
文章目录openpnp - 普通航空插头和PCB的连接要使用线对板连接器概述改进实际效果总结ENDopenpnp - 普通航空插头和PCB的连接要使用线对板连接器 概述 和同学讨论问题, 准备将航空插头连接到PCB上. 航空插头选用GX12-4公头, 拧到开孔的铁板上. 然后航空插头公头再与PCB连接. 铁…...
![](https://img-blog.csdnimg.cn/cdd8ff457875409da9db8b6b1983a0af.png)
Python3 错误和异常实例及演示
作为 Python 初学者,在刚学习 Python 编程时,经常会看到一些报错信息,在前面我们没有提及,这章节我们会专门介绍。 Python 有2种错误很容易辨认:语法错误和异常。 Python assert(断言)用于判断…...
![](https://www.ngui.cc/images/no-images.jpg)
Android 9.0第三方app根据包名设置为横屏显示
1.概述 在android9.0的系统rom定制化开发中,在某些横屏的设备比如平板电脑,tv智能电视,广告机等等设备中,通常系统是默认横批显示的,但是在安装一些竖屏app的时候, 就会旋转为竖屏,这个时候操作app也不方便,所以产品需求要求竖屏也需要根据包名横屏显示出来,这就需要在…...
![](https://www.ngui.cc/images/no-images.jpg)
MySQL会导致索引失效的情况与解决索引失效的方法
什么情况会导致索引失效 索引失效也是慢查询的主要原因之一,常见的导致索引失效的情况有下面这些: 1.使用 SELECT * 进行查询;2.创建了组合索引,但查询条件未准守最左匹配原则;3.在索引列上进行计算、函数、类型转换等操作;4.以 % 开头的 L…...
![](https://img-blog.csdnimg.cn/9cb4f23c776f4a8e9c6519850a5fa010.png)
使用nginx单独部署Vben应用
前言 本文主要介绍Vben使用nginx单独部署的方式,其实前端发展到现在已经不是当年的jsp,asp必须要和后端一起部署了。单独部署调试的工具也很多,比如vue-cli-service 和 Vben中用到的vite ,当然这些我们一般用在开发的工程中。正式…...
![](https://img-blog.csdnimg.cn/c5b652eba8904ae7a98d606937d112ad.png)
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…...
![](https://img-blog.csdnimg.cn/e9d44d73c7dc43388dec36cf4f6567d9.png)
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硬盘都有了&…...
![](https://www.ngui.cc/images/no-images.jpg)
【专业认知】抖音就业 / 保研北大教育学 / 留学南加州EE / 微软就业
2023.2.18 一. 周金辉学长分享——本科经验分享 0 简介 计算机农大本硕 硕士毕业后在抖音公司工作 1 行业前景:计算机专业能做什么? 1.1 计算机行业发展路线 远古时代: 二战开始,计算机技术发展,出现互联网 包…...
![](https://www.ngui.cc/images/no-images.jpg)
【算法题】2 的 n 次幂的背后
前言: 说实话,真的不爱写算法题相关的文章了,觉得没啥意义,但是对这种比较好玩并且简单,学会了就能很好提高算法效率的文章,还是要写一写,不哭不哭,你不会不是你的错,只是…...
![](https://www.ngui.cc/images/no-images.jpg)
【人工智能AI】一、NoSQL 企业级基础入门《NoSQL 企业级基础入门与进阶实战》
写一篇介绍什么是NoSQL的技术文章,分5个章节,每个章节细分到3级目录,重点介绍一下优缺点,适用场景,未来发展趋势等。 一、NoSQL简介 1.1 什么是NoSQL NoSQL(Not only SQL),意思是“…...
![](https://img-blog.csdnimg.cn/0f5595f6cb1e4be197a2fb8940185c0b.png)
Ubuntu安装opencv库3.4.10,并在cmake工程中引入opencv库
Windows下安装不同,Ubuntu安装OpenCV库时,需要事先安装依赖,而且不同OpenCV库所需的依赖可能会有所不同,下面的依赖亲测 3.4.10 和 4.5.5版本的有效,但是4.6以上版本安装可能会报错。 参考链接:https://bl…...
![](https://img-blog.csdnimg.cn/38cfe37407924b50ac66331b4d797796.png)
实现8086虚拟机(四)——mov 和 jmp 指令解码
文章目录mov 指令解码jmp 指令解码这篇文章举例来讲讲 mov 指令和 jmp 指令解码函数的实现,其他的指令解码函数都与这些类似。mov 指令解码 以 mov 指令中的一类:寄存器/内存 到/从 寄存器,来详细说明解码函数的实现。 机器指令格式如下&am…...
![](https://img-blog.csdnimg.cn/dec3b75aa19a476999b835fb4cc4b360.png)
数据库技术-函数依赖、键与约束、范式
一、函数依赖 给定一个x,能唯一确定一个Y,就称x确定Y,或者说Y依赖于x,例如YX*X函数。 函数依赖又可扩展以下两种规则: 部分函数依赖:A可确定C,(A,B)也可确定C,(A,B)中的一部分(即A)可以确定C&a…...
![](https://img-blog.csdnimg.cn/81e31e8ecb5e45b382b21d835d7ef587.png)
shiro CVE-2020-1957
0x00 前言 在之前只是单纯的复现了漏洞,没有记笔记,所以补充了这篇分析笔记。 影响版本:shiro < 1.5.2 0x01 环境搭建 环境用的是:https://github.com/lenve/javaboy-code-samples/tree/master/shiro/shiro-basic 0x02 漏…...
![](https://img-blog.csdnimg.cn/c5614108e1704c60a03591a4e9f870e6.png#pic_center)
RabbitMQ 入门到应用 ( 五 ) 基本应用
6.更多应用 6.1.AmqpAdmin 工具类 可以通过Spring的Autowired 注入 AmqpAdmin 工具类 , 通过这个工具类创建 队列, 交换机及绑定 import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Di…...
![](https://img-blog.csdnimg.cn/img_convert/a4015666ba468e8465fe7882f22308e2.jpeg)
部署dapr的辛酸历程
前言dapr大概的了解,个人理解他就是一个分布式服务的管理,把微服务常用的组件(缓存,消息中间件、分布式锁、安全id4等)和监控以及服务注册、发现等等一系列功能以一个很抽象的方式管理起来。可能我们部署微服务用consul、ocelot、polly套件、…...
![](https://img-blog.csdnimg.cn/beaa3a7242224e82b48635f257c3386c.png)
golang入门笔记——内存管理
文章目录自动内存管理概念自动内存管理-相关概念:追踪垃圾回收:分代GC(Generational GC)引用计数内存分配Go内存分配-分块Go内存分配——多级缓存Go内存管理优化Balanced GC自动内存管理 概念 1.动态内存 程序在运行时根据需求…...
![](https://www.ngui.cc/images/no-images.jpg)
97. 约数之和
Powered by:NEFU AB-IN Link 文章目录97. 约数之和题意思路代码97. 约数之和 题意 假设现在有两个自然数 A和 B,S是 A^B的所有约数之和。 请你求出 S mod 9901的值是多少。 思路 ABA^BAB的约数之和为:sumAB(1p1p12...p1Ba1)(1p2p22...p2Ba2)...sum_{A^B…...
谁做的怀来吧网站/市场营销师报名官网
File:MainActivity.java 将 输入框里的 IP号码保存到 SharedPreferences 指定的 config.xml文件中,再将数据回显出来。 package com.jiangge.ipdial;import android.os.Bundle; import android.app.Activity; import android.content.SharedPreferences; import an…...
![](/images/no-images.jpg)
太原市建设工程安全监督站网站/服务营销策划方案
集群高可用方案是指主备机模式,利用双机软件在主机发生故障时自动启动备机,让备机接管生产。应用的数据都需要放在外置存储上。主备机自动切换不成功有很多原因。在生产系统中,有过如下问题导致切换失败:主备机应用用户密码不一致…...
![](https://www.oschina.net/img/hot3.png)
企业网站源码哪个好/nba最新交易
2019独角兽企业重金招聘Python工程师标准>>> 在遍历数组的时候swift的forin给我们提供了更方便的模式,直接上代码解释 let btnName ["aaa", "bbb", "ccc", "ddd", "eee"] for (index,name) in btnName.enumerat…...
![](/images/no-images.jpg)
武汉网站建设供应商/郑州seo服务公司
A:close()关闭流对象,但是先刷新一次缓冲区。关闭之后,流对象不可以继续再使用了。B:flush()仅仅刷新缓冲区,刷新之后,流对象还可以继续使用。 1字符 2字节 文件中数据存储的基本单位是字节。...
如何建立一个免费的网站/seo网络优化公司
什么是内存泄露 内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪…...
![](/images/no-images.jpg)
拖拽网站怎么做的/泉州seo托管
Oracle Server包括Oracle Instance(进程部分)和Oracle Database(文件部分)。一般情况下,Oracle Instance与Oracle database一一对应。在一台机器上可以安装多个Oracle server,每个Oracle server必须有独立的…...