1. SpringSecurity介绍

官网链接: https://spring.io/projects/spring-security#learn

2. 搭建Demo工程

选择从Spring Initializr创建SpringBoot项目,勾选依赖如下:
image.png

2.1 简单的页面测试

这里写了一个简单登陆页面和一个简单的登陆接口

  1. @RequestMapping("/login")
  2. public String login(){
  3. return "redirect:main.html";
  4. }
  1. <form action="/login" method="post">
  2. 用户名:<input type="text" name="username123"/><br/>
  3. 密码:<input type="password" name="password123"/><br/>
  4. <input type="submit" value="登陆"/><br/>
  5. </form>

当我们启动应用,并且访问登陆页面时(localhost:8080/login.html),出现如下页面:
image.png
并且在IDEA的输出控制中可以看到:
image.png
这是Spring Security的默认登陆逻辑。在我们没有配置自己的登陆逻辑情况下,所有访问服务器的请求都会被拦截,出现上面的登陆界面。默认的登陆名是user,密码就是IDEA输出控制台中那串临时密码,且每次启动应用后,生成的密码都不一样。

输入了正确的用户名和密码之后,就跳转到了我们自定义的登陆页面:

image.png

2.2 自定义登陆逻辑

自定义登陆逻辑需要实现UserDetailsService接口

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.从数据库查询username,查不到则报异常(这里就不连数据库了,为了测试,直接写死)
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 2.比较密码
        // 一般情况下是需要查询数据库,将数据库的密码(加密保存的)和传进来的密码进行比较
        String encodePwd = passwordEncoder.encode("123");
        // AuthorityUtils是一个工具类,这里直接指定该user的权限列表,并且拥有的角色是abs(ROLE_ 是固定写法)
        // 入参的GrantedAuthority接口我们也可以自己实现。然后这里直接new我们的实现类即可
        return new User(username, encodePwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc,/main.html"));
    }
}

在一般的开发中,我们需要对传入的username进行数据库查找,找到之后才进行密码比对。这里为了测试,直接将用户名和密码都写死了。用户名是admin,密码是123

需要注意的是,在实际开发中,无论我们保存进数据库的密码或者是前端传给后端的密码,都是加密后的密文。我们在比较密码时,前端传过来的密码进行解密,后端从数据库中查询出的密码进行解密,两者都是解密后的密码,然后进行比较。

这里返回的User对象是SpringSecurity中定义的,其中包含了一些登陆用户的基本信息(用户名、密码、权限列表、账户状态等),注意不要与自定义的User实体类混淆。

2.3 PasswordEncoder

这里的PasswordEncoder是用来对密码进行加密和解密的,我们在SecurityConfig(后面会讲到)中自定义其实现类为BCryptPasswordEncoder,这是官方推荐的一种加密方式。同时,我们将PasswordEncoder交给了Spring管理,使得我们需要时,可以直接使用@Autowired注入bean来使用。

// 将PasswordEncoder交给spring容器进行管理
@Bean
public PasswordEncoder getPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

编写完成自定义的登陆逻辑后,我们还要配置Spring Security,不然这个登陆逻辑不会生效。

2.4 SpringSecurity配置

在demo中新建一个配置类,需要继承自WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PersistentTokenRepository tokenRepository;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单提交
        http.formLogin()
                // 自定义入参,参数名要和前端页面提交的参数名一样,不然会报错
                .usernameParameter("username123").passwordParameter("password123")
                // 自定义登陆页面
                .loginPage("/login.html")
                // 必须和表单提交的接口一样,去执行自定义的登陆逻辑
                .loginProcessingUrl("/login")
                // 登陆成功后跳转的页面 POST请求
                 .successForwardUrl("/toMain")
                // 自定义登陆成功跳转逻辑
                //.successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                // 登陆失败后跳转的页面 POST请求
                // .failureForwardUrl("/toError");
                // 自定义登陆失败跳转逻辑
                .failureHandler(new MyAuthenticationFailureHandler("/error.html"));

        // 自定义异常处理
        http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
        // 关闭csrf防护
        http.csrf().disable();

        // 授权
        http.authorizeRequests()
                // 放行login.html ,不需要认证
                .antMatchers("/login.html").permitAll()
                // 放行error.html ,不需要认证
                .antMatchers("/error.html").permitAll()
                // images目录下的所有资源放行
                // .antMatchers("/images/**").permitAll()
                // 所有目录下的jpg后缀的文件都放行
                .antMatchers("/**/*.jpg").permitAll()
                // 权限控制,拥有admin权限才可访问(严格区分大小写)
                //.antMatchers("/main1.html").hasAuthority("admin")
                // 权限控制,拥有[admin,admiN]其中一个权限即可访问
                //.antMatchers("/main1.html").hasAnyAuthority("admin","admiN")
                // 基于角色的权限控制(严格区分大小写)
                //.antMatchers("/main1.html").hasAnyRole("abc")
                // 基于access的访问控制
                //.antMatchers("/main1.html").access("hasRole('abc')")
                // 基于角色的权限控制,有任意一个角色即可访问
                //.antMatchers("/main1.html").hasAnyRole("abc", "abC")
                // 所有请求必须认证才能访问,也就是需要登陆(这句代码必须放在最后)
                .anyRequest().authenticated();
                // 自定义access方法
 //.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");

        // 设置 记住我
        http.rememberMe()
                // 设置数据源
                .tokenRepository(tokenRepository)
                // 设置过期时间(秒)
                .tokenValiditySeconds(30)
                // 设置自定义登陆逻辑
                .userDetailsService(userDetailsService);

        // 退出
        http.logout()
                // 退出url,默认就是springsecurity提供的/logout
                .logoutUrl("/logout")
                // 退出成功后跳转的url
                .logoutSuccessUrl("/login.html");
    }

    // 自定义token存储方法
    @Bean
    public PersistentTokenRepository tokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository=new JdbcTokenRepositoryImpl();
        // 设置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 设置自定建表(数据库自定建表存储登陆信息,第一次启动时开启,第二次要注释掉)
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    // 将PasswordEncoder交给spring容器进行管理
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.5 自定义登陆成功或者失败的处理

  • 自定义登陆成功的Handler

    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
      private String url;
    
      public MyAuthenticationSuccessHandler(String url) {
          this.url = url;
      }
    
      // 自定义登陆成功跳转逻辑
      @Override
      public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
          //// 获取user对象,就是我们在自定义登陆逻辑那里返回的user
          //User user = (User) authentication.getPrincipal();
          //System.out.println(user.getUsername());
          //System.out.println(user.getPassword());
          //System.out.println(user.getAuthorities());
    
          // 使用重定向跳转页面
          httpServletResponse.sendRedirect(url);
      }
    }
    
  • 自定义登陆失败的Handler

    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
      private String url;
    
      public MyAuthenticationFailureHandler(String url) {
          this.url = url;
      }
    
      // 自定义登陆失败跳转逻辑
      @Override
      public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
          httpServletResponse.sendRedirect(url);
      }
    }
    
  • 自定义异常处理Handler

    // 自定义权限异常处理
    @Component
    public class MyAccessDeniedHandler implements AccessDeniedHandler {
      @Override
      public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
          // 设置状态码
          httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
          // 返回json格式
          httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8");
          PrintWriter writer = httpServletResponse.getWriter();
          writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
          writer.flush();
          writer.close();
      }
    }
    

    2.6 自定义Access方法

    我们在SecurityConfig中的授权配置http.authorizeRequests(),本质上都是在执行一个access方法,因此我们可以自定义这个方法。

/**
 * 这个是自定义的access逻辑
 */
@Service
public class MyServiceImpl implements MyService {

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        // 得到主体(这里的主体就是我们在自定义登陆逻辑时返回的user)
        Object obj = authentication.getPrincipal();
        // 如果主体的类型是UserDetails,那就转成UserDetails
        if (obj instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) obj;
            //获取权限列表
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            // 如果权限列表中包含了请求uri,则返回true
            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
        }
        return false;
    }
}

2.7 RememberMe

平时在进行登陆时,都会有一个记住我可以勾选。勾选之后,在一定时间内,用户可以不必重复登陆,就可以访问资源。
这个设置在上面SecurityConfig也有。这里需要注意的是,我们可以自定义token存储方式。

当我们启用RememberMe功能的时候,第一次登陆时,系统会生成一个token,存储在本地或者内存中。下次登陆时,系统会去查找这个token,如果没有过期,则不用登陆,可以直接访问。如果过期了则需要重新登陆。

token存储方式有两种,一种是存储在数据库中,一种存储在内存中,这里我们自定义token存储在数据库中。

 // 自定义token存储方法
@Bean
public PersistentTokenRepository tokenRepository(){
    JdbcTokenRepositoryImpl jdbcTokenRepository=new JdbcTokenRepositoryImpl();
    // 设置数据源
    jdbcTokenRepository.setDataSource(dataSource);
    // 设置自定建表(数据库自定建表存储登陆信息,第一次启动时开启,第二次要注释掉)
    //jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
}

需要注意的是代码中的设置自动建表。同时,我们需要在项目中配置数据源,并且在数据库中建立对应的数据库

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: admin123

每次进行登陆时,可以查看数据表中的记录如下:
image.png

2.8 自定义退出逻辑

SecurityConfig代码中已经写有。
需要注意的是,SpringSecurity提供了一个内置的logout接口,我们一般直接用这个接口就好,不用修改。注意前端页面也要访问这个接口.
image.png

2.9 基于注解的访问控制

这里只讲两个注解,更多的注解使用请查看官网
要使用SpringSecurity的注解,需要在启动类上开启,并且需要具体指定该注解的Enabled = true

@SpringBootApplication
// 开启SpringSecurity的注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class DemoSpringsecurityApplication {

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

}

@Secured
// Spring Security的注解:拥有abc角色的用户可以访问此接口
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String main() {
    return "redirect:main.html";
}

@PreAuthorize
// 表示在访问前控制,判断用户是否具有角色'abc',有则允许访问该接口
// PreAuthorize在这个注解中hasRole的值可以用ROLE前缀,也可以不用。但是配置类中不用加前缀
@PreAuthorize("hasRole('abc')")
@RequestMapping("/toMain")
public String main() {
    return "redirect:main.html";
}