网站服务器租用怎样收费,企业网页设计公司,做网站用asp还是php好,网站维护排名前言#xff1a;本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本 上两篇文章介绍了Spring Security的整体架构以及认证和鉴权模块原理。本篇文章就是基于Spring Security和JWT的一个demo
一、JWT简介
JWT#xff08;JSON Web Token#xff09;#xff0c… 前言本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本 上两篇文章介绍了Spring Security的整体架构以及认证和鉴权模块原理。本篇文章就是基于Spring Security和JWT的一个demo
一、JWT简介
JWTJSON Web Token是一种开放标准RFC 7519用于在网络应用环境间安全地传输信息。本质上是一个经过数字签名的JSON对象能够携带并传递状态信息如用户身份验证、授权等
1.1、JWT的结构
JWT由三部分组成通过点号.连接这三部分分别是头部Header、载荷Payload和签名Signature。类似于xxxx.xxxx.xxxx格式。如下
eyJhbGciOiJIUzUxMiJ9.eyJMT0dJTl9USU1FIjoxNzIyMzEzMDg4NTU4LCJMT0dJTl9VU0VSIjoidXNlcjIiLCJleHAiOjE3MjIzMTY2ODh9.l-mw4sWCWvIrWSRHUPdiLlgH6tIFxbwx7KwUj0Ldf4CDbdOqQlDuj-x0y6zM4R84vmnRLBBDeH_oLRxx0rcNxQHeader头部声明了JWT的类型通常是JWT以及所使用的加密算法例如HMAC SHA256或RSAPayload载荷承载实际数据的部分可以包含预定义的声明如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 SecurityJWT认证授权流程代码代码实例
2.1、新建Springboot项目引入JAR包
新建好Springboot项目引入用到的jar包 pom文件只写出了dependencies !--Springboot父工程定义好了Springboot集成的其他jar包版本所以引入某些jar时可以不写版本号--parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.6.15/version/parentdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactIdexclusionsexclusiongroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-tomcat/artifactId/exclusion/exclusions/dependency!--使用undertow容器--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-undertow/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-thymeleaf/artifactId/dependency!--自定义配置生成元数据信息这样在配置文件中可以有提示--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency!--Spring Security--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency!--mysql--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!--mybatis-plus--dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependencydependencygroupIdcom.alibaba.fastjson2/groupIdartifactIdfastjson2/artifactId/dependencydependencygroupIdorg.apache.commons/groupIdartifactIdcommons-lang3/artifactId/dependency!-- JSON Web Token Support --dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactId/dependency!--redis--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency/dependenciesapplication.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?allowPublicKeyRetrievaltrueuseUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/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 #空闲连接存活最长时间 默认60000010分钟maximum-pool-size: 10 #连接池最大连接数默认10auto-commit: true #此属性控制从连接池返回的连接的默认自动提交行为默认truemax-lifetime: 1800000 #连接的最长生命周期0表示无限默认1800000即30分钟connection-timeout: 30000 #数据库连接超时时间默认30秒即3000connection-test-query: SELECT 1 FROM DUAL2.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 BaseMapperManagerDomain {
}Mapper
public interface PermissionMapper extends BaseMapperPermissionDomain {/*** 根据角色code获取该角色的资源权限url* param roleCode* return*/ListString getPermissionUrlByRole(String roleCode);ListString getAnonymousPermissionUrl();
}Mapper
public interface RoleMapper extends BaseMapperRoleDomain {/*** 根据用户id获取该用户拥有的角色的code* param managerId* return*/ListString getRoleCodeByManagerId(Integer managerId);/*** 获取所有角色的code* return*/ListString getAllRoleCode();
}?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapperPUBLIC -//mybatis.org//DTD Mapper 3.0//ENhttps://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.dmf.demo.jwt.security.dao.ManagerMapper/mapper?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapperPUBLIC -//mybatis.org//DTD Mapper 3.0//ENhttps://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.dmf.demo.jwt.security.dao.PermissionMapperselect idgetPermissionUrlByRole resultTypejava.lang.Stringselect 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}/selectselect idgetAnonymousPermissionUrl resultTypejava.lang.Stringselect url from permission where anonymous 1/select
/mapper?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapperPUBLIC -//mybatis.org//DTD Mapper 3.0//ENhttps://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.dmf.demo.jwt.security.dao.RoleMapperselect idgetRoleCodeByManagerId resultTypejava.lang.Stringselect r.codefrom role rleft join manager_role_rel mrr on mrr.role_id r.idWHERE mrr.manager_id #{managerId}/selectselect idgetAllRoleCode resultTypejava.lang.Stringselect code from role/select
/mapper2.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 RedisTemplateString,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处理//如果没定义AuthenticationEntryPointSpring 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_PREFIXuserDetails.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_PREFIXuserName;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 ListString roles;/*** 组装spring security的权限* return*/public Collection? extends GrantedAuthority getAuthorities() {ListSimpleGrantedAuthority 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(MapString, Object claims){return createToken(claims,DEFAULT_TTL,DEFAULT_SECRET);}/*** 生成jwt* param claims* param ttl 过期时间 ms* return*/public static String createToken(MapString, 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){MapString, 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(MapString, 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 ListString roles;public Integer getUserId() {return this.manager.getId();}// 返回当前用户的权限列表Overridepublic Collection? extends GrantedAuthority getAuthorities() {if (grantedAuthorities ! null)return this.grantedAuthorities;ListSimpleGrantedAuthority 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;}//密码是否为过期,数据库中无该字段,直接返回trueOverridepublic 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 LambdaQueryWrapperManagerDomain().eq(ManagerDomain::getLoginName,username));if(user null){throw new UsernameNotFoundException(username);}//查询用户角色ListString 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和AuthenticationProviderSpring Security提供了默认的实现ProviderManager和DaoAuthenticationProvider。直接在配置类配置这两个bean即可。
鉴权 鉴权流程主要由AccessDecisionManager鉴权管理器和AccessDecisionVoter投票器来处理。鉴权管理器使用默认实现之一的UnanimousBased一票反对只要有一票反对就不能通过然后实现自定义的投票器即可。 在实际鉴权处理前我们还需要一个过滤器来处理jwt通过jwt来拿到认证信息。 jwt过滤器
Slf4j
//Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {Resourceprivate RedisTemplateString,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_PREFIXusername;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());//这里就不往下走了直接返回失败的结果MapString,Object result new HashMap();result.put(code,-3);result.put(message,token认证失败);// 将结果对象转换成json字符串String json JSON.toJSONString(result);response.setContentType(application/json;charsetUTF-8);// 响应体response.getWriter().println(json);}
}自定义AccessDecisionVoter投票器
Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoterFilterInvocation {Resourceprivate RedisTemplateString,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, CollectionConfigAttribute 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;ListString roles dto.getRoles();String keyPrefix GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;if(!CollectionUtils.isEmpty(roles)){for(String roleCode : roles){String key keyPrefixroleCode;if(stringRedisTemplate.hasKey(key)){String val (String)stringRedisTemplate.opsForHash().get(key,requestUrl);if(val!null){//存在投同意result ACCESS_GRANTED;//结束循环break;}}else{//如果缓存没有查库ListString urls permissionMapper.getPermissionUrlByRole(roleCode);if(!CollectionUtils.isEmpty(urls)){//存缓存MapString,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 RedisTemplateString,String stringRedisTemplate;Resourceprivate RoleMapper roleMapper;Resourceprivate PermissionMapper permissionMapper;Overridepublic void run(ApplicationArguments args) throws Exception {String keyPrefix GlobalConstants.GLOBAL_PERMISSION_KEY_PREFIX;log.info(开始初始化全局资源权限缓存);ListString allRoleCode roleMapper.getAllRoleCode();if(!CollectionUtils.isEmpty(allRoleCode)){for(String roleCode : allRoleCode){ListString urls permissionMapper.getPermissionUrlByRole(roleCode);if(!CollectionUtils.isEmpty(urls)){MapString,Object map new HashMap();urls.forEach(url -{map.put(url,1);});stringRedisTemplate.opsForHash().putAll(keyPrefixroleCode,map);}}}//允许匿名访问的资源权限keyListString urls permissionMapper.getAnonymousPermissionUrl();if(!CollectionUtils.isEmpty(urls)){String key GlobalConstants.GLOBAL_PERMISSION_ANONYMOUS;MapString,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();MapString,Object result new HashMap();result.put(code,-2); // 告诉用户需要登录result.put(message,localizedMessage); //// 将结果对象转换成json字符串String json JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType(application/json;charsetUTF-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 {MapString,Object result new HashMap();result.put(code,-1); // 没有权限result.put(message,没有权限); //// 将结果对象转换成json字符串String json JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType(application/json;charsetUTF-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 AccessDecisionVoterFilterInvocation accessDecisionProcessor() {return new AccessDecisionProcessor();}/*** 鉴权管理器* return*/Beanpublic AccessDecisionManager accessDecisionManager() {// 构造一个新的AccessDecisionManager 放入两个投票器//WebExpressionVoter为配置文件投票器即在HttpSecurity 的authorizeRequests方法里定义的过滤规则使用他是为了也可以使用配置定义好放行规则ListAccessDecisionVoter? 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();}
}