• 首页

  • 归档

  • 分类

  • 标签

  • 喵星人

  • 心情

  • 关于
W e A r e F a m i l y ! m i a o ~
W e A r e F a m i l y ! m i a o ~

柴子

青春流逝,记录往昔

12月
14
后端

Spring Boot Security从入门到进阶到高级

发表于 2021-12-14 • 字数统计 61450 • 被 544 人看爆

Spring Boot Security

前言

  • Spring Boot Security 是一套安全框架

  • 它能帮你做两件事:认证、授权

  • 底层实现就是一个过滤器链责任链模式 ,每个过滤器链都只做一件事

  • 过滤器包括:

  • WebAsyncManagerIntegrationFilter:将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成

  • SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信 息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除 例如在Session中维护一个用户的安全信息就是这个过滤器处理的

  • HeaderWriterFilter:用于将头信息加入响应中

  • CsrfFilter:用于处理跨站请求伪造

  • LogoutFilter:用于处理退出登录

  • UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改

  • DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面

  • BasicAuthenticationFilter:检测和处理http basic认证

  • RequestCacheAwareFilter:主要是包装请求对象request

  • AnonymousAuthenticationFilter:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication

  • SessionManagementFilter:管理session的过滤器

  • ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常

  • FilterSecurityInterceptor:可以看做过滤器链的出口

  • RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从cookie里找出用户的信息, 如果Spring Security能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启

  • 责任链的客户类是 HttpSecurity,

  • 它负责对责任链的创建和管理,它的 addFilterAt(Filter filter, Class atFilter) 方法可在责任链中添加一个过滤器。

  • 在这个框架中 过滤器作为了 抽象处理者Handler的角色,各个具体的过滤器类是 具体处理者Concrete Handler角色 ,HttpSecueiry是 客户类角色

入门

1. 引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 引入后即刻生效,默认拦截所有请求,直接重置到登录页

  • 启动后默认账号user,默认密码会在控制台输出

111

2. 配置类

  • 继承WebSecurityConfigurerAdapter适配器,重写配置方法

  • 用户认证相关功能

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            //配置用户
            .withUser("admin").password("admin").roles("p1")
            .and()
            .withUser("chai").password("123").authorities("p2")
            .and()
            //配置密码编码器,此处使用不编码
            .passwordEncoder(NoOpPasswordEncoder.getInstance());

}

  • 这种写死的账号密码也可以写到配置文件中application.yml
spring:
  security:
    user:
      name: admin
      password: admin
      roles: admin

  • 权限控制相关功能
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable() //关闭跨域限制
            .authorizeRequests()  //以下权限按顺序开始认证匹配,匹配到就不继续往下
            .antMatchers("/r/r1").hasAnyRole("p1")  //访问/r/r1接口路由,需要拥有p1角色
            .antMatchers("/r/r2").hasAuthority("p2")
            .antMatchers("/r/**").authenticated() //r/下的所有资源需要登录
            .anyRequest().permitAll() //其他请求放行
            .and()
            .formLogin();  //打开表单登录,一旦重写了配置类,原默认表单登录就失效了,需要手动打开
}

3. 测试

  • 写个控制器进行测试

  • TestController

@RestController
public class TestController {

    @GetMapping("/t")
    public String t(){
        return "访问t资源!";
    }
    
    @GetMapping("/r/r1")
    public String r1(){
        return "访问r1资源";
    }

    @GetMapping("/r/r2")
    public String r2(){
        return "访问r2资源";
    }
}
  • 启动项目,访问http://localhost:8080/t ,访问成功,无拦截

image

  • 访问http://localhost:8080/r/r1,被重定向到了登录页,输入admin/admin登录

image_1

  • 访问http://localhost:8080/r/r2,无权访问

image_2

进价

  • 后面自定义的配置,基本全是在修改WebSecurityConfigurerAdapter适配器的配置类

1. 自定义登录页

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable() //关闭跨域限制
            .authorizeRequests()  //以下权限按顺序开始认证匹配,匹配到就不继续往下
            .antMatchers("/r/r1").hasAnyRole("p1")  //访问/r/r1接口路由,需要拥有p1角色
            .antMatchers("/r/r2").hasAuthority("p2")
            .antMatchers("/r/**").authenticated() //r/下的所有资源需要登录
            .anyRequest().permitAll() //其他请求放行
            .and()
            .formLogin() //允许表单提交
            .loginPage("/login.html")//自定义登录页,也可以重定向到接口.loginPage("/login")
            .loginProcessingUrl("/loginAction"); //自定义登录表单提交的地址,与前端的form-action对应,默认Post
}
  • 前端:resources/static/login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/loginAction" method="post">
    <div>
        用户名:<input type="text" name="username"/>
    </div>
    <div>
        密码:<input type="password" name="password"/>
    </div>
    <div>
        <input type="submit" value="登录"/>
    </div>
</form>
</body>
</html>
  • 启动项目,访问http://localhost:8080/login.html

image_3

输入admin/admin 登录成功

  • 其他登录相关配置项

  • successForwardUrl("/login-success"):登录成功后转发到接口

  • defaultSuccessUrl("/home.html", true): 登录成功后默认的返回视图,这里也可以是视图控制器的接口

  • failureForwardUrl(): 登录失败转发到接口

  • failureUrl("/login.html?error"):登录失败后返回视图

  • logoutUrl("/custom-logout"):默认注销行为为logout

  • logoutSuccessUrl(""):设置注销成功后跳转页面,默认是跳转到登录页面

  • usernameParameter("username"):配置前端用户名字段名,默认:username

  • passwordParameter("password"):配置前端密码字段名,默认:password


2. 用户认证从数据库中获取

  • 入门里用户都是写死的,实际项目中应该是动态从数据库中进行用户认证与权限查询

2.1重写加载用户逻辑

  • 创建一个类 UserDetailsServiceImpl

  • 实现 org.springframework.security.core.userdetails.UserDetailsService 接口,重写loadUserByUsername方法

import com.google.common.collect.Lists;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    /**
    * 先写个最简化版本
    * 后续去数据库里查询用户就是在这里写
    **/
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //组装用户权限,对应配置文件里的.hasAnyAuthority("p1"),如果要对应.hasAnyRole("p2"),这里的p1需要写成ROLE_p1
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("p1");
        //返回的这个User需要三个参数,用户名,密码,权限;这里返回的用户密码会和前端输入的用户名密码进行比对。
        return new User("admin","123456",grantedAuthorities);
    }
}
  • 启动服务,输入admin/123456发现可以正常登陆,并且能访问/r/r1资源

  • 重写loadUserByUsername 这个方法主要是给密码验证器DaoAuthenticationProvider用的,也可以通过继承DaoAuthenticationProvider,重写里面的方法,进行自定义密码验证,这里不演示了

  • 注入配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //注入自定义的用户加载类
        auth.userDetailsService(userDetailsService);
    }
    
    /**
    *与上面configure注入userDetailsService一样,不同的写法,任选其一
    * @Bean
    * public UserDetailsService userDetailsService(){
    *    return new UserDetailsServiceImpl();
    * }
    */

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance(); //先不加密
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/r/r2").hasAnyRole("p2")  //会自动加前缀 ROLE_
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/loginAction");
    }
}

  • hasAnyRole源码,发现自动增加了前缀,所以如果用这个方法校验权限,需要在用户权限组装那加上ROLE_ 前缀

image_4


2.2用户从数据库中读取

  • 添加数据库依赖,mysql数据库、Mybatis-plus的ORM框架、Hikari连接池
<!--   mysql     -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!--mybatisplus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.0</version>
</dependency>

<!--连接池-->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>
  • application.yml配置数据库连接
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/czadmin?useUnicode=true&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      idle-timeout: 600000
      maximum-pool-size: 10
      auto-commit: true
      pool-name: MyHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1

mybatis-plus:
  typeAliasesPackage: com.chai.entity
  configuration:
    map-underscore-to-camel-case: true
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 创建数据库czadmin ,创建表结构

  • sys_user表

-- ----------------------------
-- 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` int(11) DEFAULT NULL COMMENT '部门名称',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
  `nick_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
  `gender` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '性别',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号码',
  `email` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
  `avatar_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像地址',
  `avatar_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像磁盘路径',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
  `is_admin` tinyint(1) DEFAULT 0 COMMENT '是否超管',
  `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
  `create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建操作人',
  `update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新操作人',
  `pwd_reset_time` datetime DEFAULT NULL COMMENT '修改密码的时间',
  `create_time` datetime DEFAULT NULL COMMENT '创建日期',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`user_id`) USING BTREE,
  UNIQUE KEY `idx_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='系统用户';

-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 2, 'admin', '管理员', '男', '18888888888', '201507802@qq.com', 'avatar-20200806032259161.png', '/Users/jie/Documents/work/me/admin/eladmin/~/avatar/avatar-20200806032259161.png', '$2a$10$Egp1/gvFlt7zhlXVfEFw4OfWQCGPw0ClmMcc6FjTnvXNRVf9zdMRa', 1, 1, NULL, 'admin', '2020-05-03 16:38:31', '2018-08-23 09:11:56', '2020-09-05 10:43:31');
INSERT INTO `sys_user` VALUES (2, 2, 'test', '测试', '男', '19999999999', '231@qq.com', NULL, NULL, '$2a$10$4XcyudOYTSz6fue6KFNMHeUQnCX5jbBQypLEnGk1PmekXt5c95JcK', 0, 1, 'admin', 'admin', NULL, '2020-05-05 11:15:49', '2020-09-05 10:43:38');
COMMIT;
  • 重写loadUserByUsername
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private IUserService userService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //通过用户名查询用户信息
        SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, userName));
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        //查询用户角色权限信息,此处写死了,下面处理
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("p1");
        return new User(user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, grantedAuthorities);
    }
}

  • 用户从数据库中读取完成,密码校验交给security后续处理,接下来处理权限

2.3权限从数据库中获取

  • 创建表结构

  • sys_menu 菜单表,权限表与菜单表合一

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `menu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `pid` int(11) DEFAULT NULL COMMENT '上级菜单ID',
  `menu_type` int(2) DEFAULT NULL COMMENT '菜单类型,0:菜单1:路由2:按钮',
  `title` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '菜单标题',
  `name` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '组件名称',
  `component` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '组件',
  `menu_sort` int(5) DEFAULT NULL COMMENT '排序',
  `icon` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '图标',
  `path` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '链接地址',
  `i_frame` tinyint(1) DEFAULT '0' COMMENT '是否外链',
  `cache` tinyint(1) DEFAULT '0' COMMENT '缓存',
  `hidden` tinyint(1) DEFAULT '0' COMMENT '隐藏',
  `permission` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '权限',
  `create_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建操作人',
  `update_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '更新操作人',
  `create_time` datetime DEFAULT NULL COMMENT '创建日期',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='系统菜单';

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
BEGIN;
INSERT INTO `sys_menu` VALUES (1, NULL, 0, '系统管理', NULL, NULL, 1, 'system', 'system', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:11:29', NULL);
INSERT INTO `sys_menu` VALUES (2, 1, 1, '用户管理', 'User', 'system/user/index', 2, 'peoples', 'user', 0, 0, 0, 'user:list', NULL, NULL, '2018-12-18 15:14:44', NULL);
INSERT INTO `sys_menu` VALUES (3, 1, 1, '角色管理', 'Role', 'system/role/index', 3, 'role', 'role', 0, 0, 0, 'roles:list', NULL, NULL, '2018-12-18 15:16:07', NULL);
INSERT INTO `sys_menu` VALUES (5, 1, 1, '菜单管理', 'Menu', 'system/menu/index', 5, 'menu', 'menu', 0, 0, 0, 'menu:list', NULL, NULL, '2018-12-18 15:17:28', NULL);
INSERT INTO `sys_menu` VALUES (6, NULL, 0, '系统监控', NULL, NULL, 10, 'monitor', 'monitor', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:17:48', NULL);
INSERT INTO `sys_menu` VALUES (7, 6, 1, '操作日志', 'Log', 'monitor/log/index', 11, 'log', 'logs', 0, 1, 0, NULL, NULL, 'admin', '2018-12-18 15:18:26', '2020-06-06 13:11:57');
INSERT INTO `sys_menu` VALUES (9, 6, 1, 'SQL监控', 'Sql', 'monitor/sql/index', 18, 'sqlMonitor', 'druid', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:19:34', NULL);
INSERT INTO `sys_menu` VALUES (10, NULL, 0, '组件管理', NULL, NULL, 50, 'zujian', 'components', 0, 0, 0, NULL, NULL, NULL, '2018-12-19 13:38:16', NULL);
INSERT INTO `sys_menu` VALUES (11, 10, 1, '图标库', 'Icons', 'components/icons/index', 51, 'icon', 'icon', 0, 0, 0, NULL, NULL, NULL, '2018-12-19 13:38:49', NULL);
INSERT INTO `sys_menu` VALUES (14, 36, 1, '邮件工具', 'Email', 'tools/email/index', 35, 'email', 'email', 0, 0, 0, NULL, NULL, NULL, '2018-12-27 10:13:09', NULL);
INSERT INTO `sys_menu` VALUES (15, 10, 1, '富文本', 'Editor', 'components/Editor', 52, 'fwb', 'tinymce', 0, 0, 0, NULL, NULL, NULL, '2018-12-27 11:58:25', NULL);
INSERT INTO `sys_menu` VALUES (18, 36, 1, '存储管理', 'Storage', 'tools/storage/index', 34, 'qiniu', 'storage', 0, 0, 0, 'storage:list', NULL, NULL, '2018-12-31 11:12:15', NULL);
INSERT INTO `sys_menu` VALUES (19, 36, 1, '支付宝工具', 'AliPay', 'tools/aliPay/index', 37, 'alipay', 'aliPay', 0, 0, 0, NULL, NULL, NULL, '2018-12-31 14:52:38', NULL);
INSERT INTO `sys_menu` VALUES (28, 1, 1, '任务调度', 'Timing', 'system/timing/index', 999, 'timing', 'timing', 0, 0, 0, 'timing:list', NULL, NULL, '2019-01-07 20:34:40', NULL);
INSERT INTO `sys_menu` VALUES (30, 36, 1, '代码生成', 'GeneratorIndex', 'generator/index', 32, 'dev', 'generator', 0, 1, 0, NULL, NULL, NULL, '2019-01-11 15:45:55', NULL);
INSERT INTO `sys_menu` VALUES (32, 6, 1, '异常日志', 'ErrorLog', 'monitor/log/errorLog', 12, 'error', 'errorLog', 0, 0, 0, NULL, NULL, NULL, '2019-01-13 13:49:03', NULL);
INSERT INTO `sys_menu` VALUES (33, 10, 1, 'Markdown', 'Markdown', 'components/MarkDown', 53, 'markdown', 'markdown', 0, 0, 0, NULL, NULL, NULL, '2019-03-08 13:46:44', NULL);
INSERT INTO `sys_menu` VALUES (34, 10, 1, 'Yaml编辑器', 'YamlEdit', 'components/YamlEdit', 54, 'dev', 'yaml', 0, 0, 0, NULL, NULL, NULL, '2019-03-08 15:49:40', NULL);
INSERT INTO `sys_menu` VALUES (35, 1, 1, '部门管理', 'Dept', 'system/dept/index', 6, 'dept', 'dept', 0, 0, 0, 'dept:list', NULL, NULL, '2019-03-25 09:46:00', NULL);
INSERT INTO `sys_menu` VALUES (36, NULL, 0, '系统工具', NULL, '', 30, 'sys-tools', 'sys-tools', 0, 0, 0, NULL, NULL, NULL, '2019-03-29 10:57:35', NULL);
INSERT INTO `sys_menu` VALUES (37, 1, 1, '岗位管理', 'Job', 'system/job/index', 7, 'Steve-Jobs', 'job', 0, 0, 0, 'job:list', NULL, NULL, '2019-03-29 13:51:18', NULL);
INSERT INTO `sys_menu` VALUES (38, 36, 1, '接口文档', 'Swagger', 'tools/swagger/index', 36, 'swagger', 'swagger2', 0, 0, 0, NULL, NULL, NULL, '2019-03-29 19:57:53', NULL);
INSERT INTO `sys_menu` VALUES (39, 1, 1, '字典管理', 'Dict', 'system/dict/index', 8, 'dictionary', 'dict', 0, 0, 0, 'dict:list', NULL, NULL, '2019-04-10 11:49:04', NULL);
INSERT INTO `sys_menu` VALUES (41, 6, 1, '在线用户', 'OnlineUser', 'monitor/online/index', 10, 'Steve-Jobs', 'online', 0, 0, 0, NULL, NULL, NULL, '2019-10-26 22:08:43', NULL);
INSERT INTO `sys_menu` VALUES (44, 2, 2, '用户新增', NULL, '', 2, '', '', 0, 0, 0, 'user:add', NULL, NULL, '2019-10-29 10:59:46', NULL);
INSERT INTO `sys_menu` VALUES (45, 2, 2, '用户编辑', NULL, '', 3, '', '', 0, 0, 0, 'user:edit', NULL, NULL, '2019-10-29 11:00:08', NULL);
INSERT INTO `sys_menu` VALUES (46, 2, 2, '用户删除', NULL, '', 4, '', '', 0, 0, 0, 'user:del', NULL, NULL, '2019-10-29 11:00:23', NULL);
INSERT INTO `sys_menu` VALUES (48, 3, 2, '角色创建', NULL, '', 2, '', '', 0, 0, 0, 'roles:add', NULL, NULL, '2019-10-29 12:45:34', NULL);
INSERT INTO `sys_menu` VALUES (49, 3, 2, '角色修改', NULL, '', 3, '', '', 0, 0, 0, 'roles:edit', NULL, NULL, '2019-10-29 12:46:16', NULL);
INSERT INTO `sys_menu` VALUES (50, 3, 2, '角色删除', NULL, '', 4, '', '', 0, 0, 0, 'roles:del', NULL, NULL, '2019-10-29 12:46:51', NULL);
INSERT INTO `sys_menu` VALUES (52, 5, 2, '菜单新增', NULL, '', 2, '', '', 0, 0, 0, 'menu:add', NULL, NULL, '2019-10-29 12:55:07', NULL);
INSERT INTO `sys_menu` VALUES (53, 5, 2, '菜单编辑', NULL, '', 3, '', '', 0, 0, 0, 'menu:edit', NULL, NULL, '2019-10-29 12:55:40', NULL);
INSERT INTO `sys_menu` VALUES (54, 5, 2, '菜单删除', NULL, '', 4, '', '', 0, 0, 0, 'menu:del', NULL, NULL, '2019-10-29 12:56:00', NULL);
INSERT INTO `sys_menu` VALUES (56, 35,2, '部门新增', NULL, '', 2, '', '', 0, 0, 0, 'dept:add', NULL, NULL, '2019-10-29 12:57:09', NULL);
INSERT INTO `sys_menu` VALUES (57, 35, 2, '部门编辑', NULL, '', 3, '', '', 0, 0, 0, 'dept:edit', NULL, NULL, '2019-10-29 12:57:27', NULL);
INSERT INTO `sys_menu` VALUES (58, 35, 2, '部门删除', NULL, '', 4, '', '', 0, 0, 0, 'dept:del', NULL, NULL, '2019-10-29 12:57:41', NULL);
INSERT INTO `sys_menu` VALUES (60, 37, 2, '岗位新增', NULL, '', 2, '', '', 0, 0, 0, 'job:add', NULL, NULL, '2019-10-29 12:58:27', NULL);
INSERT INTO `sys_menu` VALUES (61, 37, 2, '岗位编辑', NULL, '', 3, '', '', 0, 0, 0, 'job:edit', NULL, NULL, '2019-10-29 12:58:45', NULL);
INSERT INTO `sys_menu` VALUES (62, 37, 2, '岗位删除', NULL, '', 4, '', '', 0, 0, 0, 'job:del', NULL, NULL, '2019-10-29 12:59:04', NULL);
INSERT INTO `sys_menu` VALUES (64, 39, 2, '字典新增', NULL, '', 2, '', '', 0, 0, 0, 'dict:add', NULL, NULL, '2019-10-29 13:00:17', NULL);
INSERT INTO `sys_menu` VALUES (65, 39, 2, '字典编辑', NULL, '', 3, '', '', 0, 0, 0, 'dict:edit', NULL, NULL, '2019-10-29 13:00:42', NULL);
INSERT INTO `sys_menu` VALUES (66, 39, 2, '字典删除', NULL, '', 4, '', '', 0, 0, 0, 'dict:del', NULL, NULL, '2019-10-29 13:00:59', NULL);
INSERT INTO `sys_menu` VALUES (73, 28, 2, '任务新增', NULL, '', 2, '', '', 0, 0, 0, 'timing:add', NULL, NULL, '2019-10-29 13:07:28', NULL);
INSERT INTO `sys_menu` VALUES (74, 28, 2, '任务编辑', NULL, '', 3, '', '', 0, 0, 0, 'timing:edit', NULL, NULL, '2019-10-29 13:07:41', NULL);
INSERT INTO `sys_menu` VALUES (75, 28, 2, '任务删除', NULL, '', 4, '', '', 0, 0, 0, 'timing:del', NULL, NULL, '2019-10-29 13:07:54', NULL);
INSERT INTO `sys_menu` VALUES (77, 18, 2, '上传文件', NULL, '', 2, '', '', 0, 0, 0, 'storage:add', NULL, NULL, '2019-10-29 13:09:09', NULL);
INSERT INTO `sys_menu` VALUES (78, 18, 2, '文件编辑', NULL, '', 3, '', '', 0, 0, 0, 'storage:edit', NULL, NULL, '2019-10-29 13:09:22', NULL);
INSERT INTO `sys_menu` VALUES (79, 18, 2, '文件删除', NULL, '', 4, '', '', 0, 0, 0, 'storage:del', NULL, NULL, '2019-10-29 13:09:34', NULL);
INSERT INTO `sys_menu` VALUES (80, 6, 1, '服务监控', 'ServerMonitor', 'monitor/server/index', 14, 'codeConsole', 'server', 0, 0, 0, 'monitor:list', NULL, 'admin', '2019-11-07 13:06:39', '2020-05-04 18:20:50');
INSERT INTO `sys_menu` VALUES (82, 36, 1, '生成配置', 'GeneratorConfig', 'generator/config', 33, 'dev', 'generator/config/:tableName', 0, 1, 1, NULL, NULL, NULL, '2019-11-17 20:08:56', NULL);
INSERT INTO `sys_menu` VALUES (83, 10, 1, '图表库', 'Echarts', 'components/Echarts', 50, 'chart', 'echarts', 0, 1, 0, NULL, NULL, NULL, '2019-11-21 09:04:32', NULL);
INSERT INTO `sys_menu` VALUES (90, NULL, 1, '运维管理', 'Mnt', '', 20, 'mnt', 'mnt', 0, 0, 0, NULL, NULL, NULL, '2019-11-09 10:31:08', NULL);
INSERT INTO `sys_menu` VALUES (92, 90, 1, '服务器', 'ServerDeploy', 'mnt/server/index', 22, 'server', 'mnt/serverDeploy', 0, 0, 0, 'serverDeploy:list', NULL, NULL, '2019-11-10 10:29:25', NULL);
INSERT INTO `sys_menu` VALUES (93, 90, 1, '应用管理', 'App', 'mnt/app/index', 23, 'app', 'mnt/app', 0, 0, 0, 'app:list', NULL, NULL, '2019-11-10 11:05:16', NULL);
INSERT INTO `sys_menu` VALUES (94, 90, 1, '部署管理', 'Deploy', 'mnt/deploy/index', 24, 'deploy', 'mnt/deploy', 0, 0, 0, 'deploy:list', NULL, NULL, '2019-11-10 15:56:55', NULL);
INSERT INTO `sys_menu` VALUES (97, 90, 1, '部署备份', 'DeployHistory', 'mnt/deployHistory/index', 25, 'backup', 'mnt/deployHistory', 0, 0, 0, 'deployHistory:list', NULL, NULL, '2019-11-10 16:49:44', NULL);
INSERT INTO `sys_menu` VALUES (98, 90, 1, '数据库管理', 'Database', 'mnt/database/index', 26, 'database', 'mnt/database', 0, 0, 0, 'database:list', NULL, NULL, '2019-11-10 20:40:04', NULL);
INSERT INTO `sys_menu` VALUES (102, 97, 2, '删除', NULL, '', 999, '', '', 0, 0, 0, 'deployHistory:del', NULL, NULL, '2019-11-17 09:32:48', NULL);
INSERT INTO `sys_menu` VALUES (103, 92, 2, '服务器新增', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:add', NULL, NULL, '2019-11-17 11:08:33', NULL);
INSERT INTO `sys_menu` VALUES (104, 92, 2, '服务器编辑', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:edit', NULL, NULL, '2019-11-17 11:08:57', NULL);
INSERT INTO `sys_menu` VALUES (105, 92, 2, '服务器删除', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:del', NULL, NULL, '2019-11-17 11:09:15', NULL);
INSERT INTO `sys_menu` VALUES (106, 93, 2, '应用新增', NULL, '', 999, '', '', 0, 0, 0, 'app:add', NULL, NULL, '2019-11-17 11:10:03', NULL);
INSERT INTO `sys_menu` VALUES (107, 93, 2, '应用编辑', NULL, '', 999, '', '', 0, 0, 0, 'app:edit', NULL, NULL, '2019-11-17 11:10:28', NULL);
INSERT INTO `sys_menu` VALUES (108, 93, 2, '应用删除', NULL, '', 999, '', '', 0, 0, 0, 'app:del', NULL, NULL, '2019-11-17 11:10:55', NULL);
INSERT INTO `sys_menu` VALUES (109, 94, 2, '部署新增', NULL, '', 999, '', '', 0, 0, 0, 'deploy:add', NULL, NULL, '2019-11-17 11:11:22', NULL);
INSERT INTO `sys_menu` VALUES (110, 94, 2, '部署编辑', NULL, '', 999, '', '', 0, 0, 0, 'deploy:edit', NULL, NULL, '2019-11-17 11:11:41', NULL);
INSERT INTO `sys_menu` VALUES (111, 94, 2, '部署删除', NULL, '', 999, '', '', 0, 0, 0, 'deploy:del', NULL, NULL, '2019-11-17 11:12:01', NULL);
INSERT INTO `sys_menu` VALUES (112, 98, 2, '数据库新增', NULL, '', 999, '', '', 0, 0, 0, 'database:add', NULL, NULL, '2019-11-17 11:12:43', NULL);
INSERT INTO `sys_menu` VALUES (113, 98, 2, '数据库编辑', NULL, '', 999, '', '', 0, 0, 0, 'database:edit', NULL, NULL, '2019-11-17 11:12:58', NULL);
INSERT INTO `sys_menu` VALUES (114, 98, 2, '数据库删除', NULL, '', 999, '', '', 0, 0, 0, 'database:del', NULL, NULL, '2019-11-17 11:13:14', NULL);
INSERT INTO `sys_menu` VALUES (116, 36, 1, '生成预览', 'Preview', 'generator/preview', 999, 'java', 'generator/preview/:tableName', 0, 1, 1, NULL, NULL, NULL, '2019-11-26 14:54:36', NULL);
COMMIT;

  • sys_role角色表
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
  `level` int(2) DEFAULT NULL COMMENT '角色级别',
  `description` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '描述',
  `data_scope` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '数据权限',
  `create_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建操作人',
  `update_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '更新操作人',
  `create_time` datetime DEFAULT NULL COMMENT '创建日期',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='角色表';

-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, '超级管理员', 1, '-', '全部', 'admin', 'admin', '2018-11-23 11:04:37', '2020-08-06 16:10:24');
INSERT INTO `sys_role` VALUES (2, '普通用户', 2, '-', '本级', 'admin', 'admin', '2018-11-23 13:09:06', '2020-09-05 10:45:12');
COMMIT;

  • sys_roles_menus角色菜单中间表
-- ----------------------------
-- Table structure for sys_roles_menus
-- ----------------------------
DROP TABLE IF EXISTS `sys_roles_menus`;
CREATE TABLE `sys_roles_menus` (
  `menu_id` int(11) NOT NULL COMMENT '菜单ID',
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`menu_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='角色菜单关联';

-- ----------------------------
-- Records of sys_roles_menus
-- ----------------------------
BEGIN;
INSERT INTO `sys_roles_menus` VALUES (1, 1);
INSERT INTO `sys_roles_menus` VALUES (1, 2);
INSERT INTO `sys_roles_menus` VALUES (2, 1);
INSERT INTO `sys_roles_menus` VALUES (2, 2);
INSERT INTO `sys_roles_menus` VALUES (3, 1);
INSERT INTO `sys_roles_menus` VALUES (5, 1);
INSERT INTO `sys_roles_menus` VALUES (6, 1);
INSERT INTO `sys_roles_menus` VALUES (6, 2);
INSERT INTO `sys_roles_menus` VALUES (7, 1);
INSERT INTO `sys_roles_menus` VALUES (7, 2);
INSERT INTO `sys_roles_menus` VALUES (9, 1);
INSERT INTO `sys_roles_menus` VALUES (9, 2);
INSERT INTO `sys_roles_menus` VALUES (10, 1);
INSERT INTO `sys_roles_menus` VALUES (10, 2);
INSERT INTO `sys_roles_menus` VALUES (11, 1);
INSERT INTO `sys_roles_menus` VALUES (11, 2);
INSERT INTO `sys_roles_menus` VALUES (14, 1);
INSERT INTO `sys_roles_menus` VALUES (14, 2);
INSERT INTO `sys_roles_menus` VALUES (15, 1);
INSERT INTO `sys_roles_menus` VALUES (15, 2);
INSERT INTO `sys_roles_menus` VALUES (18, 1);
INSERT INTO `sys_roles_menus` VALUES (19, 1);
INSERT INTO `sys_roles_menus` VALUES (19, 2);
INSERT INTO `sys_roles_menus` VALUES (21, 1);
INSERT INTO `sys_roles_menus` VALUES (21, 2);
INSERT INTO `sys_roles_menus` VALUES (22, 1);
INSERT INTO `sys_roles_menus` VALUES (22, 2);
INSERT INTO `sys_roles_menus` VALUES (23, 1);
INSERT INTO `sys_roles_menus` VALUES (23, 2);
INSERT INTO `sys_roles_menus` VALUES (24, 1);
INSERT INTO `sys_roles_menus` VALUES (24, 2);
INSERT INTO `sys_roles_menus` VALUES (27, 1);
INSERT INTO `sys_roles_menus` VALUES (27, 2);
INSERT INTO `sys_roles_menus` VALUES (28, 1);
INSERT INTO `sys_roles_menus` VALUES (30, 1);
INSERT INTO `sys_roles_menus` VALUES (30, 2);
INSERT INTO `sys_roles_menus` VALUES (32, 1);
INSERT INTO `sys_roles_menus` VALUES (32, 2);
INSERT INTO `sys_roles_menus` VALUES (33, 1);
INSERT INTO `sys_roles_menus` VALUES (33, 2);
INSERT INTO `sys_roles_menus` VALUES (34, 1);
INSERT INTO `sys_roles_menus` VALUES (34, 2);
INSERT INTO `sys_roles_menus` VALUES (35, 1);
INSERT INTO `sys_roles_menus` VALUES (36, 1);
INSERT INTO `sys_roles_menus` VALUES (36, 2);
INSERT INTO `sys_roles_menus` VALUES (37, 1);
INSERT INTO `sys_roles_menus` VALUES (38, 1);
INSERT INTO `sys_roles_menus` VALUES (39, 1);
INSERT INTO `sys_roles_menus` VALUES (41, 1);
INSERT INTO `sys_roles_menus` VALUES (44, 1);
INSERT INTO `sys_roles_menus` VALUES (45, 1);
INSERT INTO `sys_roles_menus` VALUES (46, 1);
INSERT INTO `sys_roles_menus` VALUES (48, 1);
INSERT INTO `sys_roles_menus` VALUES (49, 1);
INSERT INTO `sys_roles_menus` VALUES (50, 1);
INSERT INTO `sys_roles_menus` VALUES (52, 1);
INSERT INTO `sys_roles_menus` VALUES (53, 1);
INSERT INTO `sys_roles_menus` VALUES (54, 1);
INSERT INTO `sys_roles_menus` VALUES (56, 1);
INSERT INTO `sys_roles_menus` VALUES (57, 1);
INSERT INTO `sys_roles_menus` VALUES (58, 1);
INSERT INTO `sys_roles_menus` VALUES (60, 1);
INSERT INTO `sys_roles_menus` VALUES (61, 1);
INSERT INTO `sys_roles_menus` VALUES (62, 1);
INSERT INTO `sys_roles_menus` VALUES (64, 1);
INSERT INTO `sys_roles_menus` VALUES (65, 1);
INSERT INTO `sys_roles_menus` VALUES (66, 1);
INSERT INTO `sys_roles_menus` VALUES (73, 1);
INSERT INTO `sys_roles_menus` VALUES (74, 1);
INSERT INTO `sys_roles_menus` VALUES (75, 1);
INSERT INTO `sys_roles_menus` VALUES (77, 1);
INSERT INTO `sys_roles_menus` VALUES (78, 1);
INSERT INTO `sys_roles_menus` VALUES (79, 1);
INSERT INTO `sys_roles_menus` VALUES (80, 1);
INSERT INTO `sys_roles_menus` VALUES (80, 2);
INSERT INTO `sys_roles_menus` VALUES (82, 1);
INSERT INTO `sys_roles_menus` VALUES (82, 2);
INSERT INTO `sys_roles_menus` VALUES (83, 1);
INSERT INTO `sys_roles_menus` VALUES (83, 2);
INSERT INTO `sys_roles_menus` VALUES (90, 1);
INSERT INTO `sys_roles_menus` VALUES (92, 1);
INSERT INTO `sys_roles_menus` VALUES (93, 1);
INSERT INTO `sys_roles_menus` VALUES (94, 1);
INSERT INTO `sys_roles_menus` VALUES (97, 1);
INSERT INTO `sys_roles_menus` VALUES (98, 1);
INSERT INTO `sys_roles_menus` VALUES (102, 1);
INSERT INTO `sys_roles_menus` VALUES (103, 1);
INSERT INTO `sys_roles_menus` VALUES (104, 1);
INSERT INTO `sys_roles_menus` VALUES (105, 1);
INSERT INTO `sys_roles_menus` VALUES (106, 1);
INSERT INTO `sys_roles_menus` VALUES (107, 1);
INSERT INTO `sys_roles_menus` VALUES (108, 1);
INSERT INTO `sys_roles_menus` VALUES (109, 1);
INSERT INTO `sys_roles_menus` VALUES (110, 1);
INSERT INTO `sys_roles_menus` VALUES (111, 1);
INSERT INTO `sys_roles_menus` VALUES (112, 1);
INSERT INTO `sys_roles_menus` VALUES (113, 1);
INSERT INTO `sys_roles_menus` VALUES (114, 1);
INSERT INTO `sys_roles_menus` VALUES (116, 1);
INSERT INTO `sys_roles_menus` VALUES (116, 2);
INSERT INTO `sys_roles_menus` VALUES (120, 1);
COMMIT;
  • sys_users_roles用户角色中间表
-- ----------------------------
-- Table structure for sys_users_roles
-- ----------------------------
DROP TABLE IF EXISTS `sys_users_roles`;
CREATE TABLE `sys_users_roles` (
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='用户角色关联';

-- ----------------------------
-- Records of sys_users_roles
-- ----------------------------
BEGIN;
INSERT INTO `sys_users_roles` VALUES (1, 1);
INSERT INTO `sys_users_roles` VALUES (2, 2);
COMMIT;
  • 在loadUserByUsername中增加权限读取
package com.cz.config.security;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cz.entity.SysMenu;
import com.cz.entity.SysUser;
import com.cz.service.ISysMenuService;
import com.cz.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private ISysUserService sysUserService;

    @Autowired
    private ISysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //通过用户名查询用户信息
        SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, userName));
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<GrantedAuthority> grantedAuthorities = getGrantedAuthorities(user);//获取用户权限
        return new User(user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, grantedAuthorities);
    }

    /**
     * 后续优化会放入缓存,这里先每次查库
     *
     * @param user
     * @return
     */
    public List<GrantedAuthority> getGrantedAuthorities(SysUser user) {
        List<String> permissions;
        //是否超管
        if (user.getIsAdmin()) {
            //超管直接查所有权限
            LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<SysMenu>().select(SysMenu::getPermission).isNotNull(SysMenu::getPermission);
            permissions = sysMenuService.listObjs(queryWrapper, Object::toString);
            //只有超管支持角色访问,因为有些接口只允许超管请求,其他绝对严格通过权限标识符控制
            permissions.add("ROLE_admin");
        } else {
            //其他角色查询权限
            permissions = sysMenuService.findPermissionByUserId(user.getUserId());
        }
        String collect = String.join(",", permissions);
        return AuthorityUtils.commaSeparatedStringToAuthorityList(collect);
    }

}

  • ISysMenuService的findPermissionByUserId → 其他角色查询权限
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {

    @Autowired
    private SysMenuMapper sysMenuMapper;

    @Override
    public List<String> findPermissionByUserId(Long userId) {
        return sysMenuMapper.findPermissionByUserId(userId);
    }
}

  • SysMenuMapper的findPermissionByUserId
<select id="findPermissionByUserId" resultType="java.lang.String">
    select sm.permission permission from sys_menu  sm INNER JOIN sys_roles_menus  srm on sm.menu_id = srm.menu_id and sm.permission is not null INNER JOIN sys_users_roles srr on srr.role_id = srm.role_id
    where srr.user_id=#{userId}
</select>

2.4 测试

  • 编写controller
@PreAuthorize("hasAnyRole('admin')")
@GetMapping("/user")
public ResultVO info(Principal principal){
    return ResponseUtil.success(principal) ;
}

admin登录,访问/user返回成功,test登录,访问/user返回403,forbidden,测试完毕

hasAnyRole('admin'):管理员才能访问
hasAuthority('user:add'):需要有该权限的人才能访问
什么也不加:登录就能访问
不登录就能访问的需要在配置文件里做处理:.antMatchers("/r/r2").permitALL()


3. 自定义权限不足页面

  • 默认权限不足是一个很不友好的403 forbidden ,改造成自己的页面

  • 前端:resources/static/noAuth.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    权限不足!
</body>
</html>
  • 配置SecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
        .accessDeniedPage("/noAuth.html") //权限不足跳转到这个页面
        //...省略
}           

  • 启动项目,测试无误

image_5


4. 基于注解的方法级别权限控制

可以替代SecurityConfig配置类里的.antMatchers("/r/r1").hasAnyRole("p1")的配置

  • @Secured 用户具有某个权限,才能访问此方法
  1. 开启全局配置注解@EnableGlobalMethodSecurity(securedEnabled = true),加到自己的SecurityConfig配置类或者程序启动类都可以

  2. 添加注解到方法上

@GetMapping("/r/r1")
@Secured({"ROLE_p1"})
public String r1(){
    return "访问r1资源";
}

  • @PreAuthorize 进入方法前拦截,校验权限
  1. 开启全局配置注解@EnableGlobalMethodSecurity(prePostEnabled = true)

  2. 添加注解到方法上

@GetMapping("/r/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1(){
    return "访问r1资源";
}

  • @PostAuthorize 方法执行后拦截,校验权限
  1. 开启全局配置注解@EnableGlobalMethodSecurity(prePostEnabled = true)

  2. 添加注解到方法上

@GetMapping("/r/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1(){
    System.out.println("方法执行");
    return "访问r1资源";
}

这个注解的特点是,会先执行后校验,类似 do.....while 循环一样


这里需要注意一个点,基于注解的权限控制,当权限不足时,会抛出全局AccessDeniedException异常,无法被我们上面写的handler所处理 ,他是路由层面的,只有通过配置文件的.antMatchers("/r/r1").hasAnyAuthority("p1")才会被处理,所以需要增加全局异常处理

@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionHandlers {

    /**
     * 无权访问异常处理器
     *
     * @param e security无权访问处理器
     * @return ResponseResult
     */
    @ExceptionHandler(AccessDeniedException.class)
    public ResultVO parameterMissingExceptionHandler(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
        log.error("【无权访问异常】: err = {} url = {} ", e.getMessage(), request.getRequestURL());
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        return ResponseUtil.error(ResultEnum.NO_AUTH_ACCESS);
    }

}

5. 基于注解的数据级别控制

  • @PostFilter 对返回数据进行过滤控制
  1. 开启全局配置注解@EnableGlobalMethodSecurity(prePostEnabled = true)

  2. 添加注解到方法上

@GetMapping("/r/r1")
@PostFilter("filterObject.username=='admin1'")
public List<User> r1(){
    List<User> list = Lists.newArrayList();
    list.add(new User(1L,"admin1","123"));
    list.add(new User(2L,"admin1123","1231"));
    System.out.println(list);
    return list;
}

启动服务测试,请求/r/r1,返回结果:

image_6


  • @PreFilter 对传入参数进行过滤控制
  1. 开启全局配置注解@EnableGlobalMethodSecurity(prePostEnabled = true)

  2. 添加注解到方法上

  • UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @PreFilter(filterTarget="ids", value="filterObject%2==0")
    public List<Integer> delete(List<Integer> ids) {
        System.out.println(ids); //[2,4]
        return ids;
    }
}
  • 调用
  @Autowired
  private IUserService userService;
  
  @GetMapping("/r/r1")
  public List<Integer> r1(Integer id){
      List<Integer> ids = new ArrayList<Integer>();
      ids.add(1);
      ids.add(2);
      ids.add(3);
      ids.add(4);
      ids.add(5);
      userService.delete(ids);
      return ids;//[2,4]
  }

  • 启动,测试,返回结果 2,4,打印的ids 也是2,4 ,说明生效

  • 三个注解一起使用下,调用还是上面的/r/r1,传入1,2,3,4,5的list
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @PreFilter(filterTarget="ids", value="filterObject%2==0")  //
    @PostFilter("filterObject==2")
    @PreAuthorize("hasAnyAuthority('p1')")
    public List<Integer> delete(List<Integer> ids) {
        System.out.println(ids);[2,4]
        return ids;
    }
}

启动测试,访问/r/r1,被拦截到登录,说明权限校验生效,返回[2],说明@PreFilter和@PostFilter都生效了。over!


6. 自定义用户注销页

  • 默认退出路径/logout,退出登录后默认重定向到login页面

  • 修改配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .exceptionHandling()
            .accessDeniedPage("/noAuth.html") //自定义权限不足页
            .and()
            .authorizeRequests()
             .antMatchers("/r/r1").hasAnyRole("p1")
            .antMatchers("/r/r2").hasAuthority("p2")
            .anyRequest().permitAll()  //其他请求全部放行
            .and()
            .formLogin()
            .loginPage("/login.html") //自定义登录页
            .loginProcessingUrl("/loginAction")  //自定义登录接口
            .and()
            .logout()
            .logoutSuccessUrl("/loginOut")  //自定义退出登录成功接口
            .logoutUrl("/lg");  //自定义退出登录接口
}
  • 增加退出登录成功处理接口
    @GetMapping("/loginOut")
    public ResultVO login(){
        return "退出成功";
    }

启动测试,访问/r/r1,被拦截到登录,进行登录,然后访问 /lg, 返回 退出成功 ,测试完毕


7. 记住我

  • 记住我需要保存登录token,我们这里用数据库存储,创建表,建表语句在 JdbcTokenRepositoryImpl类里
 create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

  • 在SecurityConfig里增加配置

  • 配置数据源

  @Resource
  private DataSource dataSource;
  • 配置remember-me存储器
@Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //如果不使用上面的建表语句进行建表,也可以通过这个方式自动创建表,但是如果已经有表了,请把它删掉,否则还会去尝试创建,然后就报错
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
  • 配置HttpScurity
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .exceptionHandling()
            .accessDeniedPage("/noAuth.html")
            .and()
            .authorizeRequests()
            .antMatchers("/r/r1").hasAnyRole("p1")
            .antMatchers("/r/r2").hasAuthority("p2")
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/loginAction")
            .and()
            .logout()
            .logoutSuccessUrl("/loginOut")
            .logoutUrl("/lg")
            .and()
            .rememberMe()//开启功能
            .tokenRepository(persistentTokenRepository()) //设置token存储
            .tokenValiditySeconds(60);  //设置过期时间,单位秒
}

  • 在login.html里增加复选框
<form action="/api/loginAction" method="post">
    <div>
        用户名:<input type="text" name="username"/>
    </div>
    <div>
        密码:<input type="password" name="password"/>
    </div>
    <div>
        记住我:<input type="checkbox" name="remember-me"/>  <!--默认name就是remember-me,也可以通过修改配置字段名-->
    </div>
    <div>
        <input type="submit" value="登录"/>
    </div>
</form>

启动测试,登录时勾选复选框

image_7

登录成功后,F12查看cookie

image_8

再看数据库

image_9

ok,功能正常。


高级

1. 前后端分离

  • 前后端分离,所有请求均以json返回,不再返回页面。

  • 示例:以下代码就可以直接将各种情况以JSON的方式返回

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/r/r1").hasAuthority("p1")
            .antMatchers("/r/r2").hasAuthority("p2")
            .antMatchers("/r/**").authenticated()
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .permitAll()
            //登录失败,返回json
            .failureHandler((request,response,ex) -> {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = response.getWriter();
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",401);
                if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
                    map.put("message","用户名或密码错误");
                } else if (ex instanceof DisabledException) {
                    map.put("message","账户被禁用");
                } else {
                    map.put("message","登录失败!");
                }
                out.write(map.toString());
                out.flush();
                out.close();
            })
            //登录成功,返回json
            .successHandler((request,response,authentication) -> {
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",200);
                map.put("message","登录成功");
                map.put("data",authentication);
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write(map.toString());
                out.flush();
                out.close();
            })
            .and()
            .exceptionHandling()
            //没有权限,返回json
            .accessDeniedHandler((request,response,ex) -> {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                PrintWriter out = response.getWriter();
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",403);
                map.put("message", "权限不足");
                out.write(map.toString());
                out.flush();
                out.close();
            })
            //未登录或登录失效时,返回json
            .authenticationEntryPoint((request,response,ex) -> {
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = response.getWriter();
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",401);
                map.put("message","未登录");
                out.write(map.toString());
                out.flush();
                out.close();
            })
            .and()
            .logout()
            //退出成功,返回json
            .logoutSuccessHandler((request,response,authentication) -> {
                Map<String,Object> map = new HashMap<String,Object>();
                map.put("code",200);
                map.put("message","退出成功");
                map.put("data",authentication);
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write(map.toString());
                out.flush();
                out.close();
            });
}

2. 代码封装与统一化处理

2.1登录失败

  • 自定义的登录失败错误处理handler
/**
 * 登录失败处理handler
 */
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
         if (e instanceof CredentialsExpiredException) { //密码已过期
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.PASSWORD_IS_EXPIRED);
        } else if (e instanceof AccountExpiredException) {//账号已过期
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_EXPIRED);
        } else if (e instanceof DisabledException) {//账号已禁用
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_DISABLED);
        } else {//用户名或密码错误
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.USERNAME_OR_PASSWORD_ERROR);
        }
    }
}

  • 添加handler到Security的配置中
 @Override
 protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling()
                .accessDeniedPage("/noAuth")
                .and()
                .authorizeRequests()
                .antMatchers("/r/r2").hasAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successForwardUrl("/login/success")
                .failureHandler(customAuthFailureHandler) //添加错误处理handler
                .and()
                .logout()
                .logoutSuccessUrl("/login/out");

    }

启动测试,输入错误用户名密码,可返回JSON

  • ResponseUtil与ResultEnum代码

  • ResponseUtil

package com.cz.utils;


import com.cz.enums.ResultEnum;
import com.cz.model.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Auther: chaixuhong
 */
public class ResponseUtil {


    private static ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 错误返回
     *
     * @param resultEnum
     * @return
     */
    public static ResultVO error(ResultEnum resultEnum) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(resultEnum.getCode());
        resultVO.setErrMsg(resultEnum.getErrMsg());
        return resultVO;
    }

    /**
     * 自定义返回
     *
     * @param code
     * @param errMsg
     * @return
     */
    public static ResultVO error(int code, String errMsg) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(code);
        resultVO.setErrMsg(errMsg);
        return resultVO;
    }

    /**
     * 有参成功返回
     *
     * @param data
     * @return
     */
    public static ResultVO success(Object data) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(ResultEnum.SUCCESS.getCode());
        resultVO.setErrMsg(ResultEnum.SUCCESS.getErrMsg());
        resultVO.setData(data);
        return resultVO;
    }

    /**
     * 无参成功返回
     *
     * @return
     */
    public static ResultVO success() {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(ResultEnum.SUCCESS.getCode());
        resultVO.setErrMsg(ResultEnum.SUCCESS.getErrMsg());
        return resultVO;
    }

    /**
     * 标准枚举输出
     *
     * @param response
     * @param httpStatus
     * @param resultEnum
     * @throws IOException
     */
    public static void printOut(HttpServletResponse response, int httpStatus, ResultEnum resultEnum) throws IOException {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(resultEnum.getCode());
        resultVO.setErrMsg(resultEnum.getErrMsg());
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(httpStatus);
        PrintWriter out = response.getWriter();
        out.write(objectMapper.writeValueAsString(resultVO));
        out.flush();
        out.close();
    }
}

  • ResultEnum
package com.cz.enums;

import lombok.Getter;

@Getter
public enum ResultEnum {
    SUCCESS(1, "success"),
    SERVER_ERROR(500,"服务器异常!"),
    /**
     * 登录异常码
     */
    USERNAME_OR_PASSWORD_ERROR(800,"用户名或密码错误"),
    ACCOUNT_IS_LOCKED(801,"账号被锁定,请联系管理员"),
    PASSWORD_IS_EXPIRED(802,"密码已过期,请联系管理员"),
    ACCOUNT_IS_EXPIRED(803,"账号已过期,请联系管理员"),
    ACCOUNT_IS_DISABLED(804,"账号被禁用,请联系管理员"),
    CODE_ERROR_INPUT(805,"验证码输入错误"),
    CODE_IS_EXPIRED(806,"验证码已过期"),

    PARAM_ERROR(1001, "参数错误"),
    FILE_PARAM_ERROR(1002, "文件不能超过100M"),
    ;
    private int code;
    private String errMsg;

    ResultEnum(int code, String errMsg) {
        this.code = code;
        this.errMsg = errMsg;
    }
}


2.2登录成功

  • 自定义的登录成功处理handler
@Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        ResponseUtil.printOutData(httpServletResponse,HttpServletResponse.SC_OK, ResultEnum.SUCCESS,authentication.getPrincipal());
    }

}

  • 添加handler到Security的配置中
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling()
                .accessDeniedPage("/noAuth")
                .and()
                .authorizeRequests()
                .antMatchers("/r/r2").hasAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(customAuthSuccessHandler)//添加成功处理handler
                .failureHandler(customAuthFailureHandler)//添加错误处理handler
                .and()
                .logout()
                .logoutSuccessUrl("/login/out");

    }

2.3登录失效或未登录

  • 自定义的登录失效或未登录处理handler,这个很关键,不重写当登录失效或者没有登录请求了需要认证的接口,会重定向到Security的登录页。重写返回json
@Component
public class CustomAuthExpiredHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.LOGIN_IS_EXPIRED);
    }
}

  • 添加handler到Security的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .exceptionHandling()
            .accessDeniedPage("/noAuth")
            .and()
            .authorizeRequests()
            .antMatchers("/login/fail").hasAuthority("p2")
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .loginProcessingUrl("/login")
            .successHandler(customAuthSuccessHandler)
            .failureHandler(customAuthFailureHandler)
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(customAuthExpiredHandler) //设置未登录或登录失效handler
            .and()
            .logout()
            .logoutSuccessUrl("/login/out");

}

2.4无权访问

  • 自定义的无权访问处理handler
/**
 * 权限不足
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, ResultEnum.NO_AUTH_ACCESS);

    }
}
  • 添加handler到Security的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/login/fail").hasAuthority("p2")
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .loginProcessingUrl("/login")
            .successHandler(customAuthSuccessHandler)
            .failureHandler(customAuthFailureHandler)
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(customAuthExpiredHandler)
            .accessDeniedHandler(customAccessDeniedHandler) //权限不足处理器
            .and()
            .logout()
            .logoutSuccessUrl("/login/out");

}

2.5退出登录

  • 自定义的退出登录处理handler
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        ResponseUtil.printOut(httpServletResponse,HttpServletResponse.SC_OK, ResultEnum.SUCCESS);
    }
}
  • 添加handler到Security的配置中
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login/fail").hasAuthority("p2")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(customAuthSuccessHandler)
                .failureHandler(customAuthFailureHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(customAuthExpiredHandler)
                .accessDeniedHandler(customAccessDeniedHandler)
                .and()
                .logout()
                .logoutSuccessHandler(customLogoutSuccessHandler);//退出登录处理器

    }

3. 验证码登录

  • 为防止登录接口被恶意请求,增加验证码登录是必要的,但是其实也是防君子防不了小人

image_10

3.1增加获取验证码接口

  • 验证码工具
<!-- Java图形验证码 -->
<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>
  • 编写验证码接口,生成验证码返回给前端,并且缓存到Redis中
@GetMapping(value = "/code")
@ApiOperation("获取验证码")
public ResponseEntity<Object> getCode(@RequestParam String code) {
    ArithmeticCaptcha captcha = new ArithmeticCaptcha(111, 36);
    captcha.getArithmeticString();  // 获取运算的公式:4-9+1=?
    String text = captcha.text();// 获取运算的结果:-4
    System.out.println("获取运算的结果"+text);
    String uuid = CodeGeneratedUtil.genUUID();//验证码唯一标识,作为redis的key
    // 保存redis
    redisUtils.set(uuid, text, 5L, TimeUnit.MINUTES);
    // 验证码信息
    Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
        put("img", captcha.toBase64());
        put("uuid", uuid);
    }};
    return ResponseEntity.ok(imgResult);
}

3.2创建验证码过滤器

  • 前面提到过,Security就是一串过滤器,所以我们需要增加自己的过滤器就好了。

验证码过滤器CodeAuthenticationFilter

package com.cz.config.security;

import com.cz.enums.ResultEnum;
import com.cz.exception.ValidateCodeException;
import com.cz.utils.RedisUtil;
import com.cz.utils.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * @author chaixuhong
 * @className CodeAuthenticationFilter
 * @description 验证码登录验证过滤器
 * @date 2021年05月12日
 **/
@Slf4j
@Component
public class CodeAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private CustomAuthFailureHandler customAuthFailureHandler;//错误处理handler

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
            throws ServletException, IOException {

        //TODO:这里url最好后续改成读取配置
        if (StringUtils.equals("/api/login", httpServletRequest.getRequestURI())
                && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
            try {
                validateCode(httpServletRequest);
            }catch (ValidateCodeException e) {
                customAuthFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
                return;
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }


    /**
     * 校验验证码
     * @param request
     */
    private void validateCode(HttpServletRequest request){
        String code = request.getParameter("code");
        String uuid = request.getParameter("uuid");

        if (StringUtils.isBlank(code)) {
            log.error("提交空验证码!");
            throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
        }

        if (StringUtils.isBlank(uuid)) {
            log.error("提交空的uuid!");
            throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
        }

        String redisCode = redisUtil.get(uuid);

        if (redisCode == null) {
            throw new ValidateCodeException(ResultEnum.CODE_IS_EXPIRED);
        }

        if (!StringUtils.equals(redisCode, code)) {
            throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
        }

        redisUtil.delete(uuid);
    }

}


3.3 在登录失败handler中处理自定义异常

  • 自定义的登录失败错误处理handler。新增关于ValidateCodeException异常的处理
/**
 * 登录失败处理handler
 */
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        if (e instanceof ValidateCodeException) {//验证码错误或失效
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ((ValidateCodeException) e).getResultEnum());
        } else if (e instanceof LockedException) { //账号被锁定
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_LOCKED);
        } else if (e instanceof CredentialsExpiredException) { //密码已过期
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.PASSWORD_IS_EXPIRED);
        } else if (e instanceof AccountExpiredException) {//账号已过期
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_EXPIRED);
        } else if (e instanceof DisabledException) {//账号已禁用
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_DISABLED);
        } else {//用户名或密码错误
            ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.USERNAME_OR_PASSWORD_ERROR);
        }
    }
}

3.4配置Security

  • 加过滤器加入到Security中
package com.cz.security;

import com.cz.security.filter.CodeAuthenticationFilter;
import com.cz.security.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private CustomAuthFailureHandler customAuthFailureHandler;

    @Autowired
    private CustomAuthSuccessHandler customAuthSuccessHandler;

    @Autowired
    private CustomAuthExpiredHandler customAuthExpiredHandler;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;

    @Autowired
    private CodeAuthenticationFilter codeAuthenticationFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //注入自定义的用户加载类
        auth.userDetailsService(userDetailsService);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login","/login/code").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(customAuthSuccessHandler)
                .failureHandler(customAuthFailureHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(customAuthExpiredHandler)
                .accessDeniedHandler(customAccessDeniedHandler)
                .and()
                .logout()
                .logoutSuccessHandler(customLogoutSuccessHandler)
                .and()
                .addFilterBefore(codeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);

    }
}

  • 其他类

  • ValidateCodeException

package com.cz.exception;

import com.cz.enums.ResultEnum;
import org.springframework.security.core.AuthenticationException;

public class ValidateCodeException extends AuthenticationException {

    private ResultEnum resultEnum;

    public ValidateCodeException(ResultEnum resultEnum) {
        super(resultEnum.getErrMsg());
        this.resultEnum = resultEnum;
    }

    public ResultEnum getResultEnum() {
        return resultEnum;
    }
}

  • ResultEnum
package com.cz.enums;

import lombok.Getter;

@Getter
public enum ResultEnum {
    SUCCESS(1, "success"),
    SERVER_ERROR(500,"服务器异常!"),
    /**
     * 登录异常码
     */
    USERNAME_OR_PASSWORD_ERROR(800,"用户名或密码错误"),
    ACCOUNT_IS_LOCKED(801,"账号被锁定,请联系管理员"),
    PASSWORD_IS_EXPIRED(802,"密码已过期,请联系管理员"),
    ACCOUNT_IS_EXPIRED(803,"账号已过期,请联系管理员"),
    ACCOUNT_IS_DISABLED(804,"账号被禁用,请联系管理员"),
    CODE_ERROR_INPUT(805,"验证码输入错误"),
    CODE_IS_EXPIRED(806,"验证码已过期"),

    LOGIN_IS_EXPIRED(1000,"登录失效"),
    NO_AUTH_ACCESS(1001,"无权访问,请联系管理员"),

    PARAM_ERROR(1002, "参数错误"),
    FILE_PARAM_ERROR(1003, "文件不能超过100M"),

    ;
    private int code;
    private String errMsg;

    ResultEnum(int code, String errMsg) {
        this.code = code;
        this.errMsg = errMsg;
    }
}



4. 会话保持

  • 框架默认通过session保持会话,登录成功后,会在服务端内存里保存session会话,在前端cookie保存JSESSIONID

  • 如果是单机玩,那么认证部分就已经实现完成了

  • 如果部署集群,那需要改造了。

4.1 session共享

  • 将session会话交由第三方存储,达到集群session可以共享,那么就摆脱了单机

  • 这里我们使用redis来存储session,实现session共享

  • 添加依赖

<!-- Spring redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring session redis -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • application.yml增加配置
spring:  
  redis:
    database: 1
    host: 127.0.0.1
    port: 6379
    timeout: 6000ms
    password: 123456
  session:
    store-type: redis
  • 自此,完成!相当简单。启动测试。进行登录,登录成功后,查看redis

image_11

  • 重启后端服务,在访问需要认证的接口,发现不需要再登录了。

4.2 普通token

  • 不再依赖session-cookie机制,使用无状态restful接口风格,那么就要使用到令牌的方式了

4.2.1 封装登录用户类

  • LoginUser
package com.cz.security.bean;


import com.alibaba.fastjson.annotation.JSONField;
import com.cz.entity.SysDept;
import com.cz.entity.SysRole;
import com.cz.entity.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.List;

/**
 * 用户封装类
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class LoginUser implements UserDetails, Serializable {

    private static final long serialVersionUID = -4504630943184309233L;

    private SysUser user;

    private List<SysRole> roles;

    private SysDept sysDept;

    private List<String> authority;

    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    @JSONField(serialize = false)
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    @JSONField(serialize = false)
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    @JSONField(serialize = false)
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    @JSONField(serialize = false)
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JSONField(serialize = false)
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JSONField(serialize = false)
    public boolean isEnabled() {
        return user.getEnabled();
    }


}

4.2.2 用户登录成功后缓存到redis

  • LoginUserCache
package com.cz.cache;

import com.alibaba.fastjson.JSONObject;
import com.cz.security.bean.LoginUser;
import com.cz.utils.RedisUtil;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @className: LoginUserCache
 * @description: 登录用户信息缓存
 * @author: chaizi
 * @date: 2021/12/12
 **/
@Component
public class LoginUserCache {

    @Autowired
    private RedisUtil redisUtil;

    private final String PREFIX = "login.user:";

    private final int EXPIRED = 2;

    private String genKey(String key) {
        return PREFIX + key;
    }

    public void setLoginUser(String token, Object loginUser) {
        String key = genKey(token);
        redisUtil.set(key, JSONObject.toJSONString(loginUser), EXPIRED, TimeUnit.HOURS);
    }

    public LoginUser getLoginUser(String token) {
        String key = genKey(token);
        String str = redisUtil.get(key);
        if (Strings.isBlank(str)) {
            return null;
        }
        Long expire = redisUtil.getExpire(key);
        if (expire < 2000) { //续期token
            redisUtil.expire(key, EXPIRED, TimeUnit.HOURS);
        }
        return JSONObject.parseObject(str, LoginUser.class);
    }

    public boolean clearUser(String token) {
        String key = genKey(token);
        return redisUtil.delete(key);
    }

}

  • 登录成功后
/**
 * 登录成功处理器
 *
 * @author chaizi
 * @date 2021年05月12日
 */
@Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private LoginUserCache loginUserCache;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //生成token
        String token = CodeGeneratedUtil.genMD5(new StringBuilder(authentication.getName()).append(System.currentTimeMillis()).toString());
        loginUserCache.setLoginUser(token, authentication.getPrincipal());//缓存到redis
        Map result = MapUtil.builder().put("token",token).put("userInfo",authentication.getPrincipal()).build();
        ResponseUtil.printOutData(httpServletResponse, HttpServletResponse.SC_OK, ResultEnum.SUCCESS, result);
    }

}

4.2.3 编写token过滤器,获取到有效token后设置security的上下文,使其为登录状态,token无效的话,交给后面的过滤器处理

package com.cz.security.filter;

import com.cz.cache.LoginUserCache;
import com.cz.security.bean.LoginUser;
import com.cz.security.exception.ValidateCodeException;
import com.cz.security.handler.CustomAuthFailureHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * token验证过滤器
 */
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private LoginUserCache loginUserCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (Strings.isBlank(authHeader) || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        String authToken = authHeader.substring("Bearer ".length());

        LoginUser loginUser = loginUserCache.getLoginUser(authToken);
        if (loginUser == null) {
            filterChain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        authenticationToken.setDetails(loginUser);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request, response);
    }


}

4.2.4 将过滤器添加到security中,禁用session,不再依赖服务器状态

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
.and()
.addFilterAt(customUsernameAndPasswordAuthFilter(), UsernamePasswordAuthenticationFilter.class)

启动,测试,登录成功后,返回token,用token请求其他接口,测试成功!


自此security篇结束,后续有时间补充JWT内容。

分享到:
vscode根据eslint规则格式化vue
TypeScript语法
  • 文章目录
  • 站点概览
柴子

内蒙 柴子

what do U want?

Github QQ Email RSS
最喜欢的电影
最喜欢的游戏
最喜欢的音乐
最喜欢的图书
最喜欢的动漫
夏洛特的烦恼
英雄联盟
痴心绝对
数据库从入门到删库跑路
斗破苍穹
看爆 Top5
  • 微信getUserProfile兼容性调整以及uniapp写法 2,041次看爆
  • gateway转发微服务请求丢失header参数 1,017次看爆
  • mybatis-plus代码生成器 950次看爆
  • SpringBoot防重复提交与限流的优雅解决 547次看爆
  • Spring Boot Security从入门到进阶到高级 545次看爆
转载注明出处

站点已萌萌哒运行 00 天 00 小时 00 分 00 秒(●'◡'●)ノ♥

Copyright © 2022 柴子 京ICP备17035556号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图