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();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/387785.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Scrapy 爬取旅游景点相关数据(七):利用指纹实现“不重复爬取”

本期学习&#xff1a; 利用网页指纹去重 众所周知&#xff0c;代理是要花钱的&#xff0c;那么在爬取&#xff08;测试&#xff09;巨量网页的时候&#xff0c;就不可能对已经爬取过的网站去重复的爬&#xff0c;这样会消耗大量的时间&#xff0c;更重要的是会消耗大量的IP (金…

vite instanceof 失效

背景&#xff1a;给一个巨石单体项目进行标准化模块拆分&#xff0c;封装出来的模块代码用 vite 进行构建&#xff0c;但模块启动后页面上的表现一直和 webpack 那版不一致 一步步 debug 后&#xff0c;发现问题出在下面这个判断条件 const GeneratorFunction function* () …

【Golang 面试 - 基础题】每日 5 题(七)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

Vue 3 中使用 InMap 绘制热力图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Vue 3 中使用 InMap 绘制热力图 应用场景介绍 InMap 是一款强大的地图组件库&#xff0c;它提供了一系列丰富的可视化功能&#xff0c;包括热力图。热力图可以将数据点在地图上以颜色编码的方式可视化&#x…

微软:警惕利用VMware ESXi进行身份验证绕过攻击

微软于7月29日发布警告&#xff0c;称勒索软件团伙正在积极利用 VMware ESXi 身份验证绕过漏洞进行攻击。 该漏洞被追踪为 CVE-2024-37085&#xff0c;由微软安全研究人员 Edan Zwick、Danielle Kuznets Nohi 和 Meitar Pinto 发现&#xff0c;并在 6 月 25 日发布的 ESXi 8.0 …

如何学习自动化测试工具!

要学习和掌握自动化测试工具的使用方法&#xff0c;可以按照以下步骤进行&#xff1a; 一、明确学习目标 首先&#xff0c;需要明确你想要学习哪种自动化测试工具。自动化测试工具种类繁多&#xff0c;包括但不限于Selenium、Appium、JMeter、Postman、Robot Framework等&…

docker环境安装kafka/Flink/clickhouse镜像

1、安装Kafka服务 1、将一下三个tar文件复制到ubuntu指定目录下 2、进入到/home/cl/app目录&#xff0c;使用docker命令加载tar镜像文件 # cd /home/cl/app # docker load -i kafka.tar # docker load -i kafka-manager.tar # docker load -i kafka-zookeeper.tar3、查看d…

分布式:RocketMQ/Kafka总结(附下载链接)

文章目录 下载链接思维导图 本文总结的是关于消息队列的常见知识总结。消息队列和分布式系统息息相关&#xff0c;因此这里就将消息队列放到分布式中一并进行处理关联 下载链接 链接: https://pan.baidu.com/s/1hRTh7rSesikisgRUO2GBpA?pwdutgp 提取码: utgp 思维导图

web学习笔记(八十三)git

目录 1.Git的基本概念 2.gitee常用的命令 3.解决两个人操作不同文件造成的冲突 4.解决两个人操作同一个文件造成的冲突 1.Git的基本概念 git是一种管理代码的方式&#xff0c;广泛用于软件开发和版本管理。我们通常使用gitee&#xff08;码云&#xff09;来云管理代码。 …

《Linux运维总结:基于x86_64架构CPU使用docker-compose一键离线部署zookeeper 3.8.4容器版分布式集群》

总结&#xff1a;整理不易&#xff0c;如果对你有帮助&#xff0c;可否点赞关注一下&#xff1f; 更多详细内容请参考&#xff1a;《Linux运维篇&#xff1a;Linux系统运维指南》 一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面对不同的客户部署业务系统&#xff0…

前端如何实现更换项目主题色的功能?

1、场景 有一个换主题色的功能&#xff0c;如下图&#xff1a; 切换颜色后&#xff0c;将对页面所有部分的色值进行重新设置&#xff0c;符合最新的主题色。 2、实现思路 因为色值比较灵活&#xff0c;可以任意选取&#xff0c;所以最好的实现方式是&#xff0c;根据设置的…

全面整理人工智能(AI)学习路线图及资源推荐

在人工智能&#xff08;AI&#xff09;飞速发展的今天&#xff0c;掌握AI技术已经成为了许多高校研究者和职场人士的必备技能。从深度学习到强化学习&#xff0c;从大模型训练到实际应用&#xff0c;AI技术的广度和深度不断拓展。作为一名AI学习者&#xff0c;面对浩瀚的知识海…

递归方法清空多维数组中的null元素(对象)

源码 //【递归】说明&#xff1a;递归方法清空多维数组中的null元素&#xff08;对象&#xff09; let clearNullElementsInArray (arr) > {return (arr || []).filter(v > {if (v null) {return false;} else {if (v.children) {v.children clearNullElementsInArra…

【C语言】Linux 飞翔的小鸟

【C语言】Linux 飞翔的小鸟 零、环境部署 安装Ncurses库 sudo apt-get install libncurses5-dev壹、编写代码 代码如下&#xff1a; bird.c #include<stdio.h> #include<time.h> #include<stdlib.h> #include<signal.h> #include<curses.h>…

科普文:Linux目录详解

在 Linux/Unix 操作系统中&#xff0c;一切都是文件&#xff0c;甚至目录也是文件&#xff0c;文件是文件&#xff0c;鼠标、键盘、打印机等设备也是文件。 这篇文章&#xff0c;我们将一起学习 Linux 中的目录结构及文件。 Linux 的文件类型 Linux系统中的文件系统&#xf…

【初阶数据结构】11.排序(2)

文章目录 2.3 交换排序2.3.1 冒泡排序2.3.2 快速排序2.3.2.1 hoare版本2.3.2.2 挖坑法2.3.2.3 lomuto前后指针2.3.2.4 非递归版本 2.4 归并排序2.5 测试代码&#xff1a;排序性能对比2.6 非比较排序2.6.1 计数排序 3.排序算法复杂度及稳定性分析 2.3 交换排序 交换排序基本思想…

【包邮送书】码农职场:IT人求职就业手册

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…

遗传算法与深度学习实战——进化深度学习

遗传算法与深度学习实战——进化深度学习 0. 前言1. 进化深度学习1.1 进化深度学习简介1.2 进化计算简介 2. 进化深度学习应用场景3. 深度学习优化3.1 优化网络体系结构 4. 通过自动机器学习进行优化4.1 自动机器学习简介4.2 AutoML 工具 5. 进化深度学习应用5.1 模型选择&…

鸿蒙开发——axios封装请求、拦截器

描述&#xff1a;接口用的是PHP&#xff0c;框架TP5 源码地址 链接&#xff1a;https://pan.quark.cn/s/a610610ca406 提取码&#xff1a;rbYX 请求登录 HttpUtil HttpApi 使用方法

Qt基础 | 主机信息查询 | QHostInfo的介绍和使用 | QNetworkInterface的介绍和使用

文章目录 一、Qt 网络模块介绍二、主机信息查询1.QHostlnfo 和 QNetworkInterface 类2.QHostlnfo 的使用2.1 获取本机主机名和 IP 地址2.2 查找主机的地址信息 3.QNetworkInterface 的使用 Qt 网络模块&#xff1a; Qt基础 | 主机信息查询 | QHostInfo的介绍和使用 | QNetworkI…