SpringSecurity的安全认证的详解说明(附完整代码)
SpringSecurity
登录认证和请求过滤器以及安全配置详解说明
环境
系统环境:win10
Maven环境:apache-maven-3.8.6
JDK版本:1.8
SpringBoot版本:2.7.8
根据用户名密码登录
根据用户名和密码登录,登录成功后返回
Token
数据,将token放到请求头中
,每次请求后台携带token
数据
认证成功,返回请求数据
携带token请求后台
,后台认证成功,过滤器放行,返回请求数据
认证失败,SpringSecurity拦截请求
携带token请求后台,后台认证失败,请求被拦截
数据表结构
CREATE TABLE `sys_user` (`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
下面是本次
Demo
的项目代码和说明
项目环境依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.8</version><relativePath/></parent><groupId>cn.molu.security.jwt</groupId><artifactId>SpringSecurity-JWT</artifactId><version>0.0.1-SNAPSHOT</version><name>SpringSecurity-JWT</name><description>SpringSecurity-JWT</description><properties><java.version>1.8</java.version></properties><dependencies><!--SpringSecurity安全框架--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--启用SpringBoot对Web的支持--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--热部署插件--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><!--Lombok实体类简化组件--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!--lang3对象工具包--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!--hutool工具包,数据加解密,对象判空转换等--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.10</version></dependency><!-- UA解析工具(从request中解析出访问设备信息) --><dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency><!--生成token依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- MySQL数据连接驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.27</version></dependency><!--MyBatis-Plus操作数据库--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
项目启动入口
使用
MyBatis-Plus
操作数据库,配置扫描mapper
所在的包
@SpringBootApplication
@MapperScan("cn.molu.security.jwt.mapper")
public class SpringSecurityJwtApplication {public static void main(String[] args) {SpringApplication.run(SpringSecurityJwtApplication.class, args);}
}
项目配置文件
MySQL地址、项目访问端口、token有效期
spring:# 数据库链接配置datasource:url: jdbc:mysql://127.0.0.1:3306/security?characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverapplication:name: SpringSecurity-JWT# 热部署devtools:restart:enabled: trueadditional-paths: src/main/java# 服务端口
server:port: 8090# 测试时将token有效期为5分钟
token:expire: 300000# 用于生成JWT的盐值
jwt:secret: 1234567890
项目启动和关闭日志
项目启动和关闭时控制台打印相关提示信息
/*** @ApiNote: 项目启动和关闭时的日志打印* @Author: 陌路* @Date: 2023/2/18 9:46* @Tool: Created by IntelliJ IDEA*/
@Slf4j
@Component
public class AppStartAndStop implements ApplicationRunner, DisposableBean {@Value("${server.port}")private String port;/*** @apiNote: 项目启动时运行此方法*/@Overridepublic void run(ApplicationArguments args) {log.info("==============项目启动成功!==============");log.info("请访问地址:http://{}:{}", ApiUtils.getHostIp(), port);log.info("=======================================");}/*** @apiNote: 项目关闭时执行* @return: void*/@Overridepublic void destroy() {log.info("=======================================");log.info("============程序已停止运行!============");log.info("=======================================");}
}
封装统一响应实体类
统一返回给前台的数据实体
package cn.molu.security.jwt.vo;import cn.molu.security.jwt.utils.ApiUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.NoArgsConstructor;
import lombok.ToString;import java.io.Serializable;
import java.util.HashMap;/*** @ApiNote: 封装响应实体对象* @Author: 陌路* @Date: 2023/2/10 9:42* @Tool: Created by IntelliJ IDEA.*/
@NoArgsConstructor // 生成无参构造方法
@ToString(callSuper = true) // 重写toString方法
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> extends HashMap<String, Object> implements Serializable {private static final long serialVersionUID = 2637614641937282252L;// 返回结果数据public T result;// 返回成功失败标记public static Boolean flag;// 返回成功状态码public static final Integer SUCCESS = 200;// 返回失败状态码public static final Integer FIELD = 500;/*** @apiNote: 返回数据* @param: code 状态码 [返回给前台的状态码]* @param: msg 提示消息 [返回给前台得消息]* @param: result 响应数据结果[返回给前台得结果]* @param: flag 响应标志[true:成功,false:失败]* @return: Result*/public static Result result(Integer code, String msg, Object result, Boolean flag) {Result r = new Result();r.put("code", code);r.put("msg", msg);r.put("result", result);r.put("flag", flag);r.result = result;Result.flag = flag;return r;}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result ok(Integer code, String msg, Object result) {return result(code, msg, result, true);}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result ok(String msg, Object result) {return result(SUCCESS, msg, result, true);}/*** @apiNote: 返回成功数据* @param: result 响应数据结果* @return: Result*/public static Result ok(Object result) {return result(SUCCESS, null, result, true);}/*** @apiNote: 返回成功数据* @param: msg 提示消息* @return: Result*/public static Result ok(String msg) {return result(SUCCESS, msg, null, true);}/*** @apiNote: 返回成功数据* @return: Result*/public static Result ok() {return result(SUCCESS, null, null, true);}/*** @apiNote: 返回失败数据* @param: msg 错误消息* @param: result 响应数据结果* @return: Result*/public static Result err(Integer code, String msg, Object result) {return result(code, msg, result, false);}/*** @apiNote: 返回失败数据* @param: code 响应状态码* @param: msg 错误消息* @return: Result*/public static Result err(Integer code, String msg) {return result(code, msg, null, false);}/*** @apiNote: 返回失败数据* @param: msg 提示消息* @param: result 响应数据结果* @return: Result*/public static Result err(String msg, Object result) {return result(FIELD, msg, result, false);}/*** @apiNote: 返回失败数据* @param: result 响应数据结果* @return: Result*/public static Result err(Object result) {return result(FIELD, null, result, false);}/*** @apiNote: 返回失败数据* @param: msg 错误消息* @return: Result*/public static Result err(String msg) {return result(FIELD, msg, null, false);}/*** @apiNote: 返回失败数据* @return: Result*/public static Result err() {return result(FIELD, null, null, false);}/*** @apiNote: 返回数据* @param: [code, result, msg, flag]* @return: cn.molu.api.vo.Result*/public static Result res(Integer code, Object result, String msg, boolean flag) {return result(code, msg, result, flag);}/*** @apiNote: 返回数据* @param: [flag, result]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, Object result) {return result(flag ? SUCCESS : FIELD, null, result, flag);}/*** @apiNote: 返回数据* @param: [flag, result]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, String msg, Object result) {return result(flag ? SUCCESS : FIELD, msg, result, flag);}/*** @apiNote: 返回数据* @param: [flag, msg]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag, String msg) {return result(flag ? SUCCESS : FIELD, msg, null, flag);}/*** @apiNote: 返回数据* @param: [flag, msg]* @return: cn.molu.api.vo.Result*/public static Result res(boolean flag) {return result(flag ? SUCCESS : FIELD, null, null, flag);}/*** @apiNote: 重写HashMap的put方法* @param: [key, value]* @return: Result*/@Overridepublic Result put(String key, Object value) {super.put(key, value);return this;}public <T> T getResult() {return ApiUtils.getObj(this.result, null);}public void setRes(boolean flag, T result) {this.flag = flag;this.result = result;put("flag", flag);put("result", result);}
}
封装对象工具类
封装静态方法工具类,便于在项目中使用
/*** @ApiNote: api通用工具类* @Author: 陌路* @Date: 2023/2/10 9:26* @Tool: Created by IntelliJ IDEA.*/
public class ApiUtils {/*** @apiNote: 获取设备ip* @return: String*/public static String getHostIp() {try {return InetAddress.getLocalHost().getHostAddress();} catch (UnknownHostException e) {return "127.0.0.1";}}/*** @apiNote: 将对象转为字符串数据* @param: [obj:带转换对象]* @return: java.lang.String*/public static String getStr(Object obj) {String str = Objects.nonNull(obj) ? String.valueOf(obj).trim().replaceAll("\\s*|\r|\n|\t", "") : "";return "null".equalsIgnoreCase(str) ? "" : str;}/*** @apiNote: 将对象转为字符串数据, obj为空时返回defaultVal值* @param: [obj, defaultVal]* @return: java.lang.String*/public static String getStr(Object obj, String defaultVal) {final String str = getStr(obj);return StringUtils.isBlank(str) ? defaultVal : str;}/*** @apiNote: 当对象obj为空时返回defaultVal值* @param: [obj, defaultVal]* @return: java.lang.Object*/public static <T> T getObj(Object obj, Object defaultVal) {final String str = getStr(obj);if (StringUtils.isBlank(str) && ObjUtil.isNull(defaultVal)) {return null;}return (T) (StringUtils.isBlank(str) ? defaultVal : obj);}/*** @apiNote: 校验数据是否为空* @param: [msg, val]* @return: void*/public static void hasText(String msg, Object... val) {if (ObjUtil.hasNull(val) || !ObjUtil.isAllNotEmpty(val) || val.length == 0 || StringUtils.isBlank(getStr(val))) {Assert.hasText(null, msg);}}/*** @apiNote: 向前台输出数据* @param: [obj, response]* @return: void*/public static void printJsonMsg(Object obj, HttpServletResponse response) {if (ObjUtil.isAllNotEmpty(obj, response)) {response.reset();response.setCharacterEncoding("utf-8");response.setContentType("application/json;charset=utf-8");try (final PrintWriter writer = response.getWriter()) {writer.print(obj);writer.flush();} catch (IOException ignored) {}}}/*** @apiNote: 校验数据是否未空,为空则抛出异常* @param: tipMsg:异常提示信息* @param: params:需要校验的参数值*/public static void checkParamsIsEmpty(String tipMsg, Object... params) {if (ObjUtil.isNull(params) || !ObjUtil.isAllNotEmpty(params)) {throw new RuntimeException(getStr(tipMsg, "校验失败:参数值为空!"));}}
}
Token工具类
封装
token
工具类,用于生成token
和解析token
数据
/*** @ApiNote: token工具类* @Author: 陌路* @Date: 2023/02/10 16:00* @Tool: Created by IntelliJ IDEA*/
@Component
public class TokenUtils {@Resourceprivate ContextLoader contextLoader;@Value("${jwt.secret}")private String secret;/*** @apiNote: 生成token* @param: userId 用户id* @param: timeMillis 时间戳,每次生成的Token都不一样* @return: token*/public String createToken(Long userId, Long timeMillis) {ApiUtils.checkParamsIsEmpty("生成Token失败,userId不能为空!", userId);timeMillis = timeMillis == null ? System.currentTimeMillis() : timeMillis;String token = Jwts.builder().claim("userId", userId).claim("timeMillis", timeMillis).signWith(SignatureAlgorithm.HS256, secret).compact();contextLoader.setCache(userId + "_KEY", token);return token;}/*** @apiNote: 解析token数据* @param: token* @return: map*/public Map<String, Object> verifyToken(String token) {return StringUtils.isEmpty(token) ? new HashMap<>() : ApiUtils.getObj(Jwts.parser().setSigningKey(secret).parse(token).getBody(), new HashMap<>());}/*** @apiNote: 根据token获取userId* @param: token* @return: userId*/public String getUserId(String token) {return ApiUtils.getStr(verifyToken(token).get("userId"));}
}
通过MyBatis-Plus操作数据库
/*** @ApiNote: userMapper$* @Author: 陌路* @Date: 2023/2/18 11:13* @Tool: Created by IntelliJ IDEA*/
@Mapper
public interface UserMapper extends BaseMapper<User> {}
封装缓存工具类
封装数据缓存类,用于缓存数据(
项目中使用redis做数据缓存
)
一般数据缓存是用redis
来做的,为了简便我这里就用了Map
/*** @ApiNote: 初始化缓存加载类* @Author: 陌路* @Date: 2023/2/10 9:29* @Tool: Created by IntelliJ IDEA.* @Desc: 正式开发中缓存数据应该放到redis中*/
@Component
public class ContextLoader {// 缓存用户数据public static final Map<String, LoginUser> CACHE_USER = new HashMap<>(2);// 缓存参数数据public static final Map<String, Object> CACHE_PARAM = new HashMap<>(4);// 数据有效时长@Value("${token.expire}")private long expire;/*** @apiNote: 根据token获取用户数据* @param: [token]* @return: cn.molu.api.pojo.User*/public LoginUser getCacheUser(String token) {if (StringUtils.isNotEmpty(token) && CACHE_USER.containsKey(token)) {final LoginUser loginUser = ApiUtils.getObj(CACHE_USER.get(token), new LoginUser());Long expire = ApiUtils.getObj(loginUser.getExpire(), 0);long currentTimeMillis = System.currentTimeMillis();if ((expire > currentTimeMillis)) {if (expire - currentTimeMillis <= this.expire) {setCacheUser(token, loginUser);}return loginUser;}CACHE_USER.remove(token);}return new LoginUser();}/*** @apiNote: 添加缓存数据到CACHE_USER中* @param: [token, user]* @return: cn.molu.api.pojo.User*/public void setCacheUser(String token, LoginUser loginUser) {if (StringUtils.isNotEmpty(token)) {loginUser.setExpire(System.currentTimeMillis() + expire);CACHE_USER.put(token, loginUser);}}/*** @apiNote: 向CACHE_PARAM中添加缓存数据* @param: [key, val]* @return: void*/public void setCache(String key, Object val) {if (StringUtils.isNotEmpty(key)) {CACHE_PARAM.put(key, val);}}/*** @apiNote: 删除CACHE_USER中的用户数据* @param: key* @return: void*/public void deleteUser(String key) {if (StringUtils.isNotBlank(key) && this.CACHE_USER.containsKey(key)) {this.CACHE_USER.remove(key);}}/*** @apiNote: 删除CACHE_PARAM中的数据* @param: key* @return: void*/public void deleteParam(String key) {if (StringUtils.isNotEmpty(key) && this.CACHE_PARAM.containsKey(key)) {this.CACHE_PARAM.remove(key);}}
}
用户对象实体类
用户对象,对应数据库中的
sys_user
表
/***@ApiNote: 用户对象实体类,对应数表sys_user*@Author: 陌路*@Date: 2023/2/18 20:46*@Tool: Created by IntelliJ IDEA*/
@Data
@NoArgsConstructor
@TableName("sys_user")
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class User implements Serializable {private static final long serialVersionUID = -40356785423868312L;@TableIdprivate Long id;//主键private String userName;//用户名private String nickName;//昵称private String password;//密码private String status;//账号状态(0正常 1停用)private String email;// 邮箱private String phone;//手机号private String sex;//用户性别(0男,1女,2未知)private String avatar;//头像private String userType;//用户类型(0管理员,1普通用户)private Long createBy;//创建人的用户idprivate Date createTime;//创建时间private Long updateBy;//更新人private Date updateTime;//更新时间private Integer delFlag;//删除标志(0代表未删除,1代表已删除)
}
==SpringSecurity核心内容==
核心:用户认证(登录)
SpringSecurity
:登录业务需要实现SpringSecurity
接口(UserDetailsService
)中提供的方法(loadUserByUsername
)并返回SpringSecurity
提供的UserDetails
接口对象
/*** @ApiNote: 用户数据认证* @Author: 陌路* @Date: 2023/2/18 11:34* @Tool: Created by IntelliJ IDEA*/
@Service("userDetailsImpl")
public class UserDetailsImpl implements UserDetailsService {@Resourceprivate UserMapper userMapper;/*** @apiNote: 根据用户名获取用户数据* @param: username 用户名* @return: UserDetails*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名查询用户数据User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));ApiUtils.checkParamsIsEmpty("未获取到用户数据,请检查用户名和密码是否正确!", user);// 根据用户信息查询相关权限// TODO: 权限相关配置后面实现,目前先做认证 // 将用户数据封装到LoginUser中并返回return new LoginUser(user);}
}
核心:实现接口封装用户数据
SpringSecurity
:存储当前登录用户数据,需要实现SpringSecurity
提供的接口对象(UserDetails
),通过LoginUser
对象来接收loadUserByUsername
返回的用户登录数据
/*** @ApiNote: 封装登录用户数据* @Author: 陌路* @Date: 2023/2/18 11:55* @Tool: Created by IntelliJ IDEA*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LoginUser implements UserDetails {// 实现SpringSecurity提供的UserDetails接口来管理用户数据private User user; // 用户数据对象private Long expire; // 过期时间private String token; // token// 构造方法public LoginUser(User user) {this.user = user;}/*** @apiNote: 获取当前登录用户信息*/public static LoginUser getLoginUser() {LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();return ApiUtils.getObj(loginUser, new LoginUser());}/*** @apiNote: 用户权限信息*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}/*** @apiNote: 获取用户密码*/@Overridepublic String getPassword() {return user.getPassword();}/*** @apiNote: 获取用户名*/@Overridepublic String getUsername() {return user.getUserName();}/*** @apiNote: 是否未过期(true:未过期,false:已过期)*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** @apiNote: 是否锁定*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** @apiNote: 是否超时(true:未超时,false:已超时)*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** @apiNote: 当前用户是否可用(true:可用,false:不可用)*/@Overridepublic boolean isEnabled() {return true;}
}
核心:SpringSecurity配置类
SpringSecurity
:核心配置类,用于配置自定义过滤器、拦截和放行用户请求
WebSecurityConfigurerAdapter
:此方法已过时,可使用SecurityFilterChain
来配置,以下有说明
/*** @ApiNote: SpringSecurity配置信息* @Author: 陌路* @Date: 2023/2/18 12:14* @Tool: Created by IntelliJ IDEA*/
//@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 注入自定义的过滤器,在用户名和密码认证之前执行(UsernamePasswordAuthenticationFilter之前)@Resourceprivate TokenAuthorityFilter tokenAuthorityFilter;/*** @apiNote: 注入密码加密工具*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** @apiNote: 注入AuthenticationManager对象来实现登录逻辑管理*/@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}/*** @apiNote: 配置请求认证和拦截*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 关闭Security的CSRF功能防御http.csrf().disable()// 不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 允许所有用户访问登录路径.antMatchers("/user/login").anonymous()//匿名访问(未登录未认证的)// 除以上请求路径外,其他所有请求都必须经过认证才能访问成功.anyRequest().authenticated();// 添加自定义的请求过滤器(tokenAuthorityFilter)并定义在指定哪个过滤器(UsernamePasswordAuthenticationFilter)执行前执行http.addFilterBefore(tokenAuthorityFilter, UsernamePasswordAuthenticationFilter.class);}// 测试密码的加密和密码的验证public static void main(String[] args) {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();// 加密后的密文,每次加密结果都不一样,因为加密时会生成随机盐值String encode = passwordEncoder.encode("123456");// 校验用户输入的密码和加密后的密码是否一样,一样返回true,否则返回falseboolean matches = passwordEncoder.matches("123456", encode);System.out.println("encode = " + encode);System.out.println("matches = " + matches);}
}
以上对SpringSecurity配置的方法已过时
可以使用以下方法对SpringSecurity进行配置
/*** @ApiNote: SpringSecurity配置信息* @Author: 陌路* @Date: 2023/2/18 12:14* @Tool: Created by IntelliJ IDEA*/
@Configuration
public class SecurityConfiguration {@Resourceprivate TokenAuthorityFilter tokenAuthorityFilter;@Resourceprivate AuthenticationConfiguration authenticationConfiguration;@Beanpublic AuthenticationManager authenticationManager() throws Exception {AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();return authenticationManager;}/*** @apiNote: 注入密码加密工具*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 允许所有用户访问登录路径:anonymous(匿名访问,即允许未登录时访问,登录时则不允许访问).antMatchers("/user/login").anonymous()// 除以上请求路径外,其他所有请求都必须经过认证才能访问成功.anyRequest().authenticated().and()// 添加自定义的请求过滤器(tokenAuthorityFilter)并定义在指定哪个过滤器(UsernamePasswordAuthenticationFilter)执行前执行.addFilterBefore(tokenAuthorityFilter, UsernamePasswordAuthenticationFilter.class);// 添加异常处理器http.exceptionHandling()// 认证异常处理器.authenticationEntryPoint(authenticationEntryPoint);// 运行跨域配置//http.cors();return http.build();}
}
核心:自定义请求过滤器
SpringSecurity
:自定义请求过滤器需要继承OncePerRequestFilter
类,并重写里面的doFilterInternal
方法来实现具体的业务逻辑
/*** @ApiNote: 请求过滤器:是否认证是否有权访问* @Author: 陌路* @Date: 2023/2/18 13:04* @Tool: Created by IntelliJ IDEA*/
@Component
public class TokenAuthorityFilter extends OncePerRequestFilter {@Resourceprivate TokenUtils tokenUtils;@Resourceprivate ContextLoader contextLoader;/*** @apiNote: 请求过滤器*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 获取token数据String authorityToken = ApiUtils.getStr(request.getHeader("Authorization"));// token为空直接放行if (StringUtils.isBlank(authorityToken)) {filterChain.doFilter(request, response);return;}// 解析token数据得到userIdString userId = tokenUtils.getUserId(authorityToken);// 从缓存中获取用户信息LoginUser loginUser = contextLoader.getCacheUser(userId + "_TOKEN_" + authorityToken);ApiUtils.checkParamsIsEmpty("请求失败,认证已过期!", loginUser, loginUser.getUser());// 将用户信息封装到SecurityContextHolder中//principal:用户数据,credentials:,authenticated:权限信息UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}
}
核心:SpringSecurity异常处理
认证失败:
- 实现
SpringSecurity
提供的AuthenticationEntryPoint
接口中的commence
方法来处理认证失败后的业务- 统一处理:统一返回JSON异常提示信息
- 在
SpringSecurity
配置类(SecurityConfiguration
)中添加http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
即可
/*** @ApiNote: 认证失败处理类* @Author: 陌路* @Date: 2023/2/19 12:25* @Tool: Created by IntelliJ IDEA*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {/*** @apiNote: 认证失败处理* @return: JSON(认证失败,请重新登录)*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String authExceptionMessage = authException.getMessage();authExceptionMessage = StringUtils.isBlank(ApiUtils.getStr(authExceptionMessage)) ? "认证失败,请重新登录!" : authExceptionMessage;String jsonStr = JSONUtil.toJsonStr(Result.err(HttpStatus.UNAUTHORIZED.value(), authExceptionMessage));ApiUtils.printJsonMsg(jsonStr, response);}
}
后台请求接口
用户请求后台接口:登录接口、查询用户信息接口、注销登录接口
/*** @ApiNote: 请求接口控制器* @Author: 陌路* @Date: 2023/2/18 9:53* @Tool: Created by IntelliJ IDEA*/
@RestController
@RequestMapping("/user/*")
public class IndexController {@Resourceprivate UserService userService;/*** @apiNote: 获取用户列表* @return: Result*/@GetMapping("getUserList")public Result getUserList() {return Result.ok(userService.queryList());}/*** @apiNote: 用户登录接口* @param: User对象实体* @return: Result*/@PostMapping("login")public Result login(@RequestBody User user) {return userService.login(user);}/*** @apiNote: 用户退出登录* @return: Result*/@GetMapping("logout")public Result logout() {return Result.res(userService.logout());}
}
请求接口实现类
用户请求接口实现类型:登录、获取用户数据、注销登录
/*** @ApiNote: userService$* @Author: 陌路* @Date: 2023/2/18 11:28* @Tool: Created by IntelliJ IDEA*/
@Service("userService")
public class UserServiceImpl implements UserService {@Value("${token.expire}")private long expire;@Resourceprivate UserMapper userMapper;@Resourceprivate TokenUtils tokenUtils;@Resourceprivate ContextLoader contextLoader;@Resourceprivate AuthenticationManager authenticationManager;/*** @apiNote: 查询所有用户数据*/public List<User> queryList() {return userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getDelFlag, 0));}/*** @apiNote: 用户登录:缓存用户数据* @param: User* @return: Result*/public Result login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);ApiUtils.checkParamsIsEmpty("登录失败!", authenticate);LoginUser loginUser = ApiUtils.getObj(authenticate.getPrincipal(), new LoginUser());long currentTimeMillis = System.currentTimeMillis();String token = tokenUtils.createToken(loginUser.getUser().getId(), currentTimeMillis);loginUser.setToken(token);loginUser.setExpire(currentTimeMillis + expire);contextLoader.setCacheUser(loginUser.getUser().getId() + "_TOKEN_" + token, loginUser);return Result.ok("登录成功!", token);}/*** @apiNote: 用户退出登录,删除用户缓存数据*/public boolean logout() {LoginUser loginUser = LoginUser.getLoginUser();Long id = loginUser.getUser().getId();String token = loginUser.getToken();contextLoader.deleteUser(id + "_TOKEN_" + token);contextLoader.deleteParam(id + "_KEY");return true;}
}
项目接口调用实例
在请求体中输入用户名和密码进行登录(登录时请求头不需要携带token)请求
/user/login
接口,登录成功!
请求头中携带
token
,请求/user/getUserList
接口,获取用户列表数据,请求成功!
请求头中携带
token
请求/user/logout
接口退出登录,请求成功!
退出登录后,携带
toekn
再次访问/user/getUserList
获取用户列表接口,可以看到请求被拒绝访问,后台校验失败,提示请求失败,认证已过期!
到此SpringSecurity
登录认证部分已结束,希望这篇文章对您有所帮助
下一篇SpringSecurity
的权限校验
SpringSecurity的权限校验详解说明(附完整代码)
https://blog.csdn.net/qq_51076413/article/details/129106824
SpringSecurity的安全认证的详解说明(附完整代码)相关推荐
- C语言实现扫雷完整算法详解~(附完整代码~)
扫雷是一个常见小游戏,那么如何用C语言实现扫雷呢?学习了二维数组之后,我们可将扫雷的网格区域存储为二维数组,从而使用C语言实现扫雷. 目录 1.算法基本思路 2.算法详解 1.初始化数组与打印数组 2 ...
- ECharts 数据各种图自适应 可视化 项目过程详解(附完整代码)
前言: 本篇文章的学习目的: 1.可视化面板布局适配屏幕 2.利用ECharts 实现柱状图展示 实现的技术栈: 基于 flexible.js +rem 智能大屏适配 VScode cssrem插件 ...
- 基于MATLAB的拼图游戏设计(图文详解,附完整代码)
基于MATLAB的拼图游戏设计 内容摘要:MATL ...
- 幸运数 c++程序(详解,附完整代码)
标题[蓝桥杯][2013年第四届真题]幸运数 时间限制: 1Sec 内存限制: 128MB 提交: 696 解决: 326 题目描述 幸运数是波兰数学家乌拉姆命名的.它采用与生成素数类似的" ...
- Pytorch实现手写体识别(基于CNN卷积神经网络)实操详解(附完整代码free)
手写体作为深度学习的"Hello world",几乎所有的课程都会讲到这个,今天带着大家一些看看吧. 如果对你有所帮助,点个赞给个小关注,以后一起交流学习. 前言 首先和大家讲讲我 ...
- JAVA集合详解(附完整代码)
Collections(集合) 一.常用的集合相关接口与实现类 在Java中所有的集合类都源自Iterable接口,Colletion继承Iterable接口,Collection下有三个子接口, ...
- 动态规划——矩阵连乘问题算法及实现详解(附完整代码)
问题分析 矩阵连乘问题是经典的动态规划问题,其主要是n个矩阵进行矩阵乘法运算时,通过括号改变运算的先后顺序,减少运算次数,找到最佳划分方法,求解最少运算次数. 算法分析 矩阵连乘问题中动态规划可以帮助 ...
- 二叉树遍历详解(附完整代码)
二叉树 1.最常见的遍历种类 先序遍历 中序遍历 后序遍历 层次遍历 二:遍历的递归代码实现分别给出Java 代码和C代码实现 2.二叉树的存储结构定义: 1)顺序存储结构 2) 链式存储结构 二叉链 ...
- Three.js实例详解___旋转的精灵女孩(附完整代码和资源)(一)
Three.js实例详解___旋转的精灵女孩(附完整代码和资源)(一) 本文目录: 一.[旋转的精灵女孩]案例运行效果 二.Three.js简介 三.Three.js代码正常运行显示条件 (1)不载入 ...
最新文章
- Python能让你上天?带你挖掘隐藏彩蛋~(附代码)
- Silverlight学习笔记之使用TranslateTransform控制对象位置
- Windows消息机制要点
- Spyder常用快捷键
- Git和Github实现代码同步
- css 使用本地字体
- Mapped Statements collection already contains value for com.wen.mapper.ProjectMapper.xxx
- 网络技术人员要知道的100个安全工具
- 开发用于异构环境的可生存云多机器人框架
- 可视化展示——实现论文引用关系动图展示
- 如何申请注册163邮箱账号?
- PixelMe怎么使用?一文教你制作像素风图片
- python统计excel中重复数据_Python中用pandas对标Excel自带功能——去除重复项
- java解析excel手机号变成科学计数法形式解决
- Linux man page命令后的数字含义
- KeyLife富翁笔记
- HDU 4883 TIANKENG’s restaurant (贪心)
- 物联网开发笔记(84)- 使用Micropython开发ESP32开发板之控制LCD12864液晶屏和AHT10温度传感器
- MapReduce实战之倒排索引案例(多job串联)
- 标准G726音频解码和与H264视频封装为avi
热门文章
- 泛谈Flash文件系统
- aix内核是linux,linux和aix内核参数检查
- 不同核数cpu php速度,计算机的性能指标完全由CPU决定对吗
- gltf介绍及gltf模型免费下载网站推荐
- 360lib投影格式介绍(一) - 伪圆柱投影(ERP / EAP / AEP / ECP)
- RStudio中更改R包安装位置
- 多态与instanceof关键字
- 论文写作-结论怎么写
- Unity 旧版本下载地址
- matlab仿真高阶传递函数,Matlab/Simulink动力学系统建模与仿真(第2版)