参考

认证和授权是SpringSecurity作为一个安全框架的核心功能。

快速入门

  1. 导入依赖
// spring security
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
// web
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. 自定义前台登录页面login.html如下
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<form action="/login" method="post">
<!--    用户名和密码必须是username和password-->
    用户名:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <input type="submit" value="登录"/>
</form>
</body>
</html>
  1. 用户访问http://localhost:8080/login.html,将会跳转到spring security的内置登录页面,使用用户名user和Spring security生成的密码登录成功后才会跳转到我们自定义的登录页面。

实现自定义登录逻辑

  1. 几个重要的地方:UserDetailsService接口的loadUserByUsername方法和PasswordEncoder接口的BCryptPasswordEncoder实现类(官方推荐的一个密码解析器)
    // BCryptPasswordEncoder示例
    // 创建密码解析器
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    // 加密密码
    String encodedPassword = encoder.encode("123456");
    System.out.println(encodedPassword);
    boolean matches = encoder.matches("123456", encodedPassword);
    System.out.println(matches); // true
    
  2. 实现自定义登录逻辑步骤
    1. 配置类
    /**
     * Security配置类
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        // 往spring容器中注入passwordEncoder bean
        @Bean
        public PasswordEncoder getPw() {
            return new BCryptPasswordEncoder();
        }
    }
    
    1. 登录逻辑
    @Service
    public class UserDetailServiceImpl implements UserDetailsService {
        @Autowired
        private PasswordEncoder passwordEncoder;
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名去数据库查询
            if (!"admin".equals(username)) {
                throw new UsernameNotFoundException("用户名不存在!");
            }
            // 比较密码,匹配成功则返回UserDetails
    
            // password模拟用户注册时的密码,已经加密
            String password = passwordEncoder.encode("123");
            return new User(username, password, AuthorityUtils
                    .createAuthorityList("admin,normal"));
        }
    
    }
    
    1. 用户输入http://localhost:8080/login.html后进入spring security内置登录页面,输入用户名admin,密码123登录成功。

自定义登录页面,失败跳转页面

  1. 自定义登录页面
    1. 前台登录页面如下:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
    <form action="/login" method="post">
    <!--    用户名和密码必须是username和password-->
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="submit" value="登录"/>
    </form>
    </body>
    </html>
    
    1. 配置类如下:
    /**
     * Security配置类
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        // 往spring容器中注入passwordEncoder bean
        @Bean
        public PasswordEncoder getPw() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 表单提交
            http.formLogin()
                    // 指定登录页面为自定义的
                    .loginPage("/login.html")
                    // 自定义入参
                    .usernameParameter("username")
                    .passwordParameter("password")
                    // 登录页面表单提交地址,/login是一个控制器
                    .loginProcessingUrl("/login")
                    // 登录成功后跳转的页面,/toMain也是一个控制器
                    // .successForwardUrl("/toMain")
    
                    // 自定义登录成功处理器
                    .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
    
                    // 登录失败后跳转的页面,/toError也是一个控制器
                    // .failureForwardUrl("/toError");
    
                    // 自定义登录失败处理器
                    .failureHandler(new MyAuthenticationFailureHandler("/error.html"));
            // 授权
            http.authorizeRequests()
                    // 放行/error.html,不需要认证
                    .antMatchers("/error.html").permitAll()
                    // 放行/login.html,不需要认证
                    .antMatchers("/login.html").permitAll()
                    //.antMatchers("/css/**","/js/**","/images/**").permitAll()
                    // 放行项目下所有以jpg结尾的图片
                    //.antMatchers("/**/*.jpg").permitAll()
                    // 放行x.jpg
    //                .regexMatchers()
                    // 所有请求都必须认证才能访问,必须登录
                    .anyRequest().authenticated();
            // 关闭csrf防护
            http.csrf().disable();
        }
    }
    
  2. 自定义登录失败跳转页面
    1. 登录失败页面error.html如下
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    操作失败,请重新登录 <a href= "/login.html">跳转</a>
    </body>
    </html>
    
    1. 配置类如上

自定义登录成功处理器和登录失败处理器

  1. 自定义登录成功处理器,可以控制登录成功后做一些事情。
    1. 自定义登录成功处理器
    // 自定义登录成功处理器,可以重定向(默认行为是请求转发)
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        private  String url;
    
        public MyAuthenticationSuccessHandler() {
        }
    
        public MyAuthenticationSuccessHandler(String url) {
            this.url = url;
        }
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            // 重定向
            response.sendRedirect(url);
    
            // 获取用户名和密码,权限
            User user = (User) authentication.getPrincipal();
            System.out.println(user.getUsername());
            System.out.println(user.getPassword()); // null
            System.out.println(user.getAuthorities());
        }
    
    }
    
    1. 使用
    // 表单提交
        http.formLogin()
        // 自定义登录成功处理器,和successForwardUrl不能共存
        .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
    
    
  2. 自定义登录失败处理器
// 自定义登录失败处理器,可以重定向
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String url;

    public MyAuthenticationFailureHandler() {
    }

    public MyAuthenticationFailureHandler(String url) {
        this.url = url;
    }


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}

// 使用
// 表单提交
http.formLogin()
// 自定义登录失败处理器,和failureForwardUrl不能共存
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));

访问控制URL匹配

  1. anyRequest():匹配所有请求
  2. antMatcher():匹配URL规则
  3. regexMatchers():使用正则表达式进行匹配
  4. mvcMatchers():适用于配置了servletPath的情况
  5. mvcMatchers():适用于配置了servletPath的情况

访问控制方法

  1. permitAll():表示匹配的URL任何人都允许访问
  2. authenticated():所匹配的 URL都需要被认证才能访问。
  3. denyAll()表示所匹配的 URL 都不允许被访问。
  4. rememberMe()被“remember me”的用户允许访问
// 授权
http.authorizeRequests()
    // 放行/error.html,不需要认证
    .antMatchers("/error.html").permitAll()
    // 放行/login.html,不需要认证
    .antMatchers("/login.html").permitAll()
    //.antMatchers("/css/**","/js/**","/images/**").permitAll()
    // 放行项目下所有以jpg结尾的图片
    //.antMatchers("/**/*.jpg").permitAll()
    // 放行x.jpg
    //    .regexMatchers()
    //  指定请求方法为POST,放行所有x.jpg,不需要登录
    .regexMatchers(HttpMethod.POST,".+[.]jpg").permitAll()
    // 所有请求都必须认证才能访问,必须登录
    .anyRequest().authenticated();

基于权限控制

// 权限控制,只有用于admin权限的用户才能访问xxx.html
.antMatchers("/xxx.html").hasAuthority("admin")

基于角色控制

// 权限和角色在自定义登录逻辑中定义
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名去数据库查询,这里直接使用用户名模拟
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 比较密码,匹配成功则返回UserDetails
        // password模拟用户注册时的密码,已经加密
        String password = passwordEncoder.encode("123");
        // ROLE_abc表示给用户赋予abc这个角色
        return new User(username, password, AuthorityUtils
                .createAuthorityList("admin,normal,ROLE_abc"));
    }

}

// 角色控制
.antMatchers("/xxx.html").hasRole("abc")

基于IP控制

 // 基于IP
.antMatchers("/xxx.html").hasIpAddress("127.0.0.1")

自定义403处理方案

  1. 实现AccessDeniedHandler接口
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        out.flush();
        out.close();
    }
}
  1. 配置类中添加异常处理器
// handler是在配置类中自动注入的
http.exceptionHandling().accessDeniedHandler(handler);

自定义访问控制逻辑

  1. 示例:判断登录的用户是否具有访问当前的URL权限
    1. 接口
    public interface MyService {
        boolean hasPermission(HttpServletRequest request, Authentication authentication);
    }
    
    1. 实现类
    @Service
    public class MyServiceImpl implements MyService {
        @Override
        public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserDetails) {
                UserDetails userDetails = (UserDetails) principal;
                Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
                return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
            }
            return false;
        }
    }
    
    1. 修改配置类
    .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
    

基于注解的访问控制

前提:通过@EnableGlobalMethodSecurity注解开启用于访问控制的注解的功能

  1. @Secured:判断是否具有角色
  2. @PreAuthorize:在方法或者类执行之前先判断权限。该注解的value值可以是任何 access()支持的表达式
  3. @PostAuthorize:在方法或者类执行之后先判断权限。该注解的value值可以是任何 access()支持的表达式

RememberMe的功能实现

原理:用户在登录时添加remember me复选框。Spring security会自动将用用户信息存储到数据源中,以后就可以不登录进行访问。

  1. 添加依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
  1. 配置数据源
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= 123456
  1. 编写配置
@Configuration
public class RememberMeConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //自动建表,第一次启动时需要,第二次启动时注释掉
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}
  1. 修改SecurityConfig配置
http.rememberMe()
        // 失效时间,单位秒
        .tokenValiditySeconds(120)
        // 登录逻辑交给哪个对象
        .userDetailsService(userDetailService)
        // 持久层对象
        .tokenRepository(persistentTokenRepository);
  1. 客户端登录页面添加remember-me的复选框
<input type="checkbox" name="remember-me" value="true"/><br/>

Spring Security中的CSRF

CSRF(Cross-Site request forgery):跨站请求伪造。
从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。

  1. 前台登录页面
  2. 保持开启CSRF防护(默认开启)