一、Spring Security入门

一、Spring security框架简介

Spring SecuritySpring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架。除了常规的Authentication(认证)和Authorization(授权)之外,Spring Security还提供了诸如ACLLDAPJAASCASOAuth等高级特性以满足复杂场景下的安全需求。

spring security的主要核心功能为 Authentication(认证)Authentication(授权),所有的架构也是基于这两个核心功能去实现的:

  • Authentication(认证):认证就是判断用户身份是否合法,例如用户名密码登录就是认证,如果一个用户拥有正确的密码,即可通过认证;
  • Authorization(授权):用户认证通过,但是每个用户的权限不同,判断用户有哪些权限以及是否有权限访问某些资源,就是授权。

而在认证和授权过程中,又会涉及到三个核心概念:

  • Principle(User):标识一个认证过的实体,在大多数场景下,在Spring Security中,一个Principal只是简单地代表一个用户,所以我当我们说一个安全实体的时候,你可以将其等同于说用户。
  • Authority(Role):可以理解为已认证的用户角色,如用户是老师角色或者是学生角色
  • Permission:用户拥有的权限,比如用户权限是只读,不能添加、修改、删除操作。

这里注意AuthorityPermission,字面上都是权限,但在Spring Security中,Authority是指用户的角色(role),用户的Authority是必须以ROLE_开头的,比如ROLE_student表示学生角色。

Spring Security中,AuthorityPermission是两个完全独立,两者并没有必然的联系,但可以通过配置进行关联。比如,同样是学生角色,拥有不同的权限。

二、项目搭建

2.1、hello spring security

  1. IDEA创建一个Spring Boot项目,依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 编写Controller

    @RestController
    public class HelloController {
        @GetMapping("/hello")
        public String hello(){
            return "hello spring security";
        }
    }
    
  3. 启动应用程序,访问地址http://localhost:8080/hello,出现身份验证输入框:

    Snipaste_2019-02-26_18-24-23.png

    这是因为SpringBoot默认的Spring Security就是生效的,此时所有的请求接口都是被保护的,需要通过登录后才能正常访问。

    Spring Security提供了一个默认的用户,用户名是user,而密码则是启动项目的时候自动生成打印在控制台日志:

    Using generated security password: b0e4f3c9-1f3b-4e2e-bcba-697e148232b7
    

2.2、关闭默认身份验证

在启动类排除

// 排除
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class Securitystudy01Application {
    public static void main(String[] args) {
        SpringApplication.run(Securitystudy01Application.class, args);
    }
}

2.3、自定义用户名和密码

上面的用户名是默认的,密码是启动时随机生成的。要自定义用户名和密码,只需要在配置文件application.properties文件中添加如下配置:

spring.security.user.name = admin       
spring.security.user.password = 123456

三、获取登录用户信息

@GetMapping("/get-user1")
public Authentication getUser1(){
    return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/get-user2")
public Authentication getUser2(Authentication authentication){
    return authentication;
}
@GetMapping("/get-user3")
public Authentication getUser3(@AuthenticationPrincipal  Authentication authentication){
    return authentication;
}
@GetMapping("/get-user4")
public UserDetails getUser4(@AuthenticationPrincipal UserDetails userDetails){
    return userDetails;
}
@GetMapping("/get-user5")   
public UserDetails getUser5(){
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    return userDetails;
}

四、基于内存用户认证

4.1、配置基于内存的用户

内存初始化认证信息,需重写WebSecurityConfigurerAdapter类中的configure(AuthenticationManagerBuilder auth)方法,通过auth对象的inMemoryAuthentication()方法指定认证信息:

// 添加注解
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加第一个用户
        auth
                // 使用内存认证
                .inMemoryAuthentication()
                // 设置用户名和密码
                .withUser("admin").password("admin123")
                // 指定角色, 空为不指定
                .roles();

        // 添加第二个用户
        auth
                // 使用内存认证
                .inMemoryAuthentication()
                // 设置用户名和密码
                .withUser("user").password("user123")
                // 指定角色, 空为不指定
                .roles();
    }
}

如果是5.x之前的版本,到这里启动就可以正常访问。但5.x的版本启动后,在登录页面输入账号进行访问时,报如下错误:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

这是因为Spring Security 5.0中新增了多种加密方式,也改变了密码的格式,需要指定加密方式。

WebSecurityConfigurerAdapter有三个方法:

  • configure(AuthenticationManagerBuilder):用于通过允许AuthenticationProvider容易地添加来建立认证机制。以下定义了内置认证与内置的用户“user”和“admin”登录。
AuthenticationManagerBuilder allows 
public void configure(AuthenticationManagerBuilder auth) {
    auth
        .inMemoryAuthentication()
        .withUser("user")
        .password("password")
        .roles("USER")
    .and()
        .withUser("admin")
        .password("password")
        .roles("ADMIN","USER");
}
  • configure(HttpSecurity):允许基于选择匹配在资源级配置基于网络的安全性。以下示例将以/admin/开头的请求限制为具有ADMIN角色的用户,并指定任何其他请求需要成功验证后才能访问:
protected void configure(HttpSecurity http) throws Exception {
http
    .authorizeUrls()
    .antMatchers("/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
}
  • configure(WebSecurity):用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。例如,以下方法将导致以/resources/开头的任何请求被忽略认证,允许匿名访问。
public void configure(WebSecurity web) throws Exception {
web
    .ignoring()
    .antMatchers("/resources/**");
}

4.2、密码加密

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // 配置加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        /**
         * 使用SpringSecurity推荐的BCryptpassword加密类,该加密过程是不可逆的,
         * 相同的明文每一次加密,加密之后的密文都是不一样的
         *
         * 如果需要其他加密方式,SpringSecurity也提供有如下(在源码类PasswordEncoderFactories中可查看):
         * encoders.put("ldap", new LdapShaPasswordEncoder());
         * encoders.put("MD4", new Md4PasswordEncoder());
         * encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
         * encoders.put("noop", NoOpPasswordEncoder.getInstance());
         * encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
         * encoders.put("scrypt", new SCryptPasswordEncoder());
         * encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
         * encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
         * encoders.put("sha256", new StandardPasswordEncoder());
         *
         * 加密方法:     PasswordEncoder.encode()
         * 比较方法(判断用户密码输入是否正确): PasswordEncoder.matches(输入未加密的密码,加密的密码)
         */
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                // 加密密码
                .withUser("admin").password(passwordEncoder().encode("admin123"))
                .roles();

        auth
                .inMemoryAuthentication()
                // 加密密码
                .withUser("user").password(passwordEncoder().encode("user123"))
                .roles();
    }
}

4.3、密码明文

@EnableWebSecurity
public class WebSecirutyConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        // 密码不加密,使用明文方式
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        };
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                // 配置密码不加密,下面两种方式都可以,不会加密密码
                // .withUser("admin").password(passwordEncoder().encode("admin123"))
                .withUser("admin").password("admin123")
                .roles();

        auth
                .inMemoryAuthentication()
                // 不会加密密码
                .withUser("user").password(passwordEncoder().encode("user123"))
                .roles();
    }
}

五、用户角色认证

5.1、为用户配置角色

为用户设置角色:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("admin").password(passwordEncoder().encode("admin123"))
                // 指定为 admin 角色
                .roles("admin");
        
        auth
                .inMemoryAuthentication()
                .withUser("user").password(passwordEncoder().encode("user123"))
                // 指定为 user 角色
                .roles("user");
    }
}

5.2、开启方法安全验证

5.2.1、开启方法安全

WebSecurityConfig中添加注解@EnableGlobalMethodSecurity开启方法安全验证,该注解有几个属性:

  1. prePostEnabled 是否启用Spring Securityprepost注解,支持例如@PreAuthorize、@PostAuthorize,..
  2. secureEnabled是否启用Spring Security@Secured注解。少用,prePostEnabled能实现相同功能
  3. jsr250Enabled是否启用 JSR-250 annotations 注解[@RolesAllowed..]。少用,prePostEnabled能实现相同功能

在一个应用程序中,可以启用多个类型的注解,但是只在接口、类、方法上设置一个注解。如:

开发中,最常用的就是prePostEnabled,所以在WebSecurityConfig进行设置为true

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法安全验证,并设置prePostEnabled为true
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}

5.2.2、方法权限设置

用到的表达式

表达式

对访问方法的权限设置,有两种方法:

  1. 在方法上配置注解:

    • @PreAuthorize:在执行方法之前进行权限校验。

      @RestController
      public class HelloController {
          @GetMapping("/hello")
          public String hello() {
              return "hello";
          }
      
          @PreAuthorize("hasAnyRole('user')")     //只能 user 角色才能访问
          @GetMapping("/user")
          public String user() {
              return "user";
          }
      
          @PreAuthorize("hasAnyRole('admin')")    // 只能 admin 角色才能访问
          @GetMapping("/admin")
          public String admin() {
              return "admin";
          }
          
          @PreAuthorize("hasAnyRole('admin','admin')")    // 拥有其中一个角色便可访问(也可用下面方法)
          @GetMapping("/admin")
          public String admin() {
              return "admin";
          }
          
          @PreAuthorize("hasAnyRole('admin') or hasAnyRole('user')")  // 拥有其中一个角色便可访问(和上面方法一样)
          @GetMapping("/userOrAdmin")
          public String user3() {
              return "userOrAdmin";
          }
      
          @PreAuthorize("hasAnyRole('admin') and hasAnyRole('user')")  // 同时拥有 admin 和 user 角色才能访问
          @GetMapping("/userAndAdmin")
          public String user2() {
              return "userAndAdmin";
          }
      

      该注解还支持Spring EL表达式,可以根据参数进行校验,如:

      • @PreAuthorize("#id < 10"):方法必须有id参数,而且参数的值必须小于10
      • @PreAuthorize("principal.username.equeals(#username)"):方法必须有username参数,而且参数的值必须是登录用户的用户名
      @RestController
      public class HelloController {
          @PreAuthorize("#id < 10")// 只有参数id小于10才能访问
          @GetMapping("/user/{id}")
          public String user3(@PathVariable("id") Integer id) {
              return "只有入参id小于10才能访问";
          }
          
          @PreAuthorize("principal.username.equals.(#username)")
          @GetMapping("/user")
          public String user3(@RequestParam("username") String username) {
              return "参数的姓名是登录的用户名才可访问";
          }
      }
      
    • @PostAuthorize:在执行方法之后进行校验。

      @RestController
      public class HelloController {
          // 查询到用户信息后,再验证用户名是否和登录用户名一致
          @PostAuthorize("returnObject.name == authentication.name")
          @GetMapping("/get-user")
          public User getUser(String name){
              return userService.getUser(name);
          }
          // 验证返回的数是否是偶数
          @PostAuthorize("returnObject % 2 == 0")
          public Integer test(){
              // ...
              return id;
          }
      }
      
    • @PreFilter:对集合类型的参数执行过滤,移除结果为false的元素

      @RestController
      public class HelloController {
          @Autowired
          private ObjectMapper objectMapper;
          
          // 指定过滤的参数,仅偶数能进入方法,如入参[[1,2,3,4,5]],则返回[2,4]
          @PreFilter(filterTarget="ids", value="filterObject%2==0")
         @PostMapping("/test")
          public void test(@RequestBody List<Integer> ids){
              return objectMapper.writeValueAsString(ids);    
          }
      }
      
    • @PostFilter

      对集合类型的返回值进行过滤,移除结果为false的元素

      @RestController
      public class HelloController {
          @PostFilter("filterObject.id%2==0")
          public List<User> findAll(){
              ...
                  return userList;
          }
      }
      
  2. 处理像上面在方法上使用注解外,还可通过Spring Security配置类WebSecurityConfig中设置

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     ...
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                 // 对请求进行验证
                    .authorizeRequests()
                 // 对所有的 /user请求 和以 /user1/开头的请求 都必须 admin角色才能访问
                    .antMatchers("/user", "/user1/**").hasAnyRole("ROLE_admin")
                 // 也可指定请求方法验证
                 .antMatchers(HttpMethod.GET, "/userList").hasAnyRole("ROLE_admin")
                     // 也可以通过正则表达式进行匹配
                    .regexMatchers("users/([^/].*?)/tokens/.*").hasAnyRole("ROLE_user")
                 // 也可以对一些不需登录访问的请求(匿名请求),如注册用户就不需要登录后访问
                 .antMatchers(HttpMethod.POST,"addUser").permitAll()
                    // 所有请求都需要登录后才能访问,这里必须放在上面所有过滤请求之后
                    .anyRequest().authenticated() 
                    .and()
                    .formLogin() // 使用默认的登录页面
                    .and()
                    .csrf().disable();// post请求关闭csrf验证,不然访问报错。生产环境需开启   
        }
    }
    

    这里的配置的请求验证优先级比注解的优先级高,且这里的角色需要加ROLE_前缀。

    像上面,直接在WebSecurityConfig中配置请求路径的权限控制,如果请求路径过多,配置的权限控制也会过多,且权限控制与Security配置文件混合一起,不方便维护,所以我们可以把权限控制单独提取出来:

    1. 新建接口PathAuthenticationProvider

      public interface PathAuthenticationProvider{ 
          void config(
              ExpressionUrlAuthorizationConfigurer<HttpSecurity>
              .ExpressionInterceptUrlRegistry authorizeRequests);
      }
      

      ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry对象即是下面的authorizeRequests()

      图片

      Ctrl + 鼠标左键点击进入源码便看到:

      图片
  1. 新建PathAuthenticationProviderImpl实现PathAuthenticationProvider接口:

    @Component
    public class PathAuthenticationProviderImpl implements PathAuthenticationProvider {
        @Override
        public void config(
            ExpressionUrlAuthorizationConfigurer<HttpSecurity>
             .ExpressionInterceptUrlRegistry authorizeRequests) {
            authorizeRequests
                // 对所有的 /user请求 和以 /user1/开头的请求 都必须 admin角色才能访问
                    .antMatchers("/user", "/user1/**").hasAnyRole("ROLE_admin")
                 // 也可指定请求方法验证
                 .antMatchers(HttpMethod.GET, "/userList").hasAnyRole("ROLE_admin")
                     // 也可以通过正则表达式进行匹配
                    .regexMatchers("users/([^/].*?)/tokens/.*").hasAnyRole("ROLE_user")
                 // 也可以对一些不需登录访问的请求,如注册用户就不需要登录后访问
                 .antMatchers(HttpMethod.POST,"addUser").permitAll()
                    // 所有请求都需要登录后才能访问,这里必须放在上面所有过滤请求之后
                    .anyRequest().authenticated();
        }
    }
    

    这里的写法和WebSecurityConfig中的写法是一样的。

  2. WebSecurityConfig配置类中,把上面的路径权限配置引入:

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        // 省略...
        
        // 注入路径权限控制配置
        @Autowired
        private PathAuthenticationProvider authProvider;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .formLogin();
                    .and()
                    .csrf().disable();
    
            // 路径权限配置
            authProvider.config(http.authorizeRequests());
        }
    }
    

    像这样,单独把路径权限抽取出来,方便单独对路径权限进行配置。这里只是详细说明了http.authorizeRequests()的路径权限抽取为单独一个类,同理,其他的配置,如http.forLogin()等也可以抽取出来。

    还有一个重要配置 access()的使用,在前面的路径权限配置时,在编辑器提示可看到:

图片

提示的配置都和这一小节开头的表达式列表一一对应:

图片

但可看到多出了两个 not()access

对于not(),没什么好说的,如下:

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
           .authorizeRequests()
           // 只要用户角色不是 ROLE_admin都可访问
           .antMatchers("/user").not().hasAnyRole("ROLE_admin")
   }

而对于 access,看源码说明:

   /**
    * Allows specifying that URLs are secured by an arbitrary expression
    *
    * @param attribute the expression to secure the URLs (i.e.
    * "hasRole('ROLE_USER') and hasRole('ROLE_SUPER')")
    * @return the {@link ExpressionUrlAuthorizationConfigurer} for further
    * customization
    */
   public ExpressionInterceptUrlRegistry access(String attribute) {
       if (not) {
           attribute = "!" + attribute;
       }
       interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
       return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
   }

可知道,其参数输入为 security的表达式,所以access中的参数是和方法注解@PreAuthorize()参数是一样的,如

   @Component
   public class PathAuthenticationProviderImpl implements PathAuthenticationProvider {
       @Override
       public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests) {
           authorizeRequests
                   // access参数为表达式
                   .antMatchers("/user").access("hasRole('user')")
                   .antMatchers("/hello").access("hasAnyRole('user','admin')")
                   .anyRequest().authenticated();
       }
   }

对于access(),可以通过其实现基于数据库的RBAC(基于角色的权限访问控制)权限控制,可以实现动态配置用户的角色和权限来实现授权管理,例如:

  • 创建类`RbacService:

    @Component
    public class RbacServiceImpl {
        public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            /**
             * 假设现在系统中的 uri的权限为:
             * 请求方式   uri    所需角色
             * get       /user   ROLE_user
             * post      /user   ROLE_admin
             * put       /user   ROLE_tenant
             *
             * get       /hello  ROLE_user
             * post      /hello  ROLE_admin
             */
            // 以下为模拟的数据,(实现开发中,可以通过数据库来维护 uri 的权限)
            Map<String, Map<String, String>> map = new HashMap<>();
    
            Map<String, String> userMap = new HashMap<>();
            userMap.put("get", "ROLE_user");
            userMap.put("post", "ROLE_admin");
            userMap.put("put", "ROLE_tenant");
            map.put("/user", userMap);
    
            Map<String, String> helloMap = new HashMap<>();
            helloMap.put("get", "ROLE_user");
            helloMap.put("post", "ROLE_admin");
            map.put("/hello", helloMap);
    
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserDetails) {
                // 获取到登录用户的所拥有的角色
                Collection<? extends GrantedAuthority> authorities = ((UserDetails) principal).getAuthorities();
    
                // 获取请求的 URI
                String requestURI = request.getRequestURI();
                // 获取请求的方法
                String requestMethod = request.getMethod().toLowerCase();
    
                // 通过 URI 获取权限
                Map<String, String> stringMap = map.get(requestURI);
                // 再通过 请求方法 获取到当前请求URI的权限角色
                String uriRole = stringMap.get(requestMethod);
    
                // 判断用户是否有该角色
                for (GrantedAuthority authority : authorities) {
                    if (authority.getAuthority().equalsIgnoreCase(uriRole)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
    
  1. 修改PathAuthenticationProviderImpl的权限控制:

    @Component
    public class PathAuthenticationProviderImpl implements PathAuthenticationProvider {
        @Override
        public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests) {
            authorizeRequests
                // 所有请求都需要经过 rbacServiceImpl.hasPermission方法过滤,注意参数名称hasPermission()和类`rabcServiceImpl`一样
                .anyRequest().access("@rbacService.hasPermission(request, authentication)");
        }
    }
    

    启动程序,所有请求都会经过rbacServiceImpl.hasPermission(request, authentication)方法。

六、忽略权限认证

前面配置的权限验证都是拦截认证所有的请求URI,但有些请求是无需进行拦截认证的,如静态文件js,css等,而如果使用前面的配置,则需要:

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
           .authorizeRequests()
           // 图标访问不用认证
           .antMatchers("/favicon.ico","/js/**","/css/**").permitAll()
           // GET请求/register不用认证
           .antMatchers(HttpMethod.GET, "/register").permitAll();
   }

但其实Spring Security提供有一个configure(WebSecurity web)进行配置,在WebSecurityConfig配置类重写进行配置,如:

   @Override
   public void configure(WebSecurity web) throws Exception {
       web
           .ignoring()
           .mvcMatchers("/favicon.ico","/js/**","/css/**")
           // 其实这里配置非静态路径有坑,不推荐,请在configure(HttpSecurity http)设置
           .mvcMatchers(HttpMethod.GET, "/register");
   }

七、基于数据库用户认证

7.1、引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

这里使用mybatis-plus

7.2、数据源

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
logging:
  level:
    com.example.bdatabaserole.mapper: debug

7.3、表及数据

图片

对应的entity类:

@Data
@ToString
public class UserInfo {
    private Integer id;
    private String username;
    private String password;
    private String role;
}

对应的mapper接口:

@Repository
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}

插入一个用户并加密密码:

@SpringBootTest
class SpringSecurity20200328ApplicationTests {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Test
    void contextLoads() {
        UserInfo userInfo = new UserInfo();
        userInfo.setId(2);
        userInfo.setUsername("user");
        // 加密密码,密码匹配使用 passwordEncoder.matches(dbPwd, inputPwd)
        userInfo.setPassword(passwordEncoder.encode("123"));
        userInfo.setRole("user");
        userInfoMapper.insert(userInfo);
    }
}

7.4、配置从数据库获取用户信息验证

实现UserDetailService,重写方法:

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Map<String, Object> map = new HashMap<>();
        map.put("username", username);
        UserInfo userInfo = userInfoMapper.selectByMap(map).get(0);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 得到用户角色
        String role = userInfo.getRole();
        // 角色集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 角色必须以`ROLE_`开头,数据库中没有,则在这里加
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));

        return new User(
                userInfo.getUsername(),
                // 因为密码已加密,所以不用加密
                userInfo.getPassword(),
                authorities
        );
    }
}

WebSecurityConfig配置:

@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

重启程序,正常使用数据库的用户登录。

八、退出登录

默认的退出登录URL为:/logout,如:http://localhost:8080/logout退出登录。

如果开启了CSRF,必须使用post方式请求/logout

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容