Spring Security学习笔记(三)Spring Security+JWT认证授权流程代码实例
前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本
上两篇文章介绍了Spring Security的整体架构以及认证和鉴权模块原理。本篇文章就是基于Spring Security和JWT的一个demo
一、JWT简介
JWT(JSON Web Token),是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。本质上是一个经过数字签名的JSON对象,能够携带并传递状态信息(如用户身份验证、授权等)
1.1、JWT的结构
JWT由三部分组成,通过点号(.)连接,这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。类似于xxxx.xxxx.xxxx格式。如下:
eyJhbGciOiJIUzUxMiJ9.eyJMT0dJTl9USU1FIjoxNzIyMzEzMDg4NTU4LCJMT0dJTl9VU0VSIjoidXNlcjIiLCJleHAiOjE3MjIzMTY2ODh9.l-mw4sWCWvIrWSRHUPdiLlgH6tIFxbwx7KwUj0Ldf4CDbdOqQlDuj-x0y6zM4R84vmnRLBBDeH_oLRxx0rcNxQ
 
- Header:头部,声明了JWT的类型(通常是JWT)以及所使用的加密算法(例如HMAC SHA256或RSA)
 - Payload:载荷,承载实际数据的部分,可以包含预定义的声明(如iss(签发者)、exp(过期时间)、sub(主题)等)以及其它自定义的数据。这些信息都是铭文的,但不建议存放敏感信息。
 - Signature:签名,通过对前两部分进行编码后的信息,使用指定的密钥通过头部(Header)中声明的加密算法生成,拥有验证数据完整性和防止篡改。
 
这三部分单独使用base64编码后再通过点号(.)连接。
这里只简单介绍JWT,如果需要详细了解JWT的可以参考以下文章
https://blog.csdn.net/weixin_42753193/article/details/126294904
https://www.cnblogs.com/moonlightL/p/10020732.html
JWT官网
二、Spring Security+JWT认证授权流程代码代码实例
2.1、新建Springboot项目,引入JAR包
新建好Springboot项目,引入用到的jar包
 pom文件(只写出了dependencies):
	<!--Springboot父工程,定义好了Springboot集成的其他jar包版本,所以引入某些jar时可以不写版本号--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.15</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><!--使用undertow容器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!--自定义配置生成元数据信息,这样在配置文件中可以有提示--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!--Spring Security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!-- JSON Web Token Support --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>
 
application.yaml配置文件:
server:port: 8084servlet:context-path: /securitymybatis-plus:mapper-locations: classpath*:mapper/**/*Mapper.xml# 使用驼峰命名# 数据库表列:user_name# 实体类属性:userNameconfiguration:map-underscore-to-camel-case: trueSpring:redis:host: 127.0.0.1port: 6379lettuce:pool:max-idle: 16max-active: 32min-idle: 8datasource:# 数据源基本配置username: rootpassword: root1234url: jdbc:mysql://127.0.0.1:3306/test?allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai# driver-class需要注意mysql驱动的版本(com.mysql.cj.jdbc.Driver 或 com.mysql.jdbc.Driver)driver-class-name: com.mysql.cj.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSourcehikari:pool-name: Retail_HikariCPminimum-idle: 5 #最小空闲连接数idle-timeout: 180000 #空闲连接存活最长时间 默认600000(10分钟)maximum-pool-size: 10 #连接池最大连接数,默认10auto-commit: true #此属性控制从连接池返回的连接的默认自动提交行为,默认truemax-lifetime: 1800000 #连接的最长生命周期,0表示无限,默认1800000即30分钟connection-timeout: 30000 #数据库连接超时时间,默认30秒,即3000connection-test-query: SELECT 1 FROM DUAL
 
2.2、数据库操作相关类
数据库脚本(mysql):
create table `manager`(`id` int NOT NULL AUTO_INCREMENT,`login_name` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '登录名',`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',`id_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '身份证',`mobile` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',`email` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '管理员表';
##密码是123456
INSERT INTO `manager` (`id`, `login_name`, `password`, `name`, `id_number`, `mobile`, `email`) VALUES (1, 'user1', '$2a$10$JrdOPx3zKcNqLQnU7GrdUeE2XA3KXZgu3QqLCeBTJWPxJjOOfOHGG', '张三', NULL, NULL, NULL);
INSERT INTO `manager` (`id`, `login_name`, `password`, `name`, `id_number`, `mobile`, `email`) VALUES (2, 'user2', '$2a$10$JrdOPx3zKcNqLQnU7GrdUeE2XA3KXZgu3QqLCeBTJWPxJjOOfOHGG', '李四', NULL, NULL, NULL);create table `role`(`id` int NOT NULL AUTO_INCREMENT,`name` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名',`code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色编码',`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色类别',`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色描述',PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '角色表';INSERT INTO `role` (`id`, `name`, `code`, `type`, `description`) VALUES (1, '管理员角色', 'AdminManager', 'admin', NULL);
INSERT INTO `role` (`id`, `name`, `code`, `type`, `description`) VALUES (2, '审批用户角色', 'ApproveUser', 'approve', NULL);create table `permission`(`id` int NOT NULL AUTO_INCREMENT,`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限名',`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限编码',`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限类别',`url` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '资源权限路径',`anonymous` int NOT NULL COMMENT '是否可以匿名访问 1-是 0-否',`description` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限描述',PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '权限表';INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (1, '主页接口', 'main', 'interface', '/main', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (2, '测试接口1', 'test1', 'interface', '/adminRole', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (3, '测试接口2', 'test2', 'interface', '/touristRole', NULL, 0);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (4, '登录接口', 'login', 'interface', '/login', NULL, 1);
INSERT INTO `permission` (`id`, `name`, `code`, `type`, `url`, `description`, `anonymous`) VALUES (5, '注销接口', 'logout', 'interface', '/myLogout', NULL, 1);create table `manager_role_rel`(`id` int NOT NULL AUTO_INCREMENT,`manager_id` int NOT NULL COMMENT '用户id',`role_id` int NOT NULL COMMENT '角色id',PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '用户角色关联表';INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (1, 1, 1);
INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (2, 2, 2);
INSERT INTO `manager_role_rel` (`id`, `manager_id`, `role_id`) VALUES (3, 1, 2);create table `role_permission_rel`(`id` int NOT NULL AUTO_INCREMENT,`role_id` int NOT NULL COMMENT '用户id',`permission_id` int NOT NULL COMMENT '角色id',PRIMARY KEY (`id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic COMMENT = '角色权限关联表';INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (1, 1, 1);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (2, 1, 2);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (3, 1, 3);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (4, 2, 1);
INSERT INTO `role_permission_rel` (`id`, `role_id`, `permission_id`) VALUES (5, 2, 3);
 
实体类:
@Data
@TableName("manager")
public class ManagerDomain {@TableId(type = IdType.AUTO)private Integer id;//@TableField("user_name")private String loginName;private String password;private String name;private String idNumber;private String mobile;private String email;
}@Data
@TableName("permission")
public class PermissionDomain {@TableId(type = IdType.AUTO)private Integer id;private String name;private String code;private String type;private String url;private String description;private Integer anonymous;
}@Data
@TableName("role")
public class RoleDomain {@TableId(type = IdType.AUTO)private Integer id;private String name;private String code;private String description;
}
 
mybatis的Mapper接口及配置文件:
@Mapper
public interface ManagerMapper extends BaseMapper<ManagerDomain> {
}@Mapper
public interface PermissionMapper extends BaseMapper<PermissionDomain> {/*** 根据角色code获取该角色的资源权限url* @param roleCode* @return*/List<String> getPermissionUrlByRole(String roleCode);List<String> getAnonymousPermissionUrl();
}@Mapper
public interface RoleMapper extends BaseMapper<RoleDomain> {/*** 根据用户id获取该用户拥有的角色的code* @param managerId* @return*/List<String> getRoleCodeByManagerId(Integer managerId);/*** 获取所有角色的code* @return*/List<String> getAllRoleCode();
}
 
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.ManagerMapper"></mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.PermissionMapper"><select id="getPermissionUrlByRole" resultType="java.lang.String">select p.urlfrom role rLEFT JOIN role_permission_rel rpr on r.id = rpr.role_idleft join permission p on rpr.permission_id = p.idwhere r.code = #{roleCode}</select><select id="getAnonymousPermissionUrl" resultType="java.lang.String">select url from permission where anonymous = 1</select>
</mapper><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dmf.demo.jwt.security.dao.RoleMapper"><select id="getRoleCodeByManagerId" resultType="java.lang.String">select r.codefrom role rleft join manager_role_rel mrr on mrr.role_id = r.idWHERE mrr.manager_id= #{managerId}</select><select id="getAllRoleCode" resultType="java.lang.String">select code from role</select>
</mapper>
 
2.3、Controller和Service
@Slf4j
@Controller
public class SystemController {@Autowiredprivate SystemService systemService;/*** 登录* @param userName* @param password* @return*/@RequestMapping("/login")@ResponseBodypublic String login(String userName, String password){log.info("用户{}登录",userName);return systemService.login(userName,password);}@RequestMapping("/myLogout")@ResponseBodypublic String logout(HttpServletRequest request){systemService.logout(request);return "success";}/*** @return*/@RequestMapping("/adminRole")@ResponseBodypublic String adminRole(){return "success";}@RequestMapping("/touristRole")@ResponseBodypublic String touristRole(){return "success";}
} 
service接口及实现类
public interface SystemService {String login(String userName,String password);void logout(HttpServletRequest request);
}
 
SystemService 实现类:
@Slf4j
@Service
public class SystemServiceImpl implements SystemService {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisTemplate<String,String> stringRedisTemplate;@Overridepublic String login(String userName, String password) {//1、根据用户输入的用户名和密码创建认证凭证AuthenticationUsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(userName, password);//2、调用AuthenticationManager认证管理器的authenticate方法进行认证操作,返回认证成功后的凭证AuthenticationAuthentication authenticate = null;try {authenticate = authenticationManager.authenticate(authenticationToken);} catch (AuthenticationException e) {//这里自己捕获认证异常,自己处理,如果自己不处理的话,异常会交给自定义的AuthenticationEntryPoint处理//如果没定义AuthenticationEntryPoint,Spring Security会默认返回403log.error("登录失败!原因:{}",e.getMessage());throw new RuntimeException("登录失败!");}//3、生成jwt//拿到认证成功后的用户信息LoginUserDetails userDetails = (LoginUserDetails) authenticate.getPrincipal();String accessToken = JwtUtils.createToken(userDetails);//4、保存用户信息到redisLoginUserInfoDto loginUserInfoDto = LoginUserInfoDto.builder().loginName(userDetails.getUsername()).id(userDetails.getManager().getId()).name(userDetails.getManager().getName()).mobile(userDetails.getManager().getMobile()).roles(userDetails.getRoles()).build();String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+userDetails.getUsername();stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(loginUserInfoDto),60, TimeUnit.MINUTES);return accessToken;}@Overridepublic void logout(HttpServletRequest request) {String token = request.getHeader("token");if(StringUtils.isNotEmpty(token)){String userName = JwtUtils.getUserName(token);//清除redisif(StringUtils.isNotEmpty(userName)){String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+userName;stringRedisTemplate.delete(key);}}}
}
 
用户登录信息实体类LoginUserInfoDto:
@Data
@Builder
public class LoginUserInfoDto {private Integer id;private String loginName;private String name;private String idNumber;private String mobile;private List<String> roles;/*** 组装spring security的权限* @return*/public Collection<? extends GrantedAuthority> getAuthorities() {List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();if(!CollectionUtils.isEmpty(roles)){roles.forEach(roleCode ->{grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));});}return grantedAuthorities;}
}
 
全局常数类:
public class GlobalConstants {/*** 请求携带的token参数,参数名*/public static final String HEADER_TOKEN_NAME = "token";/*** 用户登录信息缓存KEY前缀*/public static final String LOGIN_CACHE_KEY_PREFIX = "USER_INFO:";/*** 全局资源权限缓存key*/public static final String GLOBAL_PERMISSION_KEY_PREFIX = "GLOBAL_PERMISSION:";/*** 允许匿名访问资源缓存key*/public static final String GLOBAL_PERMISSION_ANONYMOUS = "GLOBAL_PERMISSION:ANONYMOUS";
}
 
JWT工具类:
@Slf4j
public class JwtUtils {/** jwt加密秘钥*/public static final String DEFAULT_SECRET = "abcdefghijk";/** jwt数据声明里登录用户key*/public static final String LOGIN_USER = "LOGIN_USER";/** jwt数据声明里登录时间key*/public static final String LOGIN_TIME = "LOGIN_TIME";/** jwt默认过期时间*/public static Long DEFAULT_TTL = 60*60*1000l; //一个小时/*** 生成jwt使用默认设置* @param claims* @return*/public static String createToken(Map<String, Object> claims){return createToken(claims,DEFAULT_TTL,DEFAULT_SECRET);}/*** 生成jwt* @param claims* @param ttl 过期时间 ms* @return*/public static String createToken(Map<String, Object> claims,Long ttl){return createToken(claims,ttl,DEFAULT_SECRET);}/**** @param userDetails Spring Security用户信息* @param ttl 过期时间 ms* @return*/public static String createToken(UserDetails userDetails,Long ttl){Map<String, Object> claims = new HashMap<>();claims.put(LOGIN_USER,userDetails.getUsername());claims.put(LOGIN_TIME,new Date());return createToken(claims,ttl,DEFAULT_SECRET);}/**** @param userDetails* @return*/public static String createToken(UserDetails userDetails){return createToken(userDetails,DEFAULT_TTL);}/*** 生成jwt* @param claims* @return*/public static String createToken(Map<String, Object> claims,Long ttl,String secret){return Jwts.builder().setClaims(claims)  //设置数据.setExpiration(generateExpirationDate(ttl)).signWith(SignatureAlgorithm.HS512, secret) //签名,参数包括算法和秘钥.compact(); //压缩生成xxx.xxx.xxx}/*** 生成token的过期时间* @param ttl 单位是毫秒* @return*/private static Date generateExpirationDate(Long ttl) {return new Date(System.currentTimeMillis() + ttl);}/*** 解析jwt拿到数据,使用默认配置* @param token* @return*/public static Claims parseToken(String token){return  parseToken(token,DEFAULT_SECRET);}/*** 解析jwt拿到数据* @param token* @return*/public static Claims parseToken(String token,String secret){Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();return claims;}/*** 获取jwt里的用户名称* @param token* @return*/public static String getUserName(String token){return (String)parseToken(token).get(LOGIN_USER);}/*** token是否已经过期* @param claims* @return*/private static boolean isTokenExpired(Claims claims) {Date expire = claims.getExpiration();if(expire!=null){return expire.before(new Date());}return false;}
}
 
2.4、Spring Security自定义认证和鉴权
上篇文章已经介绍过Spring Security的认证和鉴权架构。
 认证:
 Spring Security的认证主要由AuthenticationManager -> AuthenticationProvider流程。而AuthenticationProvider调用UserDetailsService的loadUserByUsername方法先查询系统用户,再和用户输入的用户信息做比对认证。
 所以自定义认证,我们只需要在配置类里定义自己的AuthenticationManager和AuthenticationProvider,以及实现UserDetailsService接口。另外UserDetails类的默认实现类User使用不方便,也可以实现自定义的UserDetails来做功能扩展
自定义的UserDetails实现类:
@Data
@Builder
public class LoginUserDetails implements UserDetails {private ManagerDomain manager;private Integer id;private String username;private String password;private boolean enabled;private boolean locked;private Collection<? extends GrantedAuthority> grantedAuthorities;private List<String> roles;public Integer getUserId() {return this.manager.getId();}// 返回当前用户的权限列表@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (grantedAuthorities != null)return this.grantedAuthorities;List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<>();if(!CollectionUtils.isEmpty(roles)){roles.forEach(roleCode ->{grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + roleCode));});}return grantedAuthorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic String getUsername() {return this.username;}//账号是否未过期,直接返回true 表示账户未过期,也可以在数据库中添加该字段@Overridepublic boolean isAccountNonExpired() {return true;}//账号是否被锁, 这里和数据库中的locked字段刚好相反,所有取反@Overridepublic boolean isAccountNonLocked() {return true;}//密码是否为过期,数据库中无该字段,直接返回true@Overridepublic boolean isCredentialsNonExpired() {return true;}//账户是否可用,从数据库中获取该字段@Overridepublic boolean isEnabled() {return true;}}
 
自定义的UserDetailsService实现类:
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {@Resourceprivate ManagerMapper managerMapper;@Resourceprivate RoleMapper roleMapper;/*** UserDetails提供的字段如果不够的话,可以继承 User类,实现自己的UserDetails* 用户认证时会调用* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {ManagerDomain user = managerMapper.selectOne(new LambdaQueryWrapper<ManagerDomain>().eq(ManagerDomain::getLoginName,username));if(user == null){throw new UsernameNotFoundException(username);}//查询用户角色List<String> roles = roleMapper.getRoleCodeByManagerId(user.getId());LoginUserDetails userDetails = LoginUserDetails.builder().username(user.getLoginName()).password(user.getPassword()).manager(user).roles(roles).build();return userDetails;}@Overridepublic void createUser(UserDetails user) {}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}
}
 
AuthenticationManager和AuthenticationProvider,Spring Security提供了默认的实现ProviderManager和DaoAuthenticationProvider。直接在配置类配置这两个bean即可。
鉴权:
 鉴权流程主要由AccessDecisionManager(鉴权管理器)和AccessDecisionVoter(投票器)来处理。鉴权管理器使用默认实现之一的UnanimousBased(一票反对,只要有一票反对就不能通过),然后实现自定义的投票器即可。
 在实际鉴权处理前,我们还需要一个过滤器来处理jwt,通过jwt来拿到认证信息。
 jwt过滤器:
@Slf4j
//@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Resourceprivate RedisTemplate<String,String> stringRedisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//1、拿到tokenString token = request.getHeader("token");if(StringUtils.isNotEmpty(token)){//2、校验tokentry {String username = JwtUtils.getUserName(token);String key = GlobalConstants.LOGIN_CACHE_KEY_PREFIX+username;String userInfoStr = stringRedisTemplate.opsForValue().get(key);if(StringUtils.isNotEmpty(userInfoStr)){//得到用户账号及权限相关信息LoginUserInfoDto loginUserInfoDto = JSONObject.parseObject(userInfoStr,LoginUserInfoDto.class);//设置该用户的权限上下文信息,方便后续过滤器校验UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =new UsernamePasswordAuthenticationToken(loginUserInfoDto,null,loginUserInfoDto.getAuthorities());SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}else{throw new RuntimeException("token无效或已过期,请重新登录!");}//放行filterChain.doFilter(request,response);} catch (RuntimeException e) {//自行处理认证异常,如果不处理的话,会由Spring Security处理,如果没定义异常处理handler,最后会返回403exceptionHandle(request,response,e);}}else{//放行filterChain.doFilter(request,response);}}/*** jwt认证失败处理* @param request* @param response* @param e*/private void exceptionHandle(HttpServletRequest request, HttpServletResponse response,Exception e) throws IOException {log.info("jwt认证失败,原因:{}",e.getMessage());//这里就不往下走了,直接返回失败的结果Map<String,Object> result = new HashMap();result.put("code",-3);result.put("message","token认证失败!");//  将结果对象转换成json字符串String json = JSON.toJSONString(result);response.setContentType("application/json;charset=UTF-8");//  响应体response.getWriter().println(json);}
}
 
自定义AccessDecisionVoter(投票器):
@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {@Resourceprivate RedisTemplate<String,String> stringRedisTemplate;@Resourceprivate PermissionMapper permissionMapper;@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}@Overridepublic int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {//默认否决票int result = ACCESS_DENIED;String requestUrl = object.getRequest().getServletPath();String method = object.getRequest().getMethod();log.debug("进入自定义鉴权投票器,URI : {} {}", method, requestUrl);//判断请求是否运行匿名访问boolean anonymous = stringRedisTemplate.opsForHash().hasKey(GlobalConstants.GLOBAL_PERMISSION_ANONYMOUS,requestUrl);if(anonymous){//允许匿名访问直接同意return ACCESS_GRANTED;}//拿到用户的角色Object principal = authentication.getPrincipal();//principal不是LoginUserInfoDto表示是匿名用户或未认证的用户,且请求url未在数据库配置权限if(principal instanceof LoginUserInfoDto){LoginUserInfoDto dto = (LoginUserInfoDto)principal;List<String> roles = dto.getRoles();String keyPrefix = GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;if(!CollectionUtils.isEmpty(roles)){for(String roleCode : roles){String key = keyPrefix+roleCode;if(stringRedisTemplate.hasKey(key)){String val = (String)stringRedisTemplate.opsForHash().get(key,requestUrl);if(val!=null){//存在投同意result = ACCESS_GRANTED;//结束循环break;}}else{//如果缓存没有,查库List<String> urls = permissionMapper.getPermissionUrlByRole(roleCode);if(!CollectionUtils.isEmpty(urls)){//存缓存Map<String,Object> map = new HashMap<>();urls.forEach(url ->{map.put(url,"1");});stringRedisTemplate.opsForHash().putAll(key,map);if(urls.contains(requestUrl)){//存在投同意result = ACCESS_GRANTED;//结束循环break;}}}}}}else{//匿名用户请求,且请求url未在数据库配置权限,交给WebExpressionVoter处理,这里就不做处理result = ACCESS_ABSTAIN;}return result;}
}
 
这个投票器的主要逻辑是,去redis查询项目启动时初始化的角色权限缓存。没有缓存,则查库。拿到用户认证信息(在jwt过滤器里设置的)里的角色,判断角色权限缓存里有没有请求的url,有则表示该角色能访问该url,即用户有权访问该url。
初始化角色权限缓存:
@Component
@Slf4j
public class PermissionInitRunner implements ApplicationRunner {@Resourceprivate RedisTemplate<String,String> stringRedisTemplate;@Resourceprivate RoleMapper roleMapper;@Resourceprivate PermissionMapper permissionMapper;@Overridepublic void run(ApplicationArguments args) throws Exception {String keyPrefix = GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;log.info("开始初始化全局资源权限缓存");List<String> allRoleCode = roleMapper.getAllRoleCode();if(!CollectionUtils.isEmpty(allRoleCode)){for(String roleCode : allRoleCode){List<String> urls = permissionMapper.getPermissionUrlByRole(roleCode);if(!CollectionUtils.isEmpty(urls)){Map<String,Object> map = new HashMap<>();urls.forEach(url ->{map.put(url,"1");});stringRedisTemplate.opsForHash().putAll(keyPrefix+roleCode,map);}}}//允许匿名访问的资源权限keyList<String> urls = permissionMapper.getAnonymousPermissionUrl();if(!CollectionUtils.isEmpty(urls)){String key = GlobalConstants.GLOBAL_PERMISSION_ANONYMOUS;Map<String,Object> map = new HashMap<>();urls.forEach(url ->{map.put(url,"1");});stringRedisTemplate.opsForHash().putAll(key,map);}log.info("初始化全局资源权限缓存结束");}
}
 
自定义认证异常和鉴权异常的处理类:
 认证异常处理类:
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String localizedMessage = "未认证,请先认证!";//authException.getLocalizedMessage();Map<String,Object> result = new HashMap();result.put("code",-2);   // 告诉用户需要登录result.put("message",localizedMessage);   ////  将结果对象转换成json字符串String json = JSON.toJSONString(result);//  返回json数据到前端//  响应头response.setContentType("application/json;charset=UTF-8");//  响应体response.getWriter().println(json);//返回登录界面//response.sendRedirect(request.getContextPath()+"/myLoginPage");}
}
 
鉴权异常处理类:
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {Map<String,Object> result = new HashMap();result.put("code",-1);   // 没有权限result.put("message","没有权限");   ////  将结果对象转换成json字符串String json = JSON.toJSONString(result);//  返回json数据到前端//  响应头response.setContentType("application/json;charset=UTF-8");//  响应体response.getWriter().println(json);//返回页面//response.sendRedirect(request.getContextPath()+"/main");}
}
 
Spring Security配置类:
@Configuration
public class WebSecurityConfig {/*** 密码编码器,会对请求传入的密码进行加密* @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic UserDetailsService userDetailsService(){return new DBUserDetailsManager();}@Beanpublic AuthenticationProvider authenticationProvider(UserDetailsService  userDetailsService,PasswordEncoder passwordEncoder){DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userDetailsService);daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);return daoAuthenticationProvider;}/*** 认证管理器* @param authenticationProvider* @return*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){// ProviderManager 是 AuthenticationManager 最常用的实现return new ProviderManager(authenticationProvider);}/*** jwt过滤器* @return*/@Beanpublic JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){return new JwtAuthenticationTokenFilter();}/*** 自定义鉴权投票器* @return*/@Beanpublic AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {return new AccessDecisionProcessor();}/*** 鉴权管理器* @return*/@Beanpublic AccessDecisionManager accessDecisionManager() {// 构造一个新的AccessDecisionManager 放入两个投票器//WebExpressionVoter为配置文件投票器,即在HttpSecurity 的authorizeRequests方法里定义的过滤规则,使用他是为了也可以使用配置定义好放行规则List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());//UnanimousBased为一票否决鉴权//AffirmativeBased为一票通过鉴权,WebExpressionVoter投票如果未配置则默认为通过,所以这里需要配置为UnanimousBasedreturn new UnanimousBased(decisionVoters);}/*** Spring Security配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests(authorize ->authorize// 放行所有OPTIONS请求,跨域请求会先发一个OPTIONS请求.antMatchers(HttpMethod.OPTIONS).permitAll().antMatchers("/login").permitAll().antMatchers("/myLogout").permitAll().anyRequest()  //对所有请求开启授权保护.authenticated() //已认证的请求会被自动授权.accessDecisionManager(accessDecisionManager()));//添加自定义过滤器http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);http.exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理.accessDeniedHandler(new MyAccessDeniedHandler())   //未授权资源请求处理);//关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌http.csrf(csrf -> csrf.disable());// 关闭Session机制//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);return http.build();}
}
相关文章:
Spring Security学习笔记(三)Spring Security+JWT认证授权流程代码实例
前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本 上两篇文章介绍了Spring Security的整体架构以及认证和鉴权模块原理。本篇文章就是基于Spring Security和JWT的一个demo 一、JWT简介 JWT(JSON Web Token),…...
精装房、旧房改造智能家居,单火线也有“救”了单火模块 零线发生器
精装房、旧房改造智能家居,单火线也有“救”了单火模块 零线发生器 史新华 以前写过关于智能家居没有预留零线,导致无法安装零火开关,也没办法装触控屏,主要原因还是无法通过零火线给设备供电。今年最火的一款思万奇零线发生器救…...
使用URLSearchParams获取url地址后面的参数(window.location.href)
function getUrlParams(url) {const urlStr url.split(?)[1];const urlSearchParams new URLSearchParams(urlStr);return Object.fromEntries(urlSearchParams.entries()); }const info getUrlParams(window.location.href); // info是一个对象,包含url携带参数…...
计算机网络03
文章目录 重传机制超时重传快速重传SACK 方法Duplicate SACK 滑动窗口流量控制操作系统缓冲区与滑动窗口的关系窗口关闭糊涂窗口综合症 拥塞控制慢启动拥塞避免算法拥塞发生快速恢复 如何理解是 TCP 面向字节流协议?如何理解字节流?如何解决粘包…...
linux每个目录都是干啥的???linux目录说明
很全,没事看看,记住 / 虚拟目录的根目录。通常不会在这里存储文件 /bin 二进制目录,存放许多用户级的GNU工具启动目录,存放启动文件 /etc 系统配置目录 /dev 设备目录,Linux在这里创建设备节点系统配置文件目录 /home 主目录,Linux在…...
DB2-Db2StreamingChangeEventSource
提示:Db2StreamingChangeEventSource 类主要用于从 IBM Db2 数据库中读取变更数据捕获 (CDC, Change Data Capture) 信息。CDC 是一种技术,允许系统跟踪数据库表中数据的更改,这些更改可以是插入、更新或删除操作。在大数据和实时数据处理场景…...
在当前的数字化时代,Cobol 语言如何与新兴技术(如云计算、大数据、人工智能)进行融合和交互?
Cobol语言作为一种古老的编程语言,与新兴技术的融合和交互需要一些额外的工作和技术支持。以下是一些将Cobol与新兴技术结合的方法: 云计算:Cobol程序可以迁移到云平台上运行,通过云提供的弹性和可扩展性,为Cobol应用程…...
使用SDL库以及C++实现的简单的贪吃蛇:AI Fitten生成
简单使用AI代码生成器做了一个贪吃蛇游戏 设计的基本逻辑都是正确的,能流畅运行 免费准确率高,非常不错!支持Visual Studio系列 Fitten:https://codewebchat.fittenlab.cn/ SDL 入门指南:安装配置https://blog.csdn.n…...
【C++标准库】模拟实现string类
模拟实现string类 一.命名空间与类成员变量二.构造函数1.无参(默认)构造2.有参构造3.兼容无参和有参构造4.拷贝构造1.传统写法2.现代写法 三.析构函数四.string类对象的容量操作1.size2.capacity3.clear4.empty5.reserve6.resize 五.string类对象的访问及…...
ArcGIS for js 标记(vue代码)
一、引入依赖 import Graphic from "arcgis/core/Graphic"; import GraphicsLayer from "arcgis/core/layers/GraphicsLayer"; import Color from "arcgis/core/Color"; import TextSymbol from "arcgis/core/symbols/TextSymbol.js"…...
全网最全最新100道C++面试题:40-60
前述:本文初衷是为了总结本人在各大平台看到的C面经,我会在本文持续更新我所遇到的一些C面试问题,如有错误请一定指正我。新建立了一个收集问答的仓库,欢迎各位小伙伴来更新鸭interview_experience: 本仓库初衷是想为大家提供一个…...
RAG+内容推荐,应该如何实践?
最近业务有需求:结合RAG内容推荐,针对实践部分,做一点探究。 话不多说,直接开冲! 背景 首先回顾一下 RAG 技术定义,它可以结合信息检索和生成模型的混合。简单来说,RAG 预训练的语言模型 信…...
SFTTrainer loss多少合适
在机器学习和深度学习中,“loss”(损失函数)的合理值并没有一个固定的标准,因为它依赖于多种因素,包括模型的类型、任务的性质、数据的规模和特性等。然而,我们可以从一些通用的原则和经验值来讨论损失函数…...
HTTP协议详解(一)
协议 为了使数据在网络上从源头到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议,它最终体现为在网络上传输的数据包的格式。 一、HTTP 协议介绍 HTTP(Hyper Text Transfer Protocol): 全…...
RK3568平台(触摸篇)串口触摸屏
一.什么是串口屏 串口屏,可组态方式二次开发的智能串口控制显示屏,是指带有串口通信的TFT彩色液晶屏显示控制模组。利用显示屏显示相关数据,通过触摸屏、按键、鼠标等输入单元写入参数或者输入操作指令,进而实现用户与机器进行信…...
MySQL数据库-事务
一、什么是事务 1.概念 事务(Transaction):一个最小的不可再分的工作单元,一个事务对应一个完整的业务,一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成,事务只针对DML语句。 数据…...
qt事件类型列表
t提供了一系列丰富的事件类型,这些事件允许应用程序响应各种用户输入、系统通知以及其他类型的交互。以下是一些常见的Qt事件类型及其用途概述: QEvent::None (0): 无事件,用于初始化或作为默认值。 QEvent::Timer (1): 定时器事件ÿ…...
ElasticSearch父子索引实战
关于父子索引 ES底层是Lucene,由于Lucene实际上是不支持嵌套类型的,所有文档都是以扁平的结构存储在Lucene中,ES对父子文档的支持,实际上也是采取了一种投机取巧的方式实现的. 父子文档均以独立的文档存入,然后添加关联关系,且父子文档必须在同一分片,由于父子类型文档并没有…...
二百四十九、Linux——在Linux中创建新用户、赋予新用户root权限并对文件夹赋予新用户的权限
一、目的 安装国产化数据库OceanBase的时候,需要创建新用户、赋予新用户root权限并对文件夹赋予新用户的权限 二、创建新用户 #创建账户 oceanadmin [roothurys22 ~]#useradd -U oceanadmin -d /home/oceanadmin -s /bin/bash [roothurys22 ~]#mkdir -p /home/oc…...
com.mysql.cj.jdbc.Driver 爆红
出现这样的问题就是pom.xml文件中没有添加数据库依赖坐标 添加上这个依赖即可,添加完后重新加载一下Maven即可。 如果感觉对你有用就点个赞!!!...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Android第十三次面试总结(四大 组件基础)
Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成,用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机: onCreate() 调用时机:Activity 首次创建时调用。…...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
GraphQL 实战篇:Apollo Client 配置与缓存
GraphQL 实战篇:Apollo Client 配置与缓存 上一篇:GraphQL 入门篇:基础查询语法 依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在: https://github.com/GoldenaArcher/graphql…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...
篇章二 论坛系统——系统设计
目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...
