springsecurity初稿

news2025/1/20 5:59:30

springsecurity 课程

课程目标

  • 权限管理简介【了解】
  • 权限管理解决方案【掌握】
  • 初识Spring Security【了解】
  • Spring Security 认证配置【掌握】
  • Spring Security 鉴权配置【掌握】
  • Spring Security 底层原理【掌握】
  • Spring Security 退出操作【重点】
  • Spring Security整合JWT【重点】

一、权限管理简介

1、什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

​ 权限管理包括用户身份认证鉴权(授权)两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

2、认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

上边的流程图中需要理解以下关键对象:

  • Subject :主体

    访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;

  • Principal :身份信息

​ 是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。

  • credential :凭证信息

​ 是只有主体自己知道的安全信息,如密码、证书等。

3、授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

下图中橙色为授权流程。

授权可简单理解为who对what(which)进行How操作:

  • Who,即主体(Subject),主体需要访问系统中的资源。
  • What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
  • How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是对资源实例的权限。

主体、资源、权限关系如下图:

二、权限管理解决方案

1、基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:

上图中的判断逻辑代码可以理解为:

if(主体.hasRole("总经理角色id")){
     查询工资
}

缺点:以角色进行访问控制粒度较粗,如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断主体的角色是否是总经理或部门经理”,系统可扩展性差。

修改代码如下:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
     查询工资
}

2、基于资源的访问控制

​ RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制,比如:主体必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

上图中的判断逻辑代码可以理解为:

if(主体.hasPermission("查询工资权限标识")){
     查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也只需要将“查询工资信息权限”添加到“部门经理角色”的权限列表中,判断逻辑不用修改,系统可扩展性强。

三、Spring Security概述

1,Spring Security简介

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Spring Security 拥有以下特性:

  • 对身份验证和授权的全面且可扩展的支持
  • 防御会话固定、点击劫持,跨站请求伪造等攻击
  • 支持 Servlet API 集成
  • 支持与 Spring Web MVC 集成

Spring、Spring Boot 和 Spring Security 三者的关系如下图所示:

2、Spring Security快速入门

2.1、引入依赖

<!--springboot整合security坐标-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2、创建一个控制器

@RestController
public class HelloController {
    @GetMapping("hello")
    public String hello(){
        return "Hello Spring security";
    }
}

2.3、启动项目

访问:http://localhost:8080/hello 结果打开的是一个登录页面,其实这时候我们的请求已经被保护起来了,要想访问,需要先登录。

Spring Security 默认提供了一个用户名为 user 的用户,其密码在控制台可以找到

四、Spring Security 认证配置

1、WebSecurityConfigurerAdapter

当然还可以通过配置类的方式进行配置,创建一个配置类去继承,实现自定义用户名密码登录

/**
 * Spring Security配置类
 * 在springboot2.7 后WebSecurityConfigurerAdapter弃用了,用2.5.4
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 对密码进行加密。123 是密码明文,现在 Spring Security 强制性要求『不允许明文存储密码』。
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("tom").password(password).roles("admin");
    }
}

从 5.x 开始,强制性要求必须使用密码加密器(PasswordEncoder)对原始密码(注册密码)进行加密。因此,如果忘记指定 PasswordEncoder 会导致执行时会出现 There is no PasswordEncoder mapped for the id "null" 异常。

这是因为我们在对密码加密的时候使用到了 BCryptPasswordEncoder 对象,而 Spring Security 在对密码比对的过程中不会『自己创建』加密器,因此,需要我们在 Spring IoC 容器中配置、创建好加密器的单例对象,以供它直接使用。

所以,我们还需要在容器中配置、创建加密器的单例对象(上面那个 new 理论上可以改造成注入),修改Spring securitry配置类

/**
 * Spring Security配置类
 * 在springboot2.7后WebSecurityConfigurerAdapter弃用了,用2.5.4
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 对密码进行加密。123 是密码明文,现在 Spring Security 强制性要求『不允许明文存储密码』。
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("tom").password(password).roles("admin");
    }
       /**
     * 将PasswordEncoder注入到ioc容器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

我们获取用户名和密码都是从数据库中获取,所以用以上方式不合理,引出auth.userDetailsService() ,使用UserDetailsService来实现从数据库中查用户名和密码

Spring Security 内置的 Password Encoder 有:

加密算法名称PasswordEncoder
NOOPNoOpPasswordEncoder.getInstance()
SHA256new StandardPasswordEncoder()
BCRYPT(官方推荐)new BCryptPasswordEncoder()
LDAPnew LdapShaPasswordEncoder()
PBKDF2new Pbkdf2PasswordEncoder()
SCRYPTnew SCryptPasswordEncoder()
MD4new Md4PasswordEncoder()
MD5new MessageDigestPasswordEncoder(“MD5”)
SHA_1new MessageDigestPasswordEncoder(“SHA-1”)
SHA_256new MessageDigestPasswordEncoder(“SHA-256”)

上述 Password Encoder 中有一个『无意义』的加密器:NoOpPasswordEncoder 。它对原始密码没有做任何处理(现在也被标记为废弃)。

记得使用 @SuppressWarnings(“deprecation”) 去掉 IDE 的警告信息。

2、UserDetailsService

1、基本概念

  • AuthenticationManager

    它是 “表面上” 的做认证和鉴权比对工作的那个人,它是认证和鉴权比对工作的起点。

    ProvierderManager 是 AuthenticationManager 接口的具体实现。

  • AuthenticationProvider

    它是 “实际上” 的做认证和鉴权比对工作的那个人。从命名上很容易看出,Provider 受 ProviderManager 的管理,ProviderManager 调用 Provider 进行认证和鉴权的比对工作。

    我们最常用到 DaoAuthenticationProvider 是 AuthenticationProvider 接口的具体实现。

  • UserDetailsService

    虽然 AuthenticationProvider 负责进行用户名和密码的比对工作,但是它并不清楚用户名和密码的『标准答案』,而标准答案则是由 UserDetailsService 来提供。简单来说,UserDetailsService 负责提供标准答案 ,以供 AuthenticationProvider 使用。

  • UserDetails

    UserDetails 它是存放用户认证信息和权限信息的标准答案的 “容器” ,它也是 UserDetailService “应该” 返回的内容。

  • PasswordEncoder

    Spring Security 要求密码不能是明文,必须经过加密器加密。这样,AuthenticationProvider 在做比对时,就必须知道『当初』密码时使用哪种加密器加密的。所以,AuthenticationProvider 除了要向 UserDetailsService 『要』用户名密码的标准答案之外,它还需要知道配套的加密算法(加密器)是什么

2、用户名和密码从数据库取

​ Spring Security 要求 UserDetailsService 将用户信息的 “标准答案” 必须封装到一个 UserDetails 对象中,返回给 AuthenticationProvider 使用(做比对工作)。

我们可以直接使用 Spring Security 内置的 UserDetails 的实现类:User

  • 在service包下创建一个UserDetailsService类

    /**
     * spring security认证业务类
     */
    @Service
    public class MyUserDetailsService implements UserDetailsService {
        //为passwordEncoder注入值
        @Autowired
        private UserDao userDao;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
        
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
               //调用dao到数据库中根据username查找用户信息
            Users users = userDao.getByUserName(username);
            
             try {
                //将查找到的用户帐号与密码保存到Security的User对象中由Security进行比对
                return new User(users.getUsername(), passwordEncoder.encode(users.getPassword()),
                        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN")); //配置登录用户有哪些角色和权限,此处模拟直接写死
            }catch (Exception e){
                throw  new UsernameNotFoundException("用户"+username+"不存在");
            }
        }
    }
    
  • 修改spring security配置类

    /**
     * Spring Security配置类
     *
     *
     */
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        /**
         * 属性注入和构造注入区别
         * 1、属性注入时,Spring IOC容器“创建对象”和为对象属性赋值两件事情是分开做的。
         *    构造注入时,Spring IOC容器直接调用类的有参构造,这样“创建对象”和为对象
         *    属性赋值两件事情是一起做的
         * 2、属性注入没法表达对象创建的“先后/依赖关系”,但是构造注入可以
         *    属性注入天然能解决循环依赖问题,但是构造注入要使用@Lazy注解
         * 所以建议单例对象的必要属性用构造注入,可选属性使用属性注入
         */
        @Resource
        private MyUserDetailsService userDetailsService;
        
        public SecurityConfig(@Lazy  MyUserDetailsService myUserDetailsService) {
            this.userDetailsService = myUserDetailsService;
        }
    
        /**
         * 将PasswordEncoder注入到ioc容器
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    }
    

ProviderManager/AuthenticationProvider 在做密码密码的比对工作时,会调用 UserDetailsService 的 .loadUserByUsername() 方法,并传入『用户名』,用以查询该用户的密码和权限信息。

3、Spring Security 自带的表单认证

3.1、SpringSecurityConfig 类中的配置代码

  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();//1
        http.authorizeRequests()
                .anyRequest()
                .authenticated();   // 2
        http.csrf().disable();  // 3
    }

代码配置的链式调用连写:
	http.formLogin()
    .and()
    .authorizeRequests()
    .anyRequest().authenticated()
    .and()
    .csrf().disable();

以上配置的意思是:

#说明
1要求用户登陆时,是使用表单页面进行登陆。但是,由于我们有意/无意中没有指明登陆页面,因此,Spring Security 会使用它自己自带的一个登陆页面。
2同上,让 Spring Security 拦截所有请求。要求所有请求都必须通过认证才能放行,否则要求用户登陆。
3同上,暂且认为是固定写法。后续专项讲解。

3.2、使用自定义表单实现认证

  • 准备自定义登录页面(可以是一个纯 html 页面)

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录</title>
    </head>
    <body>
    <form action="dologin" method="post">
        <!--注意:帐号和密码的名称必须是username和password否则spring security无法识别-->
        <p>帐号:<input type="text" name="username"></p> 
        <p>密码:<input type="text" name="password"></p>
        <p><button type="submit">登录</button></p>
    </form>
    </body>
    </html>
    
  • SpringSecurityConfig 类中的配置代码

      @Override
        protected void configure(HttpSecurity http) throws Exception {             
    
            http.formLogin()
                    .loginPage("/login.html")//配置需要显示的登录页面
                    .loginProcessingUrl("/dologin") //配置登录请求路径,很from表单的 action 要对应上
                    .defaultSuccessUrl("/index") //默认登录成功后跳转地址
                    .permitAll()//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权
            
                    .and().authorizeRequests()
                    .anyRequest().authenticated();  // 除了antMatchers() 配的页面,其他都需要认证
            
            http.csrf().disable();
        }
    

五、鉴权配置

1、鉴权配置

当前用户是否有权限访问某个 URI 的相关配置也是写在 configure(HttpSecurity http) 方法中。

.antMatchers() 方法是一个采用 ANT 风格的 URL 匹配器。

权限表达式说明
permitAll()永远返回 true
denyAll()永远返回 false
anonymous()当前用户是匿名用户(anonymous)时返回 true
rememberMe()当前用户是 rememberMe 用户时返回 true
authentication当前用户不是匿名用户时,返回 true
fullyAuthenticated当前用户既不是匿名用户,也不是 rememberMe 用户时,返回 true
hasRole(“role”)当用户拥有指定身份时,返回 true
hasAnyRole(“role1”, “role2”, …)当用户返回指定身份中的任意一个时,返回 true
hasAuthority(“authority1”)当用于拥有指定权限时,返回 true
hasAnyAuthority(“authority1”, “authority2”)当用户拥有指定权限中的任意一个时,返回 true
hasIpAddress(“xxx.xxx.x.xxx”)发送请求的 ip 符合指定时,返回 true
principal允许直接访问主体对象,表示当前用户

hasRole():数据库用户角色必须加 ROLE_ 前缀,而用hasRole() security会自动加上ROLE_ 前缀,自己不能加上ROLE_ 前缀,例如

AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin")
对上
hasRole("admin")

hasAuthority() 数据库角色名称和方法内容一致 例如:

AuthorityUtils.commaSeparatedStringToAuthorityList("admin")
对上
hasAuthority("admin")
语法:
  http.authorizeRequests()
    .antMatchers("/user/insert").hasAuthority("user:insert")
    .antMatchers("/user/modify").hasAuthority("user:modify")
    .antMatchers("/user/delete").hasAuthority("user:delete")
    .antMatchers("/user/query").hasAuthority("user:query")
    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
    .antMatchers("/user-can-do").hasRole("USER") //
    .antMatchers("/admin-can-do").hasRole("ADMIN") // 同上
    .antMatchers("/all-can-do").permitAll()
    .anyRequest().authenticated(); // 除了上面的权限以外的,都必须登录才能访问

提示:本质上 .hasRole("xxx").hasAuthority("xxx") 并没有太大区别,但是,.hashRole() 在做比对时,会在里面内容前面拼上 ROLE_ 。所以,确保你的 Role 的『标准答案』是以 Role_ 开头

在使用hasRole() 和 hasAnyRole() 时候,设置角色时候要加ROLE_ 前缀

 return new User(users.getUsername(), passwordEncoder.encode(users.getPassword()),
                    AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin")); 

没有权限跳转到自定义页面

   http.exceptionHandling().accessDeniedPage("/error");  //没有权限跳转到自定义页面
        http.formLogin()
                .loginPage("/hello")//配置需要显示的登录页面
                .loginProcessingUrl("/dologin") //配置登录请求路径
                .defaultSuccessUrl("/index")
                .permitAll()//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权

                .and().authorizeRequests()
                .antMatchers("/","/hello").permitAll()// 设置哪些路劲不需要登录,能直接当问
                .antMatchers("/toupdate").hasAuthority("asd")
                .anyRequest().authenticated();  // 除了antMatchers() 配的页面,其他都需要认证

        http.csrf().disable();

2、使用注解实

在实际的使用过程中用户的鉴权并不是通过置来写的而是通过注解来进行,Spring Security 默认是禁用注解的。

要想开启注解功能需要在配置类上加入 @EnableGlobalMethodSecurity注解来判断用户对某个控制层的方法是否具有访问权限。

注解就是用来替换springSecurity配置类中的http.authorizeRequests()配置

Spring Security 支持三套注解:

注解类型注解
jsr250 注解@DenyAll、@PermitAll、@RolesAllowed
secured 注解@Secured
prePost 注解@PreAuthorize、@PostAuthorize

使用什么注解在@EnableGlobalMethodSecurity开启,默认是关闭的,例如

@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled  = true,securedEnabled=true) //开启jsr250和secured注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
  • secured 注解

@Secured 注解是 jsr250 标准出现之前,Spring Security 框架自己定义的注解。

@Secured 标注于方法上,表示只有具有它所指定的角色的用户才可以调用该方法。如果当前用户不具备所要求的角色,那么,将会抛出 AccessDenied 异常,注解和配置类都要加上ROLE_ 前缀

@RestController
public class UserController {
    @Secured({"ROLE_USER","ROLE_ADMIN"}) // 这里加前缀 ROLE_,
    @RequestMapping("/all-can-do")
    public String show7(){
        return "all-can-do";
    }

    @Secured("ROLE_USER") 
    @RequestMapping("/admin-can-do")
    public String show6(){
        return "admin-can-do";
    }
}

配置类

   return new User(users.getUsername(), passwordEncoder.encode(users.getPassword()),
                    AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); //配置登录用户有哪些角色和权限
  • JSR-250 注解

    @DenyAll: 所有用户都不可以访问

    @PermitAll:所有用户都可以访问

​ @RolesAllowed:用法同@Secured差不多,区别是注解上ROLE_ 可加可不加,但是配置类上必须加ROLE_ 前缀

  • prePost 注解

    @PreAuthorize可以用来控制一个方法是否能够被调用。

    
    @PreAuthorize(hasRole(ROLE_ADMIN))
    publicvoid addUser(User user) {
    System.out.println(“addUser…” + user);
    
    }
    
    @PreAuthorize(hasRole(ROLE_USER) or hasRole(ROLE_ADMIN))
    public User find(int id) {
    System.out.println(“find user by id…” + id);
    returnnull;
    
    }
    

    @PostAuthorize在方法调用完之后进行权限检查。

   @PostAuthorize(“returnObject.id%2==0)
    public User find(int id) {
    User user = new User();
    user.setId(id);
    return user;

}

如果返回值的id是偶数则表示校验通过,否则表示校验失败,将抛出AccessDeniedException

实际开发中最常用的写法 使用 @PreAuthorize(“hasRole(‘admin’)”)

    @RequestMapping("insert")
    @PreAuthorize("hasAnyAuthority('admin')")
    public String insert(){
        return "insert";
    }

3、 登录返回处理

​ 在某些前后端完全分离,仅靠 JSON 完成所有交互的系统中,一般会在登陆成功时返回一段 JSON 数据,告知前端,登陆成功与否。在这里,可以通过 .successHandler 方法和 .failureHandler 方法指定『认证通过』之后和『认证未通过』之后的处理逻辑。

<dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>2.0.12</version>
</dependency>
  • 创建SimpleAuthenticationSuccessHandler和SimpleAuthenticationFailureHandler类来处理登录成功和失败的业务

    /**
     * 认证成功的处理器类
     */
    @Component
    public class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                      HttpServletResponse httpServletResponse,
                                      Authentication authentication) throws IOException, ServletException {
    
             // authentication 对象携带了当前登陆用户名等相关信息
            //User user = (User) authentication.getPrincipal();
            httpServletResponse.setContentType("application/json;charset=UTF-8");
    		ResponseResult<Void> ok = new ResponseResult<>(200, "登录成功");
            
            //使用jacksong将对象序列化为josn字符串
            ObjectMapper mapper=new ObjectMapper();
            String json=mapper.writeValueAsString(ok);
            //输出json字符串到客户端
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.print(json);
            printWriter.flush();
            printWriter.close();
        }
    }
    
    /**
     * 登录失败处理器
     */
    public class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request,
                                            HttpServletResponse response,
                                            AuthenticationException exception) throws IOException, ServletException {
            
            //响应请求转码
            response.setContentType("application/json;charset=UTF-8");
            System.out.println(exception);
            //exception就是登录失败时的异常对象
            ResponseResult<Void> responseResult=null;
            if(exception instanceof InternalAuthenticationServiceException){
                responseResult=new ResponseResult<>(2001, "用户名错误!");
            }
            if(exception instanceof  BadCredentialsException){
                responseResult=new ResponseResult<>(2002, "密码错误!");
            }
    
            //使用jacksong将对象序列化为josn字符串
            ObjectMapper mapper=new ObjectMapper();
            String json=mapper.writeValueAsString(responseResult);
            //输出json字符串到客户端
            PrintWriter printWriter =response.getWriter();
            printWriter.print(json);
            printWriter.flush();
            printWriter.close();
        }
    }
    
  • 修改spring security配置类

    @Resource
    private SimpleAuthenticationSuccessHandler simpleAuthenticationSuccessHandler;
    
    	@Override
        protected void configure(HttpSecurity http) throws Exception {
            /**
             * 资源权限相关配置
             * 配置哪些路径需要什么权限才能访问
             */
            http.authorizeRequests()
                    .antMatchers("/user/insert").hasAuthority("user:insert")
                    .antMatchers("/user/modify").hasAuthority("user:modify")
                    .antMatchers("/user/delete").hasAuthority("user:delete")
                    .antMatchers("/user/query").hasAuthority("user:query")
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/user-can-do").hasRole("USER") // 这里本质上应该是 ROLE_USER,但是 ROLE_ 要移除。不过你所提供的标准答案中,又必须要有 ROLE_ !
                    .antMatchers("/admin-can-do").hasRole("ADMIN") // 同上
                    .antMatchers("/all-can-do").permitAll()
                    .anyRequest().authenticated(); // 除了上面的权限以外的,都必须登录才能访问
    
            //登录相关配置
            http.formLogin()
                    .loginPage("/login.html")//配置需要显示的登录页面
                    .loginProcessingUrl("/dologin") //告诉spring security登录页面发送这个请求时spring secruity就做登录认证
                    .successHandler(new simpleAuthenticationSuccessHandle())//配置登录成功后的处理器
                    .failureHandler(new SimpleAuthenticationFailureHandler())//配置登录失败后的处理器
                    .permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权
    
            http.csrf().disable();
        }
    

4:鉴权的异常处理

Spring Security 的认证工作是由 FilterSecurityInterceptor 处理的。FilterSecurityInterceptor 会抛出 2 种异常:

  • 在用户 “该登录而未登录” 时,抛出 AuthenticationException 异常;默认情况下,抛出 AuthenticationException 异常时,Spring Security 返回 401 错误:未授权(Unauthorized)。
  • 在用户 “权限不够” 时,抛出 AccessDeniedException 异常。默认情况下,抛出 AccessDeniedException 异常时,Spring Security 返回 403 错误:被拒绝(Forbidden)访问

在 Spring Security 配置中可以通过 http.exceptionHandling() 配置方法用来自定义鉴权环节的异常处理。配置风格如下:

http.exceptionHandling()
    .authenticationEntryPoint(...)
    .accessDeniedHandler(...);

其中:

  • AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常;
  • AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常。

示例:

  • 创建认证异常处理器

    /**
     * 认证异常处理器
     */
    public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
         @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException {
    
            ResponseResult<Void> responseResult=new ResponseResult<>(4005, "未登录,请先登录");
    
            response.setStatus(HttpServletResponse.SC_OK);
            response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            ObjectMapper objectMapper = new ObjectMapper();
            String resBody = objectMapper.writeValueAsString(responseResult);
            PrintWriter printWriter = response.getWriter();
            printWriter.print(resBody);
            printWriter.flush();
            printWriter.close();
    
        }
    }
    
  • 创建鉴权异常处理器

    /**
     *鉴权异常处理器
     */
    public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
       @Override
        public void handle(HttpServletRequest request,
                           HttpServletResponse response,
                           AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
    
            ResponseResult<Void> responseResult=new ResponseResult<>(403, "无此权限,请联系管理员");
    
            response.setStatus(HttpServletResponse.SC_OK);
            response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            ObjectMapper objectMapper = new ObjectMapper();
            String resBody = objectMapper.writeValueAsString(responseResult);
            PrintWriter printWriter = response.getWriter();
            printWriter.print(resBody);
            printWriter.flush();
            printWriter.close();
        }
    }
    
  • 修改SpringSecurity配置类

     @Override
        protected void configure(HttpSecurity http) throws Exception {
            /**
             * 资源权限相关配置
             * 配置哪些路径需要什么权限才能访问
             */
            http.authorizeRequests()
                    .anyRequest().authenticated(); // 除了上面的权限以外的,都必须登录才能访问
    
            //登录相关配置
            http.formLogin()
             .successHandler(simpleAuthenticationSuccessHandler)//配置登录成功后的处理器
             .failureHandler(new SimpleAuthenticationFailureHandler())//配置登录失败后的处理器
             .permitAll();//这句配置很重要,新手容易忘记
    
            //认证和鉴权异常配置
            http.exceptionHandling()
                    .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())//认证异常处理器
                    .accessDeniedHandler(new SimpleAccessDeniedHandler());//鉴权异常处理器
            http.csrf().disable();
        }
    

六、springsecurity 底层原理

1、Servlet Filter 链

我们先来看下最基础的 Servlet Filter 体系,在 Servlet Filter 体系中客户端发起一个请求过程是经过 0 到 N 个 Filter 然后交给 Servlet 处理。

在这里插入图片描述

Filter 不但可以修改 HttpServletRequest 和 HttpServletResponse ,可以让我们在请求响应的前后做一些事情,甚至可以终止过滤器链 FilterChain 的传递。

注意:为了形象化『链』,一般的图形图像中将 Filter Chain 都是画成『前后』的关系,但是实质上,Filter 和 Filter 之间是嵌套的『内外』的关系。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { 
    // 请求被servlet 处理前 
    if (condition) { 
        // 根据条件来进入下一个过滤器 
        chain.doFilter(request, response); 
    }
    // 请求被执行完毕后处理一些事情 
}

由于 Filter 仅影响下游 Filters 和 Servlet ,因此每个 Filter 调用的顺序非常重要。

2、Spring Security 接入 Servlet Filter

在这里插入图片描述

从上图我们可以看出 Spring Security 以一个单 Servlet Filter:FilterChainProxy 存在于整个过滤器链中,而这个 FilterChainProxy 实际内部代理着众多的 Spring Security Filter 。

提示:上图中的 FilterChainProxy 和 Spring Security Filter 的关系,有点类似 Spring MVC 中的 DispacherServlet 和 Controller 之间的关系。

七、Spring Security 退出操作

Spring Security中发送了logout请求成功后会自动跳转到默认的login.html页面。在前后端分离的项目中,所有的页面跳转都是由前端控制的,服务器端只需要返回一个json的状态码即可

1、退出成功后的操作

  • 创建登录成功后的处理器类(过滤器)

    /**
     * 退出成功后的处理器
     */
    public class SimpLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request,
                                    HttpServletResponse response,
                                    Authentication authentication) throws IOException, ServletException {
            //设置响应数据的编码格式
            response.setContentType("application/json;charset=UTF-8");
    
            //将ResponseResult转为json
            ObjectMapper mapper=new ObjectMapper();
            String json= mapper.writeValueAsString(new ResponseResult<>(200, "退出成功"));//JackSon中将对象序列		化为json字符串的方法
    
            PrintWriter out= response.getWriter();
            out.write(json);
            out.close();
        }
    }
    
  • 修改Spring Security配置类

     /**
         * 修改Spring Security默认的过滤器链【重要】
         * 在spring security中凡是涉及到登录,鉴权的异常,只能使用security提供的过滤器解决,不能使用自己写的全局异常处理类
         * 修改security默认的行为
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
           ...
    
            //退出成功后的处理器
            http.logout().logoutSuccessHandler(new SimpLogoutSuccessHandler());
    
           ...
        }
    
  • 若是前后端分离的项目关闭session

       //前后端项目中要禁用掉session
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    

八、Spring Security整合JWT

为了在前后端分离项目中使用 JWT ,我们需要达到 2 个目标:

  • 在用户登录认证成功后,需要返回一个含有 JWT token 的 json 串。

  • 在用户发起的请求中,如果携带了正确合法的 JWT token ,后台需要放行,运行它对当前 URI 的访问

在spring security项目中添加nimbus坐标即可

  <!--nimbus坐标-->
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.11.1</version>
</dependency>

1、返回 JWT token

Spring Security 中的登录认证功能是由 UsernamePasswordAuthenticationFilter 完成的,默认情况下,在登陆成功后,接下来就是页面跳转,显示你原本想要访问的 URI( 或 / ),现在,我们需要返回 JSON(其中还要包含 JWT token )。

Spring Security 支持通过实现 AuthenticationSuccessHandler 接口,来自定义在登陆成功之后你所要做的事情(之前有讲过这部分内容):

http.formLogin()
    .successHandler(new JWTAuthenticationSuccessHandler());

当用户登录成功后,Spring security返回一个jwt的token给客户端,所以要在Spring security登录成功的过滤器中实现jwt token的生成代码。

  • 创建spring security项目并添加nimbus坐标

    关键坐标如下:

    <?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">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>cn.woniu</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.5.4</version>
        </parent>
    
        <dependencies>
            <!--springmvc启动器-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!--测试启动器-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
    
            <!--springboot整合security坐标-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            <!--nimbus坐标-->
            <dependency>
                <groupId>com.nimbusds</groupId>
                <artifactId>nimbus-jose-jwt</artifactId>
                <version>9.11.1</version>
            </dependency>
        </dependencies>
    </project>
    

    配置认证UserDetailsService

    /**
     * spring security认证业务类
     */
    @Service
    public class LoginUserDetailsService implements UserDetailsService {
        @Resource
        private UserDao userDao;
        //为passwordEncoder注入值
        @Resource
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //调用dao到数据库中根据username查找用户信息
            Users users = userDao.getByUserName(username);
            try {
                //将查找到的用户帐号与密码保存到Security的User对象中由Security进行比对
                return new User(users.getUsername(), users.getPassword(),
                        AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,user:insert,user:delete")); //配置登录用户有哪些角色和权限,此处模拟直接写死
            }catch (Exception e){
                throw  new UsernameNotFoundException("用户"+username+"不存在");
            }
        }
    }
    

    spring security配置类

    /**
     * Spring Security配置类
     */
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true) //开启spring security注解
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Resource
        private final LoginUserDetailsService userDetailsService;
        public SecurityConfig(@Lazy LoginUserDetailsService myUserDetailsService) {
            this.userDetailsService = myUserDetailsService;
        }
    
        /**
         * 将PasswordEncoder注入到ioc容器
         *
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 认证
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        /**
         * 配置认证过滤器链
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //资源权限相关配置
            http.authorizeRequests().anyRequest().authenticated();
    
            //登录相关配置
            http.formLogin().permitAll();
            //认证和鉴权异常配置
            http.exceptionHandling();
            
             //前后端项目中要禁用掉session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            http.csrf().disable();
        }
    }
    
  • 创建jwt工具类

    /**
     * jwt工具类
     */
    public class JwtUtils {
        //使用uuid生成密钥
        private static final String secret= UUID.randomUUID().toString();
        //用户数据的key
        private static final String usernameKey="usernameKey";
    
        /**
         * 生成token
         * @param username 用户名
         * @param  authorityes 用户权限
         * @return
         */
        public static String createJwtToken(String username) throws Exception {
            //创建头部对象
            JWSHeader jwsHeader =
                    new JWSHeader.Builder(JWSAlgorithm.HS256)       // 加密算法
                            .type(JOSEObjectType.JWT) // 静态常量
                            .build();
    
            //创建载荷
            Map<String,Object> map=new HashMap<String,Object>();
            map.put(usernameKey, username);
            Payload payload= new Payload(map);
    
            //创建签名器
            JWSSigner jwsSigner = new MACSigner(secret);//密钥
    
            //创建签名
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
            jwsObject.sign(jwsSigner);//再+签名部分
    
            //生成token字符串
            return jwsObject.serialize();
        }
    
        /**
         * 验证jwt token是否合法
         * @param jwtStr
         * @return
         */
        @SneakyThrows
        public static boolean verify(String jwtStr) {
            JWSObject jwsObject=JWSObject.parse(jwtStr);
            JWSVerifier jwsVerifier=new MACVerifier(secret);
            return jwsObject.verify(jwsVerifier);
        }
    
        /**
         * 从token中解析出用户名
         * @param jwtStr
         * @return
         */
        @SneakyThrows
        public static String getUserNameFormJwt(String jwtStr){
            JWSObject jwsObject=JWSObject.parse(jwtStr);
            Map<String,Object> map=jwsObject.getPayload().toJSONObject();
            return (String) map.get(usernameKey);
        }
    }
    
  • 创建spring security登录成功后的处理器

    /**
     * Spring Security登录成功后的处理器
     */
    @Component
    public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        //jackson对象
        private final ObjectMapper mapper=new ObjectMapper();
         @Autowired
        private StringRedisTemplate redisMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                            HttpServletResponse httpServletResponse,
                                            Authentication authentication) throws IOException, ServletException {
            // 获得登录成功后保存在Spring Security中的用户信息
            User user =(User)authentication.getPrincipal();
            httpServletResponse.setContentType("application/json;charset=UTF-8");
    
            ResponseResult<Object> responseResult=null;
            PrintWriter out=httpServletResponse.getWriter();
            try {
                 //调用生成token方法
                String token= JwtUtils.createJwtToken(user.getUsername());
                 //保存token到redis中,有效期30分钟
                ValueOperations<String, String> operations = redisTemplate.opsForValue();
                operations.set("login:token:" + account, token, 30, TimeUnit.MINUTES);
                
                //使用统一消息返回类封装数据
                out.write(mapper.writeValueAsString(new ResponseResult<Object>(200,"登录成功",token)));
            } catch (Exception e) {
                out.write(mapper.writeValueAsString(new ResponseResult<Object>(6001,"token异常")));
            }finally {
                out.flush();
                out.close();
            }
        }
    }
    
  • 修改spring security配置类

       /**
         * 配置认证过滤器链
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //资源权限相关配置
            http.authorizeRequests().anyRequest().authenticated();
    
            //登录相关配置
            http.formLogin()
                    .successHandler(new JWTAuthenticationSuccessHandler());//配置登录成功后的处理器
    
            //认证和鉴权异常配置
            http.exceptionHandling();
            http.csrf().disable();
        }
    

2、放行携带 JWT Token 的请求

放行请求的关键在于 FilterSecurityInterceptor 不要抛异常,而 FilterSecurityInterceptor 不抛异常则需要满足两点:

  • Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且是已认证状态。

  • Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

所以实现思路的关键在于:在 FilterSecurityInterceptor 之前( 废话 )要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,我们要实现一个 Filter :

/**
 * jwt请求过滤器
 */
@Component
public class JwtFilter  extends OncePerRequestFilter {
    @Autowired
    private LoginUserDetailsService userDetailsService;
     @Autowired
    private StringRedisTemplate redisMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 1、如果Spring Security中已经有有一个Authentication Token,那么这个请求就不归JwtFilter管,直接放行
        //获得Spring Security上下文中的Authentication对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        //如果Spring Security中没有Authentication Token,这个请求就需要被JwtFilter管理,
        if (authentication != null) {
           //Security Context 中已有 Authentication Token说明是登录请求
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //2、请求中没有token信息,不用向Spring Security的上下文中保存Authentication Token
        //获得请求中的token信息,jwtToken为请求头中携带的token名称
        String jwtStr = httpServletRequest.getHeader("jwtToken");
        if(StringUtils.isEmpty(jwtStr)){
            //请求中没有token
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //3、请求中有token信息,但是token非法,不用向Spring Security的上下文中保存Authentication Token
        if(!JwtUtils.verify(jwtStr)){
            //有token,但非法
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        //4、请求中有token信息,且token合法,就从请求中获取携带的token,
        // 然后转为Spring Security的Authentication Token(需要带有用户所有的权限),保存在Spring Security上下文中
        //获得token中的username
        String username=JwtUtils.getUserNameFormJwt(jwtStr);

         //对token进行续期
        if(redisMapper.hasKey("login:token:"+account)){
            String jwtToken=redisMapper.opsForValue().get("login:token:"+account);
            //判断是不是同一个token
            if(token.equals(jwtToken)){
                //获得键的过期时间
               //redisMapper.getExpire("login:token:"+account);

                redisMapper.opsForValue().set("login:token:"+account,jwtStr,30, TimeUnit.MINUTES);
            }
        }
        
        //调用登录查询方法,获得Spring Security的UserDetails认证对象
        UserDetails userDetails=userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails.getUsername(),userDetails.getPassword(),userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest, httpServletResponse);//放行到下一个过滤器
    }
}

虽然 Spring Security Filter Chain 对过滤器没有特殊要求,只要实现了 Filter 接口即可,但是在 Spring 体系中,推荐使用 OncePerRequestFilter 来实现,它可以确保一次请求只会通过一次该过滤器(而普通的 Filter 并不能保证这一点)。

修改spring Security配置类:

 /**
     * 配置认证过滤器链
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //资源权限相关配置
        http.authorizeRequests().anyRequest().authenticated();

        //登录相关配置
        http.formLogin()
                .successHandler(new JWTAuthenticationSuccessHandler())//配置登录成功后的处理器
                .failureHandler(new SimpleAuthenticationFailureHandler());//配置登录失败后的处理器

        //认证和鉴权异常配置
        http.exceptionHandling();
        http.csrf().disable();

        //前后端项目中要禁用掉session
       http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        //将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前
        http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }

五 项目中的MyUserDetailsService

上面的UserDetailsService认证是写死的,我把我学习的项目中的MyUserDetailsService粘过来

数据库

表1结构

在这里插入图片描述

表1内容

在这里插入图片描述

表2结构

在这里插入图片描述

表2内容

在这里插入图片描述

MyUserDetailService

import com.woniu.pc.entity.PcAdmin;
import com.woniu.pc.entity.UserInfo;
import com.woniu.pc.mapper.PcAdminMapper;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private PcAdminMapper pcAdminMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        PcAdmin pcAdmin = pcAdminMapper.queryPcAdminByAccount(username);
//        System.out.println(pcAdmin);
        if (pcAdmin != null){
            String encode = passwordEncoder.encode(pcAdmin.getPassword());

            List<String> auths = pcAdmin.getAuths();
            String anthStr = String.join( ",",auths);//以什么分隔符,把集合元素组装成字符串

            return new UserInfo(pcAdmin.getUsername(),encode, AuthorityUtils
                    .commaSeparatedStringToAuthorityList(anthStr),pcAdmin.getId(),pcAdmin.getIdentityId(),pcAdmin.getStatus());
//           UserInfo userInfo= new UserInfo(pcAdmin.getUsername(),encode, AuthorityUtils
//                    .commaSeparatedStringToAuthorityList(anthStr),pcAdmin.getId(),pcAdmin.getIdentityId());
//           return new ResponseDate<>().ok(userInfo);
        }else {
            throw  new UsernameNotFoundException("用户名或密码有误");
        }
    }
}

UserInfo

注意User的路径org.springframework.security.core.userdetails.User;

package com.woniu.pc.entity;


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

public class UserInfo extends User {

    private Integer userId;
    private Integer identityId;
    private Integer status;

    public UserInfo(String username, String password, Collection<? extends GrantedAuthority> authorities, Integer userId, Integer identityId, Integer status) {
        super(username, password, authorities);
        this.userId = userId;
        this.identityId = identityId;
        this.status = status;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public Integer getIdentityId() {
        return identityId;
    }

    public void setIdentityId(Integer identityId) {
        this.identityId = identityId;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}

SQL语句

    <select id="queryPcAdminByAccount" parameterType="string" resultMap="pcAdminMap">
        SELECT tpad.id,
               tpad.username,
               tpad.password,
               tpad.identityId,
               tpa.authCode,
               tpad.status
        FROM t_user tpad
                 LEFT JOIN t_pc_admin_auth tpaa ON tpad.id = tpaa.employee_id
                 LEFT JOIN t_pc_auth tpa ON tpaa.auth_id = tpa.id
        WHERE tpad.username = #{username}
    </select>
    
    <resultMap id="pcAdminMap" type="com.woniu.pc.entity.PcAdmin">
        <id column="id" property="id"></id>
        <result column="account" property="account"></result>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="status" property="status"></result>
        <result column="createtime" property="createTime"></result>
        <result column="identityId" property="identityId"></result>

        <collection property="auths" ofType="java.lang.String">
            <result column="authCode"></result>
        </collection>
    </resultMap>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

参考RabbitMQ实现一个消息队列

文章目录 前言小小消息管家1.项目介绍2. 需求分析2.1 API2.2 消息应答2.3 网络通信协议设计 3. 开发环境4. 项目结构介绍4.1 配置信息 5. 项目演示 前言 消息队列的本质就是阻塞队列&#xff0c;它的最大用途就是用来实现生产者消费者模型&#xff0c;从而实现解耦合以及削峰填…

有什么好用的PNG素材网站吗?看看这6个

高品质PNG素材无疑能提升网站的质量&#xff0c;给用户带来更美好的使用体验&#xff0c;今天本文会与大家分享6个好用的PNG素材网站&#xff0c;一起来看看吧&#xff01; 1、即时设计资源广场 即时设计资源广场集成了多种大厂素材&#xff0c;不只是PNG素材&#xff0c;还有…

试用AI生成代码工具Fauxpilot,详细安装过程

设置服务 预先说明 需要预先安装支持NVIDIA的docker,docker compose > 1.28不能再容器里运行&#xff0c;否则出现以下报错&#xff1a; rootc536ca0dbd64:/test/fauxpilot-main# ./setup.sh Checking for curl ... /usr/bin/curl Checking for zstd ... /opt/conda/bin…

Java-认识String

目录 一、String概念及创建 1.1 String概念 1.2 String的创建 二、String常用方法 2.1 String对象的比较 2.2 字符串查找 2.3 转化 2.4 字符串替换 2.5 字符串拆分 2.6字符串的截取 2.7 其他操作方法 2.8 字符串修改 三、面试题 一、String概念及创建 1.1 String概念 Java中…

PVE虚拟化平台之安装openKylin开源操作系统

PVE虚拟化平台之安装openKylin开源操作系统 一、openKylin介绍1.1 openKylin简介1.2 openKylin特性 二、下载openKylin系统镜像2.1 官方网址2.2 下载openKylin系统镜像 三、上传镜像到PVE存储3.1 检查PVE环境3.2 上传镜像 四、创建虚拟机4.1 设置虚拟机名称4.2 操作系统设置4.3…

【六袆 - 国际化】SpringBoot国际化Message

模拟场景校验请求参数 private void checkParam(List<ReqAppAdminDTO> req) {// 校验管理员如果已存在&#xff0c;则抛出已存在异常req.forEach(item -> {AppAdminDO appAdminDO appAdminMapper.selectByAppIdAndAdminNo(item.getAppId(), item.getAdminNo());if (O…

ubuntu上回环设备/dev/loop0占用100%清理

查看磁盘占用情况时&#xff1a; df -h/dev/loopn这些设备在Linux下被称为回环设备。 终端输入&#xff1a; sudo apt autoremove --purge snapd再次查看&#xff1a;

Android安卓实战项目(8)---自行车fitting计算软件(源码在文末)可用于比赛项目或者作业参考中

Android安卓实战项目&#xff08;8&#xff09;—自行车fitting计算软件&#xff08;源码在文末&#x1f415;&#x1f415;&#x1f415;&#xff09;可用于比赛项目或者作业参考中 【bilibili演示地址】 https://www.bilibili.com/video/BV1eu4y1B7yA/?share_sourcecopy_we…

常用抓包工具

Fiddler Fiddler 是一个很好用的抓包工具&#xff0c;可以用于抓取http/https的数据包&#xff0c;常用于Windows系统的抓包&#xff0c;它有个优势就是免费 Charles Charles是由JAVA开发的&#xff0c;可以运行在window Linux MacOS&#xff0c;但它是收费的&#xff0c;和…

大厂容器云实践之路(一)

1-华为CCE容器云实践 华为企业云 | CCE容器引擎实践 ——从IaaS到PaaS到容器集群 容器部署时代的来临 IaaS服务如日中天 2014-2015年&#xff0c;大家都在安逸的使用IaaS服务&#xff1b; 亚马逊AWS的部署能力方面比所有竞争对手…

有血有肉的PPT

1、PPT是Powerpoint缩写 2、引申的含义是Powerpoint Power(力量/能量&#xff09; Point(观点/要点) 3、用PPT做的文档是讲演稿&#xff0c;讲演的内容要有力度&#xff0c;之所以要去演讲是为了能够影响受众 4、其次演讲稿上的内容要列出要点、表明观点&#xff0c;所以一般P…

Java 并发容器和框架Fork/Join详解

目 录 一 使用场景 1 大规模数据处理 2 复杂计算 3 并行搜索 4 并行排序 二 Fork/Join框架介绍 三 Fork/Join框架模块 四 Fork/Join框架核心思想 1分治思想(Divide-and-Conquer) 2 work-stealing(工作窃取)算法 五 Fork/Join框架执行流程 1 实现原理&#xff1a; 2…

Vue3 第二节 Vue3的响应式

1.Vue3的响应式原理 2.ref函数和reactive函数的对比 3.setup注意点 一.Vue3的响应式原理 1.Vue2.x中的响应式原理 ① 实现原理 对象类型&#xff1a;通过Object.defineProperty() 对属性的读取&#xff0c;修改进行拦截&#xff08;数据劫持&#xff09;数组类型&#xf…

上位机是什么?有什么实际用途?

上位机是指控制、监测或管理下位机的计算机系统&#xff0c;也可以称为主机。它通常用于工业自动化、机器人控制、数据采集和处理等领域。在工业自动化中&#xff0c;上位机负责向下位机下发指令并获取反馈信息&#xff0c;以控制生产流程。在机器人控制中&#xff0c;上位机负…

详细教程:如何搭建废品回收小程序

废品回收是一项环保举措&#xff0c;通过回收和再利用废弃物品&#xff0c;可以减少资源浪费和环境污染。近年来&#xff0c;随着智能手机的普及&#xff0c;小程序成为了推广和运营的重要工具。本文将详细介绍如何搭建一个废品回收小程序。 1. 进入乔拓云网后台 首先&#xf…

Maven: ‘mvn‘ is not recognized as an internal or external command

下载并配置好Maven之后&#xff0c;CMD测试安装是否成功&#xff1a;mvn -v 提示&#xff1a; mvn is not recognized as an internal or external command, operable program or batch file. 检查环境变量&#xff1a; MAVEN_HOME: %MAVEN_HOME%\bin: 看上去没问题&#x…

pinctrl设备树节点映射详细分析imx_dt_node_to_map

pinctrl设备树节点映射详细分析imx_dt_node_to_map 文章目录 pinctrl设备树节点映射详细分析imx_dt_node_to_mapstruct pinctrl_mapreally_probepinctrl_bind_pinscreate_pinctrlpinctrl_dt_to_mapdt_to_map_one_configdt_remember_or_free_mappinctrl_register_map add_settin…

Linux网络服务之部署yum仓库

yum &#xff1f; yum ! 一、YUM概述1.1 yum简介1.2 yum工作原理 二、yum 配置文件2.1 yum主配置文件2.2 yum仓库设置文件2.2.1 配置文件主要格式2.2.2 软件仓库的提供方式2.2.3 日志文件 三、yum命令详解3.1 安装和升级3.2 查询3.2.1 显示可用的安装包 ----- yum list3.2.2 显…

数据结构笔记--归并排序及其拓展题(小和问题、逆序对问题)

目录 1--归并排序 2--小和问题 3--逆序对问题 1--归并排序 归并排序的核心思想&#xff1a;将一个无序的序列归并排序为一个有序的系列&#xff1b;通过递归将无序的序列二分&#xff0c;从底层开始将二分的序列归并排序为有序序列&#xff1b; #include <iostream> #…

手工测试VS自动化测试到底那个更胜一筹?

手工与自动化只是一种形式&#xff0c;真正的核心是测试用例、业务模型和测试分析。当企业的产品规模开始膨胀的时候&#xff0c;尤其是产品迭代加快是不是能及时得到测试验证支持是很重要的。这些靠手工测试是基本无法实现的&#xff0c;手工测试会严重的拖慢产品进度&#xf…