springSecurity-jwt
springSecurity-jwt
简介
springSecurity是spring家族中的一个安全管理框架,相比另一个安全框架shiro,他提供了更丰富的功能,社区资源也比shiro丰富。
一般来说大型的项目都是使用springSeurity来做安全框架。小项目用shiro比较多。相比而言,shiro上手更加简单。
核心功能
- 认证:检验当前访问系统的是不是本系统的用户,并且要确定是哪个用户。
- 授权:经过认证后判断当前用户是否有权限进行某个操作。
准备工作
- 本demo中的常量类
public class Constant { /** * 验证码 */ public static final String CAPTCHA_KEY = "captcha"; /** * token前缀 */ public static final String TOKEN_PREFIX = "Bearer "; }
- 本demo中用到的工具类
快速入门
- 导入依赖
<!-- JSON工具类 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- 阿里JSON解析器 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.23</version> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> <!-- hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> <!--mybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency> <!--mybatis-plus代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.1</version> </dependency> <!--velocity模板 暂未使用--> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <!--freemarker模板--> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--swagger2依赖--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
- 配置文件
server: port: 8089 spring: #mysql配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springsecurity_jwt?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true username: root password: 123456 # redis 配置 redis: # 地址 host: localhost # 端口,默认为6379 port: 6379 # 数据库索引 database: 1 # 密码 password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 8 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms jwt: header: Authorization expire: 604800 secret: f4402f4b1c984955a4a22c7c43614ba3 # 配置日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置逻辑删除 logic-delete-value: 2 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml
认证
大致流程
- 认证流程
-
SpringSecurity完整流程
springSecurity的原理其实就是一个过滤器链,内部包含了个各种功能的过滤器。
下图只展示了核心过滤器,其他的非核心的并没有展示
UsernamePasswordAuthenticatioFilter:负责处理我们在登录后的用户名和密码的登录请求。入门案例的认证工作主要由它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException.
FilterSecurityInterceptor:负责权限校验的过滤器。
-
通过Debug插看当前系统中springSecurity过滤器链中有哪些过滤器及他们的顺序
-
认证流程详解
Authentication接口:他的实现类,表示当前访问系统的用户,封装了用户的相关信息。
AuthenticationManager接口:定义了认证Authentication的方法。
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名信息要封装成UserDetails对象返回。然后这些信息封装到Authentication对象中。
思路分析
登入操作
- 自定义登录接口,调用ProvideerManager的方法进行认证。把用户信息存到redis中
@RestController public class LoginController { @Autowired private LoginService loginService; @PostMapping("/user/login") public Result login(@RequestBody SysUser sysUser){ //登录 Result result = loginService.login(sysUser); return result; } }
LoginServiceImpl
@Service public class LoginServiceImpl implements LoginService { @Autowired private JwtUtil jwtUtil; @Autowired private RedisUtil redisUtil; @Autowired private AuthenticationManager authenticationManager; @Override public Result login(SysUser sysUser) { // AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword()); //该方法会去调用UserDetailsServiceImpl中的loadUserByUsername方法 Authentication authenticate = authenticationManager.authenticate(token); if(ObjectUtils.isEmpty(authenticate)){ //如果认证没通过,给出对应的提示 throw new UsernameNotFoundException("用户名或密码错误"); } LoginUser user = (LoginUser)authenticate.getPrincipal(); //认证通过,使用userId生成jwt String userId = user.getSysUser().getUserId().toString(); String generateToken = jwtUtil.generateToken(userId); //把完整的用户信息存入redis, userid作为key redisUtil.setCacheObject("login:"+user.getSysUser().getUserId(),user); return Result.success(200,"登入成功",generateToken); } }
- 自定义LoginUser去实现UserDetails接口
@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { //自定义系统用户 private SysUser sysUser; //存储权限信息 private List<String> permissions; private List<SimpleGrantedAuthority> authorities; public LoginUser(SysUser sysUser, List<String> permissions) { this.sysUser = sysUser; this.permissions = permissions; } @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(!ObjectUtils.isEmpty(authorities)){ return authorities; } authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } //不对此属性进行序列化 @JsonIgnore @Override public String getPassword() { return sysUser.getPassword(); } @JsonIgnore @Override public String getUsername() { return sysUser.getUserName(); } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } // false-表示不可用 @JsonIgnore @Override public boolean isEnabled() { return true; } }
- 自定义UserDetailsService实现类,在类中去查询数据库
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户信息 LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUserName, username); SysUser user = sysUserMapper.selectOne(wrapper); if(ObjectUtils.isEmpty(user)){ throw new UsernameNotFoundException("用户名或密码错误"); } // 查询对应得权限信息 return new LoginUser(user,getUserAuthority(user.getUserId())); } }
校验
- 定义jwt校验过滤器,获取token,解析token获取其中的userId,从redis中获取用户信息,存到SecurityContextHolder中
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private RedisUtil redisUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //获取token String authorization = request.getHeader(jwtUtil.getHeader()); String token = null; if(StringUtils.isNotEmpty(authorization) && authorization.startsWith(Constant.TOKEN_PREFIX)){ token = authorization.replace(Constant.TOKEN_PREFIX,""); } if(StringUtils.isEmpty(token)){ //放行 filterChain.doFilter(request,response); return; } Claims claims = jwtUtil.getClaimsByToken(token); if(ObjectUtils.isEmpty(claims)){ throw new JwtException("token异常"); } if(jwtUtil.jwtExpire(claims)){ throw new JwtException("token过期"); } //从Redis中获取用户信息 Object object = redisUtil.getCacheObject("login:" + claims.getSubject()); LoginUser loginUser = JSONObject.parseObject(JSONObject.toJSONString(object), LoginUser.class); if(ObjectUtils.isEmpty(loginUser)){ throw new UsernameNotFoundException("用户未登录"); } //存入SecurityContextHolder // 获取权限信息封装到Authentication UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); //放行 filterChain.doFilter(request,response); } }
登出操作
- 定义一个登出接口,获取SecurityContextHolder中的认证信息,删除Redis中对应的数据
@PostMapping("/user/logout") public Result logout(){ //登录 Result result = loginService.logout(); return result; }
- LoginService
public interface LoginService { Result login(SysUser sysUser); Result logout(); }
- LoginServiceImpl
@Service public class LoginServiceImpl implements LoginService { @Autowired private RedisUtil redisUtil; @Override public Result logout() { //获取SecurityContextHolder中的用户信息 UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); // 获取用户信息(userId) 在JwtAuthenticationTokenFilter中存入的 LoginUser user = (LoginUser) authentication.getPrincipal(); //删除redis中的信息 redisUtil.deleteObject("login:"+user.getSysUser().getUserId()); return Result.success(200,"退出成功",true); }
授权
-
授权基本流程
在springSecurity中,会使用默认的
FilterSecurityInterceptor
来进行校验,在FilterSecurityInterceptor中会从SecurityContextHolder
获取其中的Authentication
,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可
-
实现
springSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要是用它我们需要先开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { } -
查询对应的权限信息
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserMapper sysUserMapper; @Autowired private SysMenuService sysMenuService; @Autowired private SysRoleService sysRoleService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查询用户信息 LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUserName, username); SysUser user = sysUserMapper.selectOne(wrapper); if(ObjectUtils.isEmpty(user)){ throw new UsernameNotFoundException("用户名或密码错误"); } // 查询对应得权限信息 return new LoginUser(user,getUserAuthority(user.getUserId())); } /** * 获取用户权限信息(角色、菜单权限) * @param userId * @return */ public List<String> getUserAuthority(Long userId){ //查询角色 List<SysRole> sysRoles = sysRoleService.selectRoleByUserId(userId); //查询权限 List<String> permsList = sysMenuService.selectPermsByUserId(userId); if(ObjectUtils.isNotEmpty(sysRoles)){ String role; for (SysRole sysRole : sysRoles) { role = "ROLE_" + sysRole.getRoleKey(); permsList.add(role); } } return permsList; } }
- 根据userId查询权限的sql
<select id="selectPermsByUserId" resultType="java.lang.String"> select distinct m.perms from sys_user_role ur left join sys_user u on u.user_id = ur.user_id LEFT JOIN sys_role r on ur.role_id = r.role_id left join sys_role_menu rm on rm.role_id = r.role_id left join sys_menu m on m.menu_id = rm.menu_id where u.user_id = #{userId} and u.status = 0 and u.del_flag = 0 </select>
- 根据userId查询用户角色的sql
<select id="selectRoleByUserId" resultMap="BaseResultMap"> select distinct r.* from sys_role r left join sys_user_role ur on r.role_id = ur.role_id left join sys_user u on ur.user_id = u.user_id where u.user_id = #{userId} and u.status = 0 and u.del_flag = 0 </select>
- 在实现UserDetails接口的LoginUser中,把权限给到getAuthorities()方法并返回。
@Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { //自定义系统用户 private SysUser sysUser; //存储权限信息 private List<String> permissions; private List<SimpleGrantedAuthority> authorities; public LoginUser(SysUser sysUser, List<String> permissions) { this.sysUser = sysUser; this.permissions = permissions; } @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(!ObjectUtils.isEmpty(authorities)){ return authorities; } authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } //不对此属性进行序列化 @JsonIgnore @Override public String getPassword() { return sysUser.getPassword(); } @Override public String getUsername() { return sysUser.getUserName(); } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { // false-表示不可用 return true; } }
从数据库查询权限信息
RBAC模型
- 概念:RBAC模型(Role-Based Access Control 基于角色的访问控制)基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
- 从图中我们可以看出主要由用户、角色、权限组成
- 在项目中我们可以更加细化,例如拆分成 sys_user、sys_user_role、sys_role、sys_role_menu、sys_menu 五张表
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50731 Source Host : localhost:3306 Source Schema : springsecurity_jwt Target Server Type : MySQL Target Server Version : 50731 File Encoding : 65001 Date: 15/03/2023 18:37:31 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_menu -- ---------------------------- DROP TABLE IF EXISTS `sys_menu`; CREATE TABLE `sys_menu` ( `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID', `order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序', `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '路由地址', `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径', `query` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由参数', `is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链(0是 1否)', `is_cache` int(1) NULL DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)', `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)', `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识', `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#' COMMENT '菜单图标', `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '备注', PRIMARY KEY (`menu_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_menu -- ---------------------------- INSERT INTO `sys_menu` VALUES (1, '查询用户', 0, 1, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:list', '#', '', '2023-03-14 16:28:40', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2, '添加用户', 0, 2, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:add', '#', '', '2023-03-15 14:59:59', '', NULL, ''); INSERT INTO `sys_menu` VALUES (3, '更新用户', 0, 3, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:edit', '#', '', '2023-03-15 15:01:30', '', NULL, ''); INSERT INTO `sys_menu` VALUES (4, '删除用户', 0, 4, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:remove', '#', '', '2023-03-15 15:02:17', '', NULL, ''); INSERT INTO `sys_menu` VALUES (5, '测试', 0, 5, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:perms:test', '#', '', '2023-03-15 16:46:06', '', NULL, ''); -- ---------------------------- -- Table structure for sys_role -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称', `role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色权限字符串', `role_sort` int(4) NOT NULL COMMENT '显示顺序', `data_scope` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', `menu_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '菜单树选择项是否关联显示', `dept_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '部门树选择项是否关联显示', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色状态(0正常 1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', PRIMARY KEY (`role_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_role -- ---------------------------- INSERT INTO `sys_role` VALUES (1, '超级管理员', 'admin', 1, '1', 1, 1, '0', '0', 'admin', '2018-03-16 11:33:00', 'ry', '2018-03-16 11:33:00', '超级管理员'); INSERT INTO `sys_role` VALUES (2, '高级管理员', 'common', 2, '4', 1, 1, '0', '0', 'admin', '2018-03-16 11:33:00', 'admin', '2022-12-09 11:55:40', '普通角色'); -- ---------------------------- -- Table structure for sys_role_menu -- ---------------------------- DROP TABLE IF EXISTS `sys_role_menu`; CREATE TABLE `sys_role_menu` ( `role_id` bigint(20) NOT NULL COMMENT '角色ID', `menu_id` bigint(20) NOT NULL COMMENT '菜单ID', PRIMARY KEY (`role_id`, `menu_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_role_menu -- ---------------------------- INSERT INTO `sys_role_menu` VALUES (1, 1); INSERT INTO `sys_role_menu` VALUES (1, 2); INSERT INTO `sys_role_menu` VALUES (1, 3); INSERT INTO `sys_role_menu` VALUES (2, 4); INSERT INTO `sys_role_menu` VALUES (2, 5); -- ---------------------------- -- Table structure for sys_user -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID', `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号', `nick_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户昵称', `user_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '00' COMMENT '用户类型(00系统用户)', `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户邮箱', `phonenumber` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号码', `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)', `avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像地址', `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码', `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)', `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', `login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP', `login_date` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间', `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_user -- ---------------------------- INSERT INTO `sys_user` VALUES (6, NULL, 'admin', 'admin', '00', '123@qq.com', '13212344567', '0', 'vegjri', '$2a$2a$10$9jta9cb.61FuZ2kHMkKL7.8KWoDs7dC0vpFf1WD8Wzt1s3YHv4jzq', '0', '0', '', NULL, '', '2023-03-14 16:01:15', '', '2023-03-14 16:01:15', NULL); INSERT INTO `sys_user` VALUES (7, NULL, 'user', 'user', '00', '123@qq.com', '13212344567', '0', 'vegjri', '$2a$10$Hh61ee6Vkfj1Mh9tVp7veu6YLAQwwe4N5pv282EbZMs/taYZczs3O', '0', '0', '', NULL, '', '2023-03-15 16:37:47', '', '2023-03-15 16:37:47', NULL); -- ---------------------------- -- Table structure for sys_user_role -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `user_id` bigint(20) NOT NULL COMMENT '用户ID', `role_id` bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (`user_id`, `role_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_user_role -- ---------------------------- INSERT INTO `sys_user_role` VALUES (6, 1); INSERT INTO `sys_user_role` VALUES (6, 2); INSERT INTO `sys_user_role` VALUES (7, 2); SET FOREIGN_KEY_CHECKS = 1;
自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同的结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道springSecurity的异常处理机制。
在springSecurity中,如果我们在认证或者授权过程中出现了异常被ExceptionTranslationFiter捕获到,在ExceptionTranslationFiter回去判断是认证失败还是授权失败的异常。
如果是认证过程中出现的异常会被封装成AccessDeniedException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法进行处理。
如果我们需要自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
- 自定义实现类
- 认证失败处理
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { Result result = new Result(); result.setCode(HttpStatus.UNAUTHORIZED.value()); result.setMsg("请先登入"); result.setData(false); //处理异常 WebUtil.readerString(response, JSON.toJSONString(result)); } }
- 授权失败处理
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { Result result = new Result(); result.setCode(HttpStatus.FORBIDDEN.value()); result.setMsg("授权失败"); result.setData(false); //处理异常 WebUtil.readerString(response, JSON.toJSONString(result)); } }
- 配置给springSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { //认证失败处理 @Autowired private AuthenticationEntryPoint authenticationEntryPoint; //授权失败处理 @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; /** * 将PasswordEncoder注入到容器中 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 用户认证 * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } private static final String[] URL_WHITELIST = { "/user/login", "/captcha", "/register", "/favicon.ico" }; @Override protected void configure(HttpSecurity http) throws Exception { http //允许跨域 .cors() //关闭csrf .and().csrf().disable() //不通过session获取SecurityContent .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and().authorizeRequests() //权限配置 //.antMatchers("/user/test") //.hasAuthority("sys:user:add") //对于匿名接口,允许匿名访问 .antMatchers(URL_WHITELIST).anonymous() //除上面的所有请求全部需要授权认证 .anyRequest().authenticated() //配置异常处理器 .and().exceptionHandling() //认证异常 .authenticationEntryPoint(authenticationEntryPoint) //授权异常 .accessDeniedHandler(accessDeniedHandler) //添加过滤器 .and().addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class); } }
跨域
浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是禁止的。同源策略要求源相同才能进行正常通信,即协议、域名、端口号都完全一致。
前后端分离项目,前后端一般是不同源的,所以存在跨域问题。所以需要处理,让前端能进行跨域请求。
- 先对springBoot配置,允许跨域请求
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 设置访问源地址 config.addAllowedOriginPattern("*"); // 设置访问源请求头 config.addAllowedHeader("*"); // 设置访问源请求方法 config.addAllowedMethod("*"); // 有效期 1800秒 config.setMaxAge(1800L); source.registerCorsConfiguration("/**", config); // 返回新的CorsFilter return new CorsFilter(source); } }
- 开启springSecurity的跨域访问
@Override protected void configure(HttpSecurity http) throws Exception { //允许跨域 http.cors(); }
权限校验方法
- springSecurity内置
- springSecurity默认是禁用注解的,要开启注解,要在继承WebSecurityConfigurerAdapter的类加
@EnableGlobalMethodSecurity
注解,并在该类中将AuthenticationManager
定义为Bean。
@EnableWebSecurity @Configuration @EnableGlobalMethodSecurity( prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
prePostEnabled = true 的作用是启用springSecurity的@PreAuthorize以及@PostAuthorize注解。
securedEnabled = true 的作用是启用springSecurity的@Secured注解。
jsr250Enabled = true 的作用是启用springSecurity的@RoleAllowed注解。
- 此处以prePostEnabled = true为例,在接口中使用注解
@PreAuthorize("hasAuthority('sys:user:list')") @GetMapping("/list") public Result getUserList(){ List<SysUser> list = sysUserService.list(); return Result.success(200,"success",list); }
- @PreAuthorize("hasAuthority()") 有权限
- @PreAuthorize("hasAnyAuthority()") 可以有多个权限
- @PreAuthorize("hasRole()") 有角色 ,注意角色名称加上前缀ROLE_
- @PreAuthorize("hasAnyRole()") 可以有多个角色,注意角色名称加上前缀ROLE_
- 自定义
当SpringSecurity提供的注解不满足我们的业务需求时,我们可以自定义。
@Component("ss") public class ExpressionRoot { public boolean hasAuthority(String authority){ //设置当前用户权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判断用户权限集合是否存在authority return permissions.contains(authority); } }
使用
@PreAuthorize("@ss.hasAuthority('sys:perms:test')") @GetMapping("/hello") public String test(){ return "springSecurity"; }
登入成功处理器
登出成功处理器
测试结果
- 登入
- 登出
- 添加用户
本文来自博客园,作者:cxf0616,转载请注明原文链接:https://www.cnblogs.com/cxfbk/p/17227709.html
风起于青萍之末,浪成于微澜之间
每一份成功都源于每日的坚持
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)