前言
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架 Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的主要进行 认证 和 授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
授权:经过认证后判断当前用户是否有权限进行某个操作。
而认证和授权也是 SpringSecurity 作为安全框架的核心功能。
登录流程:
先进行登录,登录后带着生成的token进行访问,再给予对相应的权限进行操作。
(不用背,但需仔细看)
在使用Spring Security构建的Web应用程序中,登录流程涉及多个关键组件,下面详细介绍这些组件及其在登录过程中扮演的角色: 1. Filter Chain(过滤器链) SecurityContextPersistenceFilter:维护安全上下文,确保线程间的安全信息传递。 UsernamePasswordAuthenticationFilter:负责处理基于表单的登录请求,收集用户名和密码,调用AuthenticationManager进行验证。 ConcurrentSessionFilter:管理并发会话,避免同一账号多处登录。 ExceptionTranslationFilter:捕获并处理认证或授权失败的异常。 FilterSecurityInterceptor:执行访问决策,依据用户权限判断是否允许访问特定资源。 2. AuthenticationManager(认证管理器) 负责处理认证请求,它接受一个Authentication对象(包含用户凭证),并返回一个经过完全填充的(已验证的或未经验证的)Authentication对象。对于基于用户名和密码的登录,Spring Security提供了DaoAuthenticationProvider,它使用UserDetailsService来检索用户信息。 3. UserDetailsService(用户详情服务) 接口定义了一个方法loadUserByUsername(String username),用于根据用户名加载用户信息。开发者需要实现这个接口,通常从数据库中查询用户信息。返回的是UserDetails对象,它包含用户的用户名、密码(通常是加密的)、权限等安全相关信息。 4. UserDetails(用户详情) 表示用户安全信息的核心接口,包含用户名、密码、账号是否过期、凭证是否过期、账号是否锁定以及赋予用户的权限集合。一个典型的实现是org.springframework.security.core.userdetails.User。 登录流程步骤(结合Spring Security组件): 请求登录页面:用户访问登录页面,该页面由Spring Security默认或自定义的登录页面处理。 提交登录信息:用户提交用户名和密码,这些信息通过UsernamePasswordAuthenticationFilter被封装成一个UsernamePasswordAuthenticationToken,并转发给AuthenticationManager。 验证用户凭证: AuthenticationManager委托给配置的AuthenticationProvider(如DaoAuthenticationProvider)处理。 DaoAuthenticationProvider调用实现UserDetailsService的服务来加载用户详情(UserDetails)。 使用PasswordEncoder比较提交的密码与数据库中存储的密码哈希值,验证密码是否正确。 认证成功: 如果验证成功,AuthenticationProvider返回一个完全填充的Authentication对象,其中包含用户的角色和权限信息。 AuthenticationManager将此认证对象设置到安全上下文中,使得后续请求能够访问用户信息和权限。 通常会生成一个会话或JWT,并将其发送给客户端,用于后续请求的认证。 认证失败: 认证失败时,抛出异常,由ExceptionTranslationFilter捕获并处理,可能重定向到登录页面显示错误消息,或响应HTTP 401 Unauthorized。 访问控制: 用户携带令牌访问受保护资源时,FilterSecurityInterceptor基于用户的角色和权限进行访问决策,决定是否允许访问。 如上:Spring Security提供了一个强大且灵活的安全认证与授权框架,确保了登录过程的严谨性和安全性。
1. 搭建 SpringBoot工程
1) 新建 boot 项目
只要一个 web 依赖
创建好的初始目录,直接将 demos 包删除。
2) 添加依赖(pom)
<!-- security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency> <!-- jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- MybatisPlus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <!-- 数据库 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
引入依赖后我们在尝试去访问的接口就会自动跳转到一个 SpringSecurity 的默认登陆页面,默认用户名是user,密码会输出在控制台。
必须登录之后才能对接口进行访问。
2. 登录(认证)实战。
登录:①自定义登录接口:调用 ProviderManager 的方法进行认证,如果认证通过生成 jwt,把用户信息存入 redis 中。
②自定义 CustomUserDetailsService,在这个实现类中去查询数据库。
校验:①定义 Jwt 认证过滤器,获取 token,解析 token 获取其中的 userId 或者 username,从 redis 中获取用户信息,存入 SecurityContextHolder。
1) 导入相关配置
config 包:
import com.bei.utils.FastJsonRedisSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * 构建 Redis 交互 */ @Configuration public class RedisConfig { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) // unchecked 用于抑制未经检查的类型转换警告。 rawtypes 警告被抑制 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }Redis相关配置
domain包:
import com.fasterxml.jackson.annotation.JsonInclude; /** * 响应类 * @param <T> */ // @JsonInclude 控制JSON序列化时哪些属性会被包含在输出的JSON字符串中 // JsonInclude.Include.NON_NULL 的意义是只有当属性的值不为 null 时,该属性才会被包含在生成的JSON对象中。 // 如果你的Java对象有一些字段值为 null,在将这个对象转换为JSON字符串时,这些 null 的字段将不会出现在最终的JSON输出里。 @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { /** * 状态码 */ private Integer code; /** * 提示信息,如果有错误时,前端可以获取该字段进行提示 */ private String msg; /** * 查询到的结果数据, */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }响应类
utils包:
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; /** * Redis使用 FastJson 序列化 */ public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } protected JavaType getJavaType(Class<?> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } }redis使用
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 * 1000L; // 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "bei"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("bei") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }jwt工具类
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hkey */ public void delCacheMapValue(final String key, final String hkey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hkey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }redis配置
public class WebUtils { /** * 将字符串渲染到客户端 * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }响应数据
pojo包:
import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * 用户表(User)实体类 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class User implements Serializable { /** 主键 */ private Integer userId; /** 用户名 */ private String userAccount; /** 密码 */ private String password; /** 账号状态(0正常 1停用) */ private String status; /** 删除字段(0正常,1删除) */ private String delFlag; /** 用户姓名 */ private String userFullName; // 改名以避免与 UserDetails 的 getUsername 冲突 /** 用户邮箱 */ private String userEmail; /** 用户手机号 */ private String userPhone; /** 用户身份证号 */ private String userIdCard; /** 用户性别(0男,1女) */ private String userSex; }User实体类
新建数据库(security_reailty)建表:登录认证仅需要user表,五张表组成RBAC权限模型。
CREATE TABLE `sys_user` ( `user_id` int(0) NOT NULL AUTO_INCREMENT COMMENT '用户id', `user_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '账号密码', `user_status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '用户状态(0正常,1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除字段(0正常,1删除)', `user_full_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户姓名', `user_phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户手机号', `user_id_card` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户身份证', `user_sex` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别', `user_email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户邮箱', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 139 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `sys_user` VALUES (1, '100001', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '张三', '16635013132', '11010519491231002X', '男', '[email protected]'); INSERT INTO `sys_user` VALUES (2, '100002', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '李四', '14735019091', '11010519491231002X', '男', '[email protected]'); INSERT INTO `sys_user` VALUES (3, '100003', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '王五', '14735018265', '11010519491231002X', '男', '[email protected]'); INSERT INTO `sys_user` VALUES (4, '100004', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '赵六', '14735018265', '11010519491231002X', NULL, NULL); INSERT INTO `sys_user` VALUES (5, '100005', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '孙七', '14735019091', '11010519491231002X', NULL, NULL); INSERT INTO `sys_user` VALUES (6, '100006', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', '周八', '14735018265', NULL, NULL, NULL); INSERT INTO `sys_user` VALUES (7, '100007', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', NULL, '14735018265', '11010519491231002X', NULL, NULL); INSERT INTO `sys_user` VALUES (8, '100008', '$2a$10$gfRO339ZsdX7xuF4q8GkVOHVJHkGg//RKdOpiRoxC9WgKLfG97BGO', '0', '0', NULL, '14735018265', NULL, NULL, NULL);sys_user表 插入数据
CREATE TABLE `sys_role` ( `role_id` int(0) NOT NULL AUTO_INCREMENT COMMENT '角色id', `role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名字', `status` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '状态(0正常 1停用)', `del_status` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '删除状态(0正常 1删除)', `remark` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注', PRIMARY KEY (`role_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `sys_role` VALUES (1, 'admin', '0', '0', '管理员'); INSERT INTO `sys_role` VALUES (2, 'user', '0', '0', '用户');sys_role表 插入数据
CREATE TABLE `sys_user_role` ( `user_id` int(0) NOT NULL COMMENT '用户id', `role_id` int(0) NOT NULL COMMENT '角色id', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `sys_user_role` VALUES (1, 1); INSERT INTO `sys_user_role` VALUES (2, 2); INSERT INTO `sys_user_role` VALUES (3, 2); INSERT INTO `sys_user_role` VALUES (4, 2); INSERT INTO `sys_user_role` VALUES (5, 1); INSERT INTO `sys_user_role` VALUES (6, 2); INSERT INTO `sys_user_role` VALUES (7, 2); INSERT INTO `sys_user_role` VALUES (8, 2); SET FOREIGN_KEY_CHECKS = 1;sys_user_role表 插入数据
CREATE TABLE `sys_menu` ( `menu_id` int(0) NOT NULL AUTO_INCREMENT COMMENT '权限id', `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '页面路径', `explain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路径说明', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '权限状态(0正常,1停用)', `perms` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限', PRIMARY KEY (`menu_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 30 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `sys_menu` VALUES (1, '/login', '登录', '0', 'sys:login:list'); INSERT INTO `sys_menu` VALUES (2, '/information', '完善信息', '0', 'sys:user:list'); INSERT INTO `sys_menu` VALUES (3, '/page', '删除', '0', 'sys:del:list'); INSERT INTO `sys_menu` VALUES (4, '/pagess', '编辑', '0', 'jx:edit:list');sys_menu表 插入数据
CREATE TABLE `sys_menu_role` ( `menu_id` int(0) NOT NULL COMMENT '权限id', `role_id` int(0) NOT NULL COMMENT '角色id', PRIMARY KEY (`menu_id`, `role_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; INSERT INTO `sys_menu_role` VALUES (1, 1); INSERT INTO `sys_menu_role` VALUES (1, 2); INSERT INTO `sys_menu_role` VALUES (2, 1); INSERT INTO `sys_menu_role` VALUES (2, 2); INSERT INTO `sys_menu_role` VALUES (3, 1); INSERT INTO `sys_menu_role` VALUES (4, 1); INSERT INTO `sys_menu_role` VALUES (5, 1);sys_menu_role表 插入数据
yml配置:
spring: datasource: url: jdbc:mysql://localhost:3306/security_reality?characterEncoding=utf-8&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver.yml配置
application.properties 配置:
server.port=8080 spring.datasource.url=jdbc:mysql://localhost:3306/security_reality?characterEncoding=utf-8&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
2) 代码实现
(1) mapper 包
定义Mapper接口:
/** * User Mapper */ @Mapper public interface UserMapper { /** 使用 用户名 查询 用户信息 */ User selectUserByUserAccount(@RequestParam("userAccount") String userAccount); }
resources/mapper/UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bei.mapper.UserMapper"> <sql id="user"> user_id, user_account, password, user_status, user_status, user_full_name, user_phone, user_id_card, user_sex, user_email </sql> <!-- 使用用户名 查询用户全部信息 --> <select id="selectUserByUserAccount" resultType="com.bei.pojo.User"> select <include refid="user"/> from sys_user where user_account = #{userAccount}; </select> </mapper>
(2) 核心代码实现
创建一个 CustomUserDetailsService 类,实现 UserDetailsService 接口,重写其中的方法。更加用户名从数据库中查询用户信息。
/** * 实现 UserDetailsService 接口的方法 * 使用用户名查询用户在数据库的信息 */ @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 使用用户名获取用户全部信息 User user = userMapper.selectUserByUserAccount(username); // 查询不到用户信息则抛出异常 if (Objects.isNull(user)) { throw new RuntimeException("用户名或密码有误"); } // TODO 在授权时返回此处。 根据用户查询权限信息,再添加到 LoginUser 中。 return new LoginUser(user); } }
在授权时返回此处。 根据用户查询权限信息,再添加到 LoginUser 中。
因为 UserDetailsService 方法的返回值是UserDetails类型,所以需要定义一个 LoginUser 类,实现该接口,把用户信息封装在其中。
pojo 包:LoginUser 对象
/** * 封装用户信息 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserAccount(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
LoginUser 在授权时,还需要 封装存储授权信息。
密码加密存储:
使用 SpringSecurity 提供的 BCryptPasswordEncoder。只需要使用把 BCryptPasswordEncoder 对象注入 Spring容器 中,SpringSecurity 就会使用该 PasswordEncoder 来进行密码校验。
需要定义一个 SpringSecurity 的配置类,SpringSecurity 要求这个配置类要继承 WebSecurityConfigurerAdapter。
(新建 SecurityConfig 类,类内放入) :
/** 密码加密 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
登录:
自定义登录接口,然后让 SpringSecurity 对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过 AuthenticationManager 的 authenticate 方法来进行用户认证,所以需要在 SecurityConfig 中配置把 AuthenticationManager 注入容器。
退出登录:
定义一个登录接口,然后获取 SecurityContextHolder 中的认证信息,删除 redis 中对应的数据即可。
登录 认证成功的话要生成一个 jwt,放入响应中返回。并且为了让用户下回请求时能通过 jwt 识别出具体的是哪个用户,我们需要把用户信息存入 redis,可以把用户 id 或者别的信息作为key。
service 包:ILoginService 接口
/** * 登录Service */ public interface ILoginService { /** 登录 */ ResponseResult login(User user); /** 退出登录 */ ResponseResult loginOut(); }
service/impl 包:LoginServiceImpl 实现类
/** * 登录 */ @Service public class LoginServiceImpl implements ILoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; /** * 登录 * @param user 用户实体类 * @return 成功 */ @Override public ResponseResult login(User user) { /** 构造了一个基于用户名和密码的认证请求,并通过AuthenticationManager进行用户认证。 */ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserAccount(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 判空 if (Objects.isNull(authenticate)) { throw new RuntimeException("用户名或密码有误"); } // 从认证成功的Authentication对象中获取principal,将其强制转换为LoginUser类型,进而得到用户账号userAccount。 LoginUser loginUser = (LoginUser)authenticate.getPrincipal(); String users = loginUser.getUser().getUserAccount(); // 通过 Jwt 工具类 使用 userAccount 生成 token String jwt = JwtUtil.createJWT(users); // 将 authenticate 存入 redis redisCache.setCacheObject("login:" + users, loginUser); // 将 token 响应给前端 HashMap<String, Object> map = new HashMap<>(); map.put("token", jwt); map.put("token",jwt); return new ResponseResult(200, "登录成功", map); } /** * 退出登录 * @return 成功 */ @Override public ResponseResult loginOut() { // 从 存储权限的集合SecurityContextHolder内将获取到Authentication对象。里面包含已认证的用户信息,权限集合等。 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 从 获取到的对象内 取出 已认证的主体 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); // 从被认证的主体内取出用户名 String userAccount = loginUser.getUser().getUserAccount(); // 根据键为用户名userAccount删除对应的用户信息 redisCache.deleteObject("login:" + userAccount); return new ResponseResult<>(200, "退出成功"); } }
3) 认证过滤器:
自定义一个过滤器,这个过滤器会去获取请求头中的 token,对 token 进行解析取出其中的 userAccount。
使用 userAccount 去 redis 中获取对应的 LoginUser 对象。
然后封装 Authentication 对象存入 SecurityContextHolder。
filter 包:JwtAuthenticationTokenFilter 类 过滤器
/** * 过滤器 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头获取token String token = request.getHeader("token"); // 检查获取到的token是否为空或空白字符串。(判断给定的字符串是否包含文本) if (!StringUtils.hasText(token)) { // 如果token为空,则直接放行请求到下一个过滤器,不做进一步处理并结束当前方法,不继续执行下面代码。 filterChain.doFilter(request, response); return; } // 解析token String userAccount; try { Claims claims = JwtUtil.parseJWT(token); userAccount = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } // 从redis 中 获取 用户信息 String redisKey = "login:" + userAccount; // redis 获取 键 对应 数据 LoginUser loginUser = redisCache.getCacheObject(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } // 将用户信息存入 SecurityConText // UsernamePasswordAuthenticationToken 存储用户名 密码 权限的集合 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null); // SecurityContextHolder是Spring Security用来存储当前线程安全的认证信息的容器。 // 将用户名 密码 权限的集合存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
config 包:SecurityConfig 类
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http // 因为是 前后端分离 要关闭 csrf() .csrf().disable() // 不通过 session 获取 SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 登录接口公开访问 .antMatchers("/user_login/login").anonymous() // 除上面公开的接口外,所有的请求都需要鉴定认证 .anyRequest().authenticated() .and() // 添加 过滤器 .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 在 SecurityConfig 内将 token 校验过滤器添加到过滤器链内 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } /** AuthenticationManager 认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** 密码加密 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
至此,登录认证核心代码已完成。另加 退出登录。
(4) 测试登录:
3. 授权实战
授权:
就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
授权流程:
在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication,然后获取其中的权限信息。
当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入 Authentication。
然后设置我们的资源所需要的权限即可。
(1) 实体类
RBAC权限模型:
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
上面 五个表 已经建好。
实体类:
/** * 用户角色中间表 */ @Data @AllArgsConstructor @NoArgsConstructor @TableName(value = "sys_user_role") public class UserRole { /** 用户ID */ private Integer userId; /** 角色ID */ private Integer roleId; }UserRole类
/** * 权限表 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName(value = "sys_role") public class Role { /** 角色id */ private Integer roleId; /** 角色名字 */ private String roleName; /** 删除状态(0正常 1删除) */ private String delStatus; /** 状态(0正常 1停用) */ private String status; /** 备注 */ private String remark; }Role类
/** * 权限表 */ @Data @AllArgsConstructor @NoArgsConstructor @Builder @JsonInclude(JsonInclude.Include.NON_NULL) // Java 对象序列化为 JSON 字符串时,控制哪些属性应该被包含在生成的 JSON 中。 @TableName(value = "sys_menu") public class Menu { /** 权限ID */ @TableId private Integer menuId; /** 页面路径 */ private Integer path; /** 路径说明 */ private Integer explain; /** 权限状态(0正常,1停用) */ private String status; /** 权限标识 */ private String perms; }Menu类
/** * 角色权限中间表 */ @Data @AllArgsConstructor @NoArgsConstructor @TableName(value = "sys_menu_role") public class MenuRole { /** 权限ID */ private Integer menuId; /** 角色ID */ private Integer roleId; }MenuRole类
核心代码:
限制访问资源所需的权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
要使用它我们需要先开启相关配置。
SecurityConfig 类上加注解:
@EnableGlobalMethodSecurity(prePostEnabled = true)
开启后,可以在Controller接口处使用对应注解:
# @PreAuthorize("hasAuthority('允许用户使用的权限标识')") @PreAuthorize("hasAuthority('sys:del:list')")
封装权限信息:
前面在写 UserDetailsServiceImpl 时备注了,在查询出用户后还要获取对应的权限信息,封装到 UserDetails 中返回。
先直接把权限信息写死封装到 UserDetails 中进行测试。
在之前定义了 UserDetails 的实现类 LoginUser,想要让其能封装权限信息就要对其进行修改。
/** * 封装用户信息 */ @Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; // 存储权限信息 private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } // 存储 Security 所需要的权限信息的集合 @JSONField(serialize = false) private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } // 将 permissions 内字符串类型的权限信息转换为 GrantedAuthority 对象存入 authorities authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserAccount(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
LoginUser 修改完后我们就可以在 CustomUserDetailsService 中去把权限信息封装到 LoginUser 中了。
更新 SecurityConfig ,因为要将用户 权限信息存储 token。
/** * 过滤器 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头获取token String token = request.getHeader("token"); // 检查获取到的token是否为空或空白字符串。(判断给定的字符串是否包含文本) if (!StringUtils.hasText(token)) { // 如果token为空,则直接放行请求到下一个过滤器,不做进一步处理并结束当前方法,不继续执行下面代码。 filterChain.doFilter(request, response); return; } // 解析token String userAccount; try { Claims claims = JwtUtil.parseJWT(token); userAccount = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } // 从redis 中 获取 用户信息 String redisKey = "login:" + userAccount; // redis 获取 键 对应 数据 LoginUser loginUser = redisCache.getCacheObject(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } // 将用户信息存入 SecurityConText // UsernamePasswordAuthenticationToken 存储用户名 密码 权限的集合 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); // SecurityContextHolder是Spring Security用来存储当前线程安全的认证信息的容器。 // 将用户名 密码 权限的集合存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
自定义失败的方式,否则无权限时 返空。
我们希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的 json,这样可以让前端能对响应进行统一的处理。
要实现这个功能我们需要知道 SpringSecurity 的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被 ExceptionTranslationFilter 捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成 AccessDeniedException 然后调用 AccessDeniedHandler 对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义 AuthenticationEntryPoint和AccessDeniedHandler 然后配置给 SpringSecurity 即可。
import com.alibaba.fastjson.JSON; import com.bei.domain.ResponseResult; import com.bei.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 权限异常 */ @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
import com.alibaba.fastjson.JSON; import com.bei.domain.ResponseResult; import com.bei.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 认证异常 */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录"); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
配置到 如下 SecurityConfig 即可。
跨域:(postman是没有跨域的)
浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。
/** * 跨域 */ @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 设置允许跨域的路径 registry.addMapping("/**") // 设置允许跨域请求的域名 .allowedOriginPatterns("*") // 是否允许cookie .allowCredentials(true) // 设置允许的请求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 .allowedHeaders("*") // 跨域允许时间 .maxAge(3600); } }
SecurityConfig 最终配置:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http // 因为是 前后端分离 要关闭 csrf() .csrf().disable() // 不通过 session 获取 SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 登录接口公开访问 .antMatchers("/user_login/login").anonymous() // 除上面公开的接口外,所有的请求都需要鉴定认证 .anyRequest().authenticated() .and() // 添加 过滤器 .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 在 SecurityConfig 内将 token 校验过滤器添加到过滤器链内 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 异常处理 http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler); // 允许跨域 http.cors(); } /** AuthenticationManager 认证 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** 密码加密 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
此时,登录认证与授权皆已完成。
权限测试:
测试Controller 测试一下。
先不带权限。
@RestController public class TestLoginController { /** 测试登录 */ @PostMapping("/test") // @PreAuthorize("hasAuthority('sys:del:list')") public String test(){ return "登录成功"; } }
带上权限:
此时我们给的权限可以从数据库内看出 是 删除权限,而登录的 用户 没有给与权限。说明我们权限成功了。
重新给一个有权限的标识,登录后带上 token 访问,有权限,成功。
4. 拦截器
Security 可以配合 拦截器使用。
拦截器可以理解为是 aop 编程。配合前端达到更为严谨的约束。
前端拿着我们 返的 状态码 与 信息进行接口约束。
interceptor 包:(简单的 token 拦截器)
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // HttpServletRequest request:HTTP请求对象,包含请求的详细信息。 // HttpServletResponse response:HTTP响应对象,用于设置响应的详细信息。 // Object handler:被拦截的处理器(控制器)对象。 // 从HTTP请求头中获取名为"Authorization"的值,这通常用于存放JWT令牌。 String token = request.getHeader("Token"); // 检查令牌是否存在以及令牌是否以"Bearer "开头。 // token == null:检查请求头中是否包含Authorization字段。 // !token.startsWith("Bearer "):检查令牌是否以"Bearer "前缀开头,这是JWT令牌的一种标准格式。 if (token == null || token.isEmpty()) { // 如果令牌不存在或格式无效,设置HTTP响应状态为409(未授权),并在响应体中写入错误消息"Missing or invalid token"。 // 返回false,表示请求被拦截,不继续传递给控制器。 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); try { // response.getWriter().write("缺少或无效的令牌"); response.getWriter().write("{\n" + "\t\"code\": 401,\n" + "\t\"message\": \"没有权限\"\n" + "}"); } catch (Exception e) { e.printStackTrace(); } return false; } // true:请求继续传递给后续处理器。 // false:请求被拦截,后续处理器不再执行。 return true; } }
config 包:除了 公开访问的接口,都需要进行拦截。
@Configuration public class WebConfig implements WebMvcConfigurer { // 注入JwtInterceptor拦截器 @Autowired private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册JwtInterceptor拦截器,设置拦截路径和排除路径 registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") // 拦截所有请求路径 .excludePathPatterns("/user_login/login"); // 排除登录路径 } }
# JSON { "code": 401, "message": "没有权限" }
后续完善前端联调。
标签:实战,return,String,分离,SpringSecurity,user,key,import,public From: https://www.cnblogs.com/warmNest-llb/p/18214646