The Wayback Machine - http://web.archive.org/web/20240803161318/https://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao
⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

:  http://www.iocoder.cn/Spring-Boot/Spring-Security/ 


1. 

2. 

3. 使

4.  Spring Session

5.  OAuth2

6.  JWT

7. 

666. 





🙂🙂🙂****


(一)RocketMQ / MyCAT / Sharding-JDBC 

(二)RocketMQ / MyCAT / Sharding-JDBC  GitHub 

(三)

(四)

(五)





 https://github.com/YunaiV/SpringBoot-Labs   lab-01-spring-security 

 Star 

1. 


(authentication)(authorization)😈 


FROM  (authentication)  (authorization) 


authentication [ɔ,θɛntɪ'keʃən] 

authorization [,ɔθərɪ'zeʃən] 





 passport  ticketpassport  authentication

 authorization





 1234 authentication

 check  authorization 



 

 Java  Spring Security  Apache Shiro  Spring Security 


FROM Spring Security 

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security 访 Spring 

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
Spring Security  Java  Spring Spring 

2. 



lab-01-springsecurity-demo 


 Spring Security 访 API 访

2.1 


 pom.xml 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-01-springsecurity-demo</artifactId>

<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 实现对 Spring Security 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

</project>

具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

2.2 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

2.3 配置文件

application.yml 中,添加 Spring Security 配置,如下:

spring:
# Spring Security 配置项,对应 SecurityProperties 配置类
security:
# 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
user:
name: user # 账号
password: user # 密码
roles: ADMIN # 拥有角色


 spring.security  Spring Security  SecurityProperties 

Spring Boot UserDetailsServiceAutoConfiguration  InMemoryUserDetailsManager Bean 

 spring.security.user UserDetailsServiceAutoConfiguration  User 

 spring.security.user UserDetailsServiceAutoConfiguration  "user"  UUID  User 



2.4 AdminController


 cn.iocoder.springboot.lab01.springsecurity.controller  AdminController  API 

// AdminController.java

@RestController
@RequestMapping("/admin")
public class AdminController {

@GetMapping("/demo")
public String demo() {
return "示例返回";
}

}

  • 这里,我们先提供一个 "/admin/demo" 接口,用于测试未登录时,会被拦截到登录界面。

2.5 简单测试


 Application#main(String[] args) 

访 http://127.0.0.1:8080/admin/demo  Spring Security 默认登录界面
使 DefaultLoginPageGeneratingFilter 

2.3 user/user Spring Security 访 http://127.0.0.1:8080/admin/demo 访 接口的结果

3. 使



lab-01-springsecurity-demo-role 


2.  Spring Security  Spring Security 

 lab-01-springsecurity-demo-role 

3.1 


 2.1   pom.xml 

3.2 


 Spring Security 

3.2.1 SecurityConfig


 cn.iocoder.springboot.lab01.springsecurity.config  SecurityConfig  WebSecurityConfigurerAdapter  Spring Security  Web 

// SecurityConfig.java

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// ...

}


 WebSecurityConfigurerAdapter  Spring Security 


 #configure(AuthenticationManagerBuilder auth)  AuthenticationManager 

// SecurityConfig.java

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
// <X> 使用内存中的 InMemoryUserDetailsManager
inMemoryAuthentication()
// <Y> 不使用 PasswordEncoder 密码编码器
.passwordEncoder(NoOpPasswordEncoder.getInstance())
// <Z> 配置 admin 用户
.withUser("admin").password("admin").roles("ADMIN")
// <Z> 配置 normal 用户
.and().withUser("normal").password("normal").roles("NORMAL");
}


<X>  AuthenticationManagerBuilder#inMemoryAuthentication() 使 InMemoryUserDetailsManager Bean 

Spring  UserDetailsManager 

InMemoryUserDetailsManager2. 

JdbcUserDetailsManager  JDBC JdbcUserDetailsManager 



 AuthenticationManagerBuilder#userDetailsService(userDetailsService) 使 UserDetailsService 



<Y>  AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder)  PasswordEncoder 

便使 NoOpPasswordEncoder 使 PasswordEncoder 

使 BCryptPasswordEncoder  PasswordEncoder  PasswordEncoder?



<Z> admin/adminnormal/normal ADMIN  NORMAL 2. 


 #configure(HttpSecurity http)  URL 

// SecurityConfig.java

@Override
protected void configure(HttpSecurity http) throws Exception {
http
// <X> 配置请求地址的权限
.authorizeRequests()
.antMatchers("/test/echo").permitAll() // 所有用户可访问
.antMatchers("/test/admin").hasRole("ADMIN") // 需要 ADMIN 角色
.antMatchers("/test/normal").access("hasRole('ROLE_NORMAL')") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
.and()
// <Y> 设置 Form 表单登录
.formLogin()
// .loginPage("/login") // 登录 URL 地址
.permitAll() // 所有用户可访问
.and()
// 配置退出相关
.logout()
// .logoutUrl("/logout") // 退出 URL 地址
.permitAll(); // 所有用户可访问
}


  • <X>  HttpSecurity#authorizeRequests()  URL 使


    #(String... antPatterns)  URL  Ant  

    #permitAll() 访

    #denyAll() 访

    #authenticated() 访

    #anonymous() 访

    #rememberMe()  remember me 访

    #fullyAuthenticated()  remember me 访

    #hasIpAddress(String ipaddressExpression) IP访

    #hasRole(String role)  访

    #hasAnyRole(String... roles) 访

    #hasAuthority(String authority) (authority)访

    #hasAuthority(String... authorities) (authority)访

    #access(String attribute)  Spring EL  true 访




    <Y>  HttpSecurity#formLogin()  Form 


     #loginPage(String loginPage) 2. 使




    <Z>  HttpSecurity#logout() 退


    退 #logoutUrl(String logoutUrl) 2. 使退



    3.2.2 TestController


     cn.iocoder.springboot.lab01.springsecurity.controller  TestController  API 

    // TestController.java

    @RestController
    @RequestMapping("/test")
    public class TestController {

    @GetMapping("/echo")
    public String demo() {
    return "示例返回";
    }

    @GetMapping("/home")
    public String home() {
    return "我是首页";
    }

    @GetMapping("/admin")
    public String admin() {
    return "我是管理员";
    }

    @GetMapping("/normal")
    public String normal() {
    return "我是普通用户";
    }

    }


     /test/echo 访

     /test/home 访

     /test/admin admin/admin ADMIN 

     /test/normal normal/normal NORMAL 


    normal/normal访 /test/admin  403 ~

    3.3 


    使 Spring Security 

    3.3.1 SecurityConfig


     SecurityConfig  @EnableGlobalMethodSecurity  Spring Security 

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

    3.3.2 DemoController

    cn.iocoder.springboot.lab01.springsecurity.controller 包路径下,创建 DemoController 类,提供测试 API 接口。代码如下:

    // DemoController.java

    @RestController
    @RequestMapping("/demo")
    public class DemoController {

    @PermitAll
    @GetMapping("/echo")
    public String demo() {
    return "示例返回";
    }

    @GetMapping("/home")
    public String home() {
    return "我是首页";
    }

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/admin")
    public String admin() {
    return "我是管理员";
    }

    @PreAuthorize("hasRole('ROLE_NORMAL')")
    @GetMapping("/normal")
    public String normal() {
    return "我是普通用户";
    }

    }


    •  URL 3.2.2 TestController



      @PermitAll  #permitAll() 访


      3.2.1 SecurityConfig .anyRequest().authenticated() 访 @PermitAll 

      Java Config 




      @PreAuthorize  #access(String attribute)  Spring EL  true 访



      Spring Security  @Secured(), @PreAuthorize()  @RolesAllowed()

      normal/normal访 /test/admin  403 ~

      4.  Spring Session


       Spring Boot  Session 5.  Spring Security

      5.  OAuth2


       Spring Security OAuth2 

      6.  JWT


       SpringBoot + SpringSecurity + JWT + RBAC 

      7. 


       RuoYi-Vue 


       Spring Security 

       RBAC 

       Redis 

       Vue  Vue  React 


      便 Fork   https://github.com/YunaiV/RuoYi-Vue 


       Spring Security  +  App 

      https://github.com/YunaiV/ruoyi-vue-pro

      🔥  🔥 RuoYi-Vue  Pro  Spring Boot + MyBatis Plus + Vue & Element  +  RBAC SaaS Activiti + Flowable   Star 


       RuoYi-Vue 

      7.1 


       RBAC 5


       RBAC RBAC

      😈 2011  ExtJS  Spring Security  SDK 使 HTTP 
      实体 说明
      SysUser sys_user 用户信息
      SysRole sys_role 用户信息
      SysUserRole sys_user_role 用户和角色关联
      SysMenu sys_menu 菜单权限
      SysRoleMenu sys_role_menu 角色和菜单关联

      5


       SysUse  SysRole  SysUserRole 

       SysRole  SysMenu  SysRoleMenu 

      7.1.1 SysUser


      SysUser 

      // SysUser.java

      public class SysUser extends BaseEntity {

      private static final long serialVersionUID = 1L;

      @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
      private Long userId;

      @Excel(name = "部门编号", type = Type.IMPORT)
      private Long deptId;

      @Excel(name = "登录名称")
      private String userName;

      @Excel(name = "用户名称")
      private String nickName;

      @Excel(name = "用户邮箱")
      private String email;

      @Excel(name = "手机号码")
      private String phonenumber;

      @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
      private String sex;

      /** 用户头像 */
      private String avatar;

      /** 密码 */
      private String password;

      /** 盐加密 */
      private String salt;

      @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
      private String status;

      /** 删除标志(0代表存在 2代表删除) */
      private String delFlag;

      @Excel(name = "最后登录IP", type = Type.EXPORT)
      private String loginIp;

      @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
      private Date loginDate;

      @Excels({
      @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
      @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
      })
      @Transient
      private SysDept dept;

      /** 角色对象 */
      @Transient
      private List<SysRole> roles;

      /** 角色组 */
      @Transient
      private Long[] roleIds;

      /** 岗位组 */
      @Transient
      private Long[] postIds;

      // ...省略 set/get 方法

      }


       @Transient 




       SQL 

      create table sys_user (
      user_id bigint(20) not null auto_increment comment '用户ID',
      dept_id bigint(20) default null comment '部门ID',
      user_name varchar(30) not null comment '用户账号',
      nick_name varchar(30) not null comment '用户昵称',
      user_type varchar(2) default '00' comment '用户类型(00系统用户)',
      email varchar(50) default '' comment '用户邮箱',
      phonenumber varchar(11) default '' comment '手机号码',
      sex char(1) default '0' comment '用户性别(0男 1女 2未知)',
      avatar varchar(100) default '' comment '头像地址',
      password varchar(100) default '' comment '密码',
      status char(1) default '0' comment '帐号状态(0正常 1停用)',
      del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
      login_ip varchar(50) default '' comment '最后登录IP',
      login_date datetime comment '最后登录时间',
      create_by varchar(64) default '' comment '创建者',
      create_time datetime comment '创建时间',
      update_by varchar(64) default '' comment '更新者',
      update_time datetime comment '更新时间',
      remark varchar(500) default null comment '备注',
      primary key (user_id)
      ) engine=innodb auto_increment=100 comment = '用户信息表';

      7.1.2 SysRole

      SysRole ,角色实体类。代码如下:

      // SysRole.java

      public class SysRole extends BaseEntity {

      private static final long serialVersionUID = 1L;

      @Excel(name = "角色序号", cellType = ColumnType.NUMERIC)
      private Long roleId;

      @Excel(name = "角色名称")
      private String roleName;

      @Excel(name = "角色权限")
      private String roleKey;

      @Excel(name = "角色排序")
      private String roleSort;

      @Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限")
      private String dataScope;

      @Excel(name = "角色状态", readConverterExp = "0=正常,1=停用")
      private String status;

      /** 删除标志(0代表存在 2代表删除) */
      private String delFlag;

      /** 用户是否存在此角色标识 默认不存在 */
      @Transient
      private boolean flag = false;

      /** 菜单组 */
      @Transient
      private Long[] menuIds;

      /** 部门组(数据权限) */
      @Transient
      private Long[] deptIds;

      // ...省略 set/get 方法

      }

      • 每个字段比较简单,胖友自己根据注释理解下即可。

      对应表的创建 SQL 如下:

      create table sys_role (
      role_id bigint(20) not null auto_increment comment '角色ID',
      role_name varchar(30) not null comment '角色名称',
      role_key varchar(100) not null comment '角色权限字符串',
      role_sort int(4) not null comment '显示顺序',
      data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
      status char(1) not null comment '角色状态(0正常 1停用)',
      del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
      create_by varchar(64) default '' comment '创建者',
      create_time datetime comment '创建时间',
      update_by varchar(64) default '' comment '更新者',
      update_time datetime comment '更新时间',
      remark varchar(500) default null comment '备注',
      primary key (role_id)
      ) engine=innodb auto_increment=100 comment = '角色信息表';

      7.1.3 SysUserRole

      SysUserRole ,用户和角色关联实体类。代码如下:

      // SysUserRole.java

      public class SysUserRole {

      /** 用户ID */
      private Long userId;

      /** 角色ID */
      private Long roleId;

      // ...省略 set/get 方法

      }




      roleKey 使"admin,normal" 


       SQL 

      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)
      ) engine=innodb comment = '用户和角色关联表';

      7.1.4 SysMenu

      SysMenu ,菜单权限实体类。代码如下:

      // SysMenu.java

      public class SysMenu extends BaseEntity {

      private static final long serialVersionUID = 1L;

      /** 菜单ID */
      private Long menuId;

      /** 菜单名称 */
      private String menuName;

      /** 父菜单名称 */
      private String parentName;

      /** 父菜单ID */
      private Long parentId;

      /** 显示顺序 */
      private String orderNum;

      /** 路由地址 */
      private String path;

      /** 组件路径 */
      private String component;

      /** 是否为外链(0是 1否) */
      private String isFrame;

      /** 类型(M目录 C菜单 F按钮) */
      private String menuType;

      /** 菜单状态:0显示,1隐藏 */
      private String visible;

      /** 权限字符串 */
      private String perms;

      /** 菜单图标 */
      private String icon;

      /** 子菜单 */
      @Transient
      private List<SysMenu> children = new ArrayList<SysMenu>();

      // ...省略 set/get 方法

      }


      • 😈  SysResource 







        menuType F 
      • perms 属性,对应的权限标识字符串。一般格式为 ${大模块}:${小模块}:{操作} 。示例如下:

        用户查询:system:user:query
        用户新增:system:user:add
        用户修改:system:user:edit
        用户删除:system:user:remove
        用户导出:system:user:export
        用户导入:system:user:import
        重置密码:system:user:resetPwd




         @PreAuthorize("@ss.hasPermi('system:user:list')")  URL 

         perms 使"system:user:query,system:user:add" 




         SQL 

        create table sys_menu (
        menu_id bigint(20) not null auto_increment comment '菜单ID',
        menu_name varchar(50) not null comment '菜单名称',
        parent_id bigint(20) default 0 comment '父菜单ID',
        order_num int(4) default 0 comment '显示顺序',
        path varchar(200) default '' comment '路由地址',
        component varchar(255) default null comment '组件路径',
        is_frame int(1) default 1 comment '是否为外链(0是 1否)',
        menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)',
        visible char(1) default 0 comment '菜单状态(0显示 1隐藏)',
        perms varchar(100) default null comment '权限标识',
        icon varchar(100) default '#' comment '菜单图标',
        create_by varchar(64) default '' comment '创建者',
        create_time datetime comment '创建时间',
        update_by varchar(64) default '' comment '更新者',
        update_time datetime comment '更新时间',
        remark varchar(500) default '' comment '备注',
        primary key (menu_id)
        ) engine=innodb auto_increment=2000 comment = '菜单权限表';

        7.1.5 SysRoleMenu

        SysRoleMenu ,菜单权限实体类。代码如下:

        // SysRoleMenu.java

        public class SysRoleMenu {

        /** 角色ID */
        private Long roleId;

        /** 菜单ID */
        private Long menuId;

        // ...省略 set/get 方法

        }

        • 每个字段比较简单,胖友自己根据注释理解下即可。

        对应表的创建 SQL 如下:

        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)
        ) engine=innodb comment = '角色和菜单关联表';

        7.2 SecurityConfig

        SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter 抽象类,实现 Spring Security 在 Web 场景下的自定义配置。代码如下:

        // SecurityConfig.java

        @Configuration
        public class SecurityConfig extends WebSecurityConfigurerAdapter {

        // ...

        }

        • 涉及到的配置方法较多,我们逐个来看看。

        重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。代码如下:

        // SecurityConfig.java

        /**
        * 自定义用户认证逻辑
        */
        @Autowired
        private UserDetailsService userDetailsService;

        /**
        * 身份认证接口
        */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService) // <X>
        .passwordEncoder(bCryptPasswordEncoder()); // <Y>
        }

        /**
        * 强散列哈希加密实现
        */
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
        }


        <X>  AuthenticationManagerBuilder#userDetailsService(userDetailsService) 使 UserDetailsService 7.3.1  RuoYi-Vue  UserDetailsService 

        <Y>  AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder)  PasswordEncoder 使 bCryptPasswordEncoder 


         #configure(HttpSecurity httpSecurity)  URL 

        // SecurityConfig.java

        /**
        * 认证失败处理类
        */
        @Autowired
        private AuthenticationEntryPointImpl unauthorizedHandler;

        /**
        * 退出处理类
        */
        @Autowired
        private LogoutSuccessHandlerImpl logoutSuccessHandler;

        /**
        * token 认证过滤器
        */
        @Autowired
        private JwtAuthenticationTokenFilter authenticationTokenFilter;

        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
        // CRSF禁用,因为不使用session
        .csrf().disable()
        // <X> 认证失败处理类
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        // 基于token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        // 过滤请求
        .authorizeRequests()
        // <Y> 对于登录login 验证码captchaImage 允许匿名访问
        .antMatchers("/login", "/captchaImage").anonymous()
        .antMatchers(
        HttpMethod.GET,
        "/*.html",
        "/**/*.html",
        "/**/*.css",
        "/**/*.js"
        ).permitAll()
        .antMatchers("/profile/**").anonymous()
        .antMatchers("/common/download**").anonymous()
        .antMatchers("/swagger-ui.html").anonymous()
        .antMatchers("/swagger-resources/**").anonymous()
        .antMatchers("/webjars/**").anonymous()
        .antMatchers("/*/api-docs").anonymous()
        .antMatchers("/druid/**").anonymous()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated()
        .and()
        .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // <Z>
        // <P> 添加 JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }




        <X>  unauthorizedHandler 7.6.1 AuthenticationEntryPointImpl

        <Y>  /login 访使7.3  API 

        <Z>  logoutSuccessHandler 7.6.3 LogoutSuccessHandlerImpl

        <P>  JWT  authenticationTokenFilter 使 JWT  7.4 JwtAuthenticationTokenFilter


         #authenticationManagerBean  AuthenticationManager 

        // SecurityConfig.java

        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
        }

        • 在方法上,额外添加了 @Bean 注解,保证创建出 AuthenticationManager Bean 。

         Bean 

        7.3  API 


        SysLoginController#login(...)

         SysLoginController  /login 

        // SysLoginController.java

        @Autowired
        private SysLoginService loginService;

        /**
        * 登录方法
        *
        * @param username 用户名
        * @param password 密码
        * @param code 验证码
        * @param uuid 唯一标识
        * @return 结果
        */
        @PostMapping("/login")
        public AjaxResult login(String username, String password, String code, String uuid) {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(username, password, code, uuid);
        ajax.put(Constants.TOKEN, token);
        return ajax;
        }


        •  loginService#login(username, password, code, uuid)  TOKEN 
        • 登录成功后,该接口响应示例如下

          {
          "msg": "操作成功",
          "code": 200,
          "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJkN2Q4OTZiLTU2NTAtNGIyZS1iNjFjLTc0MjlkYmRkNzA1YyJ9.lkU8ot4GecLHs7VAcRAo1fLMOaFryd4W5Q_a2wzPwcOL0Kiwyd4enpnGd79A_aQczXC-JB8vELNcNn7BrtJn9A"
          }

          • 后续,前端在请求后端接口时,在请求头上带头该 token 值,作为用户身份标识。

        SysLoginService#login(...)

         SysLoginService  #login(username, password, code, uuid)  TOKEN 

        // SysLoginService.java

        @Autowired
        private TokenService tokenService;

        @Resource
        private AuthenticationManager authenticationManager;

        @Autowired
        private RedisCache redisCache;

        /**
        * 登录验证
        *
        * @param username 用户名
        * @param password 密码
        * @param code 验证码
        * @param uuid 唯一标识
        * @return 结果
        */
        public String login(String username, String password, String code, String uuid) {
        // <1> 验证图片验证码的正确性
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; // uuid 的作用,是获得对应的图片验证码
        String captcha = redisCache.getCacheObject(verifyKey); // 从 Redis 中,获得图片验证码
        redisCache.deleteObject(verifyKey); // 从 Redis 中,删除图片验证码
        if (captcha == null) { // 图片验证码不存在
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
        throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha)) { // 图片验证码不正确
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
        throw new CaptchaException();
        }
        // <2> 用户验证
        Authentication authentication;
        try {
        // 该方法会去调用 UserDetailsServiceImpl.loadUserByUsername
        authentication = authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
        // <2.1> 发生异常,说明验证不通过,记录相应的登录失败日志
        if (e instanceof BadCredentialsException) {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
        throw new UserPasswordNotMatchException();
        } else {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
        throw new CustomException(e.getMessage());
        }
        }
        // <2.2> 验证通过,记录相应的登录成功日志
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        // <3> 生成 Token
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        return tokenService.createToken(loginUser);
        }


        <1>  Redis  uuid  CaptchaController  /captchaImage 

        <2>  Spring Security  AuthenticationManager  #authenticate(UsernamePasswordAuthenticationToken authentication)  UserDetailsServiceImpl  #loadUserByUsername(String username) 7.3.1 

        <2.1> 

        <2.2> 

        7.7 



        <3>  TokenService  #createToken(LoginUser loginUser)  TOKEN 使 TOKEN 

        7.3.1 


         UserDetailsServiceImpl  Spring Security UserDetailsService  #loadUserByUsername(String username) 

        // UserDetailsServiceImpl.java

        private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

        @Autowired
        private ISysUserService userService;

        @Autowired
        private SysPermissionService permissionService;

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // <1> 查询指定用户名对应的 SysUser
        SysUser user = userService.selectUserByUserName(username);
        // <2> 各种校验
        if (StringUtils.isNull(user)) {
        log.info("登录用户:{} 不存在.", username);
        throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
        log.info("登录用户:{} 已被删除.", username);
        throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
        log.info("登录用户:{} 已被停用.", username);
        throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }

        // <3> 创建 Spring Security UserDetails 用户明细
        return createLoginUser(user);
        }

        public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user, permissionService.getMenuPermission(user));
        }

        • <1> 处,调用 ISysUserService 的 #selectUserByUserName(String userName) 方法,查询指定用户名对应的 SysUser 。代码如下:

          // SysUserServiceImpl.java
          @Autowired
          private SysUserMapper userMapper;

          @Override
          public SysUser selectUserByUserName(String userName) {
          return userMapper.selectUserByUserName(userName);
          }

          // SysUserMapper.XML
          <sql id="selectUserVo">
          select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
          d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
          r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
          from sys_user u
          left join sys_dept d on u.dept_id = d.dept_id
          left join sys_user_role ur on u.user_id = ur.user_id
          left join sys_role r on r.role_id = ur.role_id
          </sql>

          <select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
          <include refid="selectUserVo"/>
          where u.user_name = #{userName}
          </select>


           sys_user  sys_deptsys_user_rolesys_role  username  SysUser 

           SysUserResult    SysUser 




          <2>  UsernameNotFoundException  BaseException 



          <3>  SysPermissionService  #getMenuPermission(SysUser user)  SysRoleMenu 
          // SysPermissionService.java
          @Autowired
          private ISysMenuService menuService;

          public Set<String> getMenuPermission(SysUser user) {
          Set<String> roles = new HashSet<String>();
          // 管理员拥有所有权限
          if (user.isAdmin()) {
          roles.add("*:*:*"); // 所有模块
          } else {
          // 读取
          roles.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
          }
          return roles;
          }

          // SysMenuServiceImpl.java
          @Autowired
          private SysMenuMapper menuMapper;

          @Override
          public Set<String> selectMenuPermsByUserId(Long userId) {
          // 读取 SysMenu 的权限标识数组
          List<String> perms = menuMapper.selectMenuPermsByUserId(userId);
          // 逐个,按照“逗号”分隔
          Set<String> permsSet = new HashSet<>();
          for (String perm : perms) {
          if (StringUtils.isNotEmpty(perm)) {
          permsSet.addAll(Arrays.asList(perm.trim().split(",")));
          }
          }
          return permsSet;
          }

          // SysMenuMapper.xml
          <select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
          select distinct m.perms
          from sys_menu m
          left join sys_role_menu rm on m.menu_id = rm.menu_id
          left join sys_user_role ur on rm.role_id = ur.role_id
          where ur.user_id = #{userId}
          </select>




           SysUser  *:*:* 

           sys_menu  sys_role_menusys_user_role  SysUser  SysMenu 使 ","  SysMenu 




           LoginUser  Spring Security UserDetails 

          // LoginUser.java

          public class LoginUser implements UserDetails {

          private static final long serialVersionUID = 1L;

          /** 用户唯一标识 */
          private String token;

          /** 登录时间 */
          private Long loginTime;

          /** 过期时间 */
          private Long expireTime;

          /** 登录IP地址 */
          private String ipaddr;

          /** 登录地点 */
          private String loginLocation;

          /** 浏览器类型 */
          private String browser;

          /** 操作系统 */
          private String os;

          /** 权限列表 */
          private Set<String> permissions;

          /** 用户信息 */
          private SysUser user;

          // ...省略 set/get 方法,以及各种实现方法

          }

          7.3.2 创建认证 Token

          TokenService 中,定义了 #createToken(LoginUser loginUser) 方法,给认证通过的用户,生成其对应的认证 Token 。代码如下:

          // TokenService.java

          /**
          * 创建令牌
          *
          * @param loginUser 用户信息
          * @return 令牌
          */
          public String createToken(LoginUser loginUser) {
          // <1> 设置 LoginUser 的用户唯一标识。注意,这里虽然变量名叫 token ,其实不是身份认证的 Token
          String token = IdUtils.fastUUID();
          loginUser.setToken(token);
          // <2> 设置用户终端相关的信息,包括 IP、城市、浏览器、操作系统
          setUserAgent(loginUser);

          // <3> 记录缓存
          refreshToken(loginUser);

          // <4> 生成 JWT 的 Token
          Map<String, Object> claims = new HashMap<>();
          claims.put(Constants.LOGIN_USER_KEY, token);
          return createToken(claims);
          }


          •  Token  LoginUser  Redis 



            <1>  LoginUser  LoginUser.token token  Token 



            <2>  #setUserAgent(LoginUser loginUser) IP
            // TokenService.java

            public void setUserAgent(LoginUser loginUser) {
            UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            loginUser.setIpaddr(ip);
            loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
            loginUser.setBrowser(userAgent.getBrowser().getName());
            loginUser.setOs(userAgent.getOperatingSystem().getName());
            }

  • <3> 处,调用 #refreshToken(LoginUser loginUser) 方法,缓存 LoginUser 到 Redis 缓存中。代码如下:

    // application.yaml
    # token配置
    token:
    # 令牌有效期(默认30分钟)
    expireTime: 30

    // Constants.java
    /**
    * 登录用户 redis key
    */
    public static final String LOGIN_TOKEN_KEY = "login_tokens:";

    // TokenService.java
    // 令牌有效期(默认30分钟)
    @Value("${token.expireTime}")
    private int expireTime;

    @Autowired
    private RedisCache redisCache;

    public void refreshToken(LoginUser loginUser) {
    loginUser.setLoginTime(System.currentTimeMillis());
    loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
    // 根据 uuid 将 loginUser 缓存
    String userKey = getTokenKey(loginUser.getToken());
    redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    private String getTokenKey(String uuid) {
    return Constants.LOGIN_TOKEN_KEY + uuid;
    }


     Redis Key  "login_tokens:" 使 Login (LoginUser.token)




    <4>  #createToken(Map<String, Object> claims)  JWT  Token 
    // application.yaml
    # token配置
    token:
    # 令牌秘钥
    secret: abcdefghijklmnopqrstuvwxyz

    // TokenService.java
    // 令牌秘钥
    @Value("${token.secret}")
    private String secret;

    private String createToken(Map<String, Object> claims) {
    return Jwts.builder()
    .setClaims(claims)
    .signWith(SignatureAlgorithm.HS512, secret).compact();
    }


     jjwt 

     JWT JSON Web Token - Web使 JSON Web Token 

     JWT  Token  LoginUser.token 

     LoginUser.token  claims  JWT  Token  JWT  Token  claims  LoginUser.token 

     JWT  Token 使 LoginUser.token  userId  secret  userId  JWT  Token  JWT  LoginUser.token  LoginUser  Redis 7.4 JwtAuthenticationTokenFilter







    7.4 JwtAuthenticationTokenFilter


     JwtAuthenticationTokenFilter  OncePerRequestFilter  Token 

    // JwtAuthenticationTokenFilter.java

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws ServletException, IOException {
    // <1> 获得当前 LoginUser
    LoginUser loginUser = tokenService.getLoginUser(request);
    // 如果存在 LoginUser ,并且未认证过
    if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
    // <2> 校验 Token 有效性
    tokenService.verifyToken(loginUser);
    // <3> 创建 UsernamePasswordAuthenticationToken 对象,设置到 SecurityContextHolder 中
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
    // <4> 继续过滤器
    chain.doFilter(request, response);
    }

    }

    • <1> 处,调用 TokenService 的 #getLoginUser(request) 方法,获得当前 LoginUser 。代码如下:

      // application.yaml
      # token配置
      token:
      # 令牌自定义标识
      header: Authorization

      // TokenService.jav
      // 令牌自定义标识
      @Value("${token.header}")
      private String header;

      /**
      * 获取用户身份信息
      *
      * @return 用户信息
      */
      public LoginUser getLoginUser(HttpServletRequest request) {
      // <1.1> 获取请求携带的令牌
      String token = getToken(request);
      if (StringUtils.isNotEmpty(token)) {
      // <1.2> 解析 JWT 的 Token
      Claims claims = parseToken(token);
      // <1.3> 从 Redis 缓存中,获得对应的 LoginUser
      String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
      String userKey = getTokenKey(uuid);
      return redisCache.getCacheObject(userKey);
      }
      return null;
      }

      private String getToken(HttpServletRequest request) {
      String token = request.getHeader(header);
      if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
      token = token.replace(Constants.TOKEN_PREFIX, "");
      }
      return token;
      }

      private Claims parseToken(String token) {
      return Jwts.parser()
      .setSigningKey(secret)
      .parseClaimsJws(token)
      .getBody();
      }


      <1.1>  #getToken(request)  "Authorization"  Token 

      <1.2>  #parseToken(token)  JWT  Token  Claims (LoginUser.token)

      <1.3>  Redis  LoginUser 




      <2>  TokenService  #verifyToken(LoginUser loginUser) 
      // TokenService.java
      protected static final long MILLIS_SECOND = 1000;
      protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
      private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

      /**
      * 验证令牌有效期,相差不足 20 分钟,自动刷新缓存
      *
      * @param loginUser 用户
      */
      public void verifyToken(LoginUser loginUser) {
      long expireTime = loginUser.getExpireTime();
      long currentTime = System.currentTimeMillis();
      // 相差不足 20 分钟,自动刷新缓存
      if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
      String token = loginUser.getToken();
      loginUser.setToken(token);
      refreshToken(loginUser);
      }
      }


       Token  LoginUser 

      20




      <3>  UsernamePasswordAuthenticationToken  SecurityContextHolder 😈  Token 



      <4> 



      RuoYi-Vue  JWT 使 JWT  Token 

      7.5 


      3. 使 Spring Security  @PreAuthorize  Spring EL  true 访

       RuoYi-Vue  @PreAuthorize 使 PermissionService 使

      // SysDictDataController.java

      @PreAuthorize("@ss.hasPermi('system:dict:list')")
      @GetMapping("/list")


       /system/dict/data/list  PermissionService  #hasPermi(String permission) 

       @ss  Spring EL  Bean 使 @ + Bean  RuoYi-Vue  PermissionService  Bean  ss

      7.5.1 


       PermissionService  #hasPermi(String permission) 

      // PermissionService.java

      /**
      * 所有权限标识
      */
      private static final String ALL_PERMISSION = "*:*:*";

      @Autowired
      private TokenService tokenService;

      /**
      * 验证用户是否具备某权限
      *
      * @param permission 权限字符串
      * @return 用户是否具备某权限
      */
      public boolean hasPermi(String permission) {
      // 如果未设置需要的权限,强制不具备。
      if (StringUtils.isEmpty(permission)) {
      return false;
      }
      // 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      // 如果不存在,或者没有任何权限,说明权限验证不通过
      if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
      return false;
      }
      // 判断是否包含权限
      return hasPermissions(loginUser.getPermissions(), permission);
      }

      /**
      * 判断是否包含权限
      *
      * @param permissions 权限列表
      * @param permission 权限字符串
      * @return 用户是否具备某权限
      */
      private boolean hasPermissions(Set<String> permissions, String permission) {
      return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
      }





       PermissionService  #lacksPermi(String permission) 

      // PermissionService.java

      /**
      * 验证用户是否不具备某权限,与 hasPermi逻辑相反
      *
      * @param permission 权限字符串
      * @return 用户是否不具备某权限
      */
      public boolean lacksPermi(String permission) {
      return !hasPermi(permission);
      }

      在 PermissionService 中,定义了 #hasAnyPermi(String permissions) 方法,判断当前用户是否指定的任一权限。代码如下:

      // PermissionService.java

      private static final String PERMISSION_DELIMETER = ",";

      /**
      * 验证用户是否具有以下任意一个权限
      *
      * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
      * @return 用户是否具有以下任意一个权限
      */
      public boolean hasAnyPermi(String permissions) {
      // 如果未设置需要的权限,强制不具备。
      if (StringUtils.isEmpty(permissions)) {
      return false;
      }
      // 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      // 如果不存在,或者没有任何权限,说明权限验证不通过
      if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
      return false;
      }
      // 判断是否包含指定的任一权限
      Set<String> authorities = loginUser.getPermissions();
      for (String permission : permissions.split(PERMISSION_DELIMETER)) {
      if (permission != null && hasPermissions(authorities, permission)) {
      return true;
      }
      }
      return false;
      }

      7.5.2 判断是否有角色

      在 PermissionService 中,定义了 #hasRole(String role) 方法,判断当前用户是否指定的角色。代码如下:

      // PermissionService.java

      /**
      * 判断用户是否拥有某个角色
      *
      * @param role 角色字符串
      * @return 用户是否具备某角色
      */
      public boolean hasRole(String role) {
      // 如果未设置需要的角色,强制不具备。
      if (StringUtils.isEmpty(role)) {
      return false;
      }
      // 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      // 如果不存在,或者没有任何角色,说明权限验证不通过
      if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
      return false;
      }
      // 判断是否包含指定角色
      for (SysRole sysRole : loginUser.getUser().getRoles()) {
      String roleKey = sysRole.getRoleKey();
      if (SUPER_ADMIN.contains(roleKey) // 超级管理员的特殊处理
      || roleKey.contains(StringUtils.trim(role))) {
      return true;
      }
      }
      return false;
      }





       PermissionService  #lacksRole(String role) 

      // PermissionService.java

      /**
      * 验证用户是否不具备某角色,与 isRole逻辑相反。
      *
      * @param role 角色名称
      * @return 用户是否不具备某角色
      */
      public boolean lacksRole(String role) {
      return !hasRole(role);
      }

      在 PermissionService 中,定义了 #hasAnyRoles(String roles) 方法,判断当前用户是否指定的任一角色。代码如下:

      // PermissionService.java

      private static final String ROLE_DELIMETER = ",";

      /**
      * 验证用户是否具有以下任意一个角色
      *
      * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
      * @return 用户是否具有以下任意一个角色
      */
      public boolean hasAnyRoles(String roles) {
      // 如果未设置需要的角色,强制不具备。
      if (StringUtils.isEmpty(roles)) {
      return false;
      }
      // 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      // 如果不存在,或者没有任何角色,说明权限验证不通过
      if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
      return false;
      }
      // 判断是否包含指定的任一角色
      for (String role : roles.split(ROLE_DELIMETER)) {
      if (hasRole(role)) { // 这里实现有点问题,会循环调用 hasRole 方法,重复从 Redis 中读取当前 LoginUser
      return true;
      }
      }
      return false;
      }

      7.6 各种处理器


       Ruoyi-Vue 7.6  

      7.6.1 AuthenticationEntryPointImpl


       AuthenticationEntryPointImpl  Spring Security AuthenticationEntryPoint  AuthenticationException 

      // AuthenticationEntryPointImpl.java

      // 认证失败处理类 返回未授权
      @Component
      public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {

      private static final long serialVersionUID = -8970718410437077606L;

      @Override
      public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
      // 响应认证不通过
      int code = HttpStatus.UNAUTHORIZED;
      String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
      ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
      }

      }

      • 响应认证不通过的 JSON 字符串。

      7.6.2 GlobalExceptionHandler

      GlobalExceptionHandler 中,定义了对 Spring Security 的异常处理。代码如下:

      // GlobalExceptionHandler.java

      @RestControllerAdvice
      public class GlobalExceptionHandler {

      @ExceptionHandler(AccessDeniedException.class) // 没有访问权限。使用 @PreAuthorize 校验权限不通过时,就会抛出 AccessDeniedException 异常
      public AjaxResult handleAuthorizationException(AccessDeniedException e) {
      log.error(e.getMessage());
      return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
      }

      @ExceptionHandler(AccountExpiredException.class) // 账号已过期
      public AjaxResult handleAccountExpiredException(AccountExpiredException e) {
      log.error(e.getMessage(), e);
      return AjaxResult.error(e.getMessage());
      }

      @ExceptionHandler(UsernameNotFoundException.class) // 用户名不存在
      public AjaxResult handleUsernameNotFoundException(UsernameNotFoundException e) {
      log.error(e.getMessage(), e);
      return AjaxResult.error(e.getMessage());
      }

      // ... 省略对其它的异常类的处理的方法
      }


       Spring MVC  @RestControllerAdvice + @ExceptionHandler  Spring Boot SpringMVC 5. 

      7.6.3 LogoutSuccessHandlerImpl


       LogoutSuccessHandlerImpl  Spring Security LogoutSuccessHandler 退 LoginUser  Redis 

      // LogoutSuccessHandlerImpl.java

      // 自定义退出处理类 返回成功
      @Configuration
      public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {

      @Autowired
      private TokenService tokenService;

      /**
      * 退出处理
      */
      @Override
      public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
      // <1> 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(request);
      // 如果有登录的情况下
      if (StringUtils.isNotNull(loginUser)) {
      String userName = loginUser.getUsername();
      // <2> 删除用户缓存记录
      tokenService.delLoginUser(loginUser.getToken());
      // <3> 记录用户退出日志
      AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
      }
      // <4> 响应退出成功
      ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
      }

      }



      <1>  TokenService  #getLoginUser(request)  LoginUser 



      <2>  TokenService  #delLoginUser(String token)  LoginUser  Redis 
      // TokenService.java

      public void delLoginUser(String token) {
      if (StringUtils.isNotEmpty(token)) {
      String userKey = getTokenKey(token);
      // 删除缓存
      redisCache.deleteObject(userKey);
      }
      }


    • <3> 退



      <4> 退 JSON 


      7.7 


      SysLogininfor 

      // SysLogininfor.java

      public class SysLogininfor extends BaseEntity {

      private static final long serialVersionUID = 1L;

      @Excel(name = "序号", cellType = ColumnType.NUMERIC)
      private Long infoId;

      @Excel(name = "用户账号")
      private String userName;

      @Excel(name = "登录状态", readConverterExp = "0=成功,1=失败")
      private String status;

      @Excel(name = "登录地址")
      private String ipaddr;

      @Excel(name = "登录地点")
      private String loginLocation;

      @Excel(name = "浏览器")
      private String browser;

      @Excel(name = "操作系统")
      private String os;

      @Excel(name = "提示消息")
      private String msg;

      @Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
      private Date loginTime;

      // ...省略 set/get 方法
      }

      • 每个字段比较简单,胖友自己根据注释理解下即可。

      对应表的创建 SQL 如下:

      create table sys_logininfor (
      info_id bigint(20) not null auto_increment comment '访问ID',
      user_name varchar(50) default '' comment '用户账号',
      ipaddr varchar(50) default '' comment '登录IP地址',
      login_location varchar(255) default '' comment '登录地点',
      browser varchar(50) default '' comment '浏览器类型',
      os varchar(50) default '' comment '操作系统',
      status char(1) default '0' comment '登录状态(0成功 1失败)',
      msg varchar(255) default '' comment '提示消息',
      login_time datetime comment '访问时间',
      primary key (info_id)
      ) engine=innodb auto_increment=100 comment = '系统访问记录';


       RuoYi-Vue  SysLogininfor 


       AsyncFactory#recordLogininfor(username, status, message, args)  Java TimerTask 

       AsyncManager#execute(TimerTask task) 线 OPERATE_DELAY_TIME = 10 


       API Spring  @Async 便 Spring Boot 

       RuoYi-Vue  SysOperLog 

      7.8  API 


       SysLoginController  /getInfo 

      // SysLoginController.java

      /**
      * 获取用户信息
      *
      * @return 用户信息
      */
      @GetMapping("getInfo")
      public AjaxResult getInfo() {
      // <1> 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      SysUser user = loginUser.getUser();
      // <2> 角色标识的集合
      Set<String> roles = permissionService.getRolePermission(user);
      // <3> 权限集合
      Set<String> permissions = permissionService.getMenuPermission(user);
      // <4> 返回结果
      AjaxResult ajax = AjaxResult.success();
      ajax.put("user", user);
      ajax.put("roles", roles);
      ajax.put("permissions", permissions);
      return ajax;
      }



      <1>  TokenService  #getLoginUser(request)  LoginUser 



      <2>  PermissionService  #getRolePermission(SysUser user)  LoginUser 
      // SysPermissionService.java
      @Autowired
      private ISysRoleService roleService;

      /**
      * 获取角色数据权限
      *
      * @param user 用户信息
      * @return 角色权限信息
      */
      public Set<String> getRolePermission(SysUser user) {
      Set<String> roles = new HashSet<String>();
      // 管理员拥有所有权限
      if (user.isAdmin()) { // 如果是管理员,强制添加 admin 角色
      roles.add("admin");
      } else { // 如果非管理员,进行查询
      roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
      }
      return roles;
      }

      // SysRoleServiceImpl.java

      @Autowired
      private SysRoleMapper roleMapper;

      /**
      * 根据用户ID查询权限
      *
      * @param userId 用户ID
      * @return 权限列表
      */
      @Override
      public Set<String> selectRolePermissionByUserId(Long userId) {
      // 获得 userId 拥有的 SysRole 数组
      List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);
      // 遍历 SysRole 数组,生成角色标识数组
      Set<String> permsSet = new HashSet<>();
      for (SysRole perm : perms) {
      if (StringUtils.isNotNull(perm)) {
      permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));
      }
      }
      return permsSet;
      }

      // SysRoleMapper.xml
      <sql id="selectRoleVo">
      select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope,
      r.status, r.del_flag, r.create_time, r.remark
      from sys_role r
      left join sys_user_role ur on ur.role_id = r.role_id
      left join sys_user u on u.user_id = ur.user_id
      left join sys_dept d on u.dept_id = d.dept_id
      </sql>

      <select id="selectRolePermissionByUserId" parameterType="Long" resultMap="SysRoleResult">
      <include refid="selectRoleVo"/>
      WHERE r.del_flag = '0' and ur.user_id = #{userId}
      </select>


       sys_role  sys_user_rolesys_usersys_dept  userId  SysRole 

       SysRoleResult    SysRole 




      <3>  SysPermissionService  #getMenuPermission(SysUser user)  SysRoleMenu 



      <4>  AjaxResult 



       /getInfo 

      7.9 


       SysLoginController  /getRouters 

      // SysLoginController.java

      @GetMapping("getRouters")
      public AjaxResult getRouters() {
      // 获得当前 LoginUser
      LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
      // 获得用户的 SysMenu 数组
      SysUser user = loginUser.getUser();
      List<SysMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId());
      // 构建路由 RouterVo 数组。可用于 Vue 构建管理后台的左边菜单
      return AjaxResult.success(menuService.buildMenus(menus));
      }

      • 具体的代码,比较简单,胖友自己去阅读下,嘿嘿。

       /getRouters 

      7.10 


       Controller  RuoYi-Vue 


       SysUserController 

       SysRoleController 

       SysMenuController 

      7.11 


       RuoYi-Vue  RuoYi-Vue 

      666. 




       RabbitMQ 


      Spring Security   




       Spring Security  Shiro Spring Security 便
文章目录
  1. 1. 1. 概述
  2. 2. 2. 快速入门
    1. 2.1. 2.1 引入依赖
    2. 2.2. 2.2 Application
    3. 2.3. 2.3 配置文件
    4. 2.4. 2.4 AdminController
    5. 2.5. 2.5 简单测试
  3. 3. 3. 进阶使用
    1. 3.1. 3.1 引入依赖
    2. 3.2. 3.2 示例一
      1. 3.2.1. 3.2.1 SecurityConfig
      2. 3.2.2. 3.2.2 TestController
    3. 3.3. 3.3 示例二
      1. 3.3.1. 3.3.1 SecurityConfig
      2. 3.3.2. 3.3.2 DemoController
  4. 4. 4. 整合 Spring Session
  5. 5. 5. 整合 OAuth2
  6. 6. 6. 整合 JWT
  7. 7. 7. 项目实战
    1. 7.1. 7.1 表结构
      1. 7.1.1. 7.1.1 SysUser
      2. 7.1.2. 7.1.2 SysRole
      3. 7.1.3. 7.1.3 SysUserRole
      4. 7.1.4. 7.1.4 SysMenu
      5. 7.1.5. 7.1.5 SysRoleMenu
    2. 7.2. 7.2 SecurityConfig
    3. 7.3. 7.3 登录 API 接口
      1. 7.3.1. 7.3.1 加载用户信息
      2. 7.3.2. 7.3.2 创建认证 Token
    4. 7.4. 7.4 JwtAuthenticationTokenFilter
    5. 7.5. 7.5 权限验证
      1. 7.5.1. 7.5.1 判断是否有权限
      2. 7.5.2. 7.5.2 判断是否有角色
    6. 7.6. 7.6 各种处理器
      1. 7.6.1. 7.6.1 AuthenticationEntryPointImpl
      2. 7.6.2. 7.6.2 GlobalExceptionHandler
      3. 7.6.3. 7.6.3 LogoutSuccessHandlerImpl
    7. 7.7. 7.7 登录日志
    8. 7.8. 7.8 获得用户信息 API 接口
    9. 7.9. 7.9 获取路由信息
    10. 7.10. 7.10 权限管理
    11. 7.11. 7.11 小小的建议
  8. 8. 666. 彩蛋